
En el último PGConf.Russia, se habló sobre la extensión MobilityDB , y Andrei Borodin propuso la idea de expandir los métodos de índice para la tarea.
Continuaré el tema con la extensión Postgres para la tarea a resolver usando el ejemplo de la extensión hecha como parte de la HighLoad Cup 2018, el código está disponible en GithHub . En habr ya hay un artículo de Blackmaster . La extensión agrega dos tipos con soporte para índices btree y GIN.
Esquema inicial:
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);
El número de registros es de 1,300,000 , y el tamaño de la relación de interés:
relación | tamaño |
---|
cuentas | 598 MB |
cuentas_email_key | 54 MB |
cuentas_phone_key | 32 MB |
cuentas_clave | 28 MB |
interes_ids_index | 8072 kB |
El circuito final:
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 ) );
Dimensiones:
relación | tamaño antiguo | nuevo tamaño |
---|
cuentas | 598 MB | 579 MB |
cuentas_phone_key | 32 MB | 28 MB |
cuentas_clave | 28 MB | 28 MB |
interes_ids_index | 8072 kB | 8072 kB |
Se agregaron dos tipos: teléfono y bit128 .
telefono
El número de teléfono es 8 (929) 5481819 , los ocho se pueden descartar. El código del operador cabe en 10 bits, número 24 bits, resulta que necesitas 5 bytes. No es un número muy conveniente debido a la alineación de datos en la memoria. A la pregunta de cómo encajar mejor los datos en 5, 6 u 8 bytes, solo las pruebas pueden dar una respuesta.
Si sigue el ejemplo de la documentación, necesita un poco. Definir tipo:
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 helper class overloading new
:
template<typename T> class PAllocNew { public: static void* operator new(std::size_t count, const std::nothrow_t& tag) { return palloc(count); } };
La memoria asignada a través de palloc se libera cuando se completa la transacción. Agregar funciones de E / S:
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()); }
Y tipo de registro:
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 );
Porque el nuevo tipo tiene un tamaño de no más de 8 bytes, puede transmitirse no por referencia, sino por valor. En este caso, debe especificar el indicador PASSEDBYVALUE
en CREATE TYPE
. O use LIKE
:
CREATE TYPE phone ( INPUT = phone_in, OUTPUT = phone_out, LIKE = int8, COLLATE TRUE );
Entonces INTERNALLENGTH
, PASSEDBYVALUE
ALIGNMENT
y PASSEDBYVALUE
heredan de int8.
índice btree
La unicidad del campo se admite mediante la creación de un índice B-tree único. Esto se hace a través de clases de operadores y estrategias .
Operador | Numero |
---|
menos | 1 |
menor o igual | 2 |
es igual | 3 |
mayor o igual | 4 4 |
mas | 5 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 );
Los operadores devuelven bool
y 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); }
Todo esto dio una ligera disminución en los datos:
relación | tamaño | diff |
---|
cuentas | 595 MB | 3 MB |
cuentas_phone_key | 28 MB | 4 MB |
El número de cuentas con números 533,289, que es 41%. Al menos no hay comparación de cadenas cuando se trabaja con un índice.
bit128
Quería tener un campo de bits con operaciones de intersección (&&), ocurrencia (<@) y con soporte GIN. Fue suficiente 96 bits, pero siguió un camino simple y tomó 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))
Registro de tipo y operación:
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 );
Índice invertido generalizado / GIN
Egor Rogov erogov escribió bien sobre GIN en Postgres en el artículo Indexes in PostgreSQL - 7 , aplicándolo a la tarea de búsqueda de texto completo. La documentación de extensibilidad de GIN sugiere que es suficiente implementar 4 funciones:
Recuperando una matriz de claves de un objeto indexado:
Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
Recupera las claves para el valor solicitado:
Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data, bool **nullFlags, int32 *searchMode)
Por ejemplo, en una consulta:
SELECT * FROM test WHERE a && ARRAY[1, 2, 3]
extractQuery se aplica a la matriz ARRAY[1, 2, 3]
, y las claves pueden ser 1, 2, 3.
Comprueba si el valor coincide con la consulta
bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[])
Como las claves extraídas se almacenan en el árbol de búsqueda, necesitamos una función de comparación:
int compare(Datum a, Datum b)
La API puede parecer redundante, pero proporciona todo lo que puede ser útil. Pasemos a la implementación. Extracción de clave:
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); }
En las nentries
parámetros de salida nentries
escriba el número de claves y devuelva una matriz de entries
de claves. Comparación clave:
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); }
Estas dos funciones son suficientes para crear un índice. Se utilizan otras funciones cuando se busca por índice. Extracción de claves de una solicitud:
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:
El primer parámetro pasa el valor a la derecha del operador, el tercer parámetro es responsable de la estrategia (operador) por la cual se realiza la búsqueda. Acerca de los modos de búsqueda se conoce mejor en la documentación . Función de cumplimiento:
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:
Devuelve true
si el objeto indexado satisface al operador de consulta con el número de estrategia. La matriz de verificación es nkeys long, que corresponde al número de claves devueltas por gin_extract_query_bit128 . Verificar check[i] = true
significa que la i-ésima clave de gin_extract_query_bit128 está presente en el objeto indexado. Esto es suficiente para cruzar, pero porque en GIN no trabajamos con los valores en sí mismos, luego para la estrategia de entrada configuramos la verificación en verdadero. Esto llamará al operador apropiado para verificar el resultado. Clase de operador:
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;
ALMACENAMIENTO se agregó, indica qué tipo de datos se utiliza para las claves. ¿Cuál es el resultado del disco?
relación | tamaño antiguo | nuevo tamaño | diff |
---|
cuentas | 595 MB | 579 MB | 16 MB |
interes_ids_index | 8072 kB | 8072 kB | |
Un resultado interesante, porque hay matices:
- almacenamiento físico de la base de datos;
- distribución de datos
Todos los tipos creados tienen un tamaño fijo, por lo que se selecciona el almacenamiento PLAIN para ellos. La compresión y el almacenamiento por separado no se aplican a ellos. Las matrices y cadenas como tipos de longitud variable pasan por TOAST. Sí, hay gastos generales para almacenar el tamaño, pero también hay compresión.
La distribución de intereses es la siguiente:
Numero de Intereses | de usuarios |
---|
1 | 174061 |
2 | 279744 |
3 | 317212 |
4 4 | 262313 |
5 5 | 128512 |
6 6 | 48099 |
7 7 | 12228 |
8 | 2232 |
9 9 | 250 |
nulo | 75349 |
Honestamente, no sé cómo se SMALLINT[]
, pero supongamos que 4 bytes por tamaño y 2 bytes por valor. Luego, en 16 bytes, puede caber una matriz de 6 elementos. Y el 98% de los elementos cabrían en 16 bytes y diferencias de 16 MB. Parece que el tipo bit128
redundante, pero el tipo estándar es redundante en este conjunto de datos.