PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Amélioration des performances de ORDER BY sur la jointure croisée jsonb avec le groupe de jointure interne par

Créons des données de test sur postgresl 13 avec 600 ensembles de données et 45 000 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;

Votre requête d'origine est beaucoup plus rapide ici, mais c'est probablement parce que postgres 13 est juste plus intelligent.

 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

Cette requête lit d'abord une grande table (cfiles) et produit beaucoup moins de lignes en raison de l'agrégation. Ainsi, il sera plus rapide de joindre des ensembles de données après la réduction du nombre de lignes à joindre, pas avant. Déplaçons cette jointure. De plus, je me suis débarrassé du CROSS JOIN qui est inutile, lorsqu'il y a une fonction de retour d'ensemble dans un postgres SELECT fera ce que vous voulez gratuitement.

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

C'est mieux. Mais je vois une LIMITE dans votre requête, ce qui signifie que vous faites probablement quelque chose comme la pagination. Dans ce cas, il suffit de calculer l'intégralité de la requête pour l'ensemble de la table cfiles, puis de jeter la plupart des résultats en raison de la LIMITE, SI les résultats de cette grande requête peuvent changer si une ligne des ensembles de données est incluse dans le résultat final ou non. Si tel est le cas, les lignes des ensembles de données qui n'ont pas de cfiles correspondants n'apparaîtront pas dans le résultat final, ce qui signifie que le contenu des cfiles affectera la pagination. Eh bien, nous pouvons toujours tricher :pour savoir si une ligne des ensembles de données doit être incluse, il suffit qu'UNE ligne des cfiles existe avec cet identifiant...

Ainsi, afin de savoir quelles lignes de jeux de données seront incluses dans le résultat final, nous pouvons utiliser l'une de ces deux requêtes :

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;

Ceux-ci prennent environ 2-3 millisecondes. On peut aussi tricher :

CREATE INDEX datasets_name_id ON datasets(name,id);

Cela le ramène à environ 300 microsecondes. Donc, maintenant nous avons la liste des dataset_id qui seront réellement utilisés (et non jetés) afin que nous puissions l'utiliser pour effectuer la grande agrégation lente uniquement sur les lignes qui seront réellement dans le résultat final, ce qui devrait économiser beaucoup de travail inutile...

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;

Cela prend environ 30ms, aussi j'ai mis la commande par sample_name que j'avais oublié avant. Cela devrait fonctionner pour votre cas. Un point important est que le temps de requête ne dépend plus de la taille des cfiles de la table, puisqu'il ne traitera que les lignes réellement nécessaires.

Merci de poster les résultats;)