为什么会查询速度那么慢?
查询就是一个人任务,是由一系列子任务组成,每个子任务都消耗一定的时间,如果优化查询,那么实际上就要优化子任务
客户端 -> 服务器 -> 解析 -> 生成计划 -> 执行(最重要阶段) -> 返回给客户端
一些不必要的额外操作,某些操作被重复了很多次,一些操作执行很慢,优化查询就是减少和消除这些操作话费的时间
了解查询的生命周期, 时间消耗情况
对优化查询有很大意义
慢查询: 优化数据访问
很多情况下是因为访问的数据太多,一些查询不能避免需要筛选大量的数据.对于低效的查询,我们可以通过两个步骤分析:
- 确认应用程序是否在检索大量超过需要的数据,访问太多的行,有些是有可能是访问太多的列
- 确认MySQL服务器层是否在分析大量超过需要的数据
是否向数据库请求了不需要的数据
不需要的数据会带来额外的负担,增加网络开销,消耗服务器的 CPU 和内存资源
- 查询不需要的记录: 如果不需要返回所有的数据,尽可能的在后面加上
LIMIT
- 多表关联时返回全部列: 尽量避免使用
*
来查询所有的列,在多表关联的时只取需 - 重复查询相同的数据: 如果没有加入缓存,很可能出现重复执行相同的查询,然后完全相同的数据,最好当初次查询的时候缓存起来,然后从缓存取出,但是需要注意缓存失效
MySQL是否在扫描额外的记录
确保返回数据合理之后,就需要考虑是否扫描了额外的记录,衡量查询开销的三个指标
- 响应时间
- 扫描的行数
- 返回的行数
响应时间
响应时间只是表面的值,响应时间是服务时间
和排队时间
- 服务时间: 数据库处理查询真正花了多长时间
- 排队时间: 服务器等待资源的时间(I/O or Lock)
响应时间是有很多因素造成的,可以先看响应时间是否是一个合理的值,如果和预期的不一样,那很有可能是硬件或者服务器环境的问题
扫描的行数和返回的行数
查看查询扫描和返回的行数,可以看出数据的效率高不高
理想状态下扫描和返回的应该是相同的,但是这种情况并不多
一般扫描的函数在 1: 1 - 10: 1之间
扫描的行数和访问类型
在评估查询开销的时候,需要考虑查找一行的成本,有些需要扫描很多行才能返回一行
在EXPLAIN
中的type
列反映了访问类型,访问类型有多种
- 全表扫描
- 范围扫描
- 唯一索引查询
- 常数引用
这些事从慢到快,扫描的行数也是从大到小,你需要明白扫描表,扫描索引,范围访问和单值访问的概念
如果没有找到合适的访问类型,通常的办法就是增加一个合适的索引
下面一个例子来说明
EXPLAIN SELECT * FROM sakila.film_actor WHERE film_id = 1;
这个查询预期返回10行数据
key | value |
---|---|
id | 1 |
select_type | SIMPLE |
table | ref |
possible_keys | idx_fk_film_id |
key | idx_fx_film_id |
key_len | 2 |
ref | const |
rows | 10 |
Extra | |
可以看到使用索引idx_fx_film_id 扫描了10条数据,下面删除索引来看一下 |
ALTER TABLE sakila.film_actor DROP FOREIGN KEY fk_film_actor_film;
ALTER TABLE sakila.film_actor DROP KEY idx_fk_film_id;
EXPLAIN SELECT * FROM sakila.film_actor WHERE film_id=1;
key | value |
---|---|
id | 1 |
select_type | SIMPLE |
table | film_actor |
type | ALL |
possible_keys | NULL |
key | NULL |
key_len | NULL |
ref | NULL |
rows | 5073 |
Extra | Using where |
此时我们发现已经成为了全表扫描5073条数据来完成这个查询,Using where 表示MySQL通过WHERE条件筛选存储引擎返回的记录 |
一般MySQL能够使用三种方式应用WHERE,从好到坏依次为:
- 在
索引中
使用WHERE条件来过滤不匹配的记录,这是在存储引擎层完成的 - 使用索引覆盖扫描(在Extra列中出现了
Using index
)来返回记录,直接命中索引中过滤不需要的记录并返回命中结果,这是MySQL服务层完成的,无需回表 - 从
数据表
中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using Where
)这是在MySQL服务层完成的,需要先读数据然后进行过滤
索引很重要,但有些时候增加索引让扫描的行数等于返回的行数譬如COUNT()
SELECT actor_id, COUNT(*) FROM sakila.film_acor GROUP BY actor_id;
这需要读取几千行,然是仅返回200行, MySQL不会告诉你实际扫描多少行数据,只会告诉你生成了多少行, 理解一下传需要扫描多少行需要理解,查询背后的逻辑和思想,下面提供一些思路,来优化查询
- 使用索引覆盖扫描
- 改变表结构,例如使用单独的汇总表
- 重写这个复杂的查询, 让MySQL优化器能够以更优化的方式执行这个查询
重构查询的方式
- 可以通过找到一个更优的方法获得实际需要的结果
- 可以通过修改应用代码,完成另外一种方式完成查询,最终达成一样的目的
一个查询还是多个简单的查询
MySQL在设计上让连接和断开都很轻量级,在返回一个小的查询结果方面很高效
在其他条件都相同的时候,使用尽可能少的查询当然是更好的,但是讲一个大查询分解为多个小查询是很有必要的,可以衡量一下这样是不是会减少工作量
切分查询
当定期清理大量数据时,如果用一个大的语句一次性完成的话,可能需要一次锁住很多数据,占满事务日志,耗尽系统资源,阻塞很多小的但重要的查询,将一个打的DELETE切为多个小查询尽可能小的影响MySQL
分解关联查询
很多高性能的应用都会讲关联进行分解例如
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
可以使用以下查询来代替
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
# 此处需要注意 IN 的值,最好不要超过 1000 个
SELECT * FROM post WHERE post.id in (123, 456, 9098, 7804);
有以下优势:
- 缓存效率更高,许多应用程序可以方便的缓存单表查询
- 减少锁竞争
- 对数据库进行拆分,更容易做到高性能和高拓展
- 查询本身效率的提升
- 减少冗余记录的查询,每条记录只需要查询一次
- 这样相当于在应用中实现哈希关联,而不是在MySQL嵌套循环关联
查询是如何执行的?
如果能够弄清楚查询是如何执行的,就可以遵循一些原则优化器按照预想的合理的方式运行
- 客户端发送一条查询给服务器
- 服务器先检查查询缓存,如果命中则立即返回存储在缓存中的结果,否则进入下一阶段
- 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划
- MySQL根据优化器生成的直径计划,调用存储引擎的API来执行查询
- 将结果返回给客户端
MySQL 客户端/服务器 通信协议
MySQL客户端和服务器之间的通信协议是"半双工",同一时刻,要么是服务器向客户端发送数据,要么是客户端往服务器发送数据,无法也无需将消息切成小块独立来发送
这种协议快速,但是导致没有办法进行流量控制,一端一旦开始发送消息,另一端就要完整接收消息才能响应
客户端用一个单独的数据包将查询串给服务器,这就是为什么语句很长max_allowed_packet
就很重要
相反,服务器响应给用户的数据通常很多,有多个数据包组成.服务器开始响应客户端请求时,客户端必须完全接收整个返回结果,而不是简单地只取前几条,然后停止
很多的MySQL的连接库函数都可以获得全部的结果集并缓存到内存中,而MySQL通常需要等所有的数据已经发送给客户端才会释放这条查询占用的资源
查询状态
对于MySQL的连接或者一个线程,任何时刻都有一个状态,表示了MySQL当前在做什么,可以使用SHOW FULL PROCESSLIST
(返回结果中Command)就是当前的状态,在一个生命周期中,会变化很多次
- Sleep: 线程正在等待客户端发送新的请求
- Query: 线程正在执行查询或者正在将结果发送给客户端
- Locked: MySQL服务层,线程正在等待表锁,InnoDB行锁并不会在线程状态
- Analyzing and statustics: 收集存储引擎的统计信息,并生成查询执行计划
- Copying to tmp table [on disk]: 线程正在执行查询,并将结果都复制到一个临时表,一般是在做
GROUP BY
操作,要么是文件排序或者UNION
操作,如果后面有on disk
表示MySQL正在讲一个内存临时表放到磁盘上 - Sorting result: 线程正在对结果进行排序
- Sending data: 多种情况,线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据
查询缓存
在解析一个查询语句之前,如果缓存是打开的,MySQL会优先检查这个查询是否命中缓存,如果缓存是通过哈希实现的,那么要全部一样才能命中
如果使用了查询缓存,会检查权限,如果权限没有问题就会直接返回
查询优化处理
下一步是讲SQL转化为执行计划,在依照执行计划和存储引擎进行交互,包括: 解析SQL, 预处理, 优化SQL,执行计划.任何一步出错,都会终止查询,这几步可能同时执行也可能单独执行
语法解析器和预处理
首先通过SQL语句进行解析,生成一棵"解析树",使用MySQL语法规则验证和解析查询,例如验证是否使用错误的关键字,或者关键字的顺序是否正确,或者引号是否能前后正确匹配
预处理则根据一些MySQL规则进一步检查是否合法,检查数据表和数据列是否存在,解析名字和别名是否有歧义,最后会验证权限,通常验证权限会很快,除非服务器有非常多的权限配置
查询优化器
优化器会将语法书转化为执行计划,一条查询可能有很多执行方式,优化器的作用就是找到最好的执行计划
MySQL使用基于成本的优化器,尝试预测一个查询使用某种执行方式的成本,通过查询当前会话的Last_query_cost
来得知当前查询的成本
SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor;
SHOW STATUS LIKE 'Last_query_cost';
表示大概需要做1040个查找才能完成上面的查询,导致MySQL优化器选择错误的执行计划有很多原因:
- 统计信息不准确,MySQL依赖存储引擎统计信息来评估成本,有的存储引擎是准确的,有的偏差很大,例如InnoDB因为其MVCC并不能维护一个数据表的行数和精确地统计信息
- 执行计划的成本估算不等于实际执行的成本,即使统计信息精确优化器给出的执行计划可能也不是最优的,例如一些页面在磁盘一些在内存中,MySQL无法知道哪些页面在内存哪些在磁盘
- MySQL的最后和你想的不一样,你希望时间短,但是MySQL基于其他成本模型中最优的执行计划
- 没有考虑其他的并发执行,可能会影响到当前的查询速度
- MySQL不完全基于成本优化,有一些固定的规则,例如全文搜索MATCH(),则在全文索引的时候使用全文索引,即使使用WHERE要比这快依旧会用对应的全文索引
- MySQL不会考虑不受控制的成本,例如执行存储过程或者自定义函数的成本
- 优化器有时候无法估计所有可能的执行计划
MySQL的优化策略可以简单的分为两种
- 静态优化: 不依赖于数值,如WHERE条件带入常数,静态优化在第一次完成后就一直有效,即使使用不同的参数重复查询也不会发生变化
- 动态优化: 和查询上下文有关,例如WHERE条件中的取值,索引中条目对应和数据行,需要在每次查询的时候重新评估,可以认为是"运行时优化"
MySQL能够处理优化的类型
- 重新定义关联标的顺序: 数据表的关联并不是按照查询中指定的顺序进行的,决定关联的顺序是优化器很重要的部分
- 将外连接转化为内连接: 并不是所有的OUTER JOIN语句都必须以外连接方式进行,例如WHERE条件
- 等价变换规则: 合并减少一些比较,移除一些恒成立和恒不成立的判断,例如(5=5 AND a>5)会被改写为a>5, 如果(a<b AND b=c) AND a=5会被改写为(b>5 AND b=c AND a=5),这些规则对于编写条件语句很有用
- 优化COUNT(), MIN()和MAX(): 索引和列是否可以为空通常可以帮助MySQL优化这类表达式,最小值就是B-Tree索引最左端的记录,如果MySQL使用了这种类型优化,那么在EXPLAIN就可以看到,“Select tables optimized away”,表示优化器执行过程中移除了该表,并以一个常熟代替
- 预估并转化为常数表达式:
会分为两步查询,第一步: 先从 film 找到需要的行,因为在 film_id 字段上有主键索引,所以MySQL优化器知道只会返回一条数据,然后在执行第二步,MySQL将第一步中返回的film_id列作为已知取值来处理EXPLAIN SELECT film.film_id, film_actor.actor_id FROM sakila.film INNER JOIN sakila.film_actor USING(film_id) WHERE film.film_id = 1;
- 覆盖索引扫描: 当索引中的列包含所有查询需要使用的列,MySQL就可以使用索引中的数据
- 子查询优化: MySQL在某些情况下可以将子查询转换一种效率更高的形式,减少多个查询对数据的访问
- 提前终止查询: 当发现满足查询需求,MySQL总是能够立刻终止查询,使用LIMIT子句,除此之外,MySQL还有几类,MySQL会返回一个空结果
EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
- 等值传播: 如果两个值通过等式关联,那么MySQL能够把其中一个列WHERE条件传递到另一列上
这里使用film_id进行关联,这里的WHERE不仅对film表适用,对film_actor一样适用,如果其他数据库需要手动写两个SELECT film.film_id FROM sakila.film INNER JOIN sakila.film_actor UWING(film_id) WHERE film.film_id > 500;
- 列表 IN() 比较: 在很多数据库系统 IN() 完全等同于 OR 条件的多个子句,MySQL将 IN() 列表中的数据先进行排序,然后通过二分查找,这是一个O(log n)复杂度的,等价的转换为OR的复杂度为O(n),对于IN()列表中大量取值,MySQL处理速度更快
数据和索引的统计信息
服务器层有查询优化器,却没有保存数据和索引的统计信息,统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息, 查询优化器在生成对应查询计划需要向存储引擎获取相应的统计信息,包括每个表或者所引有多少页面,每个表的每个索引基数.数据行和索引长度,索引分布信息等
MySQL执行关联查询
MySQL中的关联比我们理解的要广泛,任何一次查询都是一次关联,并不仅仅是两个表匹配才叫关联
UNION查询,MySQL现将一系列单个查询结果放到一个临时表,然后重新读取临时表完成UNION查询,在MySQL中,每个查询都是一次关联,所以读取结果也是一次关联
MySQL关联的策略很简单,对任何关联执行嵌套循环关联操作,在MySQL现在一个表中循环取出单条数据,然后嵌套循环到下一个表寻找匹配的行,直到所有匹配的行都匹配,按照这样的方式找到第一个表记录,在嵌套查询下一个关联表,然后回溯到上一个表,所以叫做嵌套循环关联
执行内连接可以表示为以下形式
SELECT tbl1.col1, tbl2.col2 FROM tbl1 INNER JOIN tbl2 USING(COL3)
WHERE tbl1.cik1 IN (5, 6);
伪代码可以表示为:
outer_iter = iterator over tbl1 where col1 IN(5, 6)
outer_row = outer_iter.next
while outer_row
inner_itor = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
while inner_row
output [ outer_row.col1, inner_row.col2 ]
inner_row = inner_iter.next
end
outer_row = outer_iter.next
end
外连接的表现形式为:
SELECT tbl1.col1, tbl2.col2 FROM tbl1 LEFT OUTER JOIN tbl2 USING(col3) WHERE tbl1.col1 IN(5, 6);
伪代码表示为:
outer_iter = iterator over tbl1 where col1 IN(5, 6)
outer_row = outer_iter.next
while outer_row
inner_itor = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
if inner_row
while inner_row
output [ outer_row.col1, inner_row.col2 ]
inner_row = inner_iter.next
end
else
output [ outer_row.col1, NULL ]
end
outer_row = outer_iter.next
end
也并不是所有的查询都可以转化为上面的形式,例如全外连接就无法通过嵌套循环和回溯方式完成
执行计划
MySQL不会生成查询字节码来查询,MySQL生成查询指令书,然后通过存储引擎执行完成并返回结果,最终执行计划包含了重构的全部信息,如果对一个查询执行 EXPLAIN 之后,在执行SHOW WARNINGS
,就可以看到重构的查询
这是一颗平衡树,这并不是MySQL的查询方式,MySQL是从一个表一直嵌套循环,回溯完成所有的表关联,实际是一颗左侧深度优先的树
关联查询优化器
决定了多个表关联时的顺序,通过多表关联,关联优化器通过评估不同的顺序成本来选择代价最小的
UNION的限制
UNION的LIMIT截取部分结果集, 下面的做法会将两个查询的结果存放到同一个临时表,再取前20条,这样效率很低
(
SELECT * FROM sakila.actor
) UNION ALL
(
SELECT * FROM sakila.customer
) LIMIT 20;
下面是给出的优化方式,给每一个查询都加上LIMIT 20,就可以解决这个问题
(
SELECT * FROM sakila.actor LIMIT 20
) UNION ALL
(
SELECT * FROM sakila.customer LIMIT 20
) LIMIT 20;
松散索引扫描
MySQL不支持松散索引扫描,所以无法按照不连续的方式扫描一个索引,即使需要的数据只有索引中的几个,也需要扫描这段索引的每一个条目
最大和最小值优化
如果对一些主键最小最大化可以不用 MIN() 而使用 LIMIT 代替
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENELOPE' LIMIT 1;
无法在同一个表上同时查询和更新
MySQL不允许在同一张表进行查询和更新,这并不是优化器的限制,譬如下面的查询就无法执行
UPDATE tb1 AS outer_tb1
SET cnt = (
SELECT count(*) FROM tb1 AS inner_tb1
WHERE inner_tb1.type = outer_tb1.type
);
可以将查询改为下面的查询,这个查询实际是两个查询:
- 子查询中的 SELECT 语句, 在 UPDATE 之前就会完成
- 多表关联的UPDATE
UPDATE tb1
INNER JOIN(
SELECT type, count(*) AS cnt FROM tb1
GROUP BY type
) AS der USING(type)
SET tb1.cnt = der.cnt
查询优化器的提示(hint)
可以提供优化器提示,控制最终执行计划
- HIGH_PRIORITY 和 LOW_PRIORITY: [SELECT | INSERT]告诉MySQL同时访问一个表,那个优先级高,这两个提示只对有表锁的存储引擎有效,即使是在MyISAM中也需要注意,因为很可能会严重降低性能
- HIGH_PRIORITY: 会将语句重新调度到所有正在等待表锁以便修改数据的语句之前,实际上是将它放在表的队列的最前面而不是按照顺序等待
- LOW_PRIORITY: 正好相反,让改语句一直处于等待状态,只要队列还有语句就会先执行
- DELAYED: [INSERT | REPLACE]使用该提示将会立刻返回给客户端,并将插入的数据放到缓冲区,然后在表空闲时批量插入,日志系统这么做非常有效,或者是其他需要大量数据但是不需要等待语句完成的应用,但需要注意并不是所有的引擎都支持这样做
- STRAIGHT_JOIN: [SELECT]如果MySQL没能选择关联顺序的时候,或者太多导致MySQL无法评估所有的关联顺序,他都会很有用,会减少优化器的搜索空间
- 放置在SELECT语句的SELECT关键字之后, 让查询中所有的表按照在与剧中出现的顺序进行关联
- 放置在任何两个关联表的名字之间, 固定前后两个表的关联顺序
- SQL_SMALL_RESULT 和 SQL_BIG_RESULT: [SELECT]告诉优化器对GROUP BY或者DISTINCT查询如何放在临时表及排序
- SQL_SMALL_RESULT: 告诉优化器结果很小,可以放到内存中的临时表,避免排序
- SQL_BIG_RESULT: 告诉优化器结果集很大,建议使用磁盘临时表做排序
- SQL_BUFFER_RESULT: 告诉优化器将查询结果放到临时表,尽快释放表锁,当无法使用客户端缓存,使用服务端缓存通常很有效
- SQL_CACHE 和 SQL_NO_CACHE: [SELECT] 告诉结果集是否应该缓存在查询缓存中
- SQL_CALC_FOUND_ROWS: [SELECT] 他让MySQL返回的结果集包含更多的信息,加上该提示,MySQL会计算除去LIMIT之后要查询返回的结果集的总数,但是实际上只会返回LIMIT要求的结果集,可以通过函数FOUND_ROW()获得这个值
- FOR UPDATE 和 LOCK IN SHARE MODE: [SELECT] 控制SELECT锁机制,只对实现了行级锁存储引擎有效会对符合查询条件的数据行加锁, INSERT和UPDATE不需要这个提示,因为会自动加锁, 这两个提示会让一些优化无法使用,应该尽量避免使用这两个提示,通常都有其他更好的方式实现同样的目的
- USE INDEX, IGNORE INDEX 和 FORCE INDEX: [SELECT] 告诉优化器使用或者不适用那些索引来查询,在5.0之前不会影响分组和排序,5.1和之后会影响, OFRCE INDEX和USE INDEX很像,OFRCE INDEX会告诉优化器全表扫描成本高于索引扫描,哪怕该索引用户不大
MySQL5.0 和更新版本,新增了一些参数控制优化器的行为
- optimizer_search_depth: 此参数控制优化器在穷举执行计划时限度,长时间查询处于"Statistics"状态,可以考虑调低此参数
- optimizer_prune_level: 默认打开,优化器根据需要扫描的行数来决定是否跳过一些执行计划
- optimizer_switch: 包含了开启/关闭优化器特性的标志位,可以通过这个参数来控制禁用索引合并的特性
前两个参数用来控制优化器走一些"捷径",可以让优化器再处理复杂SQL仍然很高效,但也可能让优化器错过一些真正最优的执行,所以要根据实际情况修改这些参数
MySQL升级后的影响
在版本升级之后一些"优化器提示"可能会让新版的优化策略失效,一般升级操作都很顺利,但还是需要检查细节和边界,可以使用Percona Toolkit
中的pt-upgrade
可以检查新版运行得SQL和老版是否一样
优化特定类型的查询
优化COUNT()查询
- 统计行数尽量使用 COUNT(*) 不会扫描全部的行
- 统计一行的数量, 注意只会统计非空列
- 使用MyISAM的COUNT()会对COUNT(col)进行优化如果确定不会有NULL,会优化为COUNT(*)
简单的优化
统计编号大于5的城市
# 优化前, 扫描 4097 行数据
SELECT COUNT(*) FROM world.City WHERE ID > 5;
# 优化后,扫描 6 行数据
SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
FROM world.City WHERE ID <= 5;
同级不同颜色的商品数量,此时不能使用OR语句
# 优化前使用 OR
SELECT COUNT(color='blue' OR color='red') FROM items;
# 优化后, 使用 SUM()
SELECT
SUM(IF(color='blue', 1, 0)) AS blue,
SUM(IF(color='read', 1, 0) AS red
FROM items;
# 优化后使用 OR NULL
SELECT
COUNT(color='blue' OR NULL) AS blue,
COUNT(color='red' OR NULL) AS red
FROM items;
使用近似值
有一些业务并不完全要求精确地COUNT, 可以用近似值来代替,譬如使用 EXPLAIN来估算行数, EXPLAIN 并不会真正的执行
更复杂的优化
更加复杂的优化可以考虑
- 增加汇总表
- 增加类似 Memcached 这样的缓存系统
但是你很快又会陷入快速``精确``简单
只能满足其二
语句的优化
关联查询优化
- 在ON或者USING列上确保有索引,创建索引的时候就需要考虑关联的顺序
- 确保任何GROUP BY和ORDER BY只涉及到一个列,这样MySQL才肯那个使用索引优化这个过程
- 在升级MySQL的时候,关联语法,运算符优先级可能会发生变化
优化子查询
- 尽可能使用关联查询来代替子查询
优化GROUP BY或者DISTINCT
- 最有效的方法就是使用索引来优化
- 当无法使用索引GROUP BY会使用两种策略来完成, 对于这两种可以通过提示
SQL_BIG_RESULT
和SQL_SMALL_RESYLT
来按照这种方式运行, 如果遇到需要分组的尽量使用更高效的更短的列进行分组譬如能使用actor_id
(ID分组)就不要使用first_name,last_name
(姓名全拼)来分组- 临时表做分组
- 文件排序做分组
优化GROUP BY WITH ROLLUP
WITH ROLLUP: 将分组的结果进行的一个聚合(汇总)
可以通过EXPLAIN观察加WITH ROLLUP和去掉之后的执行计划是否相同对于WITH ROLLUP的优化分为大致几种策略:
- 使用子查询代替
- 通过临时表存放中间顺序,通过UNION来取得最后结果
- 将WITH ROLLUP放到应用程序处理
优化LIMIT分页
在偏移量很大的时候 LIMIT 1000,20 MySQL需要查询 10020 记录然后最后返回 20 条,前面 10000 条都会被无情的抛弃,这样的代价很高,可以在页面限制分页的数量要么是优化大偏移量的性能
- 最简单的优化方法就是使用索引覆盖扫描,返回关联列,然后通过JOIN,返回需要的数据
# 优化前
SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
# 优化后, 通过覆盖索引查询到ID然后关联,对于大量数据的表,这样尤其有用
SELECT film.film_id, film.description
FROM sakila.film
INNER JOIN (
SELECT film_id FROM sakila.film ORDER BY title LIMIT 50, 5
) AS lim USING(film_id);
- 使用范围扫描对应的结果,如果列上有索引,并且预算计算出了边界就可以改写为
# BETWEEN: 代表 ... 到 ... 之间
SELECT film_id, description FROM sakila.film
WHERE position BETWEEN 50 AND 54 ORDER BY position;
- LIMIT 和 OFFECT都是OFFECT的问题,导致扫描大量不需要的数据然后无情的抛弃,所以可以记录上次取数据的位置,然后下次直接从该书签记录扫描, 需要配合前端使用返回最后一个ID
# 获取第一组结果
SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20;
# 从记录点开始找
SELECT * FROM sakila.rental WHERE rental_id < 16030 ORDER BY rental_id DESC LIMIT 20;
- 可以使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键和需要排序的列,还可以使用Sphinx优化一些搜索
优化SQL_CALC_FOUND_ROWS
使用SQL_CALC_FOUND_ROWS提示可以去掉,LIMIT满足条件之后的行数,可以作为分页的总数,但是实际工作原理是扫描所有满足的行,然后抛弃不需要的行,而不是在满足LIMIT行后就停止扫描,所以这个提示的代价很高
一个改良的设计是,每页显示20条数据,那么每次查询都使用LIMIT返回21条记录并显示20条,如果第21条存在,那么就显示下一页的按钮,否则就说明没有更多的数据
另一种做法是仙还村较多的数据,比如缓存1000条,然后分次分页都从这个缓存中获取,这样做可以让应用程序根据大小采用不同的策略,如果结果集少于1000,这样就可以在页面显示所有的分页连接,如果大于1000,就在页面上设计一个额外的按钮
也可以使用 EXPLAIN 的 rows 来作为结果集总数的近似值,当需要精确结果使用 COUNT(*) 来满足需求,这都比 SQL_CALC_FOUND_ROWS 要快得多
优化 UNION
UNION的实现方式总是通过临时表,所以很多优化策略都需要手动将 WHERE, LIMIT, ORDER BY下推到 UNION 中去
如果确实需要消除重复行,否则一定要使用 UNION ALL, 如果没有 ALL, MySQL会给临时表加上 DISTINCT, 会导致临时表做唯一性检查,代价很高
使用用户自定义变量
用户自定义变量是一个用来存储内容的临时容器,连接MySQL的整个过程都存在
自定义变量的属性和限制
- 无法使用查询缓存
- 不能使用常量或者标识符的地方使用自定义变量,例如表名,LIMIT子句
- 用户自定义变量在生命周期在一个连接中有效,所以不能用它们做连接通信
- 如果使用连接池或者持久化连接,自定义变量可能看起来和毫无关系的代码交互
- 不能显式的自定义变量类型,最好初始化,但是在赋值的时候会改变
- 一些情况下变量会被优化器优化掉
- 赋值的时间和顺序不固定
- 赋值符号 “:=” 优先级很低
- 未定义变量不会产生语法错误
优化排名语句
# := 代表赋值操作, 如果只是 "=" 不会进行赋值
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM sakila.actor LIMIT 3;
加上排名之后
SELECT actor_id,COUNT(*) as cnt FROM sakila.film_actor
GROUP BY actor_id
ORDER BY cnt DESC
LIMIT 10;
# 优化后, 加入排名
SET @curr_cnt := 0, @prev_cnt := 0. @rank := 0;
SELECT actor_id,
@curr_cnt := COUNT(*) AS cnt,
@rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
@prev_cnt := @curr_cnt AS dummy
FROM sakila.film_actor
GROUP BY actor_id
ORDER BY cnt DESC
LIMIT 10;
使用用户自定义变量经常会遇到"诡异"的现象,相比带来的优势,还是可以花一些时间来研究的,使用SQL生成排名通常需要两次计算, 如果通过中间临时表方案可能比较简单
SET @curr_cnt := 0,@prev_cnt := 0, @rank := 0;
SELECT actor_id,
@curr_cnt := cnt AS cnt,
@rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
@prev_cnt := @curr_cnt AS dummy
FROM (
SELECT actor_id, COUNT(*) AS cnt
FROM sakila.film_actor
GROUP BY actor_id
ORDER BY cnt DESC
LIMIT 10
) AS der;
避免重复查询刚刚更新的数据
MySQL没有PGSQL中的UPDATE RETURNING
,但是MySQL可以通过一个变量来解決
UPDATE t1 SET lastUpdated =NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 WHERE id = 1;
# 優化後
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;
统计更新和插入的数量
如果使用INSERT ON DUPLICATE KEY UPDATE
如果想知道插入多少数据,有多少因为冲突改成更新的
# 每次由于冲突会导致变量 @x 自增,然后通过这个表达式 *0 不影响要更新的内容
INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
ON DUPLICATE KEY UPDATE
c1 = VALUES(c1) + (0 * (@x := @x + 1));
确定取值的顺序
最常见的问题就是在赋值前读取
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt
FROM sakila.actor
WHERE @rownum <= 1;
因为WHERE和SELECT是在查询不同阶段被执行的,如果加入ORDER BY结果可能会更不同
SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt
FROM sakil.actor
WHERE @rownum <= 1
ORDER BY first_name;
因为ORDER BY引入了文件排序,WHERE条件是在文件排序操作之前取值,所以查询会返回所有的记录, 解决办法就是在赋值和取值发生在执行查询的同一阶段
SET @rownum := 0;
SELECT actor_id, @rownum AS rownum
FROM sakila.actor
WHERE (@rownum := @rownum + 1) <= 1;
如果在ORDER BY上加入会是什么结果?
SET @rownum := 0;
SELECT actor_id, first_name, @rownum AS rownum
FROM sakila.actor
WHERE @rownum <= 1
ORDER BY first_namem LEAST(0, @rownum := @rownum + 1);
编写偷懒的UNION
其中一个子分支先执行,如果找到匹配的行就跳过第二个分支,一般会现在频繁访问的表中查找"热数据",找不到到另一张访问较少的表中找"冷数据"
SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
# 优化后
# GREATEST函数返回参数中的最大值
SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tb1
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'
FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1, 'reset' FROM DUAL WHERE (@found := NULL) IS NOT NULL;
用户自定义变量的其他用处
- 查询运行时计算总数和平均值
- 模拟GROUP语句中的函数FIRST()和LAST()
- 对大量数据做一些数据计算
- 计算一个大表的MD5散列值
- 编写一个样本处理函数,当样本函数超过某个边界值将其变为0
- 模拟读/写游标
- 在SHOW语句的WHERE子句中加入变量