web-dev-qa-db-ja.com

PostgreSQLでDISTINCT ONを高速化する方法は?

PostgreSQL 9.6データベースにテーブルstation_logsがあります。

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

level_sensorについて、submitted_atに基づいて最後のstation_id値を取得しようとしています。一意のstation_id値は約400個あり、station_idごとに1日あたり約2万行あります。

インデックスを作成する前に:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 一意(コスト= 4347852.14..4450301.72行= 89幅= 20)(実際の時間= 22202.080..27619.167行= 98ループ= 1)
->ソート(コスト= 4347852.14..4399076.93行= 20489916幅= 20)(実際の時間= 22202.077..26540.827 rows = 20489812 loops = 1)
ソートキー:station_id、submitted_at DESC 
ソート方法:外部マージディスク:681040kB 
-> Seq station_logsでスキャン(コスト= 0.00..598895.16行= 20489916幅= 20)(実際の時間= 0.023..3443.587行= 20489812ループ= $ 
計画時間:0.072 ms 
実行時間:27690.644 MS

インデックスを作成しています:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

インデックスを作成した後、同じクエリに対して:

 一意(コスト= 0.56..2156367.51行= 89幅= 20)(実際の時間= 0.184..16263.413行= 98ループ= 1)
-> station_logsのstation_id__submitted_atを使用したインデックススキャン(コスト= 0.56..2105142.98 rows = 20489812 width = 20)(実際の時間= 0.181..1 $ 
計画時間:0.206 ms 
実行時間:16263.490 ms

このクエリをより速くする方法はありますか?たとえば1秒のように、16秒はまだ多すぎます。

13
Kokizzu

ステーション数が400の場合のみ、このクエリは大幅に高速になります。

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle ここ
(このクエリの計画、Abelistoの代替案と元の計画の比較)

OPによって提供される結果EXPLAIN ANALYZE

 ネストされたループ(コスト= 0.56..356.65行= 102幅= 20)(実際の時間= 0.034..0.979行= 98ループ= 1)
->ステーションsのシーケンススキャン(コスト= 0.00..3.02 rows = 102 width = 4)(実際の時間= 0.009..0.016 rows = 102 loops = 1)
->制限(cost = 0.56..3.45 rows = 1 width = 16)(実際の時間= 0.009。 .0.009行= 1ループ= 102)
-> Station_logsのstation_id__submitted_atを使用したインデックススキャン(コスト= 0.56..664062.38行= 230223幅= 16)(実際の時間= 0.009 $ 
インデックス条件: (station_id = s.id)
計画時間:0.542 ms 
実行時間: 1.013ミリ秒  -!!

必要な唯一のindexは、作成したstation_id__submitted_atです。 UNIQUE制約uniq_sid_satも基本的に機能します。両方を維持することは、ディスク領域と書き込みパフォーマンスの無駄のようです。

NULLS LASTが定義されていないORDER BYであるため、 submitted_atNOT NULLに追加しました。可能であれば、理想的には、NOT NULL制約を列submitted_atに追加し、追加のインデックスを削除して、クエリからNULLS LASTを削除します。

submitted_atNULLにできる場合は、このUNIQUEインデックスを作成して、現在のインデックス一意制約の両方を置き換えます。

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

考慮してください:

これは、関連するstation_id(通常はPK)ごとに1行の個別のテーブルstationを想定しています。どちらにしても。ない場合は作成してください。繰り返しますが、veryこのrCTE手法では高速です。

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

フィドルでも使っています。同様のクエリを使用して、stationテーブルなしでタスクを直接解決できます-作成する確信がない場合。

詳細な手順、説明、代替案:

インデックスを最適化

クエリは非常に高速になるはずです。それでも読み取りパフォーマンスを最適化する必要がある場合のみ...

joanoloのように、インデックスの最後の列としてlevel_sensorを追加してインデックスのみのスキャンを許可することは意味があるかもしれませんコメント付き
Con:インデックスを大きくします-これを使用するすべてのクエリに少しコストが追加されます。
Pro:実際にインデックススキャンのみを取得する場合、手元のクエリはヒープページにまったくアクセスする必要がないため、約2倍になります。速い。しかし、これは非常に高速なクエリにとっては実質的な利益ではないかもしれません。

ただし、あなたのケースでうまくいくとは思いません。あなたは言及しました:

... station_idごとに1日あたり約2万行。

通常、これは書き込み負荷が絶え間ないことを示します(station_idごとに1つ、5秒ごとに1つ)。そして、latest行に興味があります。インデックスのみのスキャンは、すべてのトランザクションから見えるヒープページに対してのみ機能します(可視性マップのビットが設定されています)。書き込み負荷に対応するには、テーブルに対して非常に積極的なVACUUM設定を実行する必要がありますが、それでもほとんどの場合機能しません。私の仮定が正しい場合、インデックスのみのスキャンは実行されません。しないでくださいインデックスにlevel_sensorを追加してください。

OTOH、私の仮定が成り立ち、あなたのテーブルが非常に大きくなっている場合BRINインデックス役立つかもしれません。関連:

または、さらに特殊化された、より効率的な:関連性のない行の大部分を切り取るための最新の追加のみの部分インデックス:

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

新しい行が存在する必要があることを知っているタイムスタンプを選択します。次のように、一致するWHERE条件をすべてのクエリに追加する必要があります。

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

インデックスとクエリを適宜調整する必要があります。
詳細と関連する回答:

18

古典的な方法を試してください:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

ThreadStarterによるEXPLAIN ANALYZE

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
6
Abelisto