sql >> Databasteknik >  >> RDS >> PostgreSQL

Förbättra prestanda för ORDER BY på jsonb cross join med inre join group by

Låt oss skapa testdata på postgresl 13 med 600 datauppsättningar, 45k cfiler.

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;

Din ursprungliga fråga är mycket snabbare här, men det beror förmodligen på att postgres 13 bara är smartare.

 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

Den här frågan läser först en stor tabell (cfiles) och producerar mycket färre rader på grund av aggregering. Det blir alltså snabbare att sammanfoga med datauppsättningar efter att antalet rader som ska sammanfogas minskat, inte tidigare. Låt oss flytta den sammanfogningen. Jag blev också av med CROSS JOIN som är onödig, när det finns en set-return funktion i en SELECT kommer postgres att göra vad du vill gratis.

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

Det är bättre. Men jag ser en LIMIT i din fråga, vilket betyder att du förmodligen gör något som paginering. I det här fallet är det bara nödvändigt att beräkna hela frågan för hela cfiles-tabellen och sedan kasta bort de flesta resultaten på grund av LIMIT, OM resultaten av den stora frågan kan ändra om en rad från datauppsättningar ingår i slutresultatet eller inte. Om så är fallet, kommer rader i datauppsättningar som inte har motsvarande c-filer inte att visas i slutresultatet, vilket innebär att innehållet i c-filer kommer att påverka sidnumren. Tja, vi kan alltid fuska:för att veta om en rad från datauppsättningar måste inkluderas, krävs det bara att EN rad från cfiler finns med det id...

Så för att veta vilka rader med datamängder som kommer att inkluderas i slutresultatet kan vi använda en av dessa två frågor:

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;

De tar cirka 2-3 millisekunder. Vi kan också fuska:

CREATE INDEX datasets_name_id ON datasets(name,id);

Detta tar ner det till cirka 300 mikrosekunder. Så nu har vi listan över dataset_id som faktiskt kommer att användas (och inte slängas) så vi kan använda den för att utföra den stora långsamma aggregeringen endast på raderna som faktiskt kommer att vara i slutresultatet, vilket borde spara en stor summa onödigt arbete...

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;

Detta tar ca 30ms, även jag lägger beställningen av sample_name som jag hade glömt innan. Det borde fungera för ditt fall. En viktig punkt är att frågetiden inte längre beror på storleken på tabell-c-filer, eftersom den bara kommer att bearbeta de rader som faktiskt behövs.

Vänligen posta resultat;)



  1. sammansatt primärnyckel och autoinkrementkolumn men INTE primärnyckel

  2. Lämpliga värden för -Infinity &Infinity i Postgres

  3. Ta bort dynamiskt hanterade tabeller i MySQL

  4. Få appar med det högsta antalet recensioner sedan en dynamisk serie av dagar