最高の読み取りパフォーマンスを得るには、複数列のインデックスが必要です:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
インデックスのみのスキャンを行う 可能であれば、それ以外の場合は不要な列payload
を追加します INCLUDE
を使用したカバーインデックス 条項(Postgres 11以降):
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
参照:
- PostgreSQLでインデックスをカバーすると、列の結合に役立ちますか?
古いバージョンのフォールバック:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
DESC NULLS LAST
が選ばれる理由 ?
- 日付範囲クエリで未使用のインデックス
少数の場合 user_id
あたりの行数 または小さなテーブルDISTINCT ON
通常、最も速くて簡単です:
- 各GROUPBYグループの最初の行を選択しますか?
多くの場合 user_id
あたりの行数 インデックススキップスキャン (またはルーズインデックススキャン )は(はるかに)より効率的です。これはPostgres12までは実装されていません-Postgres14の作業は進行中ですが、効率的にエミュレートする方法があります。
一般的なテーブル式にはPostgres8.4+が必要です 。LATERAL
Postgresが必要9.3+ 。
次のソリューションは、 Postgres Wikiでカバーされているものを超えています 。
1。一意のユーザーを含む個別のテーブルはありません
別のusers
表、2のソリューション。 以下は通常、より単純で高速です。スキップしてください。
1a。 LATERAL
を使用した再帰CTE 参加
WITH 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。の章で詳しく説明します。 以下。
1b。相関サブクエリを使用した再帰CTE
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章を参照してください。以下。
関連:
- 行ごとに最後のN個の関連行をクエリする
- PostgreSQLで別の列で並べ替えるときに1つの列でGROUPBYする
2。個別のusers
テーブル
関連するuser_id
ごとに1行だけであれば、テーブルのレイアウトはほとんど問題になりません。 保証されています。例:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
理想的には、テーブルはlog
と同期して物理的に並べ替えられます テーブル。参照:
- Postgresタイムスタンプクエリ範囲を最適化する
または、それがほとんど問題にならないほど十分に小さい(カーディナリティが低い)。それ以外の場合、クエリ内の行を並べ替えると、パフォーマンスをさらに最適化するのに役立ちます。 GangLiangの追加を参照してください。 users
の物理的な並べ替え順序の場合 テーブルがたまたまlog
のインデックスと一致している 、これは無関係かもしれません。
2a。 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
を参照できます 同じクエリレベルのアイテム。参照:
- LATERAL JOINとPostgreSQLのサブクエリの違いは何ですか?
ユーザーごとに1つのインデックス(のみ)のルックアップが発生します。
users
に欠落しているユーザーの行を返しません テーブル。通常、外部キー 参照整合性を強制する制約はそれを除外します。
また、log
のエントリと一致しないユーザーの行はありません -元の質問に準拠しています。これらのユーザーを結果に残すには、 LEFT JOIN LATERAL ... ON true
を使用します CROSS JOIN LATERAL
の代わりに :
- 配列引数を使用して集合を返す関数を複数回呼び出す
LIMIT n
を使用します LIMIT 1
の代わりに 複数の行を取得する (すべてではありませんが)ユーザーごと。
事実上、これらはすべて同じことをします:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
ただし、最後のものの優先度は低くなります。明示的なJOIN
カンマの前にバインドします。その微妙な違いは、結合テーブルが増えると問題になる可能性があります。参照:
- Postgresクエリの「テーブルのFROM句エントリへの無効な参照」
2b。相関サブクエリ
単一の列を取得するのに適しています 単一行から 。コード例:
- グループごとの最大クエリを最適化する
複数の列でも同じことが可能です。 、しかしあなたはもっと賢い必要があります:
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
にエントリがなくてもユーザー 。 NULL
を取得します combo1
の場合 、WHERE
で簡単にフィルタリングできます 必要に応じて、外部クエリの句。
Nitpick:外部クエリでは、サブクエリで行が見つからなかったのか、すべての列の値がNULLであるのかを区別できません。同じ結果です。 NOT NULL
が必要です このあいまいさを避けるために、サブクエリの列。
相関サブクエリは、単一の値のみを返すことができます 。複数の列を複合型にラップできます。しかし、後でそれを分解するために、Postgresはよく知られている複合型を要求します。匿名レコードは、列定義リストを提供する場合にのみ分解できます。
既存のテーブルの行タイプのように、登録されたタイプを使用してください。または、複合型をCREATE TYPE
に明示的に(そして永続的に)登録します 。または、一時テーブル(セッションの終了時に自動的に削除される)を作成して、その行タイプを一時的に登録します。キャスト構文:(log_date, payload)::combo
最後に、combo1
を分解したくありません 同じクエリレベルで。クエリプランナーの弱点により、これはサブクエリを列ごとに1回評価します(Postgres 12でも当てはまります)。代わりに、それをサブクエリにして、外部クエリで分解します。
関連:
- グループごとに最初と最後の行から値を取得します
10万のログエントリと1kのユーザーを使用した4つのクエリすべてのデモンストレーション:
db <> fiddle here --pg 11
古いsqlfiddle