EMAS Serverless云數據庫使用的是MongoDB。在數據量比較大的情況下,有時候查詢操作會報錯operation exceeded time limit
。本文介紹如何進行查詢優化以避免此類問題。
設置合適的索引
如果您的查詢操作包含了過濾條件(包含等值測試和范圍過濾)或者是排序功能,則要考慮給集合添加索引。
在創建索引時,可以去掉那些沒有選擇性的等值測試字段或范圍過濾字段,以減少索引空間的占用。
針對復雜的查詢語句,包含了等值測試、范圍過濾以及排序功能,這里提供一個建立索引的方法。
索引中字段的順序需要按照等值測試字段、排序字段、范圍過濾字段的方式排列。
若等值測試包含多個字段,它們之間的順序可以任意互換,索引中的升序降序也不影響。
若排序字段包含多個字段,添加到索引中順序需要按照排序中的順序,升序降序也要一致。
若范圍過濾包含多個字段,則優先將基數值(集合中字段不同值的數量)少的字段放到前面。
如果您的集合上有多條索引,尤其是您的查詢語句較復雜時,MongoDB不一定能為您選擇正確的索引,建議您在查詢語句中指定此次查詢過程中使用的索引。
如下所示,您可以創建一個復合索引,依次包含course
,sex
, class
, name
, birthmonth
, score
。course
和sex
屬于等值測試字段,它們需要放在最前面,順序也可以互換。birthmonth
和score
屬于范圍過濾字段,需要放到最后,由于birthmonth
取值范圍是1-12,score的取值范圍是0-100,所以需要把birthmonth
放到score
前面。class
和name
屬于排序字段,應該放在中間,順序和升降序都和排序時一致。
//在分數表中,查詢數學成績大于80分并且下半年出生的男生的記錄,并且按照班級和姓名排序。
mpserverless.db.collection('score').find(
{
course: "Math",
sex: "male",
birthmonth: { $gt: 6 },
score: { $gt: 80 },
},
{
sort: { class: 1, name: 1 },
},
);
大量數據查詢優化
如果您的數據量非常大,在設置合適的索引之后仍然會查詢超時,您要考慮以下優化方案。
盡量避免使用skip,至少不應該skip比較大的值,因為skip操作MongoDB服務端依然會掃描被skip的數據,帶skip操作的耗時和skip的數量線性相關。您可以考慮使用排序和范圍查詢功能來替代直接使用skip。
對于非常大的數據可以分段來查詢,即通過一定的條件將一次查詢拆分為多次查詢操作。
分段計算count
您可以通過findOne+RangeQuery+Skip+Sort的方式來分段計算Count,注意使用該方法需要添加合適的索引。
如下面的代碼示例所示,以_id作為分段列,我們先調用findOne接口,依次查詢第100001,200001,300001....個_id值,當剩余數據條數不足100001條時,findOne返回的result為空,這時可以通過count獲取剩余的記錄數。注意在調用findOne時需要指定查詢條件_id大于等于特定值,需要指定排序規則為按_id升序排列,需要指定查詢跳過100000條記錄;在調用count查詢時需要指定_id值大于最后一次findOne返回的_id。
module.exports = async (ctx) => {
const skip = 100000;
let count = 0;
let minId = { $minKey: 1 };
const collection = ctx.mpserverless.db.collection('collectionName');
while (true) {
const query = { _id: { $gte: minId } };
const options = {
skip,
projection: { _id: 1 },
sort: { _id: 1 },
};
const findOneResult = await collection.findOne(query, options);
const { affectedDocs, success } = findOneResult;
if (!success) {
throw new Error("findOne return success false");
}
if (affectedDocs > 0) {
const newId = findOneResult.result._id;
minId = newId;
count += skip;
} else {
break;
}
}
const query = { _id: { $gte: minId } };
const countResult = await collection.count(query);
const { affectedDocs, success } = countResult;
if (!success) {
throw new Error("count return success false");
}
count += affectedDocs;
return count;
};
遍歷整個集合
您應該避免使用find+Skip+Limit的查詢方式來遍歷整個集合,因為這種方式隨著Skip數量的增長響應時間會越來越慢,還可能會造成請求超時。您可以改為使用find+RangeQuery+Limit+Sort的方式。
下面的代碼給出了一個示例。將數據按_id排列,每次查詢時都指定查詢條件大于上次查詢結果中的最后一條記錄的_id,依次遍歷到最后。
module.exports = async (ctx) => {
const pageSize = 100;
let minId = { $minKey: 1 };
const collection = ctx.mpserverless.db.collection('collectionName');
while (true) {
const query = { _id: { $gt: minId } };
const options = {
limit: pageSize,
sort: { _id: 1 },
};
const findResult = await collection.find(query, options);
const { affectedDocs, success } = findResult;
if (!success) {
throw new Error("find return success false");
}
if (affectedDocs > 0) {
const data = findResult.result;
const lastDoc = data[data.length -1];
minId = lastDoc._id;
//todo 在這里添加處理data列表的邏輯。
} else {
break;
}
}
return 'success';
};
分頁查詢優化
您應該使用上一頁、下一頁、上n頁(其中n是一個比較小的數字)、下n頁的翻頁功能來替換隨機翻頁。您可以參考百度或者谷歌的搜索結果的分頁功能,當結果頁數非常多時,不展示共有多少頁,僅支持在前10頁中支持隨機翻頁;再往下翻頁的過程中,不再支持隨機翻頁,僅支持向下翻一個較小的頁數,這樣就可以在已經查詢出結果的基礎上再使用find+RangeQuery+Skip(少量)+Limit+Sort的方式來快速查詢到結果。例如當您已經查詢出第13頁的數據后,再次查詢第17頁的數據時,就可以把第13頁的最后一條數據作為RangeQuery的判斷條件,跳過3頁的數據行數,再查詢一頁的數據就是第17頁的數據。