1. 查看 Django queryset 执行的 SQL
  2. 获得的查询结果直接以类似list方式展示(values、values_list)
  3.  如何在django中给一个字段取一个别名(extra)
  4. 如何只取出需要的字段,排除某些字段(defer、only)
  5. annotate 聚合 计数,求和,求平均数等
  6. SQL,减少多对一,一对多,多对多时查询次数(select_related、prefetch_related)
  7. 自定义一个自定义聚合功能,比如 group_concat

准备阶段

新建一个项目 mysite ,建一个 app 名称是 blog

django-admin startproject mysite
python manage.py startapp blog

把 blog 加入到 settings.py 中的 INSTALL_APPS 中

blog/models.py 代码如下

from django.db import models
 
 
class Author(models.Model):
    name = models.CharField(max_length=50)
    qq = models.CharField(max_length=10)
    addr = models.TextField()
    email = models.EmailField()
 
    def __str__(self):
        return self.name
 
 
class Article(models.Model):
    title = models.CharField(max_length=50)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    content = models.TextField()
    score = models.IntegerField()  # 文章的打分
    tags = models.ManyToManyField('Tag')
 
    def __str__(self):
        return self.title
 
 
class Tag(models.Model):
    name = models.CharField(max_length=50)
 
    def __str__(self):
        return self.name

 比较简单,假设一篇文章只有一个作者(Author),一个作者可以有多篇文章(Article),一篇文章可以有多个标签(Tag)。

 创建 migrations 然后 migrate 在数据库中生成相应的表

>>>python manage.py makemigrations
>>>python manage.py migrate

生产一些示例数据,运行initdb.py

import random
from mysite.wsgi import *
from blog.models import Author, Article, Tag

author_name_list = ['xiaoming', 'xiaohong', 'wangwu', 'laosi', 'xiaosan']
article_title_list = ['Django 文章', 'Python 文章', 'HTML 文章']


def create_authors():
    for author_name in author_name_list:
        author, created = Author.objects.get_or_create(name=author_name)
        # 随机生成9位数的QQ,
        author.qq = ''.join(
            str(random.choice(range(10))) for _ in range(9)
        )
        author.addr = 'addr_%s' % (random.randrange(1, 3))
        author.email = '%s@ziqiangxuetang.com' % (author.addr)
        author.save()


def create_articles_and_tags():
    # 随机生成文章
    for article_title in article_title_list:
        # 从文章标题中得到 tag
        tag_name = article_title.split(' ', 1)[0]
        tag, created = Tag.objects.get_or_create(name=tag_name)

        random_author = random.choice(Author.objects.all())

        for i in range(1, 21):
            title = '%s_%s' % (article_title, i)
            article, created = Article.objects.get_or_create(
                title=title, defaults={
                    'author': random_author,  # 随机分配作者
                    'content': '%s 正文' % title,
                    'score': random.randrange(70, 101),  # 随机给文章一个打分
                }
            )
            article.tags.add(tag)##多表关联


def main():
    create_authors()
    create_articles_and_tags()


if __name__ == '__main__':
    main()
    print("Done!")

执行命令导入数据

>>> python initdb.py               

Done!

打开Django终端shell

>>>python manage.py shell
>>>from blog.models import Article, Author, Tag
>>>Article.objects.all()
<QuerySet [<Author: xiaoming>, <Author: xiaohong>, <Author: wangwu>, <Author: laosi>
, <Author: xiaosan>]>

  准备工作完成

 

 1.查看 Django queryset 执行的 SQL

>>> print(str(Author.objects.all().query))
SELECT "blog_author"."id", "blog_author"."name", "blog_author"."qq", "blog_autho
r"."addr", "blog_author"."email", "blog_author"."phone" FROM "blog_author"
简化一下,就是:SELECT id, name, qq, addr, email FROM blog_author;

>>> print(str(Author.objects.filter(name='xiaoming').query))
SELECT "blog_author"."id", "blog_author"."name", "blog_author"."qq", "blog_autho
r"."addr", "blog_author"."email", "blog_author"."phone" FROM "blog_author" WHERE
 "blog_author"."name" = xiaoming
 简化一下,就是:SELECT id, name, qq, addr, email FROM blog_author WHERE name=xiaoming;

  

2. values_list 获取元组形式结果

1.查看所有作者的name和qq

>>> Author.objects.values_list('name','qq')
<QuerySet [('xiaoming', '771536497'), ('xiaohong', '704900133'), ('wangwu', '01022
8723'), ('laosi', '429075126'), ('xiaosan', '417512737')]>


#list 
>>> list(Author.objects.values_list('name','qq'))
[('xiaoming', '771536497'), 
('xiaohong', '704900133'), 
('wangwu', '010228723'), 
('laosi', '429075126'), 
('xiaosan', '417512737')]

 如果只查询1个字段,可以指定flat=True

>>> Author.objects.values_list('name',flat=True)
<QuerySet ['xiaoming', 'xiaohong', 'wangwu', 'laosi', 'xiaosan']

  通过Author表 name字段跨表查询文章标题

>>> Article.objects.filter(author__name='xiaohong').values_list('title', flat=
True)
<QuerySet ['Python 文章_1', 'Python 文章_2', 'Python 文章_3', 'Python 文章_4', '
Python 文章_5', 'Python 文章_6', 'Python 文章_7', 'Python 文章_8', 'Python 文章_
9']>

 

3. values 获取字典形式的结果

1,获取某字段的列表

#values
>>> Author.objects.values('name', 'qq')
<QuerySet [{'name': 'xiaoming', 'qq': '771536497'}, {'name': 'xiaohong', 'qq': '70
4900133'}, {'name': 'wangwu', 'qq': '010228723'}, {'name': 'laosi', 'qq': '4290751
26'}, {'name': 'xiaosan', 'qq': '417512737'}]>

#list
>>> list(Author.objects.values('name', 'qq'))
[{'name': 'xiaoming', 'qq': '771536497'}, {'name': 'xiaohong', 'qq': '704900133'},
 {'name': 'wangwu', 'qq': '010228723'}, {'name': 'laosi', 'qq': '429075126'}, {'na
me': 'xiaosan', 'qq': '417512737'}]


#跨表查询某字段
>>> Article.objects.filter(author__name='xiaohong').values('title')
<QuerySet [{'title': 'Django 文章_1'}, {'title': 'Django 文章_2'}, {'title': 'Dj
ango 文章_3'}, {'title': 'Django 文章_4'}, {'title': 'Django 文章_5'}, {'title':
 'Django 文章_6'}, {'title': 'Django 文章_7'}, {'title': 'Django 文章_8'}, {'tit
le': 'Django 文章_9'}, {'title': 'Django 文章_10'}, {'title': 'Django 文章_11'},
 {'title': 'Django 文章_12'}, {'title': 'Django 文章_13'}, {'title': 'Django 教
程_14'}, {'title': 'Django 文章_15'}, {'title': 'Django 文章_16'}, {'title': 'Dj
ango 文章_17'}, {'title': 'Django 文章_18'}, {'title': 'Django 文章_19'}, {'titl
e': 'Django 文章_20'}]>

 

注意:

1. values_list 和 values 返回的并不是真正的 列表 或 字典,也是 queryset,他们也是 lazy evaluation 的(惰性评估,通俗地说,就是用的时候才真正的去数据库查)

2. 如果查询后没有使用,在数据库更新后再使用,你发现得到在是新内容!!!如果想要旧内容保持着,数据库更新后不要变,可以 list 一下

3. 如果只是遍历这些结果,没有必要 list 它们转成列表(浪费内存,数据量大的时候要更谨慎!!!)

 

4. extra 实现 别名,条件,排序等

 extra 中可实现别名,条件,排序等,后面两个用 filter, exclude 一般都能实现,排序用 order_by 也能实现。

例如 Author 中有 name, Tag 中有 name 我们想执行

 SELECT name AS tag_name FROM blog_tag;

这样的语句,就可以用 select 来实现,如下:

>>> tags=Tag.objects.all().extra(select={'tag_name':'name'})
>>> tags[0].name
'Django'
>>> tags[1].name
'Python'
>>>

发现 name 和 tag_name 都可以使用,确认一下执行的 SQL

>>> Tag.objects.all().extra(select={'tag_name': 'name'}).query.__str__()
'SELECT (name) AS "tag_name", "blog_tag"."id", "blog_tag"."name" FROM "blog_tag"
'
>>>

查询的时候弄了两次 (name) AS "tag_name" 和 "blog_tag"."name"

如果只想其中一个能用,可以用 defer 排除掉原来的 name 

>>> Tag.objects.all().extra(select={'tag_name': 'name'}).defer('name').query.__s
tr__()
'SELECT (name) AS "tag_name", "blog_tag"."id" FROM "blog_tag"'
>>>

  为什么要改个名称,最常见的需求就是数据转变成 list,然后可视化等,如下defer

 

5.defer 排除不需要的字段

在复杂的情况下,表中可能有些字段内容非常多,取出来转化成 Python 对象会占用大量的资源。

这时候可以用 defer 来排除这些字段,比如我们在文章列表页,只需要文章的标题和作者,没有必要把文章的内容也获取出来(因为会转换成python对象,浪费内存)

>>> Article.objects.all().query.__str__()
'SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id",
 "blog_article"."content", "blog_article"."score" FROM "blog_article"'
 
 
>>> Article.objects.all().defer('content').query.__str__()
'SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id",
 "blog_article"."score" FROM "blog_article"'

#这里少了"blog_article"."content"

 

6. only 仅选择需要的字段

和 defer 相反,only 用于取出需要的字段,假如我们只需要查出 作者的名称

 

>>> Author.objects.all().only('name')
<QuerySet [<Author: xiaoming>, <Author: xiaohong>, <Author: wangwu>, <Author: laosi>
, <Author: xiaosan>]>

  

7.annotate 聚合 计数,求和,平均数等

1 计数

计算一下每个作者的文章数

>>> from django.db.models import Count

>>> Article.objects.all().values('author_id').annotate(count=Count('author')).values('author_id', 'count')
<QuerySet [{'author_id': 1, 'count': 3}, {'author_id': 2, 'count': 20}, {'author
_id': 3, 'count': 40}]>


#sql语句

>>> Article.objects.all().values('author_id').annotate(count=Count('author')).values('author_id', 'count').query.__str__()
'SELECT "blog_article"."author_id", COUNT("blog_article"."author_id") AS "count"
 FROM "blog_article" GROUP BY "blog_article"."author_id"'
 简化一下SQL: SELECT author_id, COUNT(author_id) AS count FROM blog_article GROUP BY author_id

获取作者的名称 及 作者的文章数

>>> Article.objects.all().values('author__name').annotate(count=Count('author')).values('author__name', 'count')
<QuerySet [{'author__name': 'wangwu', 'count': 40}, {'author__name': 'xiaohong', '
count': 20}, {'author__name': 'xiaoming', 'count': 3}]>

  实际上这是查询两张表,因为作者名称(author__name)在 blog_author 这张表中,而上一个例子中的 author_id 是 blog_article 表本身就有的字段

 

2.求和 与 平均值

求一个作者的所有文章的得分(score)平均值

>>> from django.db.models import Avg
>>> Article.objects.values('author_id').annotate(avg_score=Avg('score')).values('author_id', 'avg_score')
<QuerySet [{'author_id': 1, 'avg_score': 60.0}, {'author_id': 2, 'avg_score': 84.8}, 
{'author_id': 3, 'avg_score': 85.15}]>


#查看执行的SQL语句
>>> Article.objects.values('author_id').annotate(avg_score=Avg('score')).values(
...'author_id', 'avg_score').query.__str__()
'SELECT "blog_article"."author_id", AVG("blog_article"."score") AS "avg_score" F
ROM "blog_article" GROUP BY "blog_article"."author_id"'

 求一个作者所有文章的总分

>>> from django.db.models import Sum
>>>
>>> Article.objects.values('author__name').annotate(sum_score=Sum('score')).values('author__name', 'sum_score')
<QuerySet [{'author__name': 'wangwu', 'sum_score': 3406}, {'author__name': 'xiaohong', 'sum_score': 16
96}, {'author__name': 'xiaoming', 'sum_score': 180}]>

#查看执行的SQL语句
>>> Article.objects.values('author__name').annotate(sum_score=Sum('score')).values('author__name', '
...sum_score').query.__str__()
'SELECT "blog_author"."name", SUM("blog_article"."score") AS "sum_score" FROM "blog_article" INNER J
OIN "blog_author" ON ("blog_article"."author_id" = "blog_author"."id") GROUP BY "blog_author"."name"'

 

8.select_related 优化一对一,多对一查询

先修改一个 settings.py 让Django打印出在数据库中执行的语句

settings.py 尾部加上

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG' if DEBUG else 'INFO',
        },
    },
}

  这样当 DEBUG 为 True 的时候,就可以看出 django 执行了什么 SQL 语句

#假如取出10篇Django相关的文章,并需要用作者的姓名
>>>articles = Article.objects.all()[:10]
>>>
>>>a1 = articles[0]  # 取第一篇
 (0.000) SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id", "blog_article"."content", "blog_article"."score" FROM "blog_article" LIMIT 1; args=()
	#第一执行查询数据库
>>> 
>>>a1.title
'Django 文章_1'
>>> 
>>>a1.author_id
5
>>>a1.author.name   # 再次查询了数据库,注意!!!
(0.000) SELECT "blog_author"."id", "blog_author"."name", "blog_author"."qq", "blog_author"."addr", "blog_author"."email" FROM "blog_author" WHERE "blog_author"."id" = 5; args=(5,)
	#第二执行查询数据库
xiaosan

  这样的话遍历查询结果的时候就会查询很多次数据库,能不能只查询一次,把作者的信息也查出来呢?

当然可以,这时就用到 select_related,我们的数据库设计的是一篇文章只能有一个作者,一个作者可以有多篇文章。

现在要查询文章的时候连同作者一起查询出来,“文章”和“作者”的关系就是多对一,换句说说,就是一篇文章只可能有一个作者。

>>>articles = Article.objects.all().select_related('author')[:10]

>>>a1 = articles[0]  # 取第一篇
(0.000) SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id", "blog_article"."content", "blog_article"."score", "blog_author"."id", "blog_author"."name", "blog_author"."qq", "blog_author"."addr", "blog_author"."email" FROM "blog_article" INNER JOIN "blog_author" ON ("blog_article"."author_id" = "blog_author"."id") LIMIT 1; args=()
#第一次执行查询数据了


>>>a1.title
a1.author.name 

>>>a1.author.name
xiaosan	
#最后直接得出结果,没有再次查询数据库

  

9.prefetch_related 优化一对多,多对多查询

和 select_related 功能类似,但是实现不同。

select_related 是使用 SQL JOIN 一次性取出相关的内容。

prefetch_related 用于 一对多,多对多 的情况,这时 select_related 用不了,因为当前一条有好几条与之相关的内容。

prefetch_related是通过再执行一条额外的SQL语句,然后用 Python 把两次SQL查询的内容关联(joining)到一起

例如,查询文章的同时,查询文章对应的标签。“文章”与“标签”是多对多的关系。

>>>articles = Article.objects.all().prefetch_related('tags')[:10]
>>>
>>>articles
(0.000) SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id", "blog_article"."content", "blog_article"."score" FROM "blog_article" LIMIT 10; args=()
(0.001) SELECT ("blog_article_tags"."article_id") AS "_prefetch_related_val_article_id", "blog_tag"."id", "blog_tag"."name" FROM "blog_tag" INNER JOIN "blog_article_tags" ON ("blog_tag"."id" = "blog_article_tags"."tag_id") WHERE "blog_article_tags"."article_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); args=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
<QuerySet [<Article: Django 文章_1>, <Article: Django 文章_2>, <Article: Django 文章_3>, <Article: Django 文章_4>, <Article: Django 文章_5>, <Article: Django 文章_6>, <Article: Django 文章_7>, <Article: Django 文章_8>, <Article: Django 文章_9>, <Article: Django 文章_10>]>

#遍历查询的结果:

#比较:
#不用 prefetch_related 时


>>> articles = Article.objects.all()[:3]
>>>
>>> for a in articles:
...   print(a.title, a.tags.all())
...
(0.000) SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id", "blog_article"."content", "blog_article"."score" FROM "blog_article" LIMIT 3; args=()
(0.000) SELECT "blog_tag"."id", "blog_tag"."name" FROM "blog_tag" INNER JOIN "blog_article_tags" ON ("blog_tag"."id" = "blog_article_tags"."tag_id") WHERE "blog_article_tags"."article_id" = 1 LIMIT 21; args=(1,)
Django 教程_1 <QuerySet [<Tag: Django>]>
(0.000) SELECT "blog_tag"."id", "blog_tag"."name" FROM "blog_tag" INNER JOIN "blog_article_tags" ON ("blog_tag"."id" = "blog_article_tags"."tag_id") WHERE "blog_article_tags"."article_id" = 2 LIMIT 21; args=(2,)
Django 教程_2 <QuerySet [<Tag: Django>]>
(0.000) SELECT "blog_tag"."id", "blog_tag"."name" FROM "blog_tag" INNER JOIN "blog_article_tags" ON ("blog_tag"."id" = "blog_article_tags"."tag_id") WHERE "blog_article_tags"."article_id" = 3 LIMIT 21; args=(3,)
Django 教程_3 <QuerySet [<Tag: Django>]>


#用 prefetch_related 时
>>>articles = Article.objects.all().prefetch_related('tags')[:3]
>>>
>>> for a in articles:
...   print(a.title, a.tags.all())
...
(0.000) SELECT "blog_article"."id", "blog_article"."title", "blog_article"."author_id", "blog_article"."content", "blog_article"."score" FROM "blog_article" LIMIT 3; args=()
(0.000) SELECT ("blog_article_tags"."article_id") AS "_prefetch_related_val_article_id", "blog_tag"."id", "blog_tag"."name" FROM "blog_tag" INNER JOIN "blog_article_tags" ON ("blog_tag"."id" = "blog_article_tags"."tag_id") WHERE "blog_article_tags"."article_id" IN (1, 2, 3); args=(1, 2, 3)
Django 教程_1 <QuerySet [<Tag: Django>]>
Django 教程_2 <QuerySet [<Tag: Django>]>
Django 教程_3 <QuerySet [<Tag: Django>]>

#由上可见,用prefetch_related时候,可以减少对数据库的查询

  

 10.自定义聚合功能

我们前面看到了 django.db.models 中有 Count, Avg, Sum 等,但是有一些没有的,比如 GROUP_CONCAT,它用来聚合时将符合某分组条件(group by)的不同的值,连到一起,作为整体返回。

我们来演示一下,如果实现 GROUP_CONCAT 功能。

新建一个文件 比如 my_aggregate.py

from django.db.models import Aggregate, CharField
 
 
class GroupConcat(Aggregate):
    function = 'GROUP_CONCAT'
    template = '%(function)s(%(distinct)s%(expressions)s%(ordering)s%(separator)s)'
 
    def __init__(self, expression, distinct=False, ordering=None, separator=',', **extra):
        super(GroupConcat, self).__init__(
            expression,
            distinct='DISTINCT ' if distinct else '',
            ordering=' ORDER BY %s' % ordering if ordering is not None else '',
            separator=' SEPARATOR "%s"' % separator,
            output_field=CharField(),
            **extra        )

使用时先引入 GroupConcat 这个类,比如聚合后的错误日志记录有这些字段 time, level, info

我们想把 level, info 一样的 聚到到一起,按时间和发生次数倒序排列,并含有每次日志发生的时间。

ErrorLogModel.objects.values('level', 'info').annotate(
    count=Count(1), time=GroupConcat('time', ordering='time DESC', separator=' | ')
).order_by('-time', '-count')