[解決済み] PostgreSQL - あるカラムの最大値を持つ行を取得する
質問
Postgresのテーブル("lives")に、time_stamp、usr_id、transaction_id、lives_remainingの列を持つレコードを扱っています。私は、各usr_idの最新のlives_remainingの合計を与えるクエリが必要です。
-
複数のユーザー(usr_idが異なる)が存在します。
-
time_stamp は一意な識別子ではありません: 同じ time_stamp でユーザー イベント (テーブルの行ごと) が発生することがあります。
-
trans_id は、非常に小さな時間範囲でのみ一意です: 時間の経過とともに繰り返されます。
- (特定のユーザーに対する) remaining_lives は、時間の経過とともに増加または減少する可能性があります。
の例です。
タイムスタンプ|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ごとに最新のデータを持つ行の他の列にアクセスする必要があるので、私はこのような結果を与えるクエリが必要です。
タイムスタンプ|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を持つことを発見した行を取得することによって、よりシンプルにできるように思えます。テーブルquot;lives"には解析するために数千万行があるので、このクエリをできるだけ高速かつ効率的にしたいのです。私はRDBMとPostgresの初心者なので、適切なインデックスを効果的に使用する必要があることは分かっています。最適化する方法については少し迷っています。
似たような議論を見つけました。 はこちら . Oracleの分析関数に相当する何らかのPostgresを実行することは可能ですか?
集約関数(MAXなど)で使用される関連列情報へのアクセス、インデックスの作成、より良いクエリの作成などに関するアドバイスがあれば、ぜひともお願いします。
P.S. 私の例のケースを作成するために以下を使用することができます。
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);
どのように解決するのですか?
158k行の擬似ランダム行を持つテーブルで(usr_idは0から10kの間で一様に分布。
trans_id
は 0 から 30 の間に一様に分布しています)。
以下のクエリコストとは、Postgresのコストベースのオプティマイザのコスト推定値です(Postgresのデフォルトの
xxx_cost
PgAdminIIIを起動し、quot;Query/Explain options"をquot;Analyze"に設定して問い合わせを実行すると、必要なI/OとCPUリソースの計量関数であるこの推定値を得ることができます。
-
Quassnoy のクエリの推定コストは 745k (!) で、1.3 秒で完了します (複合インデックスを指定した場合 (
usr_id
,trans_id
,time_stamp
)) -
Billのクエリの推定コストは93kで、2.9秒で完了します(複合インデックスを持つ(
usr_id
,trans_id
)) -
以下のクエリ#1
は推定コストが16kで、800msで完了します(複合インデックスを持つ(
usr_id
,trans_id
,time_stamp
)) -
以下、クエリ#2
は推定コストが14kで、800msで完了します(複合関数のインデックスが(
usr_id
,EXTRACT(EPOCH FROM time_stamp)
,trans_id
))- これはPostgres固有のものです。
-
以下のクエリ#3
(Postgres 8.4+) は、クエリ#2 と同等の (またはそれ以上の) 見積もりコストと完了時間を持ちます (複合インデックスが (
usr_id
,time_stamp
,trans_id
)); をスキャンする利点があります。lives
テーブルを一度だけスキャンし、一時的に (必要なら) work_mem を一時的に増加させれば、すべてのクエリの中で圧倒的に速くなります。
上記の時間はすべて、10k行の完全な結果セットの取得を含んでいます。
目標は最小限のコスト見積もり と 最小のクエリ実行時間であり、推定コストに重点を置いています。 クエリの実行は実行時の条件(例えば、関連する行がすでに完全にメモリにキャッシュされているかどうか)に大きく依存することがありますが、コストの見積もりはそうではありません。 一方、コストの見積もりはまさに見積もりであることに留意してください。
最良の問い合わせ実行時間は、負荷のない専用のデータベースで実行したときに得られます(例えば、開発用PCでpgAdminIIIを使用して遊んでいるときなど)。 一方のクエリが他方よりわずかに(<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
);
関連
-
MHAクラスタエラーサマリーの構築
-
[解決済み] 各グループの最後のレコードを取得する - MySQL
-
[解決済み] 最初の行への結合方法
-
[解決済み] MYSQLでMAX(列の値)、PARTITIONで別の列で行を選択するにはどうすればよいですか?
-
[解決済み] コマンドライン引数でPostgreSQLの.sqlファイルを実行する
-
[解決済み] ある列の最大値を持つ行を取得する
-
[解決済み] SQLite - UPSERT *not* INSERT or REPLACE
-
[解決済み] SQL Serverで結果をページ分割する最も良い方法は何ですか?
-
[解決済み】PostgreSQLでタイムスタンプから日付(yyyy/mm/dd)を抽出する。
-
[解決済み] SQL select join: すべてのカラムに 'prefix.*' というプレフィックスを付けることは可能ですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
MySQL】1136 - 列数が1行目の値数と一致しない問題を解決
-
MHAの高可用性構成とフェイルオーバー
-
SQL-初心者のためのベスト入門SQLクエリ I
-
[解決済み] SQL Server の DateTime データ型から日付だけを返す方法
-
[解決済み] SQL Serverにおける関数とストアドプロシージャの比較
-
[解決済み] SQL JOIN - WHERE句とON句の比較
-
[解決済み] ある列の最大値を持つ行を取得する
-
[解決済み] SQL Serverには、.NETのMath.Maxのような2つの値を取るMax関数はありますか?
-
[解決済み] 項目xにアクセスできるように文字列を分割するにはどうすればよいですか?
-
[解決済み] SQL/mysql - Select distinct/UNIQUE but return all column?