sql >> Database >  >> RDS >> PostgreSQL

Verbetering van de prestaties van ORDER BY op jsonb cross join met inner join group by

Laten we testgegevens maken op postgresl 13 met 600 datasets, 45k cfiles.

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;

Je oorspronkelijke zoekopdracht is hier een stuk sneller, maar dat komt waarschijnlijk omdat postgres 13 gewoon slimmer is.

 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

Deze query leest eerst een grote tabel (cfiles) en produceert veel minder rijen vanwege aggregatie. Het zal dus sneller zijn om gegevenssets samen te voegen nadat het aantal rijen om samen te voegen is verminderd, niet eerder. Laten we die join verplaatsen. Ook heb ik de CROSS JOIN verwijderd die niet nodig is, wanneer er een set-return-functie in een SELECT is, zal postgres gratis doen wat je wilt.

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

Dat is beter. Maar ik zie een LIMIT in uw zoekopdracht, wat betekent dat u waarschijnlijk iets doet als paginering. In dit geval is het alleen nodig om de hele query voor de hele cfiles-tabel te berekenen en vervolgens de meeste resultaten weg te gooien vanwege de LIMIT, ALS de resultaten van die grote query kunnen veranderen of een rij uit datasets wordt opgenomen in het uiteindelijke resultaat of niet. Als dat het geval is, zullen rijen in datasets die geen corresponderende cfiles hebben niet verschijnen in het uiteindelijke resultaat, wat betekent dat de inhoud van cfiles de paginering zal beïnvloeden. Welnu, we kunnen altijd vals spelen:om te weten of een rij uit datasets moet worden opgenomen, is het enige dat nodig is dat er ÉÉN rij uit cfiles bestaat met die id...

Dus om te weten welke rijen met datasets in het uiteindelijke resultaat zullen worden opgenomen, kunnen we een van deze twee query's gebruiken:

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;

Die duren ongeveer 2-3 milliseconden. We kunnen ook vals spelen:

CREATE INDEX datasets_name_id ON datasets(name,id);

Dit brengt het terug naar ongeveer 300 microseconden. Dus nu hebben we de lijst met dataset_id die daadwerkelijk zal worden gebruikt (en niet wordt weggegooid), zodat we die kunnen gebruiken om de grote langzame aggregatie alleen uit te voeren op de rijen die daadwerkelijk in het uiteindelijke resultaat zullen zijn, wat een grote hoeveelheid zou moeten besparen van onnodig werk...

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;

Dit duurt ongeveer 30 ms, ook plaats ik de bestelling op sample_name die ik eerder was vergeten. Het zou voor jouw geval moeten werken. Een belangrijk punt is dat de querytijd niet langer afhankelijk is van de grootte van tabel-c-bestanden, omdat alleen de rijen worden verwerkt die echt nodig zijn.

Gelieve de resultaten te posten;)



  1. SQL-query om te zoeken op dag/maand/jaar/dag&maand/dag&jaar etc

  2. PostgreSQL gebruikt geen gedeeltelijke index

  3. Wat is Spring Integration?

  4. Hoe voeg ik een volgnummerkolom toe aan resultaatgegevens?