sql >> データベース >  >> RDS >> PostgreSQL

jsonbクロス結合でのORDERBYのパフォーマンスの改善

    600個のデータセット、45k個のcfileを使用してpostgresl13でテストデータを作成しましょう。

    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;
    

    ここでは元のクエリがはるかに高速ですが、これはおそらくpostgres13の方が賢いためです。

     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
    

    このクエリは最初に大きなテーブル(cfiles)を読み取り、集計により生成される行がはるかに少なくなります。したがって、結合する行の数が減った後ではなく、データセットとの結合が高速になります。その結合を移動しましょう。また、SELECTpostgresにset-returning関数がある場合、不要なCROSSJOINを削除しました。

    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
    

    それは良いです。しかし、クエリにLIMITが表示されます。これは、おそらくページネーションのようなことをしていることを意味します。この場合、cfilesテーブル全体のクエリ全体を計算してから、LIMITのためにほとんどの結果を破棄するだけで済みます。その大きなクエリの結果によって、データセットの行が最終結果に含まれるかどうかが変わる可能性がある場合です。か否か。その場合、対応するcfileがないデータセットの行は最終結果に表示されません。つまり、cfileの内容がページネーションに影響します。データセットの行を含める必要があるかどうかを知るために必要なのは、cfilesの1つの行がそのIDで存在することだけです...

    したがって、最終結果に含まれるデータセットの行を知るために、次の2つのクエリのいずれかを使用できます。

    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;
    

    それらは約2〜3ミリ秒かかります。チートすることもできます:

    CREATE INDEX datasets_name_id ON datasets(name,id);
    

    これにより、約300マイクロ秒になります。これで、実際に使用される(破棄されない)dataset_idのリストが取得されたので、これを使用して、実際に最終結果になる行に対してのみ大きな低速集計を実行できます。これにより、大幅な節約が可能になります。不必要な作業の...

    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;
    

    これには約30msかかります。また、前に忘れていたsample_nameで注文しました。それはあなたの場合にうまくいくはずです。重要な点は、クエリ時間は実際に必要な行のみを処理するため、テーブルcfileのサイズに依存しなくなったことです。

    結果を投稿してください;)



    1. SQL日付間隔クエリ

    2. plsqlでファイルを書き込もうとしているときに無効なパス

    3. bundle exec rake Assets:precompile-データベース構成でアダプターが指定されていません

    4. SQLServerの現在のデータベースのすべてのファイルグループを返す