一, 分析请求慢响应的主要原因
将请求执行的任务按功能分为几块, 用 time.time() 打印每个模块的执行时间, 大部分情况下性能会主要消耗在某一个模块上, 即 80% 的性能问题是出在 20% 的代码上
找到主要原因后, 就专注于优化这一个模块
二, 使用 django.db.connection.queries 查看某个请求的 sql 执行情况
- from django.db import connection
- ...
- print(connection.queries)
- # [{
- 'sql':-- 执行的 sql 语句 --, 'time':--sql 语句执行的时间 --
- }...]
注意只有在 debug=True 模式下才能获取 connection.queries
多数据库
db.connections 是一个类似字典的对象, 可以通过某个数据库连接的别名获取这个数据源的 connection. 比如 connections['my_db_alias']
- from django.db import connections
- for key in connections:
- print(key)
- # 可以打印出所有配置了的数据源别名, django 会为每个数据源创建一个 connection
通过 django/db/init.py 中
- class DefaultConnectionProxy:
- """ Proxy for accessing the default DatabaseWrapper object's attributes. If you
- need to access the DatabaseWrapper object itself, use
- connections[DEFAULT_DB_ALIAS] instead.
- """
- def __getattr__(self, item):
- return getattr(connections[DEFAULT_DB_ALIAS], item)
- def __setattr__(self, name, value):
- return setattr(connections[DEFAULT_DB_ALIAS], name, value)
- def __delattr__(self, name):
- return delattr(connections[DEFAULT_DB_ALIAS], name)
- def __eq__(self, other):
- return connections[DEFAULT_DB_ALIAS] == other
- connection = DefaultConnectionProxy()
由于 DEFAULT_DB_ALIAS='default', 可以知道 from django.db import connection 获取的就是 connections['default']
因此, 在多数据库的情况下, 可以通过 connections 获取特定数据库连接的 queries 或 cursor
- from django.db import connections
- connections['my_db_alias'].queries
- cursor = connections['my_db_alias'].cursor()
输出总的 sql 执行时间
- sql_time = 0.0
- for q in connections['my_db_alias'].queries:
- sql_time += float(q['time'])
- print('sql_time', sql_time)
三, 各种 update 写法的执行速度
数据库数据量为 60w
以下 sql 执行时间都是在 update 有实际数据的更新时记录的, 如果 update 没有实际更新, sql 执行时间会大幅缩减.
1, 使用 raw_sql 自定义查询
- cursor = connections['my_db_alias'].cursor()
- # 实例化 cursor 的时间不计入
- cursor.execute("update item set result=%s, modified_time=Now() where id=%s", (result, 10000))
- print(time()-start)
- print(connections['my_db_alias'].queries)
- # 0.004s 左右, 与 sql 执行时间相同
2, 使用 ORM 的 update 方法
- Item.objects.using('my_db_alias').filter(id=10000).update(result=result)
- # 0.008s 左右, sql 执行时间是 0.004s
3, 使用 object.save () 方法
- item = Item.objects.using('my_db_alias').filter(id=10000).first()
- item.result = result
- item.save(using='my_db_alias')
- # 0.012s 左右, sql 执行时间是 0.004s
因此, 执行 update 的效率 raw_sql>update 方法 > save() 方法
四, 使用 prefetch_related 减少数据库查询
prefetch_related 对关系使用独立的 query, 即先查出符合过滤条件的表 A 的 id, 再用这些 id 去查表 B, 并且在 python 中将两批数据关联.
假设我们有一个博客应用, 有 Blog,Comment 两张表, 一条博客可以有多个关联的评论:
- from django.db import models
- class Blog(models.Model):
- name = models.CharField(max_length=255)
- author = models.CharField(max_length=100)
- content = models.TextField()
- class Comment(models.Model):
- author = models.CharField(max_length=100)
- content = models.TextField()
- blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='comments')
现在有一个需求, 找出所有名为 "Django 教程" 的博客下的评论内容.
用这个例子可以看到使用 prefetch_related 是如何减少数据库查询的.
不使用 prefetch_related:
- def test_prefetch_related():
- blogs = Blog.objects.filter(name="Django 教程")
- for blog in blogs:
- comments = Comment.objects.filter(blog_id=blog.id)
- for comment in comments:
- print(comment.content)
- print(len(blogs)) # 34
- print(len(connection.queries)) # 39
匹配指定名称的博客有 34 个, 可以看到获取每个博客评论的时候, 都查了一次 Comment 表, 总共查询了 34 次 Comment 表, 效率是非常低的. 我们的目标应该是查询一次 Blog 表, 查询一次 Comment 表即获得所需的数据
使用 prefetch_related:
- def test_prefetch_related():
- blogs = Blog.objects.filter(name="Django 教程").prefetch_related('comments')
- for blog in blogs:
- for comment in blog.comments.all():
- print(comment.content)
- print(len(blogs)) # 34
- print(len(connection.queries)) # 6
- for query in connection.queries:
- print(query)
发起的 sql 数量由 39 个减到 6 个
具体的:
- {
- 'sql': 'SELECT @@SQL_AUTO_IS_NULL', 'time': '0.000'
- }
- {
- 'sql': 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED', 'time': '0.000'
- }
- {
- 'sql': 'SELECT VERSION()', 'time': '0.000'
- }
- {
- 'sql': 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED', 'time': '0.000'
- }
- # 找到所有符合过滤条件的博客文章
- {
- 'sql': "SELECT `blog`.`id`, `blog`.`name`, `blog`.`author`, `blog`.`content` FROM `blog` WHERE `blog`.`name` ='Django 教程'",'time':'0.014'
- }
- # 根据上面找到的博客文章 id 去找到对应的评论
- {
- 'sql': 'SELECT `comment`.`id`, `comment`.`author`, `comment`.`content`, `comment`.`blog_id` FROM `comment` WHERE `comment`.`blog_id` IN (5160, 1307, 2984, 5147, 5148, 3062, 5148, 5161, 2038, 1923, 2103, 3014, 1466, 2321, 5166, 5154, 1980, 3550, 3542, 5167, 2077, 2992, 3209, 5168, 8855, 1163, 368, 174, 3180, 5168, 8865, 2641, 3224, 4094)', 'time': '0.007'
- }
与我们的目标相符
何时 prefetch_related 缓存的数据会被忽略
要注意的是, 在使用 QuerySet 的时候, 一旦在链式操作中改变了数据库请求, 之前用 prefetch_related 缓存的数据将会被忽略掉. 这会导致 Django 重新请求数据库来获得相应的数据, 从而造成性能问题. 这里提到的改变数据库请求指各种 filter(),exclude() 等等最终会改变 SQL 代码的操作.
prefetch_related('comments') 隐含表示 blog.comments.all(), 因此 all() 并不会改变最终的数据库请求, 因此是不会导致重新请求数据库的.
然而
for comment in blog.comments.filter(author="jack"):
就会导致 Django 重新请求数据库
只需要取出部分字段
博客文章的 content 字段数据量可能非常大, 取出而不用可能会影响性能. 之前的需求中可以进一步优化只取出博客和评论中的部分字段
- blogs = Blog.objects.filter(name="Django 教程").only('id').\
- prefetch_related(
- Prefetch('comments', queryset=Comment.objects.only('id', 'content', 'blog_id'))
- )
使用 only 指定查询的字段, 使用 Prefetch 对象自定义 prefetch_related 查询的内容 (默认 queryset=Comment.objects.all())
注意 comment.blog_id 字段是必须要取出的, 因为在 python 中将 comments 拼到对应的 blog 时需要 comment.blog_id 字段与 blog.id 字段匹配, 如果在 Prefetch 对象中不取出 comment.blog_id, 拼接时会浪费很多数据库查询去找 comment.blog_id 字段
多数据库的情况
在多数据库的情况下, prefetch_related 使用的数据源与主查询指定的数据源一致.
比如:
- blogs = Blog.objects.using('my_db_alias').filter(name="Django 教程").only('id').\
- prefetch_related(
- Prefetch('comments', queryset=Comment.objects.only('id', 'content', 'blog_id'))
- )
查询 Comment 表时会使用与 Blog 一样的数据源
五, 向数据库插入数据的时候尽量使用 bulk_create
- # 以下代码会发起 10 次数据库插入:
- for i in range(10):
- Comment.objects.create(content=str(i), author="kim", blog_id=1)
- # 以下代码只会发起一次数据库插入:
- comments = []
- for i in range(10):
- comments.append(Comment(content=str(i), author="kim", blog_id=1))
- Comment.objects.bulk_create(comments, batch_size=5000)
注意:
bulk_create 不会返回 id:When you bulk insert you don't get the primary keys back
小心数据库连接超时: 如果一次性插入过多的数据会导致 MySQL has gone away 的报错. 指定 batch_size=5000 可以避免这个问题, 当插入数据 > 5000 时, 会分成多个 sql 执行数据批量插入
六, 尽量不要重复取数据
可以将数据库的数据以 id 为 key 存到内存的字典中, 这样下次用到的时候就无需再次访问数据库, 可提高效率
来源: https://www.cnblogs.com/luozx207/p/12163380.html