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存到內存的字典中,這樣下次用到的時候就無需再次訪問數據庫,可提高效率


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM