¿Cuál es el tipo de datos óptimo para un campo MD5?

35

Estamos diseñando un sistema que se sabe que es pesado en lectura (del orden de decenas de miles de lecturas por minuto).

  • Hay una tabla namesque sirve como una especie de registro central. Cada fila tiene un textcampo representationy un único keyque es un hash MD5 de eso representation. 1 Esta tabla tiene actualmente decenas de millones de registros y se espera que crezca a miles de millones durante la vida útil de la aplicación.
  • Hay docenas de otras tablas (de esquemas muy variados y recuentos de registros) que hacen referencia a la namestabla. Se garantiza que cualquier registro dado en una de estas tablas tiene una name_key, que es funcionalmente una clave foránea para la namestabla.

1: Incidentalmente, como es de esperar, los registros en esta tabla son inmutables una vez escritos.

Para cualquier tabla dada que no sea la namestabla, la consulta más común seguirá este patrón:

SELECT list, of, fields 
FROM table 
WHERE name_key IN (md5a, md5b, md5c...);

Me gustaría optimizar el rendimiento de lectura. Sospecho que mi primera parada debería ser minimizar el tamaño de los índices (aunque no me importaría que me demuestren lo contrario).

La pregunta:
¿Cuáles son / son los tipos de datos óptimos para las columnas keyy name_key?
¿Hay alguna razón para usar hex(32)más bit(128)? BTREEo GIN?

bobocopy
fuente

Respuestas:

41

El tipo de datos uuidse adapta perfectamente a la tarea. Solo ocupa 16 bytes en lugar de 37 bytes en RAM para la representación varcharo text. (O 33 bytes en el disco, pero el número impar requeriría relleno en muchos casos para que sea de 40 bytes de manera efectiva). Y el uuidtipo tiene algunas ventajas más.

Ejemplo:

SELECT md5('Store hash for long string, maybe for index?')::uuid AS md5_hash

Detalles y más explicaciones:

Puede considerar otras funciones de hash (más baratas) si no necesita el componente criptográfico de md5, pero usaría md5 para su caso de uso (principalmente de solo lectura).

Una palabra de advertencia : para su caso ( immutable once written) un PK funcionalmente dependiente (pseudo-natural) está bien. Pero lo mismo sería un dolor donde las actualizaciones textson posibles. Piense en corregir un error tipográfico: el PK y todos los índices dependientes, las columnas FK dozens of other tablesy otras referencias también tendrían que cambiar. Tabla e índice de hinchazón, problemas de bloqueo, actualizaciones lentas, referencias perdidas, ...

Si textpuede cambiar en la operación normal, un PK sustituto sería una mejor opción. Sugiero una bigserialcolumna (rango -9223372036854775808 to +9223372036854775807- eso es nueve quintillones doscientos veintitres cuatrillones trescientos setenta y dos billones treinta y seis billones ) valores distintos para billions of rows. En cualquier caso, puede ser una buena idea : ¡ 8 en lugar de 16 bytes para docenas de columnas e índices FK!). O un UUID aleatorio para cardinalidades mucho más grandes o sistemas distribuidos. Siempre puede almacenar dicho md5 (as uuid) adicionalmente para buscar filas en la tabla principal del texto original rápidamente. Relacionado:

En cuanto a su consulta :


Para abordar el comentario de @ Daniel : Si prefiere una representación sin guiones, elimine los guiones para mostrar:

SELECT replace('90b7525e-84f6-4850-c2ef-b407fae3f271', '-', '')

Pero no me molestaría. La representación predeterminada está bien. Y el problema no es realmente la representación aquí.

Si otras partes deben tener un enfoque diferente y tirar cadenas sin guiones en la mezcla, tampoco es un problema. Postgres acepta varias representaciones de texto razonables como entrada para a uuid. La documentación :

PostgreSQL también acepta las siguientes formas alternativas de entrada: uso de dígitos en mayúscula, el formato estándar rodeado de llaves, omitiendo algunos o todos los guiones, agregando un guión después de cualquier grupo de cuatro dígitos. Ejemplos son:

A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11
{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}
a0eebc999c0b4ef8bb6d6bb9bd380a11
a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11
{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}

Además, la md5()función regresa text, la usaría decode()para convertir byteay la representación predeterminada de eso es:

SELECT decode(md5('Store hash for long string, maybe for index?'), 'hex')

\220\267R^\204\366HP\302\357\264\007\372\343\362q

Debería encode()volver a obtener la representación del texto original:

SELECT encode(my_md5_as_bytea, 'hex');

Para colmo, los valores almacenados como byteaocuparían 20 bytes en RAM (y 17 bytes en el disco, 24 con relleno ) debido a la sobrecarga internavarlena , que es particularmente desfavorable para el tamaño y el rendimiento de los índices simples.

Todo funciona a favor de un uuidaquí.

Erwin Brandstetter
fuente
1
¿Es esto legítimo para "uuid"? Disculpe si soy demasiado pedante, pero creo que lo que veo es que el tipo de datos "uuid" está orientado a almacenar números de 16 octetos de longitud en formato binario. Pero el término "uuid" sugiere un algoritmo de generación / hashing particular, así como la representación textual convencional en 5 bloques de caracteres hexadecimales separados por guiones. Si este nombre de tipo sugiere fuertemente la generación de UUID / GUID, ¿no es un poco engañoso, al menos para los programadores, usar este tipo para almacenar un hash?
Andrew Wolfe el
2
@ AndrewWolfe: Totalmente legítimo, OMI. No te dejes llevar por el nombre . Es una entidad de 16 bytes con un conjunto conveniente de conversiones de tipo proporcionadas y lógica de entrada / salida. El caso en cuestión incluso requiere un "identificador único". También puede almacenar todo tipo de datos de caracteres en textcolumnas, incluso si no es un "texto" en absoluto.
Erwin Brandstetter
¿Qué pasa si el hash MD5 se convierte a la base 64, cómo lo almacenará entonces
PirateApp
2
@PirateApp, decodificar primero: SELECT encode(decode('tZmffOd5Tbh8yXaVlZfRJQ==', 'base64'), 'hex')::uuid;.
nyov
1
@nyov: uuides un tipo de 16 bytes que no puede almacenar los resultados de ningún algoritmo SHA que produzca entre 160 y 512 bits. No hay un tipo similar que se ajuste a la distribución estándar de Postgres. Podría crear uno ... En su defecto, por defecto bytea, como lo hace pg_crypto .
Erwin Brandstetter
2

Almacenaría el MD5 en una texto varcharcolumna. No hay diferencia de rendimiento entre los distintos tipos de datos de caracteres. Es posible que desee restringir la longitud de los valores md5 utilizando varchar(xxx)para asegurarse de que el valor md5 nunca exceda una cierta longitud.

Las listas IN grandes generalmente no son realmente rápidas, es mejor hacer algo como esto:

with md5vals (md5) as (
  values ('one'), ('two'), ('three')
)
select t.*
from the_table t
  join md5vals m on t.name_key  = m.md5;

Otra opción que a veces se dice que es más rápida es usar una matriz:

select t.*
from the_table t
where name_key = ANY (array['one', 'two', 'three']);

Como solo está comparando la igualdad, un índice BTree regular debería estar bien. Ambas consultas deberían poder utilizar dicho índice (especialmente si seleccionan solo una pequeña fracción de las filas).

un caballo sin nombre
fuente
¿Alguna razón en particular para no usar bit (128) o hexadecimal (32)? Se garantiza que los valores encajan perfectamente en dicho campo, y me gustaría protegerme de la asignación de valores incorrectos.
bobocopy
3
@bobocopy: no hay ningún tipo de datos "hexadecimales" en Postgres. Nunca he usado el bittipo, así que no puedo comentar sobre eso. Dado el número esperado de filas, la sugerencia de Erwin parece ser mejor debido al ahorro de espacio que obtienes al almacenar esto como UUID
a_horse_with_no_name
-1

Otra opción es usar 4 columnas INTEGER o 2 BIGINT.

happy_marmoset
fuente
2
En términos de tamaño de almacenamiento, cualquiera de las opciones encajaría, por supuesto, pero ¿con qué conveniencia sería trabajar? Tal vez podría ampliar su respuesta para mostrar un ejemplo o explicarlo de otra manera.
Andriy M