PostgreSQL - Trabalhando com matriz de milhares de elementos

8

Eu estou olhando para selecionar linhas com base em se uma coluna está contida em uma grande lista de valores que eu passo como uma matriz inteira.

Aqui está a consulta que atualmente uso:

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

A tabela está estruturada da seguinte forma:

    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)
    ...

Eu vim com esse índice depois de tentar diferentes e executar EXPLAINa consulta. Este foi o mais eficiente para consulta e classificação. Aqui está a explicação da análise da 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

O problema é que o array int pode conter até 15.000 elementos ou mais e a consulta fica bastante lenta nesse caso (cerca de 800 ms no meu laptop, um Dell XPS recente).

Eu pensei que passar o array int como parâmetro poderia ser lento, e considerando a lista de IDs pode ser armazenada previamente no banco de dados, tentei fazer isso. Guardei-os em uma matriz em outra tabela e usei item_id = ANY (SELECT UNNEST(item_ids) FROM ...), que era mais lento que minha abordagem atual. Também tentei armazená-los linha por linha e usando item_id IN (SELECT item_id FROM ...), o que era ainda mais lento, mesmo com apenas as linhas relevantes para o meu caso de teste na tabela.

Existe um jeito melhor de fazer isso?

Atualização: após os comentários de Evan , outra abordagem que tentei: cada item faz parte de vários grupos; portanto, em vez de passar os IDs dos itens do grupo, tentei adicionar os IDs do grupo na minha tabela:

    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)
    ...

Nova consulta ($ 1 é o ID do grupo de destino):

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 a análise:

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

Pode haver espaço para melhorias nos índices, mas estou tendo dificuldades para entender como o postgres os usa, por isso não tenho certeza do que mudar.

Jukurrpa
fonte
Quantas linhas em "mytable"? Quantos valores "item_id" diferentes existem?
Nick
Além disso, você não deve ter restrição de exclusividade (provavelmente único índice ainda não definido) em item_id na minha tabela? ... Editado: oh, vejo "PARTITION BY item_id", portanto, essa pergunta se transforma em "Qual é a chave natural e real dos seus dados? O que deve formar um índice exclusivo aí?"
Nick
Cerca de 12 milhões de linhas mytable, com cerca de 500 mil diferentes item_id. Não existe uma chave única natural real para esta tabela, são dados gerados automaticamente para eventos repetidos. Eu acho que o item_id+ start_date+ name(campo não mostrado aqui) pode constituir algum tipo de chave.
Jukurrpa
Você pode postar o plano de execução que está recebendo?
Colin 't Hart
Claro, adicionou a análise de explicação à pergunta.
Jukurrpa 04/04

Respostas:

1

Existe um jeito melhor de fazer isso?

Sim, use uma tabela temporária. Não há nada errado em criar uma tabela temporária indexada quando sua consulta é tão insana.

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;

Mas ainda melhor que isso ...

"500k item_id diferente" ... "int array pode conter até 15.000 elementos"

Você está selecionando 3% do seu banco de dados individualmente. Eu tenho que saber se você não está melhor criando grupos / tags etc. no próprio esquema. Eu pessoalmente nunca tive que enviar 15.000 IDs diferentes para uma consulta.

Evan Carroll
fonte
Tentei usar a tabela temporária e é mais lenta, pelo menos no caso de 15.000 IDs. Quanto à criação de grupos no próprio esquema, você quer dizer uma tabela com os IDs que passo como argumento? Tentei algo assim, mas o desempenho foi semelhante ou pior do que minha abordagem atual. Eu vou atualizar a questão com mais detalhes
Jukurrpa
Não, quero dizer. Se você tem 15.000 IDs normalmente, está armazenando algo no ID, como se o item é ou não um produto de cozinha e, em vez de armazenar o group_id que corresponde a "produto de cozinha", você está tentando encontrar todos os produtos de cozinha por seus IDs. (o que é ruim por qualquer motivo) O que esses 15.000 IDs representam? Por que não é armazenado na própria linha?
Evan Carroll
Cada item pertence a vários grupos (geralmente de 15 a 20), então tentei armazená-los como uma matriz int na minha tabela, mas não consegui descobrir como indexar isso corretamente. Atualizei a pergunta com todos os detalhes.
Jukurrpa #