¿Qué índice usar con muchos valores duplicados?


14

Hagamos algunas suposiciones:

Tengo una mesa que se ve así:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Datos sobre mi conjunto:

  • El tamaño de toda la tabla es de ~ 10 10 filas.

  • Tengo ~ 100k filas con valor aen la columna a, similar para otros valores (por ejemplo c).

  • Eso significa ~ 100k valores distintos en la columna 'a'.

  • La mayoría de mis consultas leerán todos o la mayoría de los valores para un valor dado en un, por ejemplo select sum(b) from t where a = 'c'.

  • La tabla está escrita de tal manera que los valores consecutivos están físicamente cerca (o bien está escrita en orden, o suponemos que CLUSTERse usó en esa tabla y columna a).

  • La tabla rara vez se actualiza, solo nos preocupa la velocidad de lectura.

  • La tabla es relativamente estrecha (digamos ~ 25 bytes por tupla, + 23 bytes de sobrecarga).

Ahora la pregunta es, ¿qué tipo de índice debo usar? Mi entendimiento es:

  • BTree Mi problema aquí es que el índice BTree será enorme ya que, hasta donde yo sé, almacenará valores duplicados (tiene que hacerlo, ya que no puede suponer que la tabla está físicamente ordenada). Si BTree es enorme, termino teniendo que leer tanto el índice como las partes de la tabla a las que apunta el índice. (Podemos usar fillfactor = 100para disminuir un poco el tamaño del índice).

  • BRIN Entiendo que puedo tener un pequeño índice aquí a expensas de leer páginas inútiles. Usar un pequeño pages_per_rangesignifica que el índice es más grande (lo cual es un problema con BRIN ya que necesito leer todo el índice), y tener un gran pages_per_rangemedio significa que leeré muchas páginas inútiles. ¿Existe una fórmula mágica para encontrar un buen valor pages_per_rangeque tenga en cuenta esas compensaciones?

  • GIN / GiST No estoy seguro de que sean relevantes aquí, ya que se usan principalmente para la búsqueda de texto completo, pero también escuché que son buenos para manejar claves duplicadas. ¿Ayudaría GINo un GiSTíndice aquí?

Otra pregunta es, ¿Postgres utilizará el hecho de que se edita una tabla CLUSTER(suponiendo que no haya actualizaciones) en el planificador de consultas (por ejemplo, mediante la búsqueda binaria de las páginas de inicio / fin relevantes)? Algo relacionado, ¿puedo almacenar todas mis columnas en un BTree y soltar la tabla por completo (o lograr algo equivalente, creo que esos son índices agrupados en el servidor SQL)? ¿Hay algún índice híbrido BTree / BRIN que ayudaría aquí?

Prefiero evitar el uso de matrices para almacenar mis valores, ya que mi consulta terminará siendo menos legible de esa manera (entiendo que esto reduciría el costo de los 23 bytes por sobrecarga de tupla al reducir el número de tuplas).


"utilizado principalmente para la búsqueda de texto completo" GiST es utilizado ampliamente por PostGIS.
jpmc26

Respuestas:


15

BTree

Mi problema aquí es que el índice BTree será enorme ya que, en caso de que ocurra, almacenará valores duplicados (también lo tiene, ya que no puede asumir que la tabla está físicamente ordenada). Si BTree es enorme, termino teniendo que leer tanto el índice como las partes de la tabla que el índice también señala ...

No necesariamente: tener un índice btree que 'cubra' será el tiempo de lectura más rápido, y si eso es todo lo que desea (es decir, si puede permitirse el almacenamiento adicional), entonces es su mejor opción.

BRIN

Tengo entendido que puedo tener un pequeño índice aquí a expensas de leer páginas inútiles. Usar un pequeño pages_per_rangesignifica que el índice es más grande (lo cual es un problema con BRIN ya que necesito leer todo el índice), y tener un gran pages_per_rangemedio significa que leeré muchas páginas inútiles.

Si no puede permitirse la sobrecarga de almacenamiento de un índice btree de cobertura, BRIN es ideal para usted, porque ya tiene una agrupación (esto es crucial para que BRIN sea útil). Los índices BRIN son pequeños , por lo que es probable que todas las páginas estén en la memoria si elige un valor adecuado de pages_per_range.

¿Existe una fórmula mágica para encontrar un buen valor de pages_per_range que tenga en cuenta esas compensaciones?

No hay fórmula mágica, pero comienza con pages_per_range algo menos que el tamaño promedio (en páginas) ocupado por el avalor promedio . Probablemente esté intentando minimizar: (número de páginas BRIN escaneadas) + (número de páginas de montón escaneadas) para una consulta típica. Busque Heap Blocks: lossy=nen el plan de ejecución pages_per_range=1y compare con otros valores para pages_per_range, es decir, vea cuántos bloques de montón innecesarios se están escaneando.

GIN / GiST

No estoy seguro de que sean relevantes aquí, ya que se usan principalmente para la búsqueda de texto completo, pero también escuché que son buenos para manejar claves duplicadas. ¿Ayudaría a GIN/ GiSTindex aquí?

Puede valer la pena considerar GIN, pero probablemente no GiST; sin embargo, si el agrupamiento natural es realmente bueno, entonces BRIN probablemente será una mejor apuesta.

Aquí hay una comparación de muestra entre los diferentes tipos de índice para datos ficticios un poco como el suyo:

tabla e índices:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

tamaños de relación:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
nombre | tamaño | páginas | filas / página
: ----------------- | : ------ | ----: | --------:
foo | 149 MB | 19118 | 135
foo_btree_covering | 56 MB | 7132 | 364
foo_btree | 56 MB | 7132 | 364
foo_gin | 2928 kB | 366 7103
foo_brin_2 | 264 kB | 33 78787
foo_brin_4 | 136 kB | 17 | 152941

cubriendo btree:

explain analyze select sum(b) from foo where a='a';
El | PLAN DE CONSULTA |
El | : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
El | Agregado (costo = 3282.57..3282.58 filas = 1 ancho = 8) (tiempo real = 45.942..45.942 filas = 1 bucles = 1) |
El | -> Escaneo de índice solamente usando foo_btree_covering en foo (costo = 0.43..3017.80 filas = 105907 ancho = 4) (tiempo real = 0.038..27.286 filas = 100000 bucles = 1) |
El | Índice Cond: (a = 'a' :: texto) |
El | Capturas del montón: 0 |
El | Tiempo de planificación: 0.099 ms |
El | Tiempo de ejecución: 45.968 ms |

btree simple:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
El | PLAN DE CONSULTA |
El | : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
El | Agregado (costo = 4064.57..4064.58 filas = 1 ancho = 8) (tiempo real = 54.242..54.242 filas = 1 bucles = 1) |
El | -> Escaneo de índice usando foo_btree en foo (costo = 0.43..3799.80 filas = 105907 ancho = 4) (tiempo real = 0.037..33.084 filas = 100000 bucles = 1) |
El | Índice Cond: (a = 'a' :: texto) |
El | Tiempo de planificación: 0.135 ms |
El | Tiempo de ejecución: 54.280 ms |

BRIN páginas_por_rango = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
El | PLAN DE CONSULTA |
El | : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
El | Agregado (costo = 21595.38..21595.39 filas = 1 ancho = 8) (tiempo real = 52.455..52.455 filas = 1 bucles = 1) |
El | -> Bitmap Heap Scan en foo (costo = 888.78..21330.61 filas = 105907 ancho = 4) (tiempo real = 2.738..31.967 filas = 100000 bucles = 1) |
El | Vuelva a verificar Cond: (a = 'a' :: texto) |
El | Filas eliminadas por la comprobación de índice: 96 |
El | Bloques de montón: con pérdida = 736 |
El | -> Escaneo de índice de mapa de bits en foo_brin_4 (costo = 0.00..862.30 filas = 105907 ancho = 0) (tiempo real = 2.720..2.720 filas = 7360 bucles = 1) |
El | Índice Cond: (a = 'a' :: texto) |
El | Tiempo de planificación: 0.101 ms |
El | Tiempo de ejecución: 52.501 ms |

BRIN páginas_por_rango = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
El | PLAN DE CONSULTA |
El | : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
El | Agregado (costo = 21659.38..21659.39 filas = 1 ancho = 8) (tiempo real = 53.971..53.971 filas = 1 bucles = 1) |
El | -> Bitmap Heap Scan en foo (costo = 952.78..21394.61 filas = 105907 ancho = 4) (tiempo real = 5.286..33.492 filas = 100000 bucles = 1) |
El | Vuelva a verificar Cond: (a = 'a' :: texto) |
El | Filas eliminadas por la comprobación de índice: 96 |
El | Bloques de montón: con pérdida = 736 |
El | -> Escaneo de índice de mapa de bits en foo_brin_2 (costo = 0.00..926.30 filas = 105907 ancho = 0) (tiempo real = 5.275..5.275 filas = 7360 bucles = 1) |
El | Índice Cond: (a = 'a' :: texto) |
El | Tiempo de planificación: 0.095 ms |
El | Tiempo de ejecución: 54.016 ms |

GINEBRA:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
El | PLAN DE CONSULTA |
El | : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
El | Agregado (costo = 21687.38..21687.39 filas = 1 ancho = 8) (tiempo real = 55.331..55.331 filas = 1 bucles = 1) |
El | -> Bitmap Heap Scan en foo (costo = 980.78..21422.61 filas = 105907 ancho = 4) (tiempo real = 12.377..33.956 filas = 100000 bucles = 1) |
El | Vuelva a verificar Cond: (a = 'a' :: texto) |
El | Bloques de montón: exactos = 736 |
El | -> Escaneo de índice de mapa de bits en foo_gin (costo = 0.00..954.30 filas = 105907 ancho = 0) (tiempo real = 12.271..12.271 filas = 100000 bucles = 1) |
El | Índice Cond: (a = 'a' :: texto) |
El | Tiempo de planificación: 0.118 ms |
El | Tiempo de ejecución: 55.366 ms |

dbfiddle aquí


Entonces, ¿un índice de cobertura omitiría leer la tabla por completo a expensas del espacio en disco? Parece una buena compensación. Creo que queremos decir lo mismo para el índice BRIN al 'leer todo el índice' (corrígeme si me equivoco), me refería a escanear todo el índice BRIN, que creo que es lo que está sucediendo en dbfiddle.uk/… , ¿no?
foo

@foo sobre el "(también lo tiene, ya que no puede asumir que la tabla está físicamente ordenada)". El orden físico (agrupamiento o no) de la tabla es irrelevante. El índice tiene los valores en el orden correcto. Pero los índices del árbol B de Postgres tienen que almacenar todos los valores (y sí, varias veces). Así es como están diseñados. Almacenar cada valor distinto solo una vez sería una buena característica / mejora. Podría sugerirlo a los desarrolladores de Postgres (e incluso ayudar a implementarlo). Jack debería comentar, creo que la implementación de b-trees de Oracle lo hace.
ypercubeᵀᴹ

1
@foo: tiene toda la razón, un escaneo de un índice BRIN siempre escanea todo el índice ( pgcon.org/2016/schedule/attachments/… , 2da última diapositiva), aunque eso no se muestra en el plan de explicación en el violín , ¿Lo es?
Jack dice que intente topanswers.xyz

2
@ ypercubeᵀᴹ puede usar COMPRESS en Oracle que almacena cada prefijo distinto una vez por bloque.
Jack dice que intente topanswers.xyz

@JackDouglas Leí Bitmap Index Scancomo significa 'leer todo el índice brin' pero tal vez esa sea la lectura incorrecta. Oracle COMPRESSparece algo útil aquí, ya que reduciría el tamaño del árbol B, ¡pero estoy atrapado con pg!
foo

6

Además de btree y brin, que parecen las opciones más sensatas, algunas otras opciones exóticas que vale la pena investigar, pueden ser útiles o no en su caso:

  • INCLUDEíndices . Con suerte, estarán en la próxima versión principal (10) de Postgres, en algún lugar alrededor de septiembre de 2017. Un índice en (a) INCLUDE (b)tiene la misma estructura que un índice en (a)pero incluye en las páginas de la hoja, todos los valores de b(pero sin ordenar). Lo que significa que no puede usarlo, por ejemplo, para SELECT * FROM t WHERE a = 'a' AND b = 2 ;. Es posible que se use el índice, pero si bien un (a,b)índice encontrará las filas coincidentes con una sola búsqueda, el índice de inclusión tendrá que pasar por los valores (posiblemente 100K como en su caso) que coinciden a = 'a'y verificarb valores.
    Por otro lado, el índice es un poco menos ancho que el (a,b)índice y no necesita el orden bpara calcular su consulta SUM(b). También podrías tener por ejemplo(a) INCLUDE (b,c,d) que se puede usar para consultas similares a las suyas que se agregan en las 3 columnas.

  • Índices filtrados (parciales) . Una sugerencia que puede sonar un poco loca * al principio:

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;

    Un índice para cada avalor. En su caso, alrededor de 100K índices. Si bien esto suena mucho, considere que cada índice será muy pequeño, tanto en tamaño (número de filas) como en ancho (ya que solo almacenará bvalores). Sin embargo, en todos los demás aspectos, (los índices de 100K juntos) actuarán como un índice de árbol b (a,b)mientras se utiliza el espacio de un (b)índice.
    La desventaja es que deberá crearlos y mantenerlos usted mismo, cada vez que ase agregue un nuevo valor a la tabla. Dado que su tabla es bastante estable, sin muchas (o ninguna) inserción / actualización, eso no parece ser un problema.

  • Tablas de resumen. Dado que la tabla es bastante estable, siempre puede crear y completar una tabla de resumen con los agregados más comunes que necesitará (sum(b), sum(c), sum(d), avg(b), count(distinct b) , etc.). Será pequeño (solo 100K filas) y solo tendrá que rellenarse una vez y actualizarse solo cuando las filas se inserten / actualicen / eliminen en la tabla principal.

*: idea copiada de esta compañía que ejecuta 10 millones de índices en su sistema de producción: The Heap: Ejecución de 10 millones de índices Postgresql en producción (y contando) .


1 es interesante, pero como señalas, la página 10 aún no está disponible. 2 lo hace parecer una locura (o al menos contra la 'sabiduría común'), voy a tener una lectura, ya que como usted señala que podría trabajar con mi casi no escribe flujo de trabajo. 3. ¿No trabajo para mí, que se utiliza SUMcomo un ejemplo, pero en la práctica mis preguntas no pueden ser calculados previamente (Son más como select ... from t where a = '?' and ??wjere ??habría alguna otra condición definida por el usuario.
foo

1
Bueno, no podemos ayudar si no sabemos qué ??es;)
ypercubeᵀᴹ

Usted menciona índices filtrados. ¿Qué pasa con la partición de la mesa?
jpmc26

@ jpmc26 divertido, estaba pensando en agregar en la respuesta que la sugerencia de índices filtrados es, en cierto sentido, una forma de partición. Particionar también podría ser útil aquí, pero no estoy seguro. Resultaría en muchos pequeños índices / tablas.
ypercubeᵀᴹ

2
Espero que los índices btree de cobertura parcial sean el rey del rendimiento aquí, ya que los datos casi nunca se actualizan. Incluso si eso significa 100k índices. El tamaño total del índice es el más pequeño (a excepción de un índice BRIN, pero allí Postgres tiene que leer y filtrar páginas de montón adicionalmente). La generación de índices se puede automatizar con SQL dinámico. Declaración de ejemplo DOen esta respuesta relacionada .
Erwin Brandstetter
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.