三個(gè)巧技,讓分庫分表 LIMIT 翻頁性能直接拉滿!
線上出了個(gè)離譜問題:運(yùn)營同學(xué)在后臺導(dǎo)出近 3 個(gè)月訂單時(shí),點(diǎn)擊分頁到第 100 頁,直接把數(shù)據(jù)庫查崩了。排查后發(fā)現(xiàn),代碼里寫了 LIMIT 9900, 100,在分庫分表場景下,這行 SQL 相當(dāng)于讓 8 個(gè)分片各查 1 萬條數(shù)據(jù),再拉到應(yīng)用層內(nèi)存排序,直接把內(nèi)存溢出了。
其實(shí)分庫分表的分頁查詢,藏著很多反常識的奇技淫巧,但這些技巧都有嚴(yán)格的適用邊界,用錯(cuò)了反而會(huì)埋坑。我來分享 3 個(gè)實(shí)戰(zhàn)中驗(yàn)證過的騷操作。
分庫分表 LIMIT 是性能殺手
單表分頁用 LIMIT offset, size 沒問題,但分庫分表后,數(shù)據(jù)散在多個(gè)分片里,比如你要查 LIMIT 10000, 10(第 1001 頁),會(huì)發(fā)生兩件離譜的事:
全分片掃描:每個(gè)分片都要執(zhí)行 LIMIT 0, 10010(因?yàn)椴恢榔渌制臄?shù)據(jù)分布,只能把前 10010 條都查出來,避免漏數(shù)據(jù));
內(nèi)存爆炸排序:假設(shè) 8 個(gè)分片,每個(gè)返回 10010 條,共 8 萬多條數(shù)據(jù),全拉到應(yīng)用層排序,再截取第 10000-10010 條。內(nèi)存和 CPU 直接飆紅,我之前見過最夸張的案例,offset=100000 時(shí),一個(gè)分頁請求耗時(shí) 12 秒,直接觸發(fā)服務(wù)熔斷。
所以我們下邊要講的都是繞開全分片掃描 + 內(nèi)存排序,但每個(gè)方案的適用場景天差地別,核心原則:不盲目追高性能,先看業(yè)務(wù)場景是否匹配。
錨點(diǎn)分頁
錨點(diǎn)分頁,性能最優(yōu),但僅限加載更多場景。
這是我最常用的技巧,核心思路是用數(shù)據(jù)本身的有序字段當(dāng)錨點(diǎn),替代 offset,比如按自增 ID 或時(shí)間戳分頁。但注意:不是所有有序字段都能用,必須滿足分片內(nèi) + 分片間都有序。
按 ID 范圍分片
假設(shè)訂單表按 ID 范圍分 3 個(gè)分片:
- 分片 1:ID 1-10000(分片內(nèi)有序,且小于分片 2 的 ID);
- 分片 2:ID 10001-20000(同理);
- 分片 3:ID 20001-30000(同理);
要查第 2 頁(10 條 / 頁),步驟如下:
- 查第 1 頁時(shí):執(zhí)行
ORDER BY id DESC LIMIT 10,拿到最后一條數(shù)據(jù)的 ID 是last_id=100(這個(gè) ID 就是錨點(diǎn)); - 查第 2 頁時(shí):直接用
WHERE id < 100 ORDER BY id DESC LIMIT 10; - 查第 3 頁時(shí):再用第 2 頁最后一條的 ID(比如 90)當(dāng)錨點(diǎn),執(zhí)行
WHERE id < 90 ORDER BY id DESC LIMIT 10。
為什么性能高?
每個(gè)分片都能獨(dú)立執(zhí)行 WHERE id < xxx LIMIT 10,只返回 10 條數(shù)據(jù)(不用查前 N 條)。比如查第 1001 頁,每個(gè)分片也只返回 10 條,匯總后排序取 10 條,網(wǎng)絡(luò)和內(nèi)存開銷直接降為原來的 1/1000。
必避的 2 個(gè)坑:
- 別用哈希分片:如果按ID mod 3哈希分片,分片 1 的 ID 可能是 3、6、9...,分片 2 是 1、4、7...,此時(shí) ID 全局有序但分片內(nèi)無序,執(zhí)行
WHERE id < 100仍需全量掃描分片內(nèi)數(shù)據(jù),退化為 “內(nèi)存聚合”; - 不支持跳頁:只
加載更多(下一頁依賴上一頁的錨點(diǎn)),無法直接從第 1 頁跳到第 100 頁。但可以通過產(chǎn)品設(shè)計(jì)規(guī)避,比如抖音、小紅書的列表都是加載更多,用戶體驗(yàn)反而更好。
分片標(biāo)記法
剛才的錨點(diǎn)分頁不支持跳頁,但有些場景比如后臺管理系統(tǒng),又必須要跳頁,怎么辦?我之前在電商后臺做訂單導(dǎo)出時(shí),用過分片標(biāo)記法,核心是給每個(gè)庫、表記錄數(shù)據(jù)范圍和總量,快速定位目標(biāo)頁在哪個(gè)分片。
分片標(biāo)記法支持跳頁,但必須控制元數(shù)據(jù)一致性。
用法示例
假設(shè)訂單表按用戶 ID 范圍分 2 庫,每庫按時(shí)間分 12 表(如庫 1 - 表 202401、庫 1 - 表 202402...),先在 Redis 里維護(hù) 庫、表級別的元數(shù)據(jù):
庫 - 表 | 起始 ID | 結(jié)束 ID | 數(shù)據(jù)總量 |
庫 1 - 表 202401 | 1 | 5000 | 5000 |
庫 1 - 表 202402 | 5001 | 12000 | 7000 |
庫 2 - 表 202401 | 12001 | 18000 | 6000 |
.... | .... | ..18000 | 6000 |
現(xiàn)在要查 LIMIT 15000, 10(第 1501 頁),步驟如下:
- 查元數(shù)據(jù)定位庫、表:計(jì)算累計(jì)數(shù)據(jù)量,庫 1 - 表 202401(5000)+ 庫 1 - 表 202402(7000)= 12000 <15000,再加上庫 2 - 表 202401 的 6000,累計(jì) 18000>15000,所以目標(biāo)在庫 2 - 表 202401;
- 計(jì)算表內(nèi)偏移量:表內(nèi)偏移量 = 15000 - 12000 = 3000,所以庫 2 - 表 202401 執(zhí)行
LIMIT 3000, 10; - 直接返回結(jié)果:因?yàn)閹臁⒈戆?ID 有序,查詢結(jié)果就是全局第 15000-15010 條,不用匯總其他分片。
必避的 2 個(gè)坑:
1.元數(shù)據(jù)必須實(shí)時(shí)但不能強(qiáng)同步:
數(shù)據(jù)新增、刪除時(shí),要同步更新 Redis 元數(shù)據(jù),但高并發(fā)下不能加分布式鎖(會(huì)卡住業(yè)務(wù)),建議用定時(shí) + 增量日志:每 5 分鐘全量統(tǒng)計(jì)一次,同時(shí)記錄增量(如新增 100 條、刪除 10 條),查詢時(shí)疊加增量;
若允許最終一致性(如后臺查詢允許誤差 10 條),這個(gè)方案很穩(wěn);若要強(qiáng)一致,只能放棄跳頁,用錨點(diǎn)分頁;
2.不支持非分片鍵排序:如果要按支付時(shí)間排序(分片鍵是用戶 ID),支付時(shí)間在 “庫 - 表” 內(nèi)無序,元數(shù)據(jù)無法定位,仍需全分片掃描。
反向分頁
反向分頁,僅適用于查最后 1 頁,別亂用
這個(gè)技巧最反常識,但局限性也最大。如果要查最后幾頁數(shù)據(jù)(比如用戶查最早的訂單),用普通分頁會(huì)查 LIMIT 9990, 10,但可以反向查,避開大 offset。
僅查最后 1 頁
假設(shè)訂單表按 ID 范圍分片,總數(shù)據(jù)量 10000 條(1000 頁,10 條 / 頁),要查最后 1 頁(ID 9991-10000):
- 反向查錨點(diǎn):執(zhí)行
ORDER BY id ASC LIMIT 10,拿到最前面 10 條的 ID(1-10),取最大 ID 作為反向錨點(diǎn)(10); - 查最后 1 頁:執(zhí)行
WHERE id > 10 ORDER BY id DESC LIMIT 10,拿到的就是 ID 10000-9991(最后 10 條); - 調(diào)整順序:如果需要正序展示,把結(jié)果再倒過來即可。
為什么能生效?
因?yàn)?nbsp;LIMIT 0, 10 比 LIMIT 9990, 10 快 100 倍. 每個(gè)分片查前 10 條數(shù)據(jù),匯總后取最大的 10 個(gè) ID 作為錨點(diǎn),再查大于錨點(diǎn)的數(shù)據(jù),避免了大 offset 掃描。
必避的 2 個(gè)坑:
- 僅適用于最后 1 頁:如果要查倒數(shù)第 10 頁(第 991 頁),按這個(gè)邏輯無法定位錨點(diǎn)(需要知道第 9900 條數(shù)據(jù)的 ID),只能查倒數(shù)第 1 頁;
- 數(shù)據(jù)不能有大量刪除:如果中間有大量 ID 被刪除(如 ID 5000-8000 都被刪了),總數(shù)據(jù)量變?yōu)?7000 條,此時(shí)
WHERE id >10 ORDER BY id DESC LIMIT10拿到的是 7000-6991(正確),但如果刪除的是最后 100 條(ID 9901-10000),總數(shù)據(jù)量變?yōu)?9900 條,需要重新計(jì)算反向錨點(diǎn),增加復(fù)雜度。
說在后邊
其實(shí)分庫分表分頁的核心不是炫技,而是在業(yè)務(wù)和技術(shù)之間找平衡。能通過產(chǎn)品設(shè)計(jì)規(guī)避跳頁(用加載更多),就優(yōu)先用錨點(diǎn)分頁(性能最優(yōu)),必須跳頁就用分片標(biāo)記法(接受最終一致性);實(shí)在沒辦法才考慮中間件(如 ShardingSphere 的全局排序)。































