今天,SQL 中將不再有複雜的情況和複雜的演算法。 一切都會非常簡單,在 Captain Obvious 的水平上 - 讓我們開始吧 查看事件註冊表 按時間排序。
也就是資料庫中有一個標誌 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 條,表明樣本中至少還有其他內容,因此繼續前進是有意義的。
當然,這個值不能「縫」到請求體中,而是透過參數傳遞。 但在這種情況下,PostgreSQL 調度程序將無法依賴記錄相對較少的知識 - 並且很容易選擇無效的計劃。
在應用程式介面中,查看註冊表是透過在視覺「頁面」之間切換來實現的,很長一段時間沒有人注意到任何可疑之處。 直到那一刻,為了方便起見,UI/UX 決定將介面重新設計為「無限滾動」——也就是說,所有登錄項目都繪製在用戶可以上下滾動的單一清單中。
所以,在下一次測試中,你被抓住了 重複記錄 在註冊表中。 為什麼呢,因為表有正常索引 (ts)
,您的查詢依賴哪一個?
正是因為你沒有考慮到這一點 ts
不是唯一的密鑰 在此表中。 事實上,並且 它的值並不唯一,就像現實條件下的任何“時間”一樣- 因此,由於對相同鍵值進行排序的框架內的最終順序不同,兩個相鄰查詢中的相同記錄很容易從一個頁面“跳轉”到另一個頁面。
事實上,這裡還隱藏著第二個問題,這個問題更難被注意到—— 某些條目將不會顯示 根本! 畢竟,「重複」記錄取代了別人的位置。 詳細的解釋和精美的圖片可以找到
擴大索引
狡猾的開發人員知道索引鍵需要唯一,最簡單的方法是用明顯唯一的欄位擴展它,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 條,則它是最後一個。
或同一張圖片:
因為現在我們有 樣本沒有任何特定的“開始”,那麼沒有什麼可以阻止我們向相反的方向「擴展」這個請求,並實現從「參考點」兩個方向(向下和向上)動態載入資料塊。
備註
- 是的,在這種情況下,我們訪問索引兩次,但一切都是“純粹通過索引”。 因此,子查詢只會導致 到一個額外的僅索引掃描.
- 很明顯,只有當你有價值觀時才能使用這種技術
ts
只有偶然才能穿越,並且 數量並不多。 如果您的典型情況是“00:00:00.000 有一百萬筆記錄”,您不應該這樣做。 我的意思是,你不應該允許這樣的情況發生。 但如果發生這種情況,請使用具有擴展索引的選項。
來源: www.habr.com