色彩时光 | 记录程序员世界的点点滴滴

Django ORM 查询性能优化(转)


前段时间用django做了个拨测程序。对于大量数据需在django中处理优化展示。刚好查到一篇总结得不错的BLOG文章结合程序的处理经验分享并记录。

Count() not len()

当想统计筛选出来的数据的条数时候,不要用len(),用Django QuerySet 的count() 函数,例子:


In [1]: import time
   ...: from myapp.models import ContentsOfBook
   ...: contents = ContentsOfBook.objects.all()
   ...: t1 = time.time()
   ...: print len(contents)
   ...: t2 = time.time()
   ...: print "Used time:%s" % str(t2-t1)
   ...: 
126919
Used time:4.06234192848

In [2]: import time
   ...: from jfTypein.models import ContentsOfBook
   ...: contents = ContentsOfBook.objects.all()
   ...: t1 = time.time()
   ...: print contents.count()
   ...: t2 = time.time()
   ...: print "Used time:%s" % str(t2-t1)
   ...: 
126919
Used time:0.0325939655304

可以明显看出,用len()方法的耗时是4.06s ,而用count()方法只用了0.03s ,原因在于Django的lazy机制,Django再做query set的时候不会把所有的数据给select出来,他是分页筛选出来的,所以在用len()方法的时候,相当于把整个query set遍历了一遍,把所有的数据都取出来对象化,耗时的同时也耗资源,会浪费空间。

大的query set最好不要直接遍历
我们经常会遇到一种情况,就是把我们筛选出来的query set 遍历一遍,去进行一些操作,这个时候我们一般会直接写个for 循环去遍历他。
需要注意的是,假如我们筛选出来的Query很大,尤其要注意的是我们调用 Model.objects.all()的时候,如果直接用for循环去遍历这些Query,Django会把他们全部进行实例化,如果数据比较大,就会占用大量的内存。
所以推荐一种做法,先把Query的ID或者主键列出来,然后再一个个查找。


# 比如我们要遍历 content_status == 4 的数据项
contents = ContentsOfBook.objects.filter(content_status=4)

ids = contents.values_list('id')
for content_id in ids:
    content = ContentsOfBook.objects.get(id=content_id)
    # do something

避免多次查询
有些情况我们需要筛选某个表不同条件的数据,一般我们可能会直接去写多个查询去筛选,数据项多的话,这会严重影响我们的性能:


books = [
u'book_1',
u'book_2',
u'book_3',
]
In [9]: import time
   ...: from jfTypein.models import ContentsOfBook
   ...: t1 = time.time()
   ...: for book in books:
   ...:     contents_1 = ContentsOfBook.objects.filter(book=book,exit=1)
   ...:     contents_2 = ContentsOfBook.objects.filter(book=book,exit=2)
   ...:     contents_3 = ContentsOfBook.objects.filter(book=book,exit=3)
   ...:     contents_4 = ContentsOfBook.objects.filter(book=book,content_status=4
   ...: )
   ...:     contents_5 = ContentsOfBook.objects.filter(book=book,content_status=5
   ...: )
   ...:     print contents_1.count(),contents_2.count(),contents_3.count(),conten
   ...: ts_4.count(),contents_5.count()
   ...: t2 = time.time()
   ...: print "Used time:%s" % str(t2-t1)
   ...: 
   ...: 
18 6 65 0 1041
0 0 0 0 905
0 0 0 0 882
Used time:1.456194877625

In [10]: import time
    ...: from jfTypein.models import ContentsOfBook
    ...: t1 = time.time()
    ...: for book in books:
    ...:     contents = ContentsOfBook.objects.filter(book=book).values_list('
    ...: exit','content_status')
    ...:     contents_1 = filter(lambda x:True if x[0]==1 else False,contents)
    ...:     contents_2 = filter(lambda x:True if x[0]==2 else False,contents)
    ...:     contents_3 = filter(lambda x:True if x[0]==3 else False,contents)
    ...:     contents_4 = filter(lambda x:True if x[1]==4 else False,contents)
    ...:     contents_5 = filter(lambda x:True if x[1]==5 else False,contents)
    ...:     print len(contents_1),len(contents_2),len(contents_3),len(contents_4
    ...: ),len(contents_5)
    ...: t2 = time.time()
    ...: print "Used time:%s" % str(t2-t1)
    ...: 
    ...: 
18 6 65 0 1041
0 0 0 0 905
0 0 0 0 882
Used time:0.311456871033

从上面的结果可以看到,如果进行多次的orm查询,效率比一次查询的要慢上好几倍,第二种方法中把需要的字段都一次select了出来放在内存中,然后调用python的filter函数去进行筛选,避免了多次对数据库进行连接,减少了IO。
所以我们在做不同条件的查询的时候,尽量一次把数据都查询出来,当然要避免把比较大的字段也select出来,不然会占用内存,同时也会影响到性能。

尽可能批量创建
在业务中,经常会碰到需要往数据库中插入一批数据的情况,最简单的就是写个简单的for循环create了,如下:


for i in range(100):
    ContentsOfBook.objects.create(
        book_id = 2333 + i,
        ...
    )

这样子插入数据性能比较低,因为这样子是执行了100条insert sql,这么频繁的IO,肯定会比较慢。Django提供了bulk_create方法,可以批量插入数据到DB中,如下:

need_create_objs = []
for i in range(100):
    need_create_objs.append(
        ContentsOfBook(
            book_id=2333 + i
            ....
        )
    )
ContentsOfBook.objects.bulk_create(need_create_objs)

因为Django的autocommit机制,Model每次对DB的操作都会新建一个链接并且提交过去,如果代码中get, save, create这样的方法大量使用,会影响业务的性能。下面讨论下如何提高对数据进行更新的性能:

首先假设我们有一个Model(GobalSchool),定义如下:


class GobalSchool(models.Model):
    id = models.AutoField(primary_key=True)
    school_name = models.CharField(max_length=128)

表里有里面有4k条数据,我们需要把所有的school的名字(school_name)进行更改,为了避免不必要的OOM,我们每次实验先把所有的对象id提取出来,后面的几种方法都调用这个函数

  def get_objects_id(self):
        ids = GobalSchool.objects.all().values_list('id')
        ids = list(map(lambda x:x[0], ids))
        ids.sort()
        return ids

下面是第一种写法,也是最初级性能最差的一种写法:

def run(self):
        t1 = time.time()
        ids = self.get_objects_id()
        for _id in ids:
            print(_id)
            school = GobalSchool.objects.get(id=_id)
            school.school_name = "my_school"
            school.save()
        print("use2:%s" % (time.time() - t1))

最后运行时间是:147(s),主要时间浪费在,每次get和save,在这两个函数里面,Django都是直接commit过去给SQL去执行的。
那么,减少Django对数据库的链接操作,就可以提高性能了

第二种做法是,节省大部分的get操作时间,不再是每一条条数据地取出来,每次批量取出一批数据进行操作


from functional import seq

    def multi_update(self, ids):
        schools = GobalSchool.objects.filter(id__in=ids)
        for school in schools:
            school.school_name = "my_school_save"
            school.save()

    def run(self):
        t1 = time.time()
        container_size = 1000
        ids = self.get_objects_id()
        bulk_ids = seq(ids).grouped(container_size) # 这里是把id分成长度为1000的若干份
        bulk_ids = list(map(list, bulk_ids))
        for bulk_id in bulk_ids:
            self.multi_update(bulk_id)
        print("use1:%s" % (time.time() - t1))

这次的运行时间是:73(s),可以看到时间节省了一半,通过省去大量的get操作,来提高了性能。

第三种做法是,我们进行数据操作的时候,我们建立一个长链接,去掉所有的save和get操作


from functional import seq

    def multi_update(self, ids):
        cursor = connection.cursor()
        for _id in ids:
            sql = "update gobal_schools set school_name = 'my_school_quick' where id = %s" % _id
            cursor.execute(sql)

    def run(self):
        t1 = time.time()
        container_size = 1000
        ids = self.get_objects_id()
        bulk_ids = seq(ids).grouped(container_size)
        bulk_ids = list(map(list, bulk_ids))
        for bulk_id in bulk_ids:
            self.multi_update(bulk_id)
        print("use1:%s" % (time.time() - t1))

最后我们得到的时间是:2.38(s),性能大大提高了!

Django黑魔法-transaction
为了解决频繁提交事务(commit)带来的IO消耗,Django提供了一个transaction接口,用户可以调用transaction.atomic来把Django的autocommit暂时屏蔽掉,方法如下:


from functional import seq
    from django.db import transaction

    @transaction.atomic
    def multi_update(self, ids):
        schools = GobalSchool.objects.filter(id__in=ids)
        for school in schools:
            school.school_name = "my_school_save"
            school.save()

    def run(self):
        t1 = time.time()
        container_size = 1000
        ids = self.get_objects_id()
        bulk_ids = seq(ids).grouped(container_size) 
        bulk_ids = list(map(list, bulk_ids))
        for bulk_id in bulk_ids:
            self.multi_update(bulk_id)
        print("use1:%s" % (time.time() - t1))

这样我们最后得到的运行时间也是:2.5(s)


您可能也对下面文章感兴趣:

Write a Comment


* Content (required) 10~500s

分类

热门标签

友情链接