126

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

  1. 複数のユーザーがいます(個別のusr_id)
  2. time_stampは一意の識別子ではありません。同じ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(最初)と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);
4

9 に答える 9

128

以下に基づいてクリーン バージョンを提案します ( docsDISTINCT ONを参照):

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;
于 2015-06-11T08:40:41.567 に答える
113

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) に複合インデックスがある場合) 。
  • ビルのクエリは 93,000 のコスト見積もりを持ち、2.9 秒で完了します (( usr_idtrans_id) に複合インデックスがある場合)
  • 以下のクエリ #1のコスト見積もりは 16k で、800 ミリ秒で完了します (( usr_idtrans_idtime_stamp) に複合インデックスがある場合)
  • 以下のクエリ #2のコスト見積もりは 14,000 で、800 ミリ秒で完了します (複合関数インデックスが ( usr_idEXTRACT(EPOCH FROM time_stamp)trans_id) にある場合) 。
    • これはPostgres固有です
  • 以下のクエリ #3 (Postgres 8.4+) のコスト見積もりと完了時間はクエリ #2 と同等 (またはそれ以上) です (( usr_idtime_stamptrans_id) の複合インデックスが与えられた場合)。テーブルを一度だけスキャンするという利点がlivesあり、(必要に応じて) work_memを一時的に増やしてメモリ内の並べ替えに対応すると、すべてのクエリの中ではるかに高速になります。

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

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

最適なクエリ実行時間は、負荷のない専用データベースで実行した場合に得られます (たとえば、開発用 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
 );
于 2009-02-26T01:19:08.867 に答える
7

あなたが言及した他のページの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)必要な言い回しの量が減るため、便利だと思います。

于 2010-06-15T06:46:22.403 に答える
4

実は、この問題にはハックな解決策があります。地域内の各森林の最大の木を選択するとします。

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_BYところで、 forarray_aggはPostgresql 9.0で導入されたことに注意してください

于 2013-01-18T00:04:58.977 に答える
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あなたは常に、常にあなたのテーブルにある種のものを持っているべきです。

于 2009-02-25T17:01:15.817 に答える
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 より新しいかどうかを (同じタイムスタンプで) 判断できないことを意味します。

于 2009-02-26T01:48:25.020 に答える