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