PostgreSQL - Arbeiten mit einem Array von Tausenden von Elementen

8

Ich möchte Zeilen basierend darauf auswählen, ob eine Spalte in einer großen Liste von Werten enthalten ist, die ich als ganzzahliges Array übergebe.

Hier ist die Abfrage, die ich derzeit verwende:

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

Die Tabelle ist wie folgt aufgebaut:

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

Ich habe diesen Index erstellt, nachdem ich verschiedene ausprobiert und EXPLAINdie Abfrage ausgeführt habe. Dieser war sowohl für das Abfragen als auch für das Sortieren am effizientesten. Hier ist die EXPLAIN-Analyse der Abfrage:

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

Das Problem ist, dass das int-Array bis zu 15.000 Elemente enthalten kann und die Abfrage in diesem Fall ziemlich langsam wird (ca. 800 ms auf meinem Laptop, einem aktuellen Dell XPS).

Ich dachte, das Übergeben des int-Arrays als Parameter könnte langsam sein, und da die Liste der IDs vorher in der Datenbank gespeichert werden kann, habe ich dies versucht. Ich habe sie in einem Array in einer anderen Tabelle gespeichert und verwendet item_id = ANY (SELECT UNNEST(item_ids) FROM ...), was langsamer war als mein aktueller Ansatz. Ich habe auch versucht, sie Zeile für Zeile zu speichern und zu verwenden item_id IN (SELECT item_id FROM ...), was sogar mit den für meinen Testfall relevanten Zeilen in der Tabelle noch langsamer war.

Gibt es einen besseren Weg, dies zu tun?

Update: Nach Evans Kommentaren habe ich einen anderen Ansatz versucht: Jedes Element ist Teil mehrerer Gruppen. Anstatt die Element-IDs der Gruppe zu übergeben, habe ich versucht, die Gruppen-IDs in mytable hinzuzufügen:

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

Neue Abfrage ($ 1 ist die Zielgruppen-ID):

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

Erklären Sie analysieren:

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

Bei Indizes gibt es möglicherweise Verbesserungspotenzial, aber es fällt mir schwer zu verstehen, wie Postgres sie verwendet. Daher bin ich mir nicht sicher, was ich ändern soll.

Jukurrpa
quelle
Wie viele Zeilen in "mytable"? Wie viele verschiedene "item_id" -Werte gibt es?
Nick
Sollten Sie auch keine Eindeutigkeitsbeschränkung (wahrscheinlich noch nicht definierter eindeutiger Index) für item_id in mytable haben? ... Bearbeitet: Oh, ich sehe "PARTITION BY item_id", daher verwandelt sich diese Frage in "Was ist der natürliche, echte Schlüssel für Ihre Daten? Was sollte dort einen eindeutigen Index bilden?"
Nick
Ungefähr 12 Millionen Zeilen in mytable, mit ungefähr 500.000 verschiedenen item_id. Für diese Tabelle gibt es keinen natürlichen natürlichen eindeutigen Schlüssel. Die Daten werden automatisch für sich wiederholende Ereignisse generiert. Ich denke, das item_id+ start_date+ name(Feld hier nicht gezeigt) könnte eine Art Schlüssel darstellen.
Jukurrpa
Können Sie den Ausführungsplan veröffentlichen, den Sie erhalten?
Colin 't Hart
Sicher, fügte der Frage die EXPLAIN-Analyse hinzu.
Jukurrpa

Antworten:

1

Gibt es einen besseren Weg, dies zu tun?

Ja, verwenden Sie eine temporäre Tabelle. Es ist nichts Falsches daran, eine indizierte temporäre Tabelle zu erstellen, wenn Ihre Abfrage so verrückt ist.

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;

Aber noch besser als das ...

"500k different item_id" ... "int array kann bis zu 15.000 Elemente enthalten"

Sie wählen 3% Ihrer Datenbank einzeln aus. Ich muss mich fragen, ob es nicht besser ist, Gruppen / Tags usw. im Schema selbst zu erstellen. Ich persönlich musste noch nie 15.000 verschiedene IDs in eine Abfrage senden.

Evan Carroll
quelle
Ich habe gerade versucht, die temporäre Tabelle zu verwenden, und sie ist langsamer, zumindest im Fall von 15.000 IDs. Meinen Sie beim Erstellen von Gruppen im Schema selbst eine Tabelle mit den IDs, die ich als Argument übergebe? Ich habe so etwas versucht, aber die Leistung war ähnlich oder schlechter als mein aktueller Ansatz. Ich werde die Frage mit mehr Details aktualisieren
Jukurrpa
Nein, ich meine. Wenn Sie normalerweise 15.000 IDs haben, speichern Sie etwas in der ID, z. B. ob es sich bei dem Artikel um ein Küchenprodukt handelt, und anstatt die group_id zu speichern, die dem "Küchenprodukt" entspricht, versuchen Sie, alle Küchenprodukte zu finden durch ihre IDs. (was aus jedem Grund schlecht ist ) Was bedeuten diese 15.000 IDs? Warum wird es nicht in der Zeile selbst gespeichert?
Evan Carroll
Jedes Element gehört mehreren Gruppen an (normalerweise 15 bis 20), daher habe ich versucht, sie als int-Array in mytable zu speichern, konnte jedoch nicht herausfinden, wie dies richtig indiziert werden kann. Ich habe die Frage mit allen Details aktualisiert.
Jukurrpa