PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Verbesserte Leistung von ORDER BY auf jsonb Cross Join mit Inner Join Group By

Lassen Sie uns Testdaten auf Postgresl 13 mit 600 Datensätzen und 45.000 cfiles erstellen.

BEGIN;

CREATE TABLE cfiles (
 id SERIAL PRIMARY KEY, 
 dataset_id INTEGER NOT NULL,
 property_values jsonb NOT NULL);

INSERT INTO cfiles (dataset_id,property_values)
 SELECT 1+(random()*600)::INTEGER  AS did, 
   ('{"Sample Names": ["'||array_to_string(array_agg(DISTINCT prop),'","')||'"]}')::jsonb prop 
   FROM (
     SELECT 1+(random()*45000)::INTEGER AS cid,
     'Samp'||(power(random(),2)*30)::INTEGER AS prop 
     FROM generate_series(1,45000*4)) foo 
   GROUP BY cid;

COMMIT;
CREATE TABLE datasets ( id INTEGER PRIMARY KEY, name TEXT NOT NULL );
INSERT INTO datasets SELECT n, 'dataset'||n FROM (SELECT DISTINCT dataset_id n FROM cfiles) foo;
CREATE INDEX cfiles_dataset ON cfiles(dataset_id);
VACUUM ANALYZE cfiles;
VACUUM ANALYZE datasets;

Ihre ursprüngliche Abfrage ist hier viel schneller, aber das liegt wahrscheinlich daran, dass Postgres 13 einfach intelligenter ist.

 Sort  (cost=114127.87..114129.37 rows=601 width=46) (actual time=658.943..659.012 rows=601 loops=1)
   Sort Key: datasets.name
   Sort Method: quicksort  Memory: 334kB
   ->  GroupAggregate  (cost=0.57..114100.13 rows=601 width=46) (actual time=13.954..655.916 rows=601 loops=1)
         Group Key: datasets.id
         ->  Nested Loop  (cost=0.57..92009.62 rows=4416600 width=46) (actual time=13.373..360.991 rows=163540 loops=1)
               ->  Merge Join  (cost=0.56..3677.61 rows=44166 width=78) (actual time=13.350..113.567 rows=44166 loops=1)
                     Merge Cond: (cfiles.dataset_id = datasets.id)
                     ->  Index Scan using cfiles_dataset on cfiles  (cost=0.29..3078.75 rows=44166 width=68) (actual time=0.015..69.098 rows=44166 loops=1)
                     ->  Index Scan using datasets_pkey on datasets  (cost=0.28..45.29 rows=601 width=14) (actual time=0.024..0.580 rows=601 loops=1)
               ->  Function Scan on jsonb_array_elements_text sn  (cost=0.01..1.00 rows=100 width=32) (actual time=0.003..0.004 rows=4 loops=44166)
 Execution Time: 661.978 ms

Diese Abfrage liest zuerst eine große Tabelle (cfiles) und erzeugt aufgrund der Aggregation viel weniger Zeilen. Daher ist es schneller, Datensätze zu verbinden, nachdem die Anzahl der zu verbindenden Zeilen reduziert wurde, nicht vorher. Lassen Sie uns diesen Join verschieben. Außerdem habe ich den CROSS JOIN entfernt, der unnötig ist, wenn es eine Set-Returning-Funktion in einem SELECT gibt, wird Postgres kostenlos tun, was Sie wollen.

SELECT dataset_id, d.name, sample_names FROM (
 SELECT dataset_id, string_agg(sn, '; ') as sample_names FROM (
  SELECT DISTINCT dataset_id,
   jsonb_array_elements_text(cfiles.property_values -> 'Sample Names') AS sn
   FROM cfiles
   ) f GROUP BY dataset_id
  )g JOIN datasets d ON (d.id=g.dataset_id)
 ORDER BY d.name;
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=536207.44..536207.94 rows=200 width=46) (actual time=264.435..264.502 rows=601 loops=1)
   Sort Key: d.name
   Sort Method: quicksort  Memory: 334kB
   ->  Hash Join  (cost=536188.20..536199.79 rows=200 width=46) (actual time=261.404..261.784 rows=601 loops=1)
         Hash Cond: (d.id = cfiles.dataset_id)
         ->  Seq Scan on datasets d  (cost=0.00..10.01 rows=601 width=14) (actual time=0.025..0.124 rows=601 loops=1)
         ->  Hash  (cost=536185.70..536185.70 rows=200 width=36) (actual time=261.361..261.363 rows=601 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 170kB
               ->  HashAggregate  (cost=536181.20..536183.70 rows=200 width=36) (actual time=260.805..261.054 rows=601 loops=1)
                     Group Key: cfiles.dataset_id
                     Batches: 1  Memory Usage: 1081kB
                     ->  HashAggregate  (cost=409982.82..507586.70 rows=1906300 width=36) (actual time=244.419..253.094 rows=18547 loops=1)
                           Group Key: cfiles.dataset_id, jsonb_array_elements_text((cfiles.property_values -> 'Sample Names'::text))
                           Planned Partitions: 4  Batches: 1  Memory Usage: 13329kB
                           ->  ProjectSet  (cost=0.00..23530.32 rows=4416600 width=36) (actual time=0.030..159.741 rows=163540 loops=1)
                                 ->  Seq Scan on cfiles  (cost=0.00..1005.66 rows=44166 width=68) (actual time=0.006..9.588 rows=44166 loops=1)
 Planning Time: 0.247 ms
 Execution Time: 269.362 ms

Das ist besser. Aber ich sehe ein LIMIT in Ihrer Abfrage, was bedeutet, dass Sie wahrscheinlich so etwas wie Paginierung durchführen. In diesem Fall ist es nur erforderlich, die gesamte Abfrage für die gesamte cfiles-Tabelle zu berechnen und dann die meisten Ergebnisse aufgrund des LIMIT zu verwerfen, WENN sich die Ergebnisse dieser großen Abfrage ändern können, ob eine Zeile aus Datensätzen im Endergebnis enthalten ist oder nicht. Wenn dies der Fall ist, werden Zeilen in Datensätzen, die keine entsprechenden cfiles haben, nicht im Endergebnis angezeigt, was bedeutet, dass der Inhalt von cfiles die Paginierung beeinflusst. Nun, wir können immer schummeln:Um zu wissen, ob eine Zeile aus Datensätzen eingeschlossen werden muss, ist alles, was erforderlich ist, dass EINE Zeile aus cfiles mit dieser ID existiert ...

Um also zu wissen, welche Zeilen von Datensätzen in das Endergebnis aufgenommen werden, können wir eine dieser beiden Abfragen verwenden:

SELECT id FROM datasets WHERE EXISTS( SELECT * FROM cfiles WHERE cfiles.dataset_id = datasets.id )
ORDER BY name LIMIT 20;

SELECT dataset_id FROM 
  (SELECT id AS dataset_id, name AS dataset_name FROM datasets ORDER BY dataset_name) f1
  WHERE EXISTS( SELECT * FROM cfiles WHERE cfiles.dataset_id = f1.dataset_id )
  ORDER BY dataset_name
  LIMIT 20;

Diese dauern etwa 2-3 Millisekunden. Wir können auch schummeln:

CREATE INDEX datasets_name_id ON datasets(name,id);

Dies bringt es auf etwa 300 Mikrosekunden herunter. Jetzt haben wir also die Liste der dataset_id, die tatsächlich verwendet (und nicht weggeworfen) wird, damit wir sie verwenden können, um die große langsame Aggregation nur für die Zeilen durchzuführen, die tatsächlich im Endergebnis enthalten sind, was eine große Menge einsparen sollte von unnötiger Arbeit...

WITH ds AS (SELECT id AS dataset_id, name AS dataset_name
 FROM datasets WHERE EXISTS( SELECT * FROM cfiles WHERE cfiles.dataset_id = datasets.id )
 ORDER BY name LIMIT 20)

SELECT dataset_id, dataset_name, sample_names FROM (
 SELECT dataset_id, string_agg(DISTINCT sn, '; ' ORDER BY sn) as sample_names FROM (
  SELECT dataset_id, 
   jsonb_array_elements_text(cfiles.property_values -> 'Sample Names') AS sn 
   FROM ds JOIN cfiles USING (dataset_id)
  ) g GROUP BY dataset_id
  ) h JOIN ds USING (dataset_id)
 ORDER BY dataset_name;

Dies dauert ungefähr 30 ms, außerdem habe ich die Bestellung von sample_name eingegeben, die ich zuvor vergessen hatte. Es sollte für Ihren Fall funktionieren. Ein wichtiger Punkt ist, dass die Abfragezeit nicht mehr von der Größe der Tabellen-Cfiles abhängt, da nur die Zeilen verarbeitet werden, die tatsächlich benötigt werden.

Bitte Ergebnisse posten;)