PostgreSQL - Lavorare con array di migliaia di elementi


8

Sto cercando di selezionare le righe in base al fatto che una colonna sia contenuta in un ampio elenco di valori che passo come un array intero.

Ecco la query che attualmente 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

La tabella è strutturata come tale:

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

Ho trovato questo indice dopo averne provato diversi e aver eseguito EXPLAINla query. Questo era il più efficiente sia per le query che per l'ordinamento. Ecco la spiegazione dell'analisi della query:

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

Il problema è che l'array int può contenere fino a 15.000 elementi circa e in questo caso la query diventa piuttosto lenta (circa 800 ms sul mio laptop, un Dell XPS recente).

Ho pensato che il passaggio dell'array int come parametro potesse essere lento, quindi, considerando che l'elenco di ID può essere precedentemente memorizzato nel database, ho provato a farlo. Li ho archiviati in un array in un'altra tabella e usato item_id = ANY (SELECT UNNEST(item_ids) FROM ...), che era più lento del mio approccio attuale. Ho anche provato a memorizzarli riga per riga e utilizzando item_id IN (SELECT item_id FROM ...), che era ancora più lento, anche con solo le righe relative al mio caso di test nella tabella.

C'è un modo migliore per farlo?

Aggiornamento: seguendo i commenti di Evan , ho provato un altro approccio: ogni elemento fa parte di più gruppi, quindi invece di passare gli ID degli elementi del gruppo, ho provato ad aggiungere gli ID di gruppo in 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)
    ...

Nuova query ($ 1 è l'id del gruppo targetizzato):

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

Spiega analizzare:

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

Potrebbero esserci margini di miglioramento con gli indici, ma faccio fatica a capire come li utilizza Postgres, quindi non sono sicuro di cosa cambiare.


Quante righe in "mytable"? Quanti diversi valori "item_id" ci sono?
Nick,

Inoltre, non dovresti avere un vincolo di unicità (probabilmente un indice univoco non ancora definito) su item_id in mytable? ... Modificato: oh, vedo "PARTITION BY item_id", quindi questa domanda si trasforma in "Qual è la chiave naturale e reale per i tuoi dati? Cosa dovrebbe formare un indice univoco lì?"
Nick,

Circa 12 milioni di file in mytable, con circa 500k diversi item_id. Non esiste una chiave unica naturale reale per questa tabella, sono i dati generati automaticamente per gli eventi ricorrenti. Suppongo che il item_id+ start_date+ name(campo non mostrato qui) possa costituire una sorta di chiave.
Jukurrpa,

Puoi pubblicare il piano di esecuzione che stai ricevendo?
Colin 't Hart,

Certo, ha aggiunto la spiegazione analizza alla domanda.
Jukurrpa,

Risposte:


1

C'è un modo migliore per farlo?

Sì, usa una tabella temporanea. Non c'è niente di sbagliato nel creare una tabella temporanea indicizzata quando la tua query è così folle.

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;

Ma anche meglio di così ...

"500k different item_id" ... "int array può contenere fino a 15.000 elementi"

Stai selezionando il 3% del tuo database individualmente. Mi chiedo se non è meglio creare gruppi / tag ecc. Nello schema stesso. Non ho mai dovuto inviare personalmente 15.000 ID diversi in una query.


Ho appena provato a utilizzare la tabella temporanea ed è più lenta, almeno nel caso di 15.000 ID. Per quanto riguarda la creazione di gruppi nello schema stesso, intendi una tabella con gli ID che passo come argomento? Ho provato qualcosa del genere, ma le prestazioni erano simili o peggiori del mio approccio attuale. Aggiornerò la domanda con maggiori dettagli
Jukurrpa,

No, intendo. Se hai 15.000 ID normalmente stai memorizzando qualcosa nell'ID, ad esempio se l'articolo è un prodotto da cucina e invece di memorizzare il group_id che corrisponde a "prodotto da cucina", stai cercando di trovare tutti i prodotti da cucina dai loro ID. (che è un male per ogni motivo) Cosa rappresentano quei 15.000 ID? Perché non è memorizzato nella riga stessa?
Evan Carroll,

Ogni articolo appartiene a più gruppi (di solito 15-20 di essi), quindi ho provato a memorizzarli come array int in mytable ma non sono riuscito a capire come indicizzarlo correttamente. Ho aggiornato la domanda con tutti i dettagli.
Jukurrpa,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.