現在、SQL には複雑なケースや高度なアルゴリズムは存在しません。 キャプテン・オブビアスのレベルでは、すべてが非常に簡単になります - やってみましょう イベントレジストリの表示 時間順に並べ替えます。
つまり、データベースにサインがあります events
、そして彼女は畑を持っています ts
- これらのレコードを順番に表示したい正確な時刻:
CREATE TABLE events(
id
serial
PRIMARY KEY
, ts
timestamp
, data
json
);
CREATE INDEX ON events(ts DESC);
そこに十数件のレコードが存在しないことは明らかなので、何らかの形式の ページナビゲーション.
#0。 「私は母のポグロミストです」
cur.execute("SELECT * FROM events;")
rows = cur.fetchall();
rows.sort(key=lambda row: row.ts, reverse=True);
limit = 26
print(rows[offset:offset+limit]);
それはほとんど冗談ではありません。まれですが、野生で発見されます。 ORM を使用した後、SQL を使用した「直接」作業に切り替えるのが難しい場合があります。
#1. オフセット
SELECT
...
FROM
events
ORDER BY
ts DESC
LIMIT 26 OFFSET $1; -- 26 - записей на странице, $1 - начало страницы
26という数字はどこから来たのでしょうか? これは、25 つの画面を満たすエントリのおおよその数です。 より正確には、表示された 1 レコードに XNUMX レコードを加えたもので、サンプル内に少なくともさらに何か別のものがあり、先に進むのが合理的であることを示しています。
もちろん、この値はリクエストの本文に「縫い込む」ことはできませんが、パラメーターを通じて渡されます。 ただし、この場合、PostgreSQL スケジューラは、レコードが比較的少ないはずであるという知識に頼ることができず、簡単に非効率なプランを選択してしまいます。
また、アプリケーション インターフェイスでは、レジストリの表示が視覚的な「ページ」間の切り替えとして実装されていますが、長い間誰も不審な点に気づきません。 利便性との闘いの中で、UI/UX がインターフェイスを「無限スクロール」に作り直すことを決定するその瞬間まで、つまり、すべてのレジストリ エントリが単一のリストに描画され、ユーザーが上下にスクロールできるようになります。
そして、次のテスト中にあなたは捕らえられます 記録の重複 レジストリにあります。 なぜかというと、テーブルには通常のインデックスがあるからです。 (ts)
、クエリはどれに依存していますか?
まさにそれを考慮していなかったからだ ts
一意のキーではありません この表では。 実は、そして その値は一意ではありません、実際の状況における他の「時間」と同様に、同じキー値をソートするフレームワーク内の最終順序が異なるため、XNUMX つの隣接するクエリ内の同じレコードがページからページへと簡単に「ジャンプ」します。
実際、ここにはさらに気づきにくい XNUMX つ目の問題が隠されています。 一部のエントリは表示されません 全然! 結局のところ、「重複」記録が他の誰かの代わりになったのです。 美しい写真付きの詳しい説明が見られます
インデックスの展開
狡猾な開発者は、インデックス キーを一意にする必要があることを理解しています。最も簡単な方法は、明らかに一意なフィールドでインデックス キーを拡張することです。PK は次の場合に最適です。
CREATE UNIQUE INDEX ON events(ts DESC, id DESC);
そしてリクエストは次のように変化します。
SELECT
...
ORDER BY
ts DESC, id DESC
LIMIT 26 OFFSET $1;
#2. 「カーソル」に切り替えます
しばらくして、DBA があなたのところにやって来て、あなたの要求に「喜んで」くれます。
SELECT
...
WHERE
(ts, id) < ($1, $2) -- последние полученные на предыдущем шаге значения
ORDER BY
ts DESC, id DESC
LIMIT 26;
それが来るまで、あなたは安堵のため息をつきました...
#3. インデックスのクリーニング
なぜなら、ある日、DBA が読んだからです。 (ts DESC)
.
しかし、ページ間でレコードを「ジャンプ」するという最初の問題はどうすればよいでしょうか?.. そしてすべては簡単です - 固定されていない数のレコードを持つブロックを選択する必要があります。
一般に、「ちょうど 26」ではなく「26 未満ではない」と読むことを誰が禁じているでしょうか。 たとえば、次のブロックには次のようになります。 明らかに異なる意味を持つ記録 ts
- そうすれば、ブロック間でレコードを「ジャンプ」しても問題はありません。
これを実現する方法は次のとおりです。
SELECT
...
WHERE
ts < $1 AND
ts >= coalesce((
SELECT
ts
FROM
events
WHERE
ts < $1
ORDER BY
ts DESC
LIMIT 1 OFFSET 25
), '-infinity')
ORDER BY
ts DESC;
何が起きてる?
- 25 個のレコードを「ダウン」してステップを実行し、「境界」値を取得します。
ts
. - そこにすでに何もない場合は、NULL 値を次の値に置き換えます。
-infinity
. - 受信した値の間の値のセグメント全体を減算します
ts
およびインターフェイスから渡された $1 パラメーター (前回の「最後に」レンダリングされた値)。 - 返されたブロックのレコード数が 26 未満の場合、それは最後のブロックです。
または同じ写真:
なぜなら今私たちは サンプルには特定の「始まり」がありません。そうすると、このリクエストを逆方向に「拡張」し、「参照ポイント」から両方向 (下方向と上方向) にデータ ブロックを動的にロードすることを妨げるものはありません。
注意:
- はい、この場合、インデックスに XNUMX 回アクセスしますが、すべては「純粋にインデックスによって」行われます。 したがって、サブクエリの結果は次のとおりです。 追加のインデックスのみのスキャンに XNUMX つ.
- この手法は、値がある場合にのみ使用できることは明らかです。
ts
偶然にしか渡れないし、 それらはあまり多くありません。 典型的なケースが「00:00:00.000 に XNUMX 万件のレコード」である場合は、これを行うべきではありません。 つまり、そのような事件が起こることを許すべきではありません。 ただし、このような場合は、拡張インデックスを備えたオプションを使用してください。
出所: habr.com