¿Cómo almacenan las bases de datos valores de clave de índice (en disco) para campos de longitud variable?


16

Contexto

Esta pregunta se refiere a los detalles de implementación de bajo nivel de los índices en los sistemas de bases de datos SQL y NoSQL. La estructura real del índice (árbol B +, hash, SSTable, etc.) es irrelevante ya que la pregunta se refiere específicamente a las claves almacenadas dentro de un solo nodo de cualquiera de esas implementaciones.

Antecedentes

En las bases de datos SQL (por ejemplo, MySQL) y NoSQL (CouchDB, MongoDB, etc.), cuando crea un índice en una columna o campo de datos de un documento JSON, lo que realmente está haciendo que la base de datos haga es crear esencialmente una lista ordenada de todos esos valores junto con un archivo desplazado en el archivo de datos principal donde vive el registro perteneciente a ese valor.

(En aras de la simplicidad, puedo descartar otros detalles esotéricos de implicaciones específicas)

Ejemplo de SQL clásico simple

Considere una tabla SQL estándar que tiene una clave primaria simple de 32 bits en la que creamos un índice, terminaremos con un índice en el disco de las claves enteras ordenadas y asociadas con un desplazamiento de 64 bits en el archivo de datos donde el registro vive, por ejemplo:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

La representación en disco de las claves en el índice se ve así:

[4-bytes][8-bytes] --> 12 bytes for each indexed value

Siguiendo las reglas generales estándar sobre la optimización de E / S de disco con sistemas de archivos y sistemas de bases de datos, digamos que almacena claves en bloques de 4KB en el disco, lo que significa:

4096 bytes / 12 bytes per key = 341 keys per block

Ignorando la estructura general del índice (árbol B +, hash, lista ordenada, etc.), leemos y escribimos bloques de 341 claves a la vez en la memoria y de regreso al disco según sea necesario.

Consulta de ejemplo

Usando la información de la sección anterior, supongamos que entra una consulta para "id = 2", la búsqueda clásica del índice de base de datos es la siguiente:

  1. Lea la raíz del índice (en este caso, 1 bloque)
  2. Búsqueda binaria en el bloque ordenado para encontrar la clave
  3. Obtener el desplazamiento del archivo de datos del valor
  4. Busque el registro en el archivo de datos utilizando el desplazamiento
  5. Devolver los datos a la persona que llama.

Configuración de preguntas ...

Ok, aquí es donde se junta la pregunta ...

El paso n. ° 2 es la parte más importante que permite que estas consultas se ejecuten en tiempo O (log) ... la información debe ordenarse, PERO debe ser capaz de atravesar la lista de manera rápida ... más específicamente, debe poder saltar a compensaciones bien definidas a voluntad para leer el valor de la clave de índice en esa posición.

Después de leer en el bloque, debe poder saltar a la posición 170 inmediatamente, leer el valor clave y ver si lo que está buscando es GT o LT esa posición (y así sucesivamente ...)

La única forma en que podría saltar los datos en el bloque de esa manera es si los tamaños de los valores clave estuvieran bien definidos, como en nuestro ejemplo anterior (4 bytes y luego 8 bytes por clave).

PREGUNTA

Ok, aquí es donde me estoy quedando estancado con un diseño de índice eficiente ... para columnas varchar en bases de datos SQL o más específicamente, campos de forma totalmente libre en bases de datos de documentos como CouchDB o NoSQL, donde cualquier campo que desee indexar puede ser cualquiera longitud ¿cómo implementa los valores clave que están dentro de los bloques de la estructura de índice a partir de la cual construye sus índices?

Por ejemplo, supongamos que usa un contador secuencial para una ID en CouchDB y está indexando tweets ... tendrá valores que van de "1" a "100,000,000,000" después de unos meses.

Supongamos que crea el índice en la base de datos el día 1, cuando solo hay 4 tweets en la base de datos, CouchDB podría verse tentado a usar la siguiente construcción para los valores clave dentro de los bloques de índice:

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

En algún momento, esto se rompe y necesita un número variable de bytes para almacenar su valor clave en los índices.

El punto es aún más evidente si decides indexar un campo de longitud realmente variable como un "tweet_message" o algo así.

Dado que las claves tienen una longitud totalmente variable y la base de datos no tiene forma de adivinar de forma inteligente algún "tamaño máximo de clave" cuando se crea y actualiza el índice, ¿cómo se almacenan realmente estas claves dentro de los bloques que representan segmentos de los índices en estas bases de datos? ?

Obviamente, si sus claves son de tamaño variable y lee en un bloque de claves, no solo no tiene idea de cuántas claves hay realmente en el bloque, sino que no tiene idea de cómo saltar al medio de la lista para hacer un binario busca en ellos.

Aquí es donde me estoy tropezando.

Con campos de tipo estático en bases de datos SQL clásicas (como bool, int, char, etc.), entiendo que el índice puede predefinir la longitud de la clave y mantenerla ... pero en este mundo de almacenes de datos de documentos, estoy Perplejo cómo están modelando eficientemente estos datos en el disco de modo que todavía se puedan escanear en tiempo O (log) y agradecería cualquier aclaración aquí.

¡Avíseme si necesita alguna aclaración!

Actualización (respuesta de Greg)

Por favor, vea mis comentarios adjuntos a la respuesta de Greg. Después de una semana más de investigación, creo que realmente se ha topado con una sugerencia maravillosamente simple y efectiva de que en la práctica es extremadamente fácil de implementar y usar, al tiempo que proporciona grandes ganancias para evitar la deserialización de valores clave que no le interesan.

He examinado 3 implementaciones de DBMS separadas (CouchDB, kivaloo e InnoDB) y todas ellas manejan este problema deserializando todo el bloque en la estructura de datos interna antes de buscar los valores dentro de su entorno de ejecución (erlang / C).

Esto es lo que creo que es tan brillante sobre la sugerencia de Greg; un tamaño de bloque normal de 2048 normalmente tendría 50 o menos compensaciones, lo que da como resultado un bloque muy pequeño de números que necesitaría leerse.

Actualización (Posibles inconvenientes de la sugerencia de Greg)

Para continuar mejor este diálogo conmigo mismo, me di cuenta de las siguientes desventajas de esto ...

  1. Si cada "bloque" está encabezado con datos de desplazamiento, no podría permitir que el tamaño del bloque se ajuste en la configuración más adelante, ya que podría terminar leyendo datos que no comenzaron con un encabezado correctamente o un bloque que contenía múltiples encabezados.

  2. Si está indexando valores de clave enormes (digamos que alguien está tratando de indexar una columna de char (8192) o blob (8192)) es posible que las claves no quepan en un solo bloque y necesiten desbordarse en dos bloques uno al lado del otro . Esto significa que su primer bloque tendría un encabezado de desplazamiento y el segundo bloque comenzaría inmediatamente con los datos clave.

La solución a todo esto es tener un tamaño de bloque de base de datos fijo que no sea ajustable y desarrollar estructuras de datos de bloque de encabezado a su alrededor ... por ejemplo, arregla todos los tamaños de bloque a 4KB (generalmente el más óptimo de todos modos) y escribe un muy pequeño encabezado de bloque que incluye el "tipo de bloque" al principio. Si es un bloque normal, entonces inmediatamente después del encabezado del bloque debe ser el encabezado de compensaciones. Si se trata de un tipo de "desbordamiento", inmediatamente después del encabezado del bloque hay datos de clave sin procesar.

Actualización (potencial increíble al alza)

Después de leer el bloque como una serie de bytes y decodificar las compensaciones; técnicamente, simplemente podría codificar la clave que está buscando en bytes sin procesar y luego hacer comparaciones directas en la secuencia de bytes.

Una vez que se encuentra la clave que está buscando, el puntero se puede decodificar y seguir.

¡Otro increíble efecto secundario de la idea de Greg! El potencial para la optimización del tiempo de CPU aquí es lo suficientemente grande como para que valga la pena establecer un tamaño de bloque fijo solo para obtener todo esto.


Para cualquier otra persona interesada en este tema, el desarrollador principal de Redis se encontraba con este problema exacto mientras intentaba implementar el componente desaparecido "disco de almacenamiento" para Redis. Originalmente optó por un tamaño de clave estática "suficientemente grande" de 32 bytes, pero se dio cuenta de la posibilidad de problemas y optó por almacenar el hash de las claves (sha1 o md5) solo para tener un tamaño consistente. Esto mata la capacidad de hacer consultas a distancia, pero equilibra muy bien el árbol FWIW. Detalles aquí redis.hackyhack.net/2011-01-12.html
Riyad Kalla

Alguna información más que encontré. Parece que SQLite tiene un límite en el tamaño de las claves o realmente trunca el valor de la clave en algún límite superior y coloca el resto en una "página de desbordamiento" en el disco. Esto puede hacer que las consultas para claves enormes sean horribles ya que la E / S aleatoria se duplica. Desplácese hacia abajo a la sección "Páginas del árbol B" aquí sqlite.org/fileformat2.html
Riyad Kalla

Respuestas:


7

Puede almacenar su índice como una lista de compensaciones de tamaño fijo en el bloque que contiene sus datos clave. Por ejemplo:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(bueno, los datos clave se ordenarían en un ejemplo real, pero se entiende la idea).

Tenga en cuenta que esto no refleja necesariamente cómo se construyen realmente los bloques de índice en cualquier base de datos. Este es simplemente un ejemplo de cómo podría organizar un bloque de datos de índice donde los datos clave son de longitud variable.


Greg, todavía no he elegido tu respuesta como la respuesta de facto porque espero recibir más comentarios, así como investigar un poco más sobre otros DBMS (estoy agregando mis comentarios a la Q original). Hasta ahora, el enfoque más común parece ser un límite superior y luego el resto de la clave en una tabla de desbordamiento que solo se verifica cuando se necesita la clave completa. No es tan elegante. Su solución tiene algo de elegancia que me gusta, pero en el caso de que las teclas soplen el tamaño de su página, su camino aún necesitaría una tabla de desbordamiento o simplemente no lo permitiría.
Riyad Kalla

Me quedé sin espacio ... En resumen, si el diseñador de db podría vivir con algunos límites duros en el tamaño de la clave, creo que su enfoque es el más eficiente y flexible. Bonito combo de espacio y eficiencia de la CPU. Las tablas de desbordamiento son más flexibles, pero pueden ser útiles para agregar E / S aleatorias a las búsquedas de claves que se desbordan constantemente. Gracias por el aporte en esto!
Riyad Kalla

Greg, he estado pensando en esto cada vez más, buscando soluciones alternativas y creo que lo lograste con la idea del encabezado offset. Si mantuviera sus bloques pequeños, podría salirse con compensaciones de 8 bits (1 byte), con bloques más grandes de 16 bits sería más seguro incluso hasta bloques de 128KB o 256KB que deberían ser razonables (asumiría claves de 4 u 8byte). La gran victoria es cuán barato y rápido puede leer en los datos de compensación y cuánta deserialización ahorra como resultado. Excelente sugerencia, gracias de nuevo.
Riyad Kalla

Este es también el enfoque utilizado en UpscaleDB: upscaledb.com/about.html#varlength
Mathieu Rodic
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.