PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Poprawa wydajności ORDER BY na jsonb cross join z wewnętrznym join group by

Utwórzmy dane testowe na postgreslu 13 z 600 zestawami danych, 45k plików c.

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;

Twoje pierwotne zapytanie jest tutaj o wiele szybsze, ale prawdopodobnie dlatego, że postgres 13 jest po prostu mądrzejszy.

 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

To zapytanie najpierw odczytuje dużą tabelę (pliki c) i generuje znacznie mniej wierszy z powodu agregacji. Dzięki temu łączenie z zestawami danych będzie szybsze po zmniejszeniu liczby wierszy do połączenia, a nie wcześniej. Przenieśmy to połączenie. Pozbyłem się też CROSS JOIN, które jest niepotrzebne, gdy istnieje funkcja zwracania zestawu w SELECT, postgres zrobi to, co chcesz za darmo.

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

Tak jest lepiej. Ale widzę LIMIT w twoim zapytaniu, co oznacza, że ​​prawdopodobnie robisz coś takiego jak paginacja. W tym przypadku wystarczy obliczyć całe zapytanie dla całej tabeli cfiles, a następnie odrzucić większość wyników ze względu na LIMIT, JEŚLI wyniki tego dużego zapytania mogą zmienić to, czy wiersz ze zbiorów danych jest uwzględniony w końcowym wyniku albo nie. W takim przypadku wiersze w zbiorach danych, które nie mają odpowiednich plików c, nie pojawią się w końcowym wyniku, co oznacza, że ​​zawartość plików c wpłynie na stronicowanie. Cóż, zawsze możemy oszukiwać:aby wiedzieć, czy wiersz ze zbiorów danych musi zostać uwzględniony, wystarczy, że istnieje JEDEN wiersz z plików cfile o tym identyfikatorze...

Tak więc, aby wiedzieć, które wiersze zbiorów danych zostaną uwzględnione w końcowym wyniku, możemy użyć jednego z tych dwóch zapytań:

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;

Zajmują one około 2-3 milisekund. Możemy też oszukiwać:

CREATE INDEX datasets_name_id ON datasets(name,id);

To sprowadza go do około 300 mikrosekund. Tak więc, teraz mamy listę dataset_id, które będą faktycznie używane (a nie wyrzucane), więc możemy użyć tego do wykonania dużej powolnej agregacji tylko na wierszach, które faktycznie znajdą się w końcowym wyniku, co powinno zaoszczędzić dużą ilość niepotrzebnej pracy...

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;

Trwa to około 30ms, również złożyłem zamówienie pod nazwą sample_name, o którym wcześniej zapomniałem. To powinno działać w twoim przypadku. Ważną kwestią jest to, że czas zapytania nie zależy już od rozmiaru plików c-tabeli, ponieważ przetworzy tylko te wiersze, które są rzeczywiście potrzebne.

Opublikuj wyniki;)



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Zwróć wiersze, które mają maksymalnie jedną kolumnę w Postgresql

  2. Czy dodanie zbędnych tabel w klauzulach WITH spowalnia zapytanie w PostgreSQL?

  3. Zapytanie o dane w polu danych tablicy JSON

  4. Funkcja PostgreSQl zwraca wiele dynamicznych zestawów wyników

  5. Kontener Postgres ulega awarii, gdy `pliki bazy danych są niezgodne z serwerem` po zaktualizowaniu obrazu kontenera do najnowszej wersji