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:
- Lea la raíz del índice (en este caso, 1 bloque)
- Búsqueda binaria en el bloque ordenado para encontrar la clave
- Obtener el desplazamiento del archivo de datos del valor
- Busque el registro en el archivo de datos utilizando el desplazamiento
- 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 ...
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.
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.