Django 统计接口性能优化
背景描述:
场景:
app_model接口用于展示应用模型的权限列表,并附加该模型在“昨日”和“本月”的调用统计数据。
现象:随着权限数据量的增加,接口响应时间呈线性增长,耗时严重,用户体验极差。
原始逻辑:
- 使用 DRF 的
Serializer序列化出权限列表(result_data)。- 遍历该列表,在
for循环中针对每一条记录,分别执行 2 次 SQL 查询(查昨日、查本月)。
性能瓶颈分析
通过代码审查,定位到以下核心性能杀手:
- 循环内执行 SQL (N+1 问题):
- 这是最严重的瓶颈。假设列表有 N 个模型权限,总查询次数为 1+2N。
- 如果返回 50 条数据,就会额外执行 100 次数据库交互。网络 IO 和 数据库连接开销巨大。
- DRF Serializer 性能开销:
ModelPermissionSerializer(many=True)在数据量大时,对象实例化和字段校验会消耗大量 CPU 时间。对于只读接口,这是不必要的开销。
- 缺乏数据库索引:
- 统计表
model_expense_day如果没有针对查询字段的联合索引,每次SELECT SUM都会导致全表扫描或低效扫描。
- 统计表
优化方案
策略一:批量查询 + 内存聚合(解决 N+1)
思路:将循环内的多次查询合并为“一次批量查询”。
- 提取所有涉及的
app_id,model,env。 - 使用 SQL 的
IN语句一次性查出本月所有相关日志。 - 在 Python 内存中利用
HashMap(字典) 进行数据的分组和累加。
策略二:绕过 Serializer(降低 CPU 消耗)
思路:对于纯读取展示的接口,使用 Django ORM 的 .values() 方法直接获取字典列表,避免 Serializer 的序列化开销,提升速度可达 10 倍。
策略三:数据库索引优化
思路:确保统计表有正确的联合索引。
- SQL建议:
CREATE INDEX idx_app_model_env_date ON model_expense_day (app_id, model, env, stats_date);
重构后的代码实现
def app_model(self, request):
""" 应用模型信息 - 修复版 """
from datetime import datetime, timedelta
from django.db import connections
from django.db.models import Q
app_param = request.query_params.get('app')
model_name = request.query_params.get('model')
envs = request.query_params.get('env')
filters = {}
if not request.user.is_staff:
perm_filter = (
Q(managers__contains=request.user.username) |
Q(user=request.user) |
Q(readers__contains=request.user.username)
)
# app的uuid
valid_app_ids = Application.objects.filter(perm_filter).values_list('id', flat=True)
filters['app__id__in'] = valid_app_ids
if app_param:
if app_param.isdigit():
# 直接筛选 ModelPermission 关联的 app_id 字段 (注意这里 app_id 字面意思是业务ID)
filters['app__app_id__icontains'] = app_param
else:
filters['app__app_name__icontains'] = app_param
if model_name:
filters['model__name__icontains'] = model_name
if envs:
filters['env'] = envs
try:
queryset = ModelPermission.objects.filter(**filters).values(
'app__app_id',
'app__app_name',
'app__id',
'model__name',
'model__id',
'model__provider',
'env',
'limit_qps',
'create_time',
'update_time'
)
except Exception as e:
print(f"AppModel View Query Error: {str(e)}")
return make_rsp(500, 'Internal Server Error', [])
result_data = []
stats_keys = set()
for item in queryset:
c_time = item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] else None
u_time = item['update_time'].strftime('%Y-%m-%d %H:%M:%S') if item['update_time'] else None
app_business_id = item['app__app_id'] or ''
model_real_name = item['model__name'] or ''
env_val = item['env'] or ''
row = {
"app_id": app_business_id,
"app_name": item['app__app_name'] or '',
"app_uuid": item['app__id'] or '',
"model_name": model_real_name,
"model_id": item['model__id'] or '',
"model_provider": item['model__provider'] or '',
"env": env_val,
"limit_qps": item['limit_qps'],
"create_time": c_time,
"update_time": u_time,
"yesterday_req": 0.0,
"month_req": 0.0
}
result_data.append(row)
if app_business_id and model_real_name and env_val:
stats_keys.add((app_business_id, model_real_name, env_val))
if not result_data:
return make_rsp(1, 'success', [])
if stats_keys:
yesterday_str = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
start_of_month_str = datetime.now().replace(day=1).strftime('%Y-%m-%d')
env_map = {
'prd': 'model_expense_prd',
'pre': 'model_expense_pre',
'test': 'model_expense_test'
}
db_alias = env_map.get(settings.PROJECT_ENV, 'model_expense_test')
raw_sql = """
SELECT
app_id,
model,
env,
SUM(CASE WHEN stats_date = %s THEN req_count ELSE 0 END) as yesterday_req,
SUM(CASE WHEN stats_date >= %s THEN req_count ELSE 0 END) as month_req
FROM model_expense_day
WHERE (app_id, model, env) IN %s
AND stats_date >= %s
GROUP BY app_id, model, env
"""
params = [
yesterday_str,
start_of_month_str,
tuple(stats_keys),
start_of_month_str
]
stats_map = {}
try:
with connections[db_alias].cursor() as cursor:
cursor.execute(raw_sql, params)
rows = cursor.fetchall()
for r in rows:
key = (r[0], r[1], r[2])
y_req = float(r[3]) if r[3] is not None else 0.0
m_req = float(r[4]) if r[4] is not None else 0.0
stats_map[key] = (y_req, m_req)
except Exception as e:
print(f"Stats query error: {e}")
for row in result_data:
key = (row['app_id'], row['model_name'], row['env'])
if key in stats_map:
row['yesterday_req'] = stats_map[key][0]
row['month_req'] = stats_map[key][1]
return make_rsp(0, '', result_data)
优化总结
通过此次优化,我们将接口的时间复杂度从 O(N)O(N) 的数据库交互降低到了 O(1)O(1)(固定相关查询次数),在 50 条数据量级下,预计接口响应时间可从 2s+ 降低至 200ms 以内。
关键点回顾:
- 拒绝循环 SQL:永远不要在
for循环里写 SQL 查询。 - 空间换时间:先取出较大的数据集,利用 Python 的字典(Hash Map)在内存中进行匹配,速度远快于网络 IO。
- 精兵简政:对于大数据量的列表接口,慎用 Heavy 的 Serializer,
.values()原生字典取值更轻量。

浙公网安备 33010602011771号