Django ORM查询性能测试:指定字段查询慢于全字段查询排查
Django ORM查询性能测试问题解答
一、当前测试方法的合理性判断
你当前的测试方法存在核心缺陷,得到的结果完全不具备SQL性能参考价值,核心问题如下:
- 最根本的错误:Django QuerySet是懒加载机制,你写的两条测试语句仅在Python层构造了QuerySet对象,根本没有实际向数据库发送SQL执行。你统计到的耗时完全是Django ORM内部构造对象的Python层开销,和SQL查询效率没有任何关系。这也是为什么
values('status')看起来更慢——values()方法本身会额外执行字段校验、返回结果结构转换的逻辑,Python层开销比普通QuerySet更高。 - 测试样本量过小:100次执行总耗时仅10ms左右,操作系统进程调度、Python GC、数据库连接初始化的随机干扰占比极高,结果波动范围会远大于两个查询本身的性能差。
- 缓存干扰未排除:Sqlite是文件型数据库,查询命中后数据会存入数据库自身缓存、操作系统页缓存,后续重复查询几乎不会产生磁盘IO,无法反映真实业务场景下的性能表现。
- 未做预热处理:测试前没有提前执行初始化逻辑,冷启动产生的一次性开销会混入统计结果。
二、更准确的Django ORM性能测试方案
按照以下流程调整测试,可以得到更贴近真实情况的性能对比结果:
- 第一步:强制触发QuerySet求值,确保SQL真实执行。在所有测试语句外层套
list(),遍历QuerySet的过程会强制触发SQL执行,修正后的测试语句示例:
# 错误写法:仅构造QuerySet,不执行SQL # stmt1 = """AwsConsoleAccess.objects.filter(request_id='8548a2d5-4bb7-4fa9-add2-a41219dc8785')""" # 正确写法:转成列表强制触发SQL执行 stmt1 = """list(AwsConsoleAccess.objects.filter(request_id='8548a2d5-4bb7-4fa9-add2-a41219dc8785'))""" stmt2 = """list(AwsConsoleAccess.objects.filter(request_id='8548a2d5-4bb7-4fa9-add2-a41219dc8785').values('status'))"""
- 测试前增加预热环节:先连续执行3-5次待测试的查询,完成数据库连接建立、模块加载、缓存预热,排除冷启动一次性开销的干扰。
- 提高测试迭代量级:将
timeit的number参数调整到1000次以上,让总测试耗时达到秒级,降低系统随机波动的影响;同时可以重复跑3-5组测试取平均值,避免单次运行的偶然误差。 - 分层统计耗时:如果需要拆分ORM层开销和数据库执行开销,可以用Django自带的连接工具统计单条SQL的真实执行时间,排除Python层逻辑的干扰,示例代码:
from django.db import connection, reset_queries from .models import AwsConsoleAccess reset_queries() # 执行目标查询 list(AwsConsoleAccess.objects.filter(request_id='8548a2d5-4bb7-4fa9-add2-a41219dc8785').values('status')) # 输出最后一条执行的SQL、耗时 print(connection.queries[-1])
- 控制缓存变量:如果要测试冷查询性能,每次测试前需要清理数据库缓存、操作系统页缓存(清理操作需要对应系统权限,仅建议在本地测试环境执行);如果测试热查询性能,要保证两组测试执行前的缓存状态完全一致。
补充说明:当查询条件命中唯一索引(比如你测试用的request_id字段如果加了唯一索引)时,数据库定位目标行的开销占总查询耗时的90%以上,此时返回单字段和返回全字段的性能差异本身就极小。只有当查询返回的结果行数多、表中存在大文本/二进制大字段时,
values()指定返回字段的性能优势才会明显体现。
内容的提问来源于stack exchange,提问作者jebaseelan ravi
相关产品推荐
相关产品推荐

