拡張可能なPostgres







前回のPGConf.Russiaで、 MobilityDB拡張機能に関する講演があり、Andrei Borodinがタスクのインデックスメソッドを拡張するというアイデアを提案しました。







HighLoad Cup 2018の一部として作成された拡張機能の例を使用して解決するタスクのPostgres拡張機能でトピックを継続します 。コードはGithHubで入手できます。 habrにはすでにblackmasterからの記事あります。 この拡張機能は、btreeおよびGINインデックスをサポートする2つのタイプを追加します。







初期スキーム:







CREATE TABLE accounts ( id INTEGER PRIMARY KEY, email VARCHAR(100) UNIQUE, fname SMALLINT, sname VARCHAR(50), phone VARCHAR(16) UNIQUE, birth TIMESTAMP NOT NULL, country SMALLINT, city SMALLINT, email_domain SMALLINT, joined TIMESTAMP NOT NULL, status status, sex sex NOT NULL, interests SMALLINT[], premium tsrange, likes_ids INTEGER[], likes_tss INTEGER[], CHECK ( joined >= '01.01.2011'::timestamp ), CHECK ( joined <= '01.01.2018'::timestamp ) ); CREATE INDEX IF NOT EXISTS interests_ids_index ON accounts USING GIN(interests);
      
      





レコード数は1,300,000で 、関心のある関係のサイズは次のとおりです。







関係 サイズ
アカウント 598 MB
accounts_email_key 54 MB
accounts_phone_key 32 MB
accounts_pkey 28 MB
interests_ids_index 8072 kB


最終回路:







 CREATE UNLOGGED TABLE accounts ( id INTEGER PRIMARY KEY, email VARCHAR(100) UNIQUE, fname SMALLINT, sname VARCHAR(50), phone phone UNIQUE, birth TIMESTAMP NOT NULL, country SMALLINT, city SMALLINT, email_domain SMALLINT, joined TIMESTAMP NOT NULL, status status, sex sex NOT NULL, interests bit128, premium tsrange, likes_ids INTEGER[], likes_tss INTEGER[], CHECK ( joined >= '01.01.2011'::timestamp ), CHECK ( joined <= '01.01.2018'::timestamp ) );
      
      





寸法:







関係 古いサイズ 新しいサイズ
アカウント 598 MB 579 MB
accounts_phone_key 32 MB 28 MB
accounts_pkey 28 MB 28 MB
interests_ids_index 8072 kB 8072 kB


phonebit128の 2つのタイプが追加されました。







電話



電話番号は8(929)5481819で、8つは破棄できます。 演算子コードは10ビットに収まり、数値は24ビットであり、5バイト必要です。 メモリ内のデータ配置のため、あまり便利な数値ではありません。 データを5、6、または8バイトに収める最適な方法については、テストでしか答えを出すことができません。







ドキュメントのに従う場合は、少し必要です。 タイプを定義:







 class Phone : public PAllocNew<Phone> { public: bool fromCString(char* str) noexcept; char* toCString() const noexcept; int code() const noexcept; bool operator==(const Phone& b) const noexcept; bool operator<(const Phone& b) const noexcept; bool operator<=(const Phone& b) const noexcept; bool operator>(const Phone& b) const noexcept; bool operator>=(const Phone& b) const noexcept; private: uint64_t m_data = 0; }; #define GETARG_PHONE(x) reinterpret_cast<Phone*>(PG_GETARG_POINTER(x))
      
      





newをオーバーロードするPAllocNewヘルパークラス:







 template<typename T> class PAllocNew { public: static void* operator new(std::size_t count, const std::nothrow_t& tag) { return palloc(count); } };
      
      





pallocを介して割り当てられたメモリは、トランザクションが完了すると解放されます。 I / O関数を追加します。







 Datum phone_in(PG_FUNCTION_ARGS) { char* str = PG_GETARG_CSTRING(0); auto v = new(std::nothrow) Phone; if (!v->fromCString(str)) { ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg( "invalid input syntax for phone: \"%s\"", str ))); } PG_RETURN_POINTER(v); } Datum phone_out(PG_FUNCTION_ARGS) { const auto phone = GETARG_PHONE(0); PG_RETURN_CSTRING(phone->toCString()); }
      
      





そして登録タイプ:







 CREATE TYPE phone; CREATE OR REPLACE FUNCTION phone_in ( cstring ) RETURNS phone AS 'libhighloadcup' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION phone_out ( phone ) RETURNS cstring AS 'libhighloadcup' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE phone ( INTERNALLENGTH = 8, INPUT = phone_in, OUTPUT = phone_out, ALIGNMENT = int4, COLLATE TRUE );
      
      





なぜなら 新しいタイプのサイズは8バイト以下であり、参照ではなく値によって送信できます。 この場合、 CREATE TYPE



PASSEDBYVALUE



フラグを指定する必要があります。 または、 LIKE



使用します。







 CREATE TYPE phone ( INPUT = phone_in, OUTPUT = phone_out, LIKE = int8, COLLATE TRUE );
      
      





その後、 INTERNALLENGTH



ALIGNMENT



およびPASSEDBYVALUE



int8から継承されます。







btreeインデックス



一意のBツリーインデックスを作成することにより、フィールドの一意性がサポートされます。 これは、演算子と戦略のクラスを通じて行われます。







オペレーター
少ない 1
より小さいか等しい 2
等しい 3
より大きいか等しい 4
もっと 5


機能
等しい 1


 CREATE OPERATOR = ( PROCEDURE = phone_equal, LEFTARG = phone, RIGHTARG = phone, COMMUTATOR = =, NEGATOR = != ); CREATE OPERATOR < ( PROCEDURE = phone_lt, LEFTARG = phone, RIGHTARG = phone, NEGATOR = > ); CREATE OPERATOR <= ( PROCEDURE = phone_le, LEFTARG = phone, RIGHTARG = phone ); CREATE OPERATOR >= ( PROCEDURE = phone_ge, LEFTARG = phone, RIGHTARG = phone ); CREATE OPERATOR > ( PROCEDURE = phone_gt, LEFTARG = phone, RIGHTARG = phone, NEGATOR = < ); CREATE OPERATOR CLASS btree_phone_ops DEFAULT FOR TYPE phone USING btree AS OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 =, OPERATOR 4 >=, OPERATOR 5 >, FUNCTION 1 phone_equal_internal ( phone, phone );
      
      





演算子はbool



およびphone_equal_internal



intをphone_equal_internal



ます。







 Datum phone_equal_internal(PG_FUNCTION_ARGS) { const auto a = GETARG_PHONE(0); const auto b = GETARG_PHONE(1); if (*a < *b) { PG_RETURN_INT32(-1); } if (*a > *b) { PG_RETURN_INT32(1); } PG_RETURN_INT32(0); }
      
      





これにより、データがわずかに減少しました。







関係 サイズ diff
アカウント 595 MB 3 MB
accounts_phone_key 28 MB 4 MB


番号533,289のアカウントの数は41%です。 少なくとも、インデックスを操作する場合、文字列の比較はありません。







bit128



交差(&&)、オカレンス(<@)、およびGINサポートの操作を行うビットフィールドが必要でした。 96ビットで十分でしたが、単純なパスuint128









 class BitSet128: public PAllocNew<BitSet128> { public: bool haveIntersection(const BitSet128& b) const noexcept; bool contains(const BitSet128& b) const noexcept; bool operator==(const BitSet128& b) const noexcept; bool operator<(const BitSet128& b) const noexcept; bool operator>(const BitSet128& b) const noexcept; bool fromCString(char* str) noexcept; char* toCString() const noexcept; std::vector<uint8_t> keys() const; private: uint128 m_bits = 0; }; #define GETARG_BIT128(x) reinterpret_cast<BitSet128*>(PG_GETARG_POINTER(x))
      
      





タイプと操作の登録:







 CREATE TYPE bit128 ( INTERNALLENGTH = 16, INPUT = bit128_in, OUTPUT = bit128_out, ALIGNMENT = int4 ); CREATE OPERATOR = ( PROCEDURE = bit128_equal, LEFTARG = bit128, RIGHTARG = bit128, COMMUTATOR = =, NEGATOR = != ); CREATE OPERATOR && ( PROCEDURE = bit128_intersection, LEFTARG = bit128, RIGHTARG = bit128 ); CREATE OPERATOR @> ( PROCEDURE = bit128_containts, LEFTARG = bit128, RIGHTARG = bit128 );
      
      





一般化逆索引/ GIN



Egor Rogov erogovはPostgresのGINについて記事PostgreSQLのインデックス -7でよく書いており、全文検索タスクにそれを適用しています。 GIN拡張性ドキュメントは、4つの機能を実装するのに十分であることを示唆しています。







インデックス付きオブジェクトからキーの配列を抽出する:







 Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
      
      





要求された値のキーを取得します。







 Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data, bool **nullFlags, int32 *searchMode)
      
      





たとえば、クエリでは:







 SELECT * FROM test WHERE a && ARRAY[1, 2, 3]
      
      





extractQueryARRAY[1, 2, 3]



配列に適用され、キーは1、2、3になります。

値がクエリと一致するかどうかを確認します。







 bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[])
      
      





抽出されたキーは検索ツリーに保存されるため、比較関数が必要です。







 int compare(Datum a, Datum b)
      
      





APIは冗長に見えるかもしれませんが、役に立つかもしれないすべてを提供します。 実装に移りましょう。 キー抽出:







 Datum gin_extract_value_bit128(PG_FUNCTION_ARGS) { auto bitset = GETARG_BIT128(0); const auto bits = bitset->keys(); int32* nentries = (int32*) PG_GETARG_POINTER(1); *nentries = bits.size(); Datum* entries = NULL; entries = (Datum*) palloc(sizeof(Datum) * bits.size()); for (int i = 0; i < bits.size(); ++i) { entries[i] = DatumGetInt16(bits[i]); } PG_RETURN_POINTER(entries); }
      
      





出力パラメーターnentries



キーの数を書き込み、キーentries



配列を返しentries



。 主な比較:







 Datum bit128_cmp(PG_FUNCTION_ARGS) { const auto a = PG_GETARG_INT16(0); const auto b = PG_GETARG_INT16(1); if (a < b) { PG_RETURN_INT32(-1); } if (a > b) { PG_RETURN_INT32(1); } PG_RETURN_INT32(0); }
      
      





これら2つの関数は、インデックスを作成するのに十分です。 他の関数は、インデックスで検索するときに使用されます。 リクエストからキーを抽出する:







 Datum gin_extract_query_bit128(PG_FUNCTION_ARGS) { const auto a = GETARG_BIT128(0); int32* nentries = (int32*) PG_GETARG_POINTER(1); StrategyNumber strategy = PG_GETARG_UINT16(2); int32* searchMode = (int32*) PG_GETARG_POINTER(6); Datum* res = nullptr; const auto keys = a->keys(); *nentries = keys.size(); res = (Datum*) palloc(sizeof(Datum) * keys.size()); for (int i = 0; i < keys.size(); ++i) { res[i] = DatumGetInt16(keys[i]); } switch (strategy) { case RTOverlapStrategyNumber: // && if (*nentries > 0) { *searchMode = GIN_SEARCH_MODE_DEFAULT; } else { *searchMode = GIN_SEARCH_MODE_ALL; } break; case RTContainsStrategyNumber: // @> if (*nentries > 0) { *searchMode = GIN_SEARCH_MODE_DEFAULT; } else { *searchMode = GIN_SEARCH_MODE_ALL; } break; } PG_RETURN_POINTER(res); }
      
      





最初のパラメーターは演算子の右側に値を渡し、3番目のパラメーターは検索が行われる戦略(演算子)を担当します。 poskのモードについては、 ドキュメントをよく読んでください。 コンプライアンス機能:







 Datum gin_bit128_consistent(PG_FUNCTION_ARGS) { bool* check = (bool*) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); int32 nkeys = PG_GETARG_INT32(3); bool* recheck = (bool*) PG_GETARG_POINTER(5); bool res = true; switch (strategy) { case RTOverlapStrategyNumber: // && { for (int i = 0; i < nkeys; ++i) { if (check[i]) { res = true; } }; *recheck = false; } break; case RTContainsStrategyNumber: // @> { for (int i = 0; i < nkeys; ++i) { if (check[i]) { res = true; } }; *recheck = true; } break; } PG_RETURN_BOOL(res); }
      
      





インデックス付きオブジェクトがストラテジー番号でクエリ演算子を満たす場合、 true



返します。 チェック配列の長さはnkeysで 、これはgin_extract_query_bit128によって返されるキーの数に対応します。 check[i] = true



すると、 gin_extract_query_bit128の i番目のキーがインデックス付きオブジェクトに存在することを意味します。 これは交差するには十分ですが、 GINでは、値自体を処理しません。その後、エントリ戦略では、 recheckをtrueに設定します。 これにより、適切な演算子が呼び出され、結果が確認されます。 演算子クラス:







 CREATE OR REPLACE FUNCTION gin_extract_value_bit128 ( bit128, internal, internal) RETURNS internal AS 'libhighloadcup' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION gin_extract_query_bit128(bit128, internal, int2, internal, internal) RETURNS internal AS 'libhighloadcup' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION gin_bit128_consistent(internal, int2, anyelement, int4, internal, internal) RETURNS bool AS 'libhighloadcup' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR CLASS bit128_ops DEFAULT FOR TYPE bit128 USING gin AS OPERATOR 3 &&, OPERATOR 6 =, OPERATOR 7 @>, FUNCTION 1 bit128_cmp (int2, int2 ), FUNCTION 2 gin_extract_value_bit128(bit128, internal, internal), FUNCTION 3 gin_extract_query_bit128(bit128, internal, int2, internal, internal), FUNCTION 4 gin_bit128_consistent(internal, int2, anyelement, int4, internal, internal), STORAGE int2;
      
      





STORAGEが追加されました。これは、キーに使用されるデータのタイプを示します。 ディスクの結果は何ですか:







関係 古いサイズ 新しいサイズ diff
アカウント 595 MB 579 MB 16 MB
interests_ids_index 8072 kB 8072 kB


ニュアンスがあるため、興味深い結果が得られます。









作成されたタイプはすべて固定サイズであるため、 PLAIN ストレージが選択されています。 圧縮と個別のストレージは適用されません。 可変長型としての配列と文字列は、TOASTを通過します。 はい、サイズを保存するためのオーバーヘッドがありますが、圧縮もあります。







関心の分布は次のとおりです。







興味の数 ユーザーの
1 174061
2 279744
3 317212
4 262313
5 128512
6 48099
7 12228
8 2232
9 250
ヌル 75349


正直なところ、 SMALLINT[]



リリース方法はわかりませんが、サイズごとに4バイト、値ごとに2バイトと仮定します。 次に、16バイトで、6つの要素の配列に適合できます。 また、要素の98%は16バイトで16 MBの差に収まります。 bit128



タイプbit128



冗長であるように見えますが、標準タイプはこのデータセットでは冗長です。








All Articles