
Di PGConf.Russia terakhir, ada pembicaraan tentang ekstensi MobilityDB , dan Andrei Borodin mengusulkan gagasan untuk memperluas metode indeks untuk tugas tersebut.
Saya akan melanjutkan topik dengan ekstensi Postgres agar tugas diselesaikan dengan menggunakan contoh ekstensi yang dibuat sebagai bagian dari HighLoad Cup 2018, kode ini tersedia di GithHub . Di habr sudah ada artikel dari blackmaster . Ekstensi menambahkan dua jenis dengan dukungan untuk indeks btree dan GIN.
Skema Awal:
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);
Jumlah catatan adalah 1.300.000 , dan ukuran hubungan yang menarik:
hubungan | ukuran |
---|
akun | 598 MB |
accounts_email_key | 54 MB |
accounts_phone_key | 32 MB |
accounts_pkey | 28 MB |
minat_ids_index | 8072 kB |
Sirkuit terakhir:
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 ) );
Dimensi:
hubungan | ukuran lama | ukuran baru |
---|
akun | 598 MB | 579 MB |
accounts_phone_key | 32 MB | 28 MB |
accounts_pkey | 28 MB | 28 MB |
minat_ids_index | 8072 kB | 8072 kB |
Dua jenis ditambahkan: ponsel dan bit128 .
telepon
Nomor telepon 8 (929) 5481819 , delapan dapat dibuang. Kode operator cocok menjadi 10 bit, angka 24 bit, ternyata Anda membutuhkan 5 byte. Bukan angka yang sangat nyaman karena penyelarasan data dalam memori. Untuk pertanyaan bagaimana cara terbaik memasukkan data ke dalam 5, 6 atau 8 byte, hanya tes yang dapat memberikan jawaban.
Jika Anda mengikuti contoh dari dokumentasi, Anda perlu sedikit. Tentukan Jenis:
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))
PAllocNew kelas pembantu kelebihan new
:
template<typename T> class PAllocNew { public: static void* operator new(std::size_t count, const std::nothrow_t& tag) { return palloc(count); } };
Memori yang dialokasikan melalui palloc dibebaskan ketika transaksi selesai. Tambahkan Fungsi 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()); }
Dan jenis pendaftaran:
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 );
Karena tipe baru memiliki ukuran tidak lebih dari 8 byte, dapat ditransmisikan bukan dengan referensi, tetapi dengan nilai. Dalam hal ini, Anda harus menentukan flag PASSEDBYVALUE
di CREATE TYPE
. Atau gunakan LIKE
:
CREATE TYPE phone ( INPUT = phone_in, OUTPUT = phone_out, LIKE = int8, COLLATE TRUE );
Kemudian INTERNALLENGTH
, ALIGNMENT
dan PASSEDBYVALUE
diwarisi dari int8.
indeks btree
Keunikan bidang didukung dengan membuat indeks B-tree yang unik. Ini dilakukan melalui kelas operator dan strategi .
Operator | Nomor |
---|
kurang | 1 |
kurang dari atau sama | 2 |
sama dengan | 3 |
lebih besar atau sama | 4 |
lebih lanjut | 5 |
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 );
Operator mengembalikan bool
, dan phone_equal_internal
int:
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); }
Semua ini memberikan sedikit penurunan data:
hubungan | ukuran | beda |
---|
akun | 595 MB | 3 MB |
accounts_phone_key | 28 MB | 4 MB |
Jumlah akun dengan angka 533.289, yaitu 41%. Setidaknya tidak ada perbandingan string saat bekerja dengan indeks.
bit128
Saya ingin memiliki sedikit bidang dengan operasi persimpangan (&&), kemunculan (<@) dan dengan dukungan GIN. Itu sudah cukup 96 bit, tetapi menempuh jalan yang sederhana dan mengambil 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))
Registrasi jenis dan operasi:
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 );
Indeks Terbalik Umum / GIN
Egor Rogov erogov menulis dengan baik tentang GIN di Postgres dalam artikel Indeks di PostgreSQL - 7 , menerapkannya pada tugas pencarian teks lengkap. Dokumentasi ekstensibilitas GIN menunjukkan bahwa cukup untuk mengimplementasikan 4 fungsi:
Mengambil larik kunci dari objek yang diindeks:
Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
Mengambil kunci untuk nilai yang diminta:
Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data, bool **nullFlags, int32 *searchMode)
Misalnya, dalam kueri:
SELECT * FROM test WHERE a && ARRAY[1, 2, 3]
ekstrakQuery diterapkan ke ARRAY[1, 2, 3]
, dan kunci mungkin 1, 2, 3.
Cek apakah nilainya cocok dengan kueri:
bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[])
Karena kunci yang diekstraksi disimpan di pohon pencarian, kami memerlukan fungsi perbandingan:
int compare(Datum a, Datum b)
API mungkin tampak berlebihan, tetapi menyediakan segala yang berguna. Mari beralih ke implementasi. Ekstraksi kunci:
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); }
Dalam parameter nentries
keluaran nentries
tulis jumlah kunci dan kembalikan entries
kunci. Perbandingan Utama:
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); }
Dua fungsi ini cukup untuk membuat indeks. Fungsi lain digunakan saat mencari berdasarkan indeks. Mengekstrak kunci dari permintaan:
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:
Parameter pertama melewati nilai di sebelah kanan operator, parameter ketiga bertanggung jawab atas strategi (operator) di mana pencarian dilakukan. Tentang mode pencarian lebih baik berkenalan dalam dokumentasi . Fungsi kepatuhan:
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:
Mengembalikan nilai true
jika objek yang diindeks memenuhi operator kueri dengan nomor strategi. Array cek memiliki panjang nkeys , yang sesuai dengan jumlah kunci yang dikembalikan oleh gin_extract_query_bit128 . Memeriksa check[i] = true
berarti bahwa kunci ke-i dari gin_extract_query_bit128 ada di objek yang diindeks. Ini cukup untuk menyeberang, tetapi karena di GIN kita tidak bekerja dengan nilai-nilai itu sendiri, maka untuk strategi entri kita atur ulang menjadi true. Ini akan memanggil operator yang tepat untuk memeriksa hasilnya. Kelas Operator:
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 ditambahkan, ini menunjukkan tipe data apa yang digunakan untuk kunci. Apa hasil dari disk:
hubungan | ukuran lama | ukuran baru | beda |
---|
akun | 595 MB | 579 MB | 16 MB |
minat_ids_index | 8072 kB | 8072 kB | |
Hasil yang menarik, karena ada nuansa:
- penyimpanan fisik database;
- distribusi data.
Semua tipe yang dibuat memiliki ukuran tetap, sehingga penyimpanan PLAIN dipilih untuk mereka. Kompresi dan penyimpanan terpisah tidak berlaku untuk mereka. Array dan string sebagai tipe panjang variabel melalui TOAST. Ya, ada overhead untuk ukuran penyimpanan, tetapi ada juga kompresi.
Distribusi kepentingan adalah sebagai berikut:
Jumlah Minat | pengguna |
---|
1 | 174061 |
2 | 279744 |
3 | 317212 |
4 | 262313 |
5 | 128512 |
6 | 48099 |
7 | 12228 |
8 | 2232 |
9 | 250 |
null | 75349 |
Jujur, saya tidak tahu bagaimana SMALLINT[]
dirilis, tetapi anggaplah 4 byte per ukuran dan 2 byte per nilai. Kemudian dalam 16 byte Anda bisa memasukkan 6 elemen. Dan 98% elemen akan muat dalam 16 byte dan perbedaan 16 MB. Tampaknya tipe bit128
berlebihan, tetapi tipe standar berlebihan pada kumpulan data ini.