Postgres 9.2には、ユーザーメッセージ(簡略化された形式)に関する次のログテーブルがあります。
_CREATE TABLE log (
log_date DATE,
user_id INTEGER,
payload INTEGER
);
_
ユーザーごとおよび1日あたり最大1つのレコードが含まれます。 300日間、1日あたり約50万件のレコードがあります。ペイロードは、ユーザーごとに増え続けています(重要な場合)。
特定の日付より前に各ユーザーの最新レコードを効率的に取得したい。私のクエリは次のとおりです。
_SELECT user_id, max(log_date), max(payload)
FROM log
WHERE log_date <= :mydate
GROUP BY user_id
_
これは非常に遅いです。私も試しました:
_SELECT DISTINCT ON(user_id), log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC;
_
同じ計画を持ち、同様に遅いです。
これまでのところ、log(log_date)
には単一のインデックスがありますが、あまり役に立ちません。
そして、すべてのユーザーを含むusers
テーブルがあります。また、一部のユーザー(_payload > :value
_のユーザー)の結果を取得したい。
これを高速化するために使用する必要がある他のインデックス、または私が望むものを達成するための他の方法はありますか?
最適な読み取りパフォーマンスを得るには、 マルチカラムインデックス が必要です。
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST)
インデックスのみのスキャンを可能にするには、必要のない列payload
を追加します。
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload)
なぜDESC NULLS LAST
?
fewuser_id
または小さなテーブルごとの行DISTINCT ON
は通常、最も速くて最も簡単です。
-多くuser_id
ごとの行 index skip scan(またはloose index scan) は(はるかに)より効率的です。これはPostgres 12まで実装されていません- Postgres 13の作業は進行中です 。しかし、それを効率的にエミュレートする方法があります。
共通テーブル式 Postgresが必要8.4 +。LATERAL
にはPostgresが必要です9.3 +。
次のソリューションは、 Postgres Wiki でカバーされているものを超えています。
別のusers
テーブルを使用すると、以下の2。のソリューションは通常、よりシンプルで高速になります。先にスキップします。
LATERAL
joinを使用した再帰CTEWITH RECURSIVE cte AS (
( -- parentheses required
SELECT user_id, log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC NULLS LAST
LIMIT 1
)
UNION ALL
SELECT l.*
FROM cte c
CROSS JOIN LATERAL (
SELECT l.user_id, l.log_date, l.payload
FROM log l
WHERE l.user_id > c.user_id -- lateral reference
AND log_date <= :mydate -- repeat condition
ORDER BY l.user_id, l.log_date DESC NULLS LAST
LIMIT 1
) l
)
TABLE cte
ORDER BY user_id;
これは任意の列を簡単に取得でき、おそらく現在のPostgresで最適です。詳細は2a。の章で説明しています。
WITH RECURSIVE cte AS (
( -- parentheses required
SELECT l AS my_row -- whole row
FROM log l
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC NULLS LAST
LIMIT 1
)
UNION ALL
SELECT (SELECT l -- whole row
FROM log l
WHERE l.user_id > (c.my_row).user_id
AND l.log_date <= :mydate -- repeat condition
ORDER BY l.user_id, l.log_date DESC NULLS LAST
LIMIT 1)
FROM cte c
WHERE (c.my_row).user_id IS NOT NULL -- note parentheses
)
SELECT (my_row).* -- decompose row
FROM cte
WHERE (my_row).user_id IS NOT NULL
ORDER BY (my_row).user_id;
単一列または全行を取得すると便利です。この例では、テーブルの行タイプ全体を使用しています。他のバリエーションも可能です。
前の反復で行が見つかったことをアサートするには、単一のNOT NULL列(主キーなど)をテストします。
このクエリの詳細な説明は第2b章にあります。以下。
関連:
users
テーブルを使用関連するuser_id
ごとに1行だけが保証されている限り、テーブルレイアウトはほとんど問題になりません。例:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
理想的には、テーブルはlog
テーブルと同期して物理的にソートされます。見る:
または、それは重要ではないほど十分に小さい(カーディナリティが低い)。それ以外の場合、クエリ内の行を並べ替えることで、パフォーマンスをさらに最適化できます。 Gang Liangの追加を参照してください。users
テーブルの物理ソート順がlog
のインデックスと一致する場合、これは無関係かもしれません。
LATERAL
参加SELECT u.user_id, l.log_date, l.payload
FROM users u
CROSS JOIN LATERAL (
SELECT l.log_date, l.payload
FROM log l
WHERE l.user_id = u.user_id -- lateral reference
AND l.log_date <= :mydate
ORDER BY l.log_date DESC NULLS LAST
LIMIT 1
) l;
JOIN LATERAL
は、同じクエリレベルで先行するFROM
アイテムを参照できます。見る:
ユーザーごとに1つのインデックス(のみ)のルックアップが行われます。
users
テーブルにないユーザーの行を返しません。通常、参照整合性を強制するforeign key制約はそれを除外します。
また、log
に一致するエントリがないユーザーの行はありません-元の質問に準拠しています。これらのユーザーを結果に保持するには、LEFT JOIN LATERAL ... ON true
の代わりにCROSS JOIN LATERAL
を使用します。
LIMIT n
の代わりにLIMIT 1
を使用して、ユーザーごとに複数の行を取得(ただし、すべてではない) 。
事実上、これらはすべて同じことを行います。
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
ただし、最後の優先順位は低くなっています。明示的なJOIN
はコンマの前にバインドします。この微妙な違いは、結合テーブルが多いほど重要になります。見る:
単一行から単一列を取得するのに適しています。コード例:
複数の列でも同じことが可能ですが、もっとスマートが必要です:
CREATE TEMP TABLE combo (log_date date, payload int);
SELECT user_id, (combo1).* -- note parentheses
FROM (
SELECT u.user_id
, (SELECT (l.log_date, l.payload)::combo
FROM log l
WHERE l.user_id = u.user_id
AND l.log_date <= :mydate
ORDER BY l.log_date DESC NULLS LAST
LIMIT 1) AS combo1
FROM users u
) sub;
上記のLEFT JOIN LATERAL
と同様に、このバリアントにはlog
にエントリがなくてもallユーザーが含まれます。 combo1
に対してNULL
を取得します。必要に応じて、外部クエリのWHERE
句で簡単にフィルタリングできます。
Nitpick:外部クエリでは、サブクエリが行を見つけられなかったか、すべての列の値がNULLであるかを区別できません-同じ結果です。このあいまいさを回避するには、サブクエリにNOT NULL
列が必要です。
相関サブクエリは、単一値のみを返すことができます。複数の列を複合型にラップできます。しかし、後でそれを分解するために、Postgresはよく知られている複合型を要求します。匿名レコードは、列定義リストを提供する場合にのみ分解できます。
既存のテーブルの行タイプのような登録済みタイプを使用します。または、CREATE TYPE
を使用して明示的に(そして永続的に)複合型を登録します。または、一時テーブル(セッションの終了時に自動的に削除される)を作成して、その行タイプを一時的に登録します。キャスト構文:(log_date, payload)::combo
最後に、同じクエリレベルでcombo1
を分解したくありません。クエリプランナの弱点により、これは各列に対してサブクエリを1回評価します(Postgres 12でもまだ当てはまります)。代わりに、サブクエリにし、外部クエリで分解します。
関連:
10万のログエントリと1万のユーザーを使用した4つのクエリすべてのデモ:
db <> fiddle here-pg 11
古い sqlfiddle -pg 9.6
これはスタンドアロンの回答ではなく、@ Erwinの answer へのコメントです。横結合の例である2aの場合、users
テーブルをソートしてlog
のインデックスの局所性を活用することにより、クエリを改善できます。
SELECT u.user_id, l.log_date, l.payload
FROM (SELECT user_id FROM users ORDER BY user_id) u,
LATERAL (SELECT log_date, payload
FROM log
WHERE user_id = u.user_id -- lateral reference
AND log_date <= :mydate
ORDER BY log_date DESC NULLS LAST
LIMIT 1) l;
理由は、user_id
値がランダムである場合、インデックス検索が高価になることです。最初にuser_id
をソートすることにより、後続のラテラル結合はlog
のインデックスの単純なスキャンのようになります。どちらのクエリプランも同じように見えますが、特に大きなテーブルの場合、実行時間は大きく異なります。
user_id
フィールドにインデックスがある場合は特に、ソートのコストは最小限です。
おそらく、テーブル上の別のインデックスが役立つでしょう。これを試してください:log(user_id, log_date)
。 Postgresがdistinct on
。
だから、私はそのインデックスに固執し、このバージョンを試してみました:
select *
from log l
where not exists (select 1
from log l2
where l2.user_id = l.user_id and
l2.log_date <= :mydate and
l2.log_date > l.log_date
);
これにより、ソート/グループ化がインデックス検索に置き換えられます。速いかもしれません。