zoukankan      html  css  js  c++  java
  • Django ORM调优实践

    一、分析请求慢响应的主要原因

    将请求执行的任务按功能分为几块,用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)
    

    注意:

    1. bulk_create不会返回id:When you bulk insert you don't get the primary keys back

    2. 小心数据库连接超时:如果一次性插入过多的数据会导致Mysql has gone away的报错。指定batch_size=5000可以避免这个问题,当插入数据>5000时,会分成多个sql执行数据批量插入

    六、尽量不要重复取数据

    可以将数据库的数据以id为key存到内存的字典中,这样下次用到的时候就无需再次访问数据库,可提高效率

  • 相关阅读:
    深度学习:Keras入门(一)之基础篇(转)
    《神经网络与机器学习》导言
    GAN综述
    VS2017专业版和企业版激活密钥
    IntelliJ Idea 常用快捷键列表
    数据库SQL优化大总结之 百万级数据库优化方案
    git使用教程
    VS2015常用快捷键总结
    心跳包实现
    基于OAuth 2.0的第三方认证 -戈多编程
  • 原文地址:https://www.cnblogs.com/luozx207/p/12163380.html
Copyright © 2011-2022 走看看