Consulta lenta en tabla grande con GROUP BY y ORDER BY


14

Tengo una tabla con 7.2 millones de tuplas que se ve así:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Ahora quiero seleccionar algunos valores, pero la consulta es increíblemente lenta:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

La hashcolumna es el hash de md5 stringy tiene un índice. Entonces, creo que mi problema es que toda la tabla está ordenada por id y no por hash, por lo que lleva un tiempo ordenarla primero y luego agruparla.

La tabla nostringcontiene solo una lista de hashes que no quiero tener. Pero necesito que ambas tablas tengan todos los valores. Por lo tanto, no es una opción eliminarlos.

información adicional: ninguna de las columnas puede ser nula (fijada en la definición de la tabla) y estoy usando postgresql 9.2.


1
Siempre proporcione la versión de PostgreSQL que usa. ¿Cuál es el porcentaje de NULLvalores en la columna method? ¿Hay duplicados en string?
Erwin Brandstetter

Respuestas:


18

La respuesta deLEFT JOIN in @ dezso debería ser buena. Sin embargo, un índice difícilmente será útil (per se), porque la consulta tiene que leer toda la tabla de todos modos; la excepción son los escaneos de solo índice en Postgres 9.2+ y condiciones favorables, ver más abajo.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Ejecutar EXPLAIN ANALYZEen la consulta. Varias veces para excluir los efectos de cobro y el ruido. Compara los mejores resultados.

Cree un índice de varias columnas que coincida con su consulta:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

¿Espere? ¿Después de que dije que un índice no ayudaría? Bueno, lo necesitamos para CLUSTERla mesa:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Vuelva a ejecutar EXPLAIN ANALYZE. ¿Más rápido? Debería ser.

CLUSTERes una operación única para reescribir toda la tabla en el orden del índice utilizado. También es efectivamente a VACUUM FULL. Si quieres estar seguro, ejecutarías una prueba previa VACUUM FULLsolo para ver qué se puede atribuir a eso.

Si su tabla ve muchas operaciones de escritura, el efecto se degradará con el tiempo. Programe CLUSTERfuera de horario para restaurar el efecto. El ajuste fino depende de su caso de uso exacto. El manual sobre CLUSTER.

CLUSTEREs una herramienta bastante tosca, necesita un candado exclusivo sobre la mesa. Si no puede permitirse eso, considere pg_repackqué puede hacer lo mismo sin un bloqueo exclusivo. Más en esta respuesta posterior:


Si el porcentaje de NULLvalores en la columna methodes alto (más de ~ 20 por ciento, dependiendo del tamaño real de las filas), un índice parcial debería ayudar:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(Su actualización posterior muestra que sus columnas son NOT NULL, por lo que no es aplicable).

Si está ejecutando PostgreSQL 9.2 o posterior (como comentó @deszo ), los índices presentados pueden ser útiles sin CLUSTERque el planificador pueda utilizar escaneos de solo índice . Solo aplicable en condiciones favorables: no hay operaciones de escritura que afecten el mapa de visibilidad ya que la última VACUUMy todas las columnas de la consulta deben estar cubiertas por el índice. Básicamente, las tablas de solo lectura pueden usar esto en cualquier momento, mientras que las tablas muy escritas son limitadas. Más detalles en el Wiki de Postgres.

El índice parcial mencionado anteriormente podría ser aún más útil en ese caso.

Si , por otro lado, no hay NULL valores en la columna method, debe
1.) definirlo NOT NULLy
2.) usar en count(*)lugar de count(method), eso es un poco más rápido y hace lo mismo en ausencia de NULLvalores.

Si tiene que llamar a esta consulta con frecuencia y la tabla es de solo lectura, cree un MATERIALIZED VIEW.


Punto fino exótico: su tabla lleva un nombre nostring, pero parece contener hashes. Al excluir los hashes en lugar de las cadenas, existe la posibilidad de que excluya más cadenas de las previstas. Extremadamente improbable, pero posible.


con el clúster es mucho más rápido. todavía necesito alrededor de 5 minutos para la consulta, pero eso es mucho mejor que ejecutarlo toda la noche: D
reox

@reox: ya que ejecuta v9.2: ¿Probó solo con el índice, antes de la agrupación? Sería interesante si vieras la diferencia. (No puede reproducir la diferencia después de la agrupación). Además (y esto sería barato), ¿EXPLAIN muestra un escaneo de índice o un escaneo de tabla completo ahora?
Erwin Brandstetter

5

Bienvenido a DBA.SE!

Puede intentar reformular su consulta de esta manera:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

u otra posibilidad:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN es un sumidero típico para el rendimiento ya que es difícil usar un índice con él.

Esto puede mejorarse aún más con los índices. Un índice sobre nostring.hashparece útil. Pero primero: ¿qué obtienes ahora? (Sería mejor ver la producción EXPLAIN ANALYZEya que los costos en sí mismos no indican el tiempo que tomaron las operaciones).


ya se creó un índice en nostring.hash, pero creo que los postgres no lo usan debido a demasiadas tuplas ... cuando exploto deshabilitar la exploración de secuencia, usa el índice. si uso la combinación izquierda obtengo un costo de 32 millones, así que es mucho mejor ... pero estoy tratando de optimizarlo más ...
reox

3
El costo es solo para que el planificador pueda diseñar un plan suficientemente bueno. Los tiempos reales generalmente se correlacionan con él, pero no necesariamente. Entonces, si quieres estar seguro, úsalo EXPLAIN ANALYZE.
dezso

1

Como hash es un md5, probablemente intentes convertirlo en un número: puedes almacenarlo como un número o simplemente crear un índice funcional que calcule ese número en una función inmutable.

Otras personas ya crearon una función pl / pgsql que convierte (parte de) un valor md5 de texto a cadena. Consulte /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql para ver un ejemplo.

Creo que realmente está pasando mucho tiempo en la comparación de cadenas mientras escanea el índice. Si logra almacenar ese valor como un número, entonces debería ser realmente más rápido.


1
Dudo que esta conversión acelere las cosas. Todas las consultas aquí usan la igualdad para la comparación. Calcular representaciones numéricas y luego verificar la igualdad no promete grandes ganancias para mí.
dezso

2
Creo que almacenaría md5 como bytea en lugar de un número para la eficiencia del espacio: sqlfiddle.com/#!12/d41d8/252
Jack dice que intente topanswers.xyz

Además, bienvenido a dba.se!
Jack dice que intente topanswers.xyz

@JackDouglas: ¡Comentario interesante! 16 bytes por md5 en lugar de 32 es bastante para tablas grandes.
Erwin Brandstetter

0

Me encontré mucho con este problema y descubrí un simple truco de 2 partes.

  1. Cree un índice de subcadena en el valor hash: (7 suele ser una buena longitud)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Haga que sus búsquedas / combinaciones incluyan una coincidencia de subcadena, por lo que se sugiere que el planificador de consultas use el índice:

    antiguo: WHERE hash = :kwarg

    nuevo: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

También debe tener un índice en bruto hashtambién.

el resultado (generalmente) es que el planificador consultará primero el índice de subcadena y eliminará la mayoría de las filas. luego hace coincidir el hash completo de 32 caracteres con el índice (o tabla) correspondiente. Este enfoque ha reducido las consultas de 800ms a 4 para mí.

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.