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


13

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

    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 個、1日あたり約20,000行station_idです。

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

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行= 20489812ループ= 1)
         ソートキー:station_id、submitted_at DESC
         ソート方法:外部マージディスク:681040kB
         -> 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行= 20489812幅= 20)(実際の時間= 0.181..1 $
 計画時間:0.206ミリ秒
 実行時間:16263.490ミリ秒

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


2
ステーションIDはいくつありますか、つまり、クエリは何行を返しますか?そして、どのバージョンのPostgresですか?
ypercubeᵀᴹ

Postgre 9.6、約400のユニークなstation_id、station_idごとに1日あたり約2万件のレコード
Kokizzu

このクエリは「各station_idについて、submitted_atに基づく最後のlevel_sensor値」を返します。DISTINCT ONには、不要な場合を除いてランダムな選択が含まれます。
philipxy 2017年

回答:


18

ステーション数がわずか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の代替案とあなたのオリジナルの比較)

EXPLAIN ANALYZEOPによって提供される結果:

 ネストされたループ(コスト= 0.56..356.65行= 102幅= 20)(実際の時間= 0.034..0.979行= 98ループ= 1)
   ->ステーションsのシーケンススキャン(コスト= 0.00..3.02行= 102幅= 4)(実際の時間= 0.009..0.016行= 102ループ= 1)
   ->制限(コスト= 0.56..3.45行= 1幅= 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 ms-   !!

必要な唯一のインデックスは、作成したインデックスですstation_id__submitted_atUNIQUE制約はuniq_sid_satまた、基本的には、仕事をしていません。両方を維持することは、ディスク領域と書き込みパフォーマンスの無駄のようです。

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

場合submitted_atすることができNULL、この作成UNIQUEあなたの現在のインデックスの両方交換するためのインデックス一意性制約を:

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

検討してください:

これは、関連(通常はPK)ごとに1行の個別のテーブルstationを想定してstation_idいます-どちらの方法でもかまいません。ない場合は作成してください。繰り返しますが、この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を許可するために、インデックスに最後の列を追加することは意味があるかもしれません。欠点:インデックスが大きくなるため、それを使用するすべてのクエリに少しコストがかかります。プロ:実際にインデックススキャンのみを取得する場合、手元のクエリはヒープページにアクセスする必要がまったくないため、約2倍の速度になります。しかし、それは現在、非常に高速なクエリにとっては実質的な利益ではないかもしれません。

しかし、私はそれがあなたのケースのために働くことを期待しません。あなたは言及しました:

... 1日あたり約2万行station_id

通常、これは書き込み負荷が絶え間ないことを示します(station_id5秒ごとに1つ)。そして、あなたは最新の行に興味があります。インデックスのみのスキャンは、すべてのトランザクションから見えるヒープページに対してのみ機能します(可視性マップのビットが設定されています)。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'
...

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


ネストされたループが必要であることがわかっているときはいつでも、LATERALを使用すると、多くの状況でパフォーマンスが向上します。
ポールドレイパー

6

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

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.