MySQL的逻辑架构图
1.客户端层:连接处理、授权认证、安全等功能均在这一层处理
2.MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。
3.最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux下的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。
MySQL查询过程
1.客户端/服务端通信协议
MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它。
2.查询缓存
在解析一个查询语句前,如果查询缓存是打开的,那么MySQL会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。
工作流程:
-
服务器接收SQL,以SQL和一些其他条件为key查找缓存表(额外性能消耗)
-
如果找到了缓存,则直接返回缓存(性能提升)
-
如果没有找到缓存,则执行SQL查询,包括原来的SQL解析,优化等.
-
执行完SQL查询结果以后,将SQL查询结果存入缓存表(额外性能消耗)
表数据修改不频繁、数据较静态。
查询(Select)重复度高。
查询结果集小于 1 MB。
查询缓存参考:https://blog.csdn.net/Lei_Da_Gou/article/details/90030371
3.语法解析和预处理
MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。
4.查询优化
多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。
5.查询执行引擎
在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果,整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成。
6.返回结果给客户端
查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。
如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。
总结:
- 客户端向MySQL服务器发送一条查询请求
- 服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
- 服务器进行SQL解析、预处理、再由优化器生成对应的执行计划
- MySQL根据执行计划,调用存储引擎的API来执行查询
- 将结果返回给客户端,同时缓存查询结果
性能优化建议
1.Scheme设计与数据类型优化
- 选择数据类型只要遵循小而简单的原则就好,越小的数据类型通常会更快,占用更少的磁盘、内存,处理时需要的CPU周期也更少。
- 对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用32位(4个字节)存储空间,那么它的表示范围已经确定,所以INT(1)和INT(20)对于存储和计算是相同的。
- 尽量避免NULL。如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。
- schema的列不要太多。原因是存储引擎的API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是非常高的。如果列太多而实际使用的列又很少的话,有可能会导致CPU占用过高。
- 大表ALTER TABLE非常耗时,MySQL执行大部分修改表结果操作的方法是用新的结构创建一个张空表,从旧表中查出所有的数据插入新表,然后再删除旧表。尤其当内存不足而表又很大,而且还有很大索引的情况下,耗时更久。
2.创建高性能索引
索引是提高MySQL查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。
3.特定类型查询优化,优化COUNT()查询
COUNT()函数,它有两种不同的作用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值是非空的,它不会统计NULL。如果确认括号中的表达式不可能为空时,实际上就是在统计行数。最简单的就是当使用COUNT(*)时,并不是我们所想象的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计行数。
4.优化关联查询
MySQL是如何执行关联查询的。当前MySQL关联执行的策略非常简单,它对任何的关联都执行嵌套循环关联操作,即先在一个表中循环取出单条数据,然后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为为止。然后根据各个表匹配的行,返回查询中需要的各个列。
5.优化UNION
MySQL处理UNION的策略是先创建临时表,然后再把各个查询结果插入到临时表中,最后再来做查询。
除非确实需要服务器去重,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致整个临时表的数据做唯一性检查,这样做的代价非常高。当然即使使用ALL关键字,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。虽然很多时候没有这个必要,比如有时候可以直接把每个子查询的结果返回给客户端。
Django ORM 查询优化
1.select_related(基于连表查询)
对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化。
在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。
例子
# Hits the database.
article = models.Article.objects.get(nid=2)
# Hits the database again to get the related Blog object.
print(article.category.title)
对应SQL:
SELECT
"blog_article"."nid",
"blog_article"."title",
"blog_article"."desc",
"blog_article"."read_count",
"blog_article"."comment_count",
"blog_article"."up_count",
"blog_article"."down_count",
"blog_article"."category_id",
"blog_article"."create_time",
"blog_article"."blog_id",
"blog_article"."article_type_id"
FROM "blog_article"
WHERE "blog_article"."nid" = 2; args=(2,)
SELECT
"blog_category"."nid",
"blog_category"."title",
"blog_category"."blog_id"
FROM "blog_category"
WHERE "blog_category"."nid" = 4; args=(4,)
使用select_related()函数:
articleList=models.Article.objects.select_related("category").all()
for article_obj in articleList:
# Doesn't hit the database, because article_obj.category
# has been prepopulated in the previous query.
print(article_obj.category.title)
对应SQL:
SELECT
"blog_article"."nid",
"blog_article"."title",
"blog_article"."desc",
"blog_article"."read_count",
"blog_article"."comment_count",
"blog_article"."up_count",
"blog_article"."down_count",
"blog_article"."category_id",
"blog_article"."create_time",
"blog_article"."blog_id",
"blog_article"."article_type_id",
"blog_category"."nid",
"blog_category"."title",
"blog_category"."blog_id"
FROM "blog_article"
LEFT OUTER JOIN "blog_category" ON ("blog_article"."category_id" = "blog_category"."nid");
总结:
- select_related主要针一对一和多对一关系进行优化。
- select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
- 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。
- 没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
- 也可以通过depth参数指定递归的深度,Django会自动缓存指定深度内所有的字段。如果要访问指定深度外的字段,Django会再次进行SQL查询。
- 也接受无参数的调用,Django会尽可能深的递归查询所有的字段。但注意有Django递归的限制和性能的浪费。
2.prefetch_related(基于子查询)
对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。
prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。
prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。
3.extra
有些情况下,Django的查询语法难以简单的表达复杂的 WHERE 子句,对于这种情况, Django 提供了 extra() QuerySet修改机制 — 它能在 QuerySet生成的SQL从句中注入新子句
extra(select=None, where=None, params=None,
tables=None, order_by=None, select_params=None)
extra可以指定一个或多个 参数,例如 select, where or tables. 这些参数都不是必须的,但是你至少要使用一个!要注意这些额外的方式对不同的数据库引擎可能存在移植性问题.(因为你在显式的书写SQL语句),除非万不得已,尽量避免这样做
select 参数可以让你在 SELECT 从句中添加其他字段信息,它应该是一个字典,存放着属性名到 SQL 从句的映射。
queryResult=models.Article.objects.extra(select={'is_recent': "create_time > '2017-09-05'"})
where / tables参数
您可以使用where定义显式SQL WHERE子句 - 也许执行非显式连接。您可以使用tables手动将表添加到SQL FROM子句。
where和tables都接受字符串列表。所有where参数均为“与”任何其他搜索条件。
举例来讲:
queryResult=models.Article.objects.extra(where=['nid in (1,3) OR title like "py%" ','nid>2'])
4.整体插入数据
在Django中需要向数据库中插入多条数据(list)。使用如下方法,每次save()的时候都会访问一次数据库。导致性能问题:
创建对象时,尽可能使用bulk_create()来减少SQL查询的数量。
例子:
device_obj_list = []
for i in range(int(nums)):
device_obj_list.append(
Device(
category=category,
seat=seat_obj,
asset_code='---',
asset_num='{}-xxxx'.format(category.name), # 类型-xxxx
use_info='---',
operator=operator,
op_type=1
)
)
# 使用django.db.models.query.QuerySet.bulk_create()批量创建对象,减少SQL查询次数
Device.objects.bulk_create(device_obj_list)
5.创建索引
索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件
通过db_index和Meta index选项给数据表字段建立索引
使用索引可快速访问数据库表中的特定信息。数据库索引好比是一本书前面的目录,没有索引目录的话,你访问书中某个页面需要从第1页遍历到最后一页,如果有目录,你可以快速地根据目录查找到所需要的页面。Django项目中如果你需要频繁地对数据表中的某些字段(如title)使用filter(), exclude()和order_by()方法进行查询,我们强烈建议你对这些字段建议索引(index), 提升查询效率。
要对模型中的某个字段建立数据库索引,你可以使用db_index选项,也可以使用Meta选项建立索引。使用Meta选项的好处是你可以一次性对多个字段建立索引,还可以对多个字段建立组合索引。
方法一: 使用db_index选项
class Article(models.Model):
"""文章模型"""
# 使用db_index=True对title建立索引
title = models.CharField('标题', max_length=200, db_index=True)
方法二: 使用Meta选项
class Article(models.Model):
"""文章模型"""
title = models.CharField('标题', max_length=200,)
class Meta:
indexes = [models.Index(fields=['title']),]
unique_together 联合主键,包含 index_together
index_together 组合索引
class Meta:
unique_together = [["store", "barcode"], ["store", "mac_addr_no"]]
class Meta:
index_together = ["store", "sensor"]
Django索引原则:
主键必定是索引
Django默认会为每个Foreginkey创建一个索引