使用PyMongo判断MongoDB集合值存在性的优化方法
问题说明
现有MongoDB集合文档结构示例:
[ {"url":"www.example1.com", "name":"hans","age":30}, {"url":"www.example2.com", "name":"x","age":34}, {"url":"www.example3.com", "name":"y","age":35}, {"url":"www.example4.com", "name":"z","age":36}, {"url":"www.example5.com", "name":"b","age":37} ]
需求为两层判断:先校验指定url是否在table1存在,存在的前提下再校验是否在table2存在。原有实现可正确返回结果,但查询耗时过高,需要PyMongo层面的优化方案。
原有实现代码:
val = "www.example1.com" if list(table1.find({"url": {"$eq": val}})): print("exist in table 1") if list(table2.find({"url": {"$eq": val}})): print("exist in table 2") else: print("not exist in table 2") else: print("not exist in table 1")
性能损耗原因
原有写法慢的核心问题有三个:
- 调用
list(table.find(...))会拉取所有匹配文档的全部字段加载到内存,而判断存在性根本不需要name、age这类无关字段,匹配结果越多,网络传输、内存序列化的开销越大 - 未对查询字段
url建索引时,每次查询都要遍历全集合扫描,数据量越大耗时越长 - 没有限制返回结果条数,哪怕已经查到匹配的文档,还是会继续遍历完整个集合才结束查询
优化方案
基础优化:改轻量存在性查询
判断存在性不需要拉取全量结果,两种等效的轻量写法都可以:
- 用
find_one:匹配到第一条符合条件的文档就终止查询,通过投影参数只返回_id字段,不传输多余数据 - 用
count_documents加limit=1参数:统计到1条匹配结果就立刻停止扫描,开销极低
索引优化
给两个集合的url字段建升序索引,等值查询的时间复杂度会从全表扫描的O(n)降到索引查询的O(log n),数据量越大提升越明显:
# background=True表示后台建索引,不阻塞集合的正常读写操作 table1.create_index([("url", 1)], background=True) table2.create_index([("url", 1)], background=True)
如果业务上url在集合内本身就是唯一值,可以加unique=True参数建唯一索引,查询性能还会进一步提升。
优化后完整代码
val = "www.example1.com" # 仅查询_id字段,匹配到第一条结果就返回,无多余开销 exist_t1 = table1.find_one({"url": val}, {"_id": 1}) is not None if exist_t1: print("exist in table 1") exist_t2 = table2.find_one({"url": val}, {"_id": 1}) is not None print("exist in table 2" if exist_t2 else "not exist in table 2") else: print("not exist in table 1")
如果习惯用计数写法,也可以把存在性判断替换为:
exist_t1 = table1.count_documents({"url": val}, limit=1) > 0
两种写法性能基本一致,相比原有的全量转列表写法,在万级以上数据量的集合中性能可以提升几十到上百倍。
可选进阶优化
如果业务逻辑调整为不需要等table1结果再查table2,需要同时获取两个表的存在性结果,可以用并发查询同时向两个集合发请求,总耗时可以降到两个查询中较慢的那个的时长,比串行查询快近一倍。当前需求是table1不存在就跳过table2查询,保持串行逻辑即可。
另外查询条件里的{"$eq": val}可以简写为直接传入val,PyMongo会默认按等值匹配处理,写法更简洁,性能没有区别。
内容的提问来源于stack exchange,提问作者imhans4305
相关产品推荐
相关产品推荐

