性能优化后MongoDB查询函数返回结果不一致,求原因分析
问题背景
优化MongoDB查询函数时发现:旧函数单次调用耗时约32秒,新函数仅需350毫秒,但二者返回结果不一致,需明确差异原因。
函数参数
- md5Hashes:字符串数组
- excludedTextId:字符串
函数功能
基于以下Mongoose Schema查询MongoDB:
const sentenceFeatureSchema = new mongoose.Schema({ text_id: { type: String, required: true }, md5_hash: { type: String, required: true }, offset: { type: Number, required: true }, length: { type: Number, required: true }, num_words: { type: Number, required: true }, });
要求:
- 查询匹配给定md5哈希数组的所有文档
- 若提供excludedTextId,排除text_id与之相同的文档
- 哈希与text_id无排他性,允许同哈希同text_id但其他字段不同的文档
- 仅查询所需数据,避免冗余
新旧函数实现
旧函数(循环多轮查询)
const getMatchingHashSentences = async (md5Hashes, excludedTextId = null) => { try { const entries = []; for (const md5Hash of md5Hashes) { const queryConditions = { md5_hash: md5Hash }; if (excludedTextId) { queryConditions.text_id = { $ne: excludedTextId }; } const result = await SentenceFeature.find(queryConditions); entries.push(...result); } logger.debug(`entries length: ${entries.length}`); return entries; } catch (error) { throw error; } };
新函数(单次批量查询)
const getMatchingHashSentences = async (md5Hashes, excludedTextId = null) => { try { const query = SentenceFeature.find({ md5_hash: { $in: md5Hashes } }); if (excludedTextId) { query.where("text_id").ne(excludedTextId); } const results = await query.exec(); return results; } catch (error) { throw error; } };
测试情况
小数据量测试下二者结果一致,但大数据量场景出现差异,推测存在边缘触发条件。测试步骤如下:
环境搭建
mkdir demo && cd demo npm init -y && npm i mongoose mongodb
测试代码(demo.js)
const mongoose = require("mongoose"); // Define schema const sentenceFeatureSchema = new mongoose.Schema({ text_id: { type: String, required: true }, md5_hash: { type: String, required: true }, offset: { type: Number, required: true }, length: { type: Number, required: true }, num_words: { type: Number, required: true }, }); // Define model const SentenceFeature = mongoose.model("features", sentenceFeatureSchema); const getMatchingHashSentences = async (md5Hashes, excludedTextId = null) => { try { const entries = []; for (const md5Hash of md5Hashes) { const queryConditions = { md5_hash: md5Hash }; if (excludedTextId) { queryConditions.text_id = { $ne: excludedTextId }; } const result = await SentenceFeature.find(queryConditions); entries.push(...result); } return entries; } catch (error) { throw error; } }; const getMatchingHashSentences2 = async (md5Hashes, excludedTextId = null) => { try { const query = SentenceFeature.find({ md5_hash: { $in: md5Hashes } }); if (excludedTextId) { query.where("text_id").ne(excludedTextId); } const results = await query.exec(); return results; } catch (error) { throw error; } }; async function main() { try { await mongoose.connect("mongodb://localhost:27017/mydatabase", { useNewUrlParser: true, useUnifiedTopology: true, }); console.log("Connected to MongoDB"); for (let i = 1; i <= 10; i++) { const feature = new SentenceFeature({ text_id: `${i}`, md5_hash: `h${i}`, offset: 0, length: 0, num_words: 0, }); await feature.save(); console.log(`Saved Feature ${i}`); } const oldres = await getMatchingHashSentences(["h1", "h3", "h6"], "3"); console.log("old:", oldres); const newres = await getMatchingHashSentences2(["h1", "h3", "h6"], "3"); console.log("new:", newres); } catch (error) { console.error("Error:", error); } finally { await mongoose.disconnect(); console.log("Disconnected from MongoDB"); } } main();
启动MongoDB
docker run --rm -d -p 27017:27017 --name my-mongodb mongo
运行测试
node demo.js
差异原因分析
从逻辑上看,新旧函数的查询条件等价,但大数据量下出现差异,核心原因大概率是以下几点:
跨查询数据变更:旧函数循环执行多次查询,期间可能有其他操作对数据库进行插入/更新/删除,导致不同批次查询的结果集不一致;新函数是单次原子查询,基于同一数据快照,不会受此影响。
结果排序差异:旧函数的结果是按循环中每个md5Hash的查询结果依次拼接,顺序由md5Hashes数组顺序决定;新函数的结果是MongoDB默认的文档存储顺序(或索引顺序),二者结果顺序不同,但内容应一致。如果是顺序差异被误认为结果不一致,属于正常现象。
游标分页/截断问题:当单批次查询结果量极大时,Mongoose的
find可能隐式触发游标分页(尽管默认返回全部),旧函数多次查询的游标处理可能与新函数单次查询存在差异,导致部分文档遗漏或重复。索引执行计划差异:大数据量下,MongoDB查询优化器对
$in查询和多次单值查询可能生成不同的执行计划,比如在分片集群中,$in查询可能涉及更多分片,而多次单值查询可能因路由策略差异获取不同结果。
验证与解决方案
快照一致性验证:在同一事务中执行新旧函数,确保两次查询基于同一数据快照,排除数据变更影响。MongoDB支持快照读(添加
{ snapshot: true }选项),可在旧函数的find中添加该参数,再对比结果。结果内容对比:提取新旧函数返回结果的
_id集合,对比是否存在缺失或重复的文档,明确差异是内容问题还是顺序问题。执行计划分析:使用
explain()查看新旧函数的查询执行计划,确认是否因索引或路由策略导致结果差异。
结论
新函数的逻辑完全符合需求,性能优势显著。大数据量下的结果差异几乎不可能是逻辑错误导致,更可能是旧函数多次查询期间的数据变更、结果顺序差异或游标处理差异引发。建议通过快照读验证一致性,若确认是数据变更问题,新函数的单次查询更能保证结果的一致性。
内容的提问来源于stack exchange,提问作者no_rasora

