凡事做過(guò)頁(yè)面的驮捍,一般對(duì)分頁(yè)不會(huì)陌生,也不會(huì)覺(jué)得它有多難:就是limit + offset的組合就可以了呀脚曾。但是东且,危險(xiǎn)往往都是從最不起眼的地方開(kāi)始的。在這里本讥,我先說(shuō)一下我之前在用MongoDB時(shí)遇到的問(wèn)題苇倡。這類問(wèn)題同樣會(huì)出現(xiàn)在這種分頁(yè)方式上。
當(dāng)時(shí)囤踩,我需要對(duì)于MongoDB中的數(shù)據(jù)進(jìn)行處理旨椒,每次處理一批,也相當(dāng)于是按頁(yè)來(lái)操作數(shù)據(jù)啦堵漱。這個(gè)沒(méi)啥難度综慎,直接使用API中的find + skip + limit就可以輕易搞定。迅速把程序?qū)懲曛缶烷_(kāi)始拿產(chǎn)品庫(kù)開(kāi)搞了勤庐。剛開(kāi)始一切正常示惊,可過(guò)了沒(méi)多久,就發(fā)現(xiàn)整個(gè)程序的性能下降了愉镰。進(jìn)入Mongo一查米罚,發(fā)現(xiàn)是Table Scan。哇丈探,那個(gè)collection中有上千萬(wàn)的數(shù)據(jù)奥荚瘛!
此處略去3000字碗降。
總之隘竭,問(wèn)題最后解決了,程序又運(yùn)行如飛讼渊。而解決之道很簡(jiǎn)單:只用find + limit动看,不再使用skip(原因自己想)。只不過(guò)在find中加了一個(gè)條件:上一批的最后一個(gè)document的_id爪幻。整個(gè)代碼形似(groovy代碼):
if (docId) {
batch = collection.find(['_id': ['$gt': docId]] as Document).limit(BATCH_SIZE)
} else {
batch = collection.find().limit(BATCH_SIZE)
}
docId = batch[-1]['_id']
它的原理很簡(jiǎn)單菱皆,其實(shí)就是利用可以利用的index來(lái)加速分頁(yè)须误。這種思想跟今天看到的文章的思路如出一轍,不再使用offset仇轻,尋找能達(dá)到同樣效果的index京痢,用它來(lái)助力搜索。因此拯田,文中給出的方案跟上面的代碼類似:
SELECT ...
FROM ...
WHERE ...
AND id < ?last_seen_id
ORDER BY id DESC
FETCH FIRST 10 ROWS ONLY
這種分頁(yè)方式被稱為“seek method”历造,其中的id被稱為“seek predicate”甩十。典型的seek predicate還可以是日期船庇。需要提醒的是,seek predicate上需要有index才有意義侣监,而且它可以有多列鸭轮!采用這種方式的分頁(yè)可以避免上述分頁(yè)的潛在危險(xiǎn):當(dāng)頁(yè)數(shù)達(dá)到一定量之后,分頁(yè)速度會(huì)嚴(yán)重下降橄霉。
關(guān)于seek method窃爷,還可以參考下面的文章: