PostgreSQL-列の最大値を持つ行をフェッチします


96

私は、time_stamp、usr_id、transaction_id、およびlives_remainingの列を持つレコードを含むPostgresテーブル( "lives"と呼ばれます)を処理しています。各usr_idの最新のlives_remaining合計を取得するクエリが必要です

  1. 複数のユーザーがいる(個別のusr_id)
  2. time_stampは一意の識別子ではありません。ユーザーのイベント(テーブルの行ごとに1つ)が同じtime_stampで発生することがあります。
  3. trans_idは、非常に狭い時間範囲でのみ一意です。時間の経過とともに繰り返します。
  4. remaining_lives(特定のユーザーの場合)は、時間の経過とともに増加および減少する可能性があります

例:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 2 | 3 | 3    
  10:00 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

特定のusr_idごとに最新のデータが含まれる行の他の列にアクセスする必要があるため、次のような結果を返すクエリが必要です。

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

先に述べたように、各usr_idはライフを獲得または喪失する可能性があり、これらのタイムスタンプ付きイベントが非常に接近して発生して、同じタイムスタンプを持つこともあります!したがって、このクエリは機能しません。

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

代わりに、正しい行を識別するために、time_stamp(1番目)とtrans_id(2番目)の両方を使用する必要があります。次に、その情報をサブクエリからメインクエリに渡して、適切な行の他の列のデータを提供する必要もあります。これは、私が機能させたハッキン​​グされたクエリです。

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

さて、これはうまくいきますが、私はそれが好きではありません。クエリ内のクエリ、自己結合が必要で、MAXが最大のタイムスタンプとtrans_idを持っていることがわかった行を取得することで、はるかに簡単になるように思えます。テーブル「lives」には解析する数千万の行があるため、このクエリをできるだけ高速かつ効率的にしたいと思います。特にRDBMとPostgresは初めてなので、適切なインデックスを効果的に使用する必要があることはわかっています。最適化の方法について少し迷っています。

私はここで同様の議論を見つけました。Oracle分析関数と同等のタイプのPostgresを実行できますか?

集計関数(MAXなど)によって使用される関連する列情報へのアクセス、インデックスの作成、およびより適切なクエリの作成についてのアドバイスは大歓迎です!

PSあなたは私の例のケースを作成するために以下を使用することができます:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh、クエリが自己結合するなどの事実は気に入らないかもしれませんが、RDBMSに関しては問題ありません。
vladr 2009

1
自己結合が実際に変換するのは、単純なインデックスマッピングです。この場合、内部SELECT(MAXを持つもの)がインデックスをスキャンし、無関係なエントリを破棄し、外部SELECTがテーブルの残りの列を取得します。絞り込まれたインデックスに対応。
vladr 2009

ヴラド、ヒントと説明をありがとう。データベースの内部の仕組みを理解し始める方法とクエリを最適化する方法に私の目が開かれました。Quassnoi、素晴らしいクエリと主キーのヒントに感謝します。ビルも。非常に役立ちます。
Joshua Berry、

MAX BY2列を取得する方法を教えてくれてありがとう!

回答:


90

158kの疑似ランダム行(usr_idは0〜10kに均一に分散され、0〜30に均一に分散)を持つテーブルではtrans_id

以下のクエリコストでは、Postgresのコストベースのオプティマイザのコスト見積もり(Postgresのデフォルトxxx_cost値を使用)を参照しています。これは、必要なI / OおよびCPUリソースの重み付けされた関数見積もりです。これは、PgAdminIIIを起動し、「Query / Explainオプション」を「Analyze」に設定したクエリで「Query / Explain(F7)」を実行することで取得できます。

  • Quassnoyのクエリは、((上の化合物指数与え745k(!)のコスト見積もりを持っており、1.3秒で完了usr_idtrans_idtime_stamp))
  • Billのクエリのコスト見積もりは93,000で、2.9秒で完了します((usr_idtrans_id)に複合インデックスがある場合)
  • 以下のクエリ#1は、((上の化合物インデックス所与16Kのコスト推定値を有し、800msで完了usr_idtrans_idtime_stamp))
  • 以下のクエリ#2((上の化合物の機能指数所与14Kのコスト推定値を有し、800msで完了usr_idEXTRACT(EPOCH FROM time_stamp)trans_id))
    • これはPostgres固有です
  • 以下のクエリ#3(Postgresは8.4+)クエリ#2と同等(またはそれ以上)コスト推定値と終了時刻有する(複合インデックス(上の所与をusr_idtime_stamptrans_id)); livesテーブルを1回だけスキャンするという利点があり、メモリ内の並べ替えに対応するためにwork_memを一時的に(必要に応じて)増やすと、すべてのクエリの中で最速になります。

上記のすべての時間には、完全な10k行の結果セットの取得が含まれます。

目標は、見積もりコストに重点を置いた、最小限のコスト見積もり最小限のクエリ実行時間です。クエリの実行は、実行時の条件(たとえば、関連する行がすでにメモリに完全にキャッシュされているかどうか)に大きく依存する可能性がありますが、コスト見積もりはそうではありません。一方、コスト見積もりは正確に見積もりであることに注意してください。

最適なクエリ実行時間は、負荷のない専用データベースで実行した場合に得られます(たとえば、開発用PCでpgAdminIIIを使用して再生します)。クエリ時間は、実際のマシンの負荷/データアクセスの広がりに基づいて、運用環境によって異なります。1つのクエリが他のクエリよりもわずかに高速(20%未満)であるがコストがはるかに高い場合、通常は実行時間が長くコストが低いクエリを選択する方が賢明です。

クエリの実行時に本番マシンのメモリの競合がないことが予想される場合(たとえば、RDBMSキャッシュとファイルシステムキャッシュは、同時クエリやファイルシステムアクティビティによってスラッシュされない)、クエリ時間スタンドアロン(開発用PCのpgAdminIIIなど)モードの場合が代表的です。生産システムの競合がある場合は、より低コストでのクエリがキャッシュに限り依存しないように、クエリ時間は、見積原価率に比例して低下するのに対し、高いコストを持つクエリが何度も同じデータを再訪します(トリガー安定したキャッシュがない場合の追加のI / O)、例:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

ANALYZE lives必要なインデックスを作成したら、一度実行することを忘れないでください。


クエリ#1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

クエリ#2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

2013/01/29更新

最後に、バージョン8.4の時点で、Postgresはウィンドウ関数をサポートしています。つまり、次のようにシンプルで効率的なものを記述できます。

クエリ#3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

(usr_id、trans_id、times_tamp)の複合インデックスによって、「CREATE INDEX lives_blah_idx ON lives(usr_id、trans_id、time_stamp)」のような意味ですか?または、列ごとに3つの個別のインデックスを作成する必要がありますか?デフォルトの "USING btree"を使用する必要がありますよね?
Joshua Berry、

1
最初の選択肢は「はい」です。つまり、CREATE INDEX lives_blah_idx ON lives(usr_id、trans_id、time_stamp)を意味します。:)乾杯。
vladr 2009

コスト比較vladrをしてくれてありがとう!非常に完全な答えです!
アダム

@vladr私はちょうどあなたの答えに出会いました。クエリ1のコストは16kで、クエリ2のコストは14kなので、少し混乱しています。しかし、さらに下の表では、クエリ1のコストは5k、クエリ2のコストは50kであるとしています。それでは、どのクエリを使用するのが推奨されますか?:)感謝
Houman 2012

1
@Kave、テーブルは、OPの2つのクエリではなく、例を示すための架空のクエリのペア用です。混乱を減らすために名前を変更します。
vladr 2012

77

私はDISTINCT ONdocsを参照)に基づいたクリーンなバージョンを提案します:

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
これは非常に短くて正解です。また、良いリファレンスがあります!これは受け入れられる答えになるはずです。
Prakhar Agrawal 2017

これは、他の方法では機能しない、わずかに異なるアプリケーションで機能するように見えました。明らかにより多くの可視性のために上げられるべきです。
ジムファクター

8

次に、相関サブクエリまたはGROUP BYを使用しない別の方法を示します。私はPostgreSQLのパフォーマンスチューニングの専門家ではないので、これと他の人々から提供されたソリューションの両方を試して、どちらがより効果的かを確認することをお勧めします。

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

これはtrans_id、少なくともの特定の値に対して一意であると想定していますtime_stamp


4

私はあなたが言及した他のページでマイクウッドハウスの答えのスタイルが好きです。以上の最大化されているものだけでサブクエリがちょうど使用することができ、その場合には、単一の列である場合、それは特に簡潔だMAX(some_col)GROUP BY他の列が、あなたの場合にはあなたが最大化する2部構成の量を持って、あなたはまだ使用して行うことができますORDER BYさらに、LIMIT 1代わりに(Quassnoiが行ったように):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

行コンストラクター構文を使用すると、WHERE (a, b, c) IN (subquery)必要な表現の量を削減できるので便利です。


3

実際、この問題にはハックな解決策があります。地域内の各フォレストの最大のツリーを選択するとします。

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

樹木を森林でグループ化すると、分類されていない木のリストが表示され、最大のものを見つける必要があります。最初にすべきことは、行をサイズで並べ替え、リストの最初の行を選択することです。効率が悪いように見えるかもしれませんが、数百万の行がある場合、JOINWHERE条件を含むソリューションよりもかなり高速になります。

ところで、ORDER_BYfor array_aggはPostgresql 9.0で導入されていることに注意してください。


エラーがあります。ORDER BY tree_size.size DESCと書く必要があります。また、著者のタスクのためのコードは次のようになります。 SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky

2

Postgressql 9.5には、DISTINCT ONと呼ばれる新しいオプションがあります。

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

重複する行を削除し、ORDER BY句で定義された最初の行のみを残します。

公式ドキュメントを見る


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

にインデックスを作成すると、(usr_id, time_stamp, trans_id)このクエリが大幅に改善されます。

あなたはいつも、いつもある種のPRIMARY KEYテーブルを持っているべきです。


0

ここで大きな問題が1つあると思います。特定の行が別の行よりも後に発生したことを保証する単調に増加する「カウンター」はありません。この例を見てみましょう:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

このデータから最新のエントリを特定することはできません。2回目ですか、最後ですか?正しい答えを与えるためにこのデータのいずれかに適用できるsortまたはmax()関数はありません。

タイムスタンプの解像度を上げることは大きな助けになるでしょう。データベースエンジンは要求をシリアル化するので、十分な解決により、2つのタイムスタンプが同じにならないことが保証されます。

または、非常に長い時間ロールオーバーしないtrans_idを使用します。ロールオーバーするtrans_idがあると、複雑な計算を行わない限り、trans_id 6がtrans_id 1よりも新しいかどうかを(同じタイムスタンプで)判別できません。


はい、理想的には、シーケンス(自動インクリメント)列が適切です。
vladr 2009

上記の仮定は、小さな時間増分では、trans_idがロールオーバーしないことでした。テーブルには、繰り返されないtrans_idのような一意のプライマリインデックスが必要であることに同意します。(PS私は今コメントするのに十分なカルマ/レピュテーションポイントを持っていることを嬉しく思います!)
ジョシュアベリー

Vladは、trans_idが頻繁に入れ替わるかなり短いサイクルを持っていると述べています。私のテーブルの真ん中の2行(trans_id = 6と1)のみを考慮しても、どちらが最新かはわかりません。したがって、特定のタイムスタンプにmax(trans_id)を使用しても機能しません。
バリーブラウン、

はい、アプリケーション作成者の(time_stamp、trans_id)タプルが特定のユーザーに対して一意であるという保証に依存しています。そうでない場合、 "SELECT l1.usr_id、l1.lives_left、... FROM ... WHERE ..."は "SELECT l1.usr_id、MAX / MIN(l1.lives_left)、... FROMになる必要があります。 .. WHERE ... GROUP BY l1.usr_id、...
vladr 2009

0

あなたが役立つかもしれない別の解決策。

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.