web-dev-qa-db-ja.com

PostgreSQL-列の最大値を持つ行を取得します

私は、time_stamp、usr_id、transaction_id、lives_remainingの列を持つレコードを含むPostgresテーブル(「ライブ」と呼ばれる)を扱っています。各usr_idの最新のlives_remaining合計を提供するクエリが必要です

  1. 複数のユーザーがいます(異なるusr_idの)
  2. time_stampは一意の識別子ではありません。同じtime_stampでユーザーイベント(表の行ごとに1つ)が発生する場合があります。
  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(最初)と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を持つことがわかった行を取得することで、はるかに簡単になると思われます。テーブルの「存続中」には、解析する数千万行があるため、このクエリを可能な限り高速かつ効率的にしたいと思います。特にRDBMとPostgresは初めてなので、適切なインデックスを効果的に使用する必要があることを知っています。最適化の方法が少しわかりません。

同様の議論を見つけました here 。 Oracle分析関数に相当するある種のPostgresを実行できますか?

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

追伸以下を使用して、私の事例を作成できます。

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);
84
Joshua Berry

158kの擬似ランダム行(usr_idが0から10kの間で均一に分布し、trans_idが0から30の間で均一に分布)を持つテーブルでは、

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

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

上記のすべての時間には、1万行すべての結果セットの取得が含まれます。

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

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

クエリの実行時にプロダクションマシンのメモリに競合がないと予想される場合(たとえば、RDBMSキャッシュとファイルシステムキャッシュは、同時クエリおよび/またはファイルシステムアクティビティによってスラッシングされません)、取得したクエリ時間スタンドアロン(開発PC上のpgAdminIIIなど)モードが代表的です。本番システムで競合がある場合、低コストのクエリはキャッシュにそれほど依存しないため、クエリ時間は推定コスト比に比例して低下しますwhereas高コストのクエリは同じデータが何度も繰り返される(安定したキャッシュがない場合に追加の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 update

最後に、バージョン8.4の時点で、Postgresは Window Function をサポートしています。つまり、次のようにシンプルで効率的なものを作成できます。

クエリ#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
 );
81
vladr

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;
57
Marco

相関サブクエリや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

7
Bill Karwin

あなたが言及した他のページの Mike Woodhouseの答え のスタイルが好きです。最大化されているものが単一の列である場合は特に簡潔です。その場合、サブクエリは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) Niceを使用すると、必要な冗長性の量を削減できるので便利です。

4
j_random_hacker

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

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

フォレストごとにツリーをグループ化すると、ソートされていないツリーのリストが表示され、最大のものを見つける必要があります。最初にすべきことは、行をサイズでソートし、リストの最初の行を選択することです。効率が悪いように思えるかもしれませんが、数百万行ある場合、JOIN 'sおよびWHERE条件を含むソリューションよりもかなり高速になります。

ところで、ORDER_BY ために array_aggはPostgresql 9.0で導入されました

3
burak emre

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

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

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

公式を参照してください ドキュメント

2
Eden
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をテーブルに含める必要があります。

1
Quassnoi

ここには大きな問題があると思います。特定の行が別の行よりも遅れて発生したことを保証する単調に増加する「カウンター」はありません。次の例をご覧ください。

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よりも新しいかどうかを(同じタイムスタンプに対して)知ることができません。

0
Barry Brown