• 模型层进阶相关


    模型层进阶相关

    选择合适的层级工作

    要在对应的level (MVC) 做对应的事. 例如计算 count, 在最低的数据库 level 里是最快的 (如果只需要知道此记录是否存在的话,用 exists() 会更快).
    但要 注意 : queryset 是 lazy 的,所以有时候在 higher level (例如模板) 里控制 queryset 是否真的执行,说不定会更高效.

    下面这段代码很好的解释了不同 level 的意思:

    # QuerySet operation on the database
    # fast, because that's what databases are good at
    # 执行效率最快, 属于数据库层级
    my_bicycles.count()
    
    # counting Python objects
    # slower, because it requires a database query anyway, and processing
    # of the Python objects
    # 很慢, 属于在Python对象的级别层次处理
    len(my_bicycles)
    
    # Django template filter
    # slower still, because it will have to count them in Python anyway,
    # and because of template language overheads
    # 仍然很慢, 模板层的本质还是需要在Python层面上进行数据的处理
    {{ my_bicycles|length }}
    

    理解QuerySet对象

    切片

    QuerySet可以支持切片语法, 这等同于SQL的limit和offset. 但是它不支持负数索引

    print(models.Book.objects.all()[:4])
    print(models.Book.objects.all()[4:8])
    
    (0.002) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4; 
    (0.000) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4 OFFSET 4; 
    

    可迭代

    QuerySet对象支持迭代.

    for book in models.Book.objects.filter(pk__gt=5):
        print(book)
    

    惰性查询

    惰性查询是Queryset对象的一个比较重要的特性. 看下面这个例子

    q = models.Book.objects.filter(name__startswith='西')
    q = q.filter(pk__gt=5)
    q = q.filter(pub_time__year=2018)
    
    print(q)
    

    这上面的例子看起来是对数据库进行了3次查询, 但是实际上只有执行打印的时候才真正查询数据库了. 创建查询集后只有我们需要获取具体的数据, 然后orm才会去数据库"请求"值给我们.

    官方推荐写法:

    q = models.Book.objects.filter(
        name__startswith='西'
    ).filter(
        pk__gt=5
    ).filter(
        pub_time__year=2018
    )
    

    那么什么才是后去具体数据的时机呢? 官方文档描述了下列几种情形.

    1. Iteration: ie. 对 Queryset 进行 For 循环的操作.
    2. slice: q = models.Book.objects.all()[5:10:2] 当指定了步长的切片才会马上去执行数据库查询.
    3. picling/caching
    4. repr/str
    5. len (Note: 如果你只想知道这个 queryset 结果的长度的话,最高效的还是在数据库的层级调用 count () 方法,也就是 sql 中的 COUNT ().)
    6. list()
    7. bool()

    缓存机制

    每个查询集都包含一个缓存来最小化对数据库的请求, 充分理解缓存的工作机制能帮助我们写出高效的代码.

    当我们创建了一个新的查询集之后, 一旦发生了上面描述的7种情形, 就会在请求数据库之后, 可能生成cache(保存在查询集对象内),之后对相同的查询集做操作就不会重新去请求数据库获取数据了.

    可以看看下面的结果

    # 第一种方式
    print([p.name for p in models.Publish.objects.all()])
    print([p.addr for p in models.Publish.objects.all()])
    
    # 第二种方式
    q = models.Publish.objects.all()
    print([p.name for p in q])
    print([p.addr for p in q])
    

    第一种方式实际是请求了两次数据库, QuerySet对象生成之后就直接弃用了, 缓存机制没有用上.

    第二种方式只请求了一次数据库, 在第一次遍历QuerySet之后, 就将结果缓存起来了, 接下来就是对同一个QuerySet对象进行Python层面上的操作了.

    会发生缓存的情形

    [entry for entry in queryset]  # 遍历整个查询集
    bool(queryset)				  # 做布尔值运算
    entry in queryset			  # in运算
    list(queryset)				  # 转换成列表
    

    特别要注意一下这些是不会发生缓存的.

    q = models.Publish.objects.all()
    
    print(q[2:])  # 做切片操作, 这里会查询数据库, 但不会将结果缓存到原来的查询集中
    print(q[2])   # 做索引操作, 也会查询数据库, 也不会将结果缓存.
    
    print(q)      # 这里单纯的打印不会发生缓存
    print(q)
    
    # values, values_list都不会发生缓存. 下面也会发生
    print(q.values('name', 'addr'))
    print(q.values('addr'))
    

    查询优化

    官方提供的几种优化策略

    • 利用 queryset lazy 的特性去优化代码,尽可能的减少连接数据库的次数.
    • 如果查出的 queryset 只用一次,可以使用 iterator () 去来防止占用太多的内存,
    • 尽可能把一些数据库层级的工作放到数据库,例如使用 filter/exclude, F, annotate, aggregate (可以理解为 groupby)
    • 一次性拿出所有你要的数据,不去取那些你不需要的数据.
      意思就是要巧用 select_related (), prefetch_related () 和 values_list (), values (), 例如如果只需要 id 字段的话,用 values_list ('id', flat=True) 也能节约很多资源。或者使用 defer()only() 方法:不加载某个字段 (用到这个方法就要反思表设计的问题了) / 只加载某些字段.
    • 如果不用 select_related 的话,去取外键的属性就会连数据再去查找.
    • bulk (批量) 地去操作数据,比如 bulk_create
    • 查找一条数据时,尽量用有索引的字段去查询,O (1) 或 O (log n) 和 O (n) 差别还是很大的
    • count() 代替 len(queryset), 用 exists() 代替 if queryset:

    下面再详细总结其中几种优化方式

    对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用 select_related 来对QuerySet进行优化。

    select_related 返回一个QuerySet,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。

    简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。

    下面是它和普通查询的区别

    # 普通查询
    book = models.Book.objects.filter(pk=2).first()  # type: models.Book
    print(book.publish.name)
    
    SELECT
    	`app01_book`.`id`,
    	`app01_book`.`name`,
    	`app01_book`.`pub_time`,
    	`app01_book`.`publish_id` 
    FROM
    	`app01_book` 
    WHERE
    	`app01_book`.`id` = 2 
    ORDER BY
    	`app01_book`.`id` ASC 
    	LIMIT 1;
    	
    SELECT
    	`app01_publish`.`id`,
    	`app01_publish`.`name`,
    	`app01_publish`.`addr`,
    	`app01_publish`.`pub_detail_id` 
    FROM
    	`app01_publish` 
    WHERE
    	`app01_publish`.`id` = 2;
    

    上面的查询一共执行了两句sql语句.

    使用select_related方法来执行查询的效率之比较.

    books = models.Book.objects.filter(pk__lt=4).select_related('publish')
        
    for book in books:
        print(book.publish.name)
    
    SELECT
    	`app01_book`.`id`,
    	`app01_book`.`name`,
    	`app01_book`.`pub_time`,
    	`app01_book`.`publish_id`,
    	`app01_publish`.`id`,
    	`app01_publish`.`name`,
    	`app01_publish`.`addr`,
    	`app01_publish`.`pub_detail_id` 
    FROM
    	`app01_book`
    	INNER JOIN `app01_publish` ON ( `app01_book`.`publish_id` = `app01_publish`.`id` ) 
    WHERE
    	`app01_book`.`id` < 4;
    

    由于使用了select_related提前将字段关联, 后面的跨表查询并没有继续操作数据库.

    select_related还支持连接多个外键, 可以通过一个外键字段一直关联下去. 下面就是跨了3张表

    books = models.Book.objects.filter(pk__lt=3).select_related('publish__pub_detail')
    
    for book in books:
        print(book.publish.pub_detail.email)
    

    小结:

    1. select_related主要针一对一和多对一关系进行优化。
    2. select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
    3. 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。
    4. 没有指定的字段不会缓存,如果要访问的话Django会再次进行SQL查询。

    对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。

    prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。

    prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。

    # 只查询了两次数据库
    books = models.Book.objects.prefetch_related('authors')
    for book in books:
    	print(book.authors.all())
    

    defer与only

    only(*field): 返回一个对象, 只对括号内的字段属性做了查询优化

    defer(*field): 返回一个对象, 对括号外的字段属性做了优化, 与only相反

    上面依然可以获取优化之外的字段属性, 但是却需要进行数据库的查询获取.

    books = models.Book.objects.only('name', 'pk')
    books2 = models.Book.objects.values('name', 'pk')
    books3 = models.Book.objects.defer('id')
    print(books)
    print(books2)
    print(books3)
    
    SELECT `app01_book`.`id`, `app01_book`.`name` FROM `app01_book` LIMIT 21;
    SELECT `app01_book`.`name`, `app01_book`.`id` FROM `app01_book` LIMIT 21;
    SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 21;
    

    从上面的执行sql语句可以上看出来, only和values执行的是一样的, 只是only返回的是列表套对象, 而values是列表套字典的形式. defer原理与only一样, 查询的是与only相反的数据. 所以如果只需要用到很少的数据, 又需要一个对象的形式, 就可以用到上面两个方法.

    only, defer不能跨表优化, 就像下面这样, 有多少书, 就需要执行多少次数据库, 效率非常低下.

    books = models.Book.objects.only('pk', 'publish')
    for book in books:
        print(book.pk, book.publish.name)
    

    事务优化

    事务操作不仅能够保证数据的安全, 还有一个很有用过的作用就是, 可以通过事务隔离Django默认的autocommit, 来避免Django频繁的向数据库提交数据. 这也能够很好的提升性能.

    在Django中开启事务的语法非常简单.

    from django.db import transaction
    with transaction.atomic():
        pass
    

    批量操作

    在QuerySet中有许多批量操作的方式, 例如delete update bulk_create...

    这些批量操作对应于数据库层面的批量操作, 能够有效的防止批频繁请求数据库.

    details = [models.PublishDetail(email=f'email{i}') for i in range(5)]
    for d in details:
        d.save()
    
    # 批量操作
    models.PublishDetail.objects.bulk_create(details)
    
    

    上面for循环5次, 需要请求数据库5次, 而bulk_create只需要请求数据库一次. 数据越多, 效率上的差距越明显.

  • 相关阅读:
    Android Binder机制中的异步回调
    VS加载项目时报错 尚未配置为Web项目XXXX指定的本地IIS
    下班前码个2013总结吧
    android ListView 在初始化时多次调用getView()原因分析
    Android BindService中遇到的一个小问题
    C#读书笔记之并行任务
    Android系统启动分析(Init->Zygote->SystemServer->Home activity)
    浅析Java异常
    在Ubuntu-14.04.3配置并成功编译Android6_r1源码
    (转)Android Binder设计与实现 – 设计篇
  • 原文地址:https://www.cnblogs.com/yscl/p/11609940.html
Copyright © 2020-2023  润新知