PostgreSQL: trabajar con una matriz de miles de elementos

8

Estoy buscando seleccionar filas en función de si una columna está contenida en una gran lista de valores que paso como una matriz entera.

Aquí está la consulta que uso actualmente:

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        item_id = ANY ($1) -- Integer array
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

La tabla está estructurada como tal:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 ...


 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    ...

Se me ocurrió este índice después de probar diferentes y ejecutar EXPLAINla consulta. Este fue el más eficiente tanto para consultar como para ordenar. Aquí está el análisis de explicación de la consulta:

Subquery Scan on x  (cost=0.56..368945.41 rows=302230 width=73) (actual time=0.021..276.476 rows=168395 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 90275
  ->  WindowAgg  (cost=0.56..357611.80 rows=906689 width=73) (actual time=0.019..248.267 rows=258670 loops=1)
        ->  Index Scan using idx_dtr_query on mytable  (cost=0.56..339478.02 rows=906689 width=73) (actual time=0.013..130.362 rows=258670 loops=1)
              Index Cond: ((item_id = ANY ('{/* 15,000 integers */}'::integer[])) AND (end_date > '2018-03-30 12:08:00'::timestamp without time zone))
Planning time: 30.349 ms
Execution time: 284.619 ms

El problema es que la matriz int puede contener hasta 15,000 elementos más o menos y la consulta se vuelve bastante lenta en este caso (aproximadamente 800 ms en mi computadora portátil, un Dell XPS reciente).

Pensé que pasar la matriz int como parámetro podría ser lento, y teniendo en cuenta que la lista de identificadores se puede almacenar de antemano en la base de datos, intenté hacer esto. Los almacené en una matriz en otra tabla y los usé item_id = ANY (SELECT UNNEST(item_ids) FROM ...), que era más lento que mi enfoque actual. También intenté almacenarlos fila por fila y usarlos item_id IN (SELECT item_id FROM ...), lo que fue aún más lento, incluso con solo las filas relevantes para mi caso de prueba en la tabla.

¿Hay una mejor manera de hacer esto?

Actualización: siguiendo los comentarios de Evan , probé otro enfoque: cada elemento es parte de varios grupos, así que en lugar de pasar los identificadores de ítems del grupo, intenté agregar los identificadores de grupo en mytable:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 group_ids     | integer[]                   |           | not null | 
 ...

 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    "idx_dtr_group_ids" gin (group_ids)
    ...

Nueva consulta ($ 1 es la identificación del grupo objetivo):

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        $1 = ANY (group_ids)
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Explique analizar:

Subquery Scan on x  (cost=123356.60..137112.58 rows=131009 width=74) (actual time=811.337..1087.880 rows=172023 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 219726
  ->  WindowAgg  (cost=123356.60..132199.73 rows=393028 width=74) (actual time=811.330..1040.121 rows=391749 loops=1)
        ->  Sort  (cost=123356.60..124339.17 rows=393028 width=74) (actual time=811.311..868.127 rows=391749 loops=1)
              Sort Key: item_id, start_date, allowed
              Sort Method: external sort  Disk: 29176kB
              ->  Seq Scan on mytable (cost=0.00..69370.90 rows=393028 width=74) (actual time=0.105..464.126 rows=391749 loops=1)
                    Filter: ((end_date > '2018-04-06 12:00:00'::timestamp without time zone) AND (2928 = ANY (group_ids)))
                    Rows Removed by Filter: 1482567
Planning time: 0.756 ms
Execution time: 1098.348 ms

Puede haber margen de mejora con los índices, pero me cuesta entender cómo los utiliza Postgres, por lo que no estoy seguro de qué cambiar.

Jukurrpa
fuente
¿Cuántas filas en "mytable"? ¿Cuántos valores diferentes de "item_id" hay?
Nick
Además, ¿no debería tener una restricción de unicidad (probablemente un índice único aún no definido) en item_id en mytable? ... Editado: oh, veo "PARTICIÓN POR item_id", por lo que esta pregunta se transforma en "¿Cuál es la clave real y natural para sus datos? ¿Qué debería formar un índice único allí?"
Nick
Alrededor de 12 millones de filas mytable, con aproximadamente 500k diferentes item_id. No hay una clave única natural real para esta tabla, son los datos que se generan automáticamente para repetir eventos. Supongo que el item_id+ start_date+ name(campo no mostrado aquí) podría constituir algún tipo de clave.
Jukurrpa
¿Puedes publicar el plan de ejecución que estás recibiendo?
Colin 't Hart
Claro, agregó el análisis de explicar a la pregunta.
Jukurrpa

Respuestas:

1

¿Hay una mejor manera de hacer esto?

Sí, usa una tabla temporal. No hay nada de malo en crear una tabla temporal indexada cuando su consulta es tan loca.

BEGIN;
  CREATE TEMP TABLE myitems ( item_id int PRIMARY KEY );
  INSERT INTO myitems(item_id) VALUES (1), (2); -- and on and on
  CREATE INDEX ON myitems(item_id);
COMMIT;

ANALYZE myitems;

SELECT item_id, other_stuff, ...
FROM (
  SELECT
      -- Partitioned row number as we only want N rows per id
      ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
      item_id, other_stuff, ...
  FROM mytable
  INNER JOIN myitems USING (item_id)
  WHERE end_date > $2
  ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12;

Pero incluso mejor que eso ...

"500k elemento_id diferente" ... "la matriz int puede contener hasta 15,000 elementos"

Estás seleccionando el 3% de tu base de datos individualmente. Tengo que preguntarme si no es mejor crear grupos / etiquetas, etc. en el esquema en sí. Nunca tuve que enviar personalmente 15,000 ID diferentes en una consulta.

Evan Carroll
fuente
Solo intenté usar la tabla temporal y es más lenta, al menos en el caso de los 15,000 ids. En cuanto a la creación de grupos en el esquema en sí, ¿te refieres a una tabla con los identificadores que paso como argumento? Intenté algo como esto, pero el rendimiento fue similar o peor que mi enfoque actual. Actualizaré la pregunta con más detalles
Jukurrpa
No me refiero. Si tiene 15,000 ids normalmente está almacenando algo en la ID, como si el artículo es o no un producto de cocina, y en lugar de almacenar el group_id que corresponde a "producto de cocina", está tratando de encontrar todos los productos de cocina por sus identificadores. (lo cual es malo por cualquier razón) ¿Qué representan esos 15,000 ids? ¿Por qué no se almacena en la fila misma?
Evan Carroll
Cada elemento pertenece a varios grupos (generalmente 15-20 de ellos), así que intenté almacenarlos como una matriz int en mytable pero no pude encontrar la forma de indexar esto correctamente. Actualicé la pregunta con todos los detalles.
Jukurrpa