web-dev-qa-db-ja.com

正しい列が存在していても、集計された列により全表スキャンが発生する

Date_added列で並べ替えられたテーブルデータセットから最初の数行をフェッチするクエリがあります。ソートされる列にはインデックスが付けられているため、このテーブルの基本バージョンは非常に高速です。

SELECT datasets.id FROM datasets ORDER BY date_added LIMIT 25
"Limit  (cost=0.28..6.48 rows=25 width=12) (actual time=0.040..0.092 rows=25 loops=1)"
"  ->  Index Scan using datasets_date_added_idx2 on datasets  (cost=0.28..1244.19 rows=5016 width=12) (actual time=0.037..0.086 rows=25 loops=1)"
"Planning time: 0.484 ms"
"Execution time: 0.139 ms"

しかし、クエリをもう少し複雑にすると問題が発生します。多対多の関係を表す別のテーブルを結合し、結果を配列列に集約したいと思います。そのためには、GROUP BY id句を追加する必要があります。

SELECT datasets.id FROM datasets GROUP BY datasets.id ORDER BY date_added LIMIT 25
"Limit  (cost=551.41..551.47 rows=25 width=12) (actual time=9.926..9.931 rows=25 loops=1)"
"  ->  Sort  (cost=551.41..563.95 rows=5016 width=12) (actual time=9.924..9.926 rows=25 loops=1)"
"        Sort Key: date_added"
"        Sort Method: top-N heapsort  Memory: 26kB"
"        ->  HashAggregate  (cost=359.70..409.86 rows=5016 width=12) (actual time=7.016..8.604 rows=5016 loops=1)"
"              Group Key: datasets_id"
"              ->  Seq Scan on datasets  (cost=0.00..347.16 rows=5016 width=12) (actual time=0.009..1.574 rows=5016 loops=1)"
"Planning time: 0.502 ms"
"Execution time: 10.235 ms"

GROUP BY句を追加するだけで、クエリは、以前のようにdate_added列のインデックスを使用する代わりに、データセットテーブルのフルスキャンを実行します。

私がやりたい実際のクエリの簡単なバージョンは次のとおりです:

SELECT 
    datasets.id,
    array_remove(array_agg(other_table.some_column), NULL) AS other_table
FROM datasets 
LEFT JOIN other_table 
    ON other_table.id = datasets.id
GROUP BY datasets.id 
ORDER BY date_added 
LIMIT 25

GROUP BY句によってインデックスが無視され、テーブル全体が強制的にスキャンされるのはなぜですか?そして、このクエリを書き換えて、並べ替えの基準となる列のインデックスを使用する方法はありますか?

私はWindowsでPostgres 9.5.4を使用しています。問題のテーブルには現在5000行ありますが、数十万行になることもあります。 EXPLAIN ANALYZEの前に、両方のテーブルでANALYZEを手動で実行しました。

テーブル定義:

CREATE TABLE public.datasets
(
  id integer NOT NULL DEFAULT nextval('datasets_id_seq'::regclass),
  date_added timestamp with time zone,
  ...
  CONSTRAINT datasets_pkey PRIMARY KEY (id)
)

CREATE TABLE public.other_table
(
  id integer NOT NULL,
  some_column integer NOT NULL,
  CONSTRAINT other_table_pkey PRIMARY KEY (id, some_column)
)

匿名化された無関係な列を含む\d datasetsの出力:

                                                   Table "public.datasets"
             Column              |           Type           |                           Modifiers
---------------------------------+--------------------------+------------------------------------------------------
 id                              | integer                  | not null default nextval('datasets_id_seq'::regclass)
 key                             | text                     |
 date_added                      | timestamp with time zone |
 date_last_modified              | timestamp with time zone |
 *****                           | integer                  |
 ********                        | boolean                  | default false
 *****                           | boolean                  | default false
 ***************                 | integer                  |
 *********************           | integer                  |
 *********                       | boolean                  | default false
 ********                        | integer                  |
 ************                    | integer                  |
 ************                    | integer                  |
 ****************                | timestamp with time zone |
 ************                    | text                     | default ''::text
 *****                           | text                     |
 *******                         | integer                  |
 *********                       | integer                  |
 **********************          | text                     | default ''::text
 *******************             | text                     |
 ****************                | integer                  |
 **********************          | text                     | default ''::text
 *******************             | text                     | default ''::text
 **********                      | integer                  |
 ***********                     | text                     |
 ***********                     | text                     |
 **********************          | integer                  |
 ******************************* | text                     | default ''::text
 ************************        | text                     | default ''::text
 ***********                     | integer                  | default 0
 *************                   | text                     |
 *******************             | integer                  |
 ****************                | integer                  | default 0
 ***************                 | text                     |
 **************                  | text                     |
Indexes:
    "datasets_pkey" PRIMARY KEY, btree (id)
    "datasets_date_added_idx" btree (date_added)
    "datasets_*_idx" btree (*)
    "datasets_*_idx" btree (*)
    "datasets_*_idx" btree (*)
    "datasets_*_idx" btree (*)
    "datasets_*_idx" btree (*)
    "datasets_*_idx1" btree (*)
    "datasets_*_idx" btree (*)
4
Mad Scientist

問題は、2番目のクエリです。

_SELECT datasets.id 
FROM datasets 
GROUP BY datasets.id 
ORDER BY date_added 
LIMIT 25 ;
_

あなたが期待することを意味するものではありません。 idがテーブルの主キーであるため、_date_added_で順序付けられた最初の25行が表示されるため、結果を変更せずに_GROUP BY_を削除できます。

ただし、オプティマイザが常に冗長な_GROUP BY_を削除するとは限らないため、別の計画が作成されるようです。理由はわかりません。これらの単純化を行うオプティマイザのさまざまな機能は、すべてのケースをカバーするのにはほど遠いです。

クエリを変更して_GROUP BY_および_ORDER BY_句を一致させるようにクエリを変更すると、mightがより良いプランを取得します。

_SELECT d.id 
FROM datasets AS d 
GROUP BY d.date_added, d.id 
ORDER BY d.date_added, d.id 
LIMIT 25 ;
_

しかし、いずれにせよ、私のアドバイスは「より単純な構文がある場合は冗長で複雑な構文を使用しないこと」です。

次に、3番目のクエリで、結合を使用し、_GROUP BY_メソッドが機能している間に、標準のSQLウィンドウ関数(ROW_NUMBER())またはPostgres _DISTINCT ON_を使用するか、派生テーブルへ(これは最初のクエリを使用します!、細かい変更はありません):

_SELECT  
    d.id,
    array_remove(array_agg(o.some_column), NULL) AS other_table
FROM 
  ( SELECT d.id, d.date_added
    FROM datasets AS d 
    ORDER BY d.date_added 
    LIMIT 25 
  ) AS d
LEFT JOIN other_table AS o
    ON o.id = d.id
GROUP BY d.date_added, d.id
ORDER BY d.date_added
LIMIT 25 ;
_

_GROUP BY_を完全に回避することもできます(まあ、インラインサブクエリには隠されています)。

_SELECT  
    d.id,
    ( SELECT array_remove(array_agg(o.some_column), NULL)
      FROM other_table AS o
      WHERE o.id = d.id
    ) AS other_table
FROM  datasets AS d 
ORDER BY d.date_added 
LIMIT 25 ;
_

どちらのクエリも、作成されたプランが最初に(高速)制限サブクエリを実行し、次に結合を実行するように記述されているため、どちらのテーブルのフルテーブルスキャンも回避されます。

より多くの列からの集計が必要な場合、3番目の方法は、LATERAL句で相関(FROM)サブクエリを使用して、上記の両方を組み合わせます。

_SELECT  
    d.id,
    o.other_table
    -- more aggregates
FROM 
    ( SELECT d.id, d.date_added
      FROM datasets AS d 
      ORDER BY d.date_added 
      LIMIT 25 
    ) AS d
  LEFT JOIN LATERAL
    ( SELECT array_remove(array_agg(o.some_column), NULL) AS other_table
             -- more aggregates
      FROM other_table AS o
      WHERE o.id = d.id
    ) AS o
    ON TRUE
ORDER BY d.date_added
LIMIT 25 ;
_
9
ypercubeᵀᴹ