FreezeJ' Blog

Django优化数据库操作

2022-08-18

官方推荐的一些优化:
https://docs.djangoproject.com/zh-hans/3.2/topics/db/optimization/

优化关联查询

关于select_relatedprefetch_related可以参考:
https://www.cnblogs.com/kylin5201314/p/14738195.html
https://docs.djangoproject.com/zh-hans/3.2/ref/models/querysets/#select-related

外键连接使用select_related,多对多连接使用prefetch_related

select_related的工作方式是创建一个 SQL 连接,并在 SELECT 语句中包含相关对象的字段。出于这个原因,select_related 在同一个数据库查询中得到相关对象。然而,为了避免因跨越“many”关系进行连接而产生更大的结果集,select_related 仅限于单值关系——外键和一对一。

prefetch_related对每个关系进行单独的查找,并在 Python 中进行“joining”。这使得它除了支持 select_related 的外键和一对一关系外,还可以预取多对多和多对一的对象,这是用 select_related 无法做到的。

优化prefetch_related

举例:输出每个用户当前激活的权限名称

from django.db.models import Prefetch

queryset = RBACPermission.objects.filter(active=True).only('name')
users = RBACUser.objects.only('rbac_permissions').prefetch_related(
    Prefetch(
        'rbac_permissions',  # 关联字段
        queryset=queryset,  # 关联查询集
        to_attr='active_permissions'  # 保存到属性名称
    )
)
for u in users:
  print([p.name for p in u.active_permissions])

最终查询语句:

SELECT (`rbac_user_rbac_permissions`.`rbacuser_id`) AS `_prefetch_related_val_rbacuser_id`, `rbac_permission`.`id`, `rbac_permission`.`name` FROM `rbac_permission` INNER JOIN `rbac_user_rbac_permissions` ON (`rbac_permission`.`id` = `rbac_user_rbac_permissions`.`rbacpermission_id`) WHERE (`rbac_permission`.`active` AND `rbac_user_rbac_permissions`.`rbacuser_id` IN (SELECT `rbac_user`.`id` FROM `rbac_user` ORDER BY `rbac_user`.`id` DESC)) ORDER BY `rbac_permission`.`code` DESC;

使用count()统计数量

使用len()来统计会执行QuerySet,但是使用count()统计数量,是在数据库层面执行统计(SELECT COUNT(*)),效率更高。

count = len(User.objects.all())  # x
count = User.objects.all().count()  # √

使用exists()判断是否存在

if queryResult.exists():
    #SELECT (1) AS "a" FROM "blog_article" LIMIT 1; args=()
    print("exists...")

使用iterator迭代数据减少内存占用

MySQL 不支持流式结果,因此 Python 数据库驱动将整个结果集加载到内存中。当数据库后端是mysql时,就算使用iterator也是会一次查询所有结果到内存,但是结果不会缓存到queryset中。
细节可以看官方文档:https://docs.djangoproject.com/zh-hans/3.2/ref/models/querysets/#iterator

objs = Book.objects.all().iterator()
for obj in objs:
    print(obj.title)

# 迭代器已经循环到底,再次遍历已经没有对象可以循环,但是也不会触发异常
for obj in objs:
    print(obj.title)

使用only或defer减少查询字段

q1 = User.objects.all().only('name')  # 只查询某列,延迟查询其它列,defer用法相反,延迟查询某一列
print(q1.query)
# SELECT `user`.`id`, `user`.`name` FROM `user` ORDER BY `user`.`id` DESC

# 与values不同,only和defer只是一个延迟查询,延迟查询的字段通过额外的sql同样可以获取到(框架自动完成),而values不可以。
q2 = User.objects.all().values('name')
print(q2.query)
# SELECT `user`.`id`, `user`.`name` FROM `user` ORDER BY `user`.`id` DESC
# 查询语句是一样的,但是通过values的结果,不能查询其它字段

避免直接对queryset进行索引操作

u = User.objects.all()
for i in range(10):
    print(u[i])  # 每次产生一条新的查询,因为queryset没有缓存

list(u)  # 计算queryset,并把记录缓存到queryset
for i in range(10):
    print(u[i])  # 不会有新的查询

数据库添加索引

主键默认会添加索引,查询最好使用主键字段。

指定字段添加索引:
https://docs.djangoproject.com/zh-hans/3.2/ref/models/fields/#django.db.models.Field.db_index

https://docs.djangoproject.com/zh-hans/3.2/ref/models/options/#django.db.models.Options.indexes

联合索引:
https://docs.djangoproject.com/zh-hans/3.2/ref/models/options/#index-together

批量操作

批量创建

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

批量更新

entries = Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

entries[0].headline = 'This is not a test'
entries[1].headline = 'This is no longer a test'
Entry.objects.bulk_update(entries, ['headline'])

批量插入

多对多关系:

my_band.members.add(me, my_friend)

批量删除

多对多关系:

my_band.members.remove(me, my_friend)

普通查询集:

Entry.objects.all().delete()

调试SQL查询

查看代码执行的sql语句

from django.db import connections, reset_queries
reset_queries()  # 重置查询

# 执行sql操作
some_sql_ops()

# 打印sql
for c in connections.all():
    print(f"Queries per connection: Database: {c.settings_dict['NAME']} {c.queries}")

查看语句详情

print(User.objects.filter(id__gt=10).query)

查看语句执行分析

print(Audit.objects.filter(id__gt=10).explain(format='JSON'))
{
  "query_block": {
    "select_id": 1,
    "ordering_operation": {
      "using_filesort": false,
      "table": {
        "table_name": "audit",
        "access_type": "range",
        "possible_keys": [
          "PRIMARY"
        ],
        "key": "PRIMARY",
        "used_key_parts": [
          "id"
        ],
        "key_length": "4",
        "rows": 2476,
        "filtered": 100,
        "attached_condition": "(`DBNAME`.`audit`.`id` > 10)"
      }
    }
  }
}