查询性能低下最根本的缘由是访问的数据太多。某些查询可能不可避免地须要筛选大量数据,但这并不常见。大部分性能低下的查询均可以经过减小访问的数据量进行优化。对于低效的查询,能够经过下面两个步骤分析:mysql
有些查询会请求超过实际须要的数据,而后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增长网络开销,另外也会消耗应用服务器的CPU和内存资源。算法
典型案例:sql
对于MySQL,最简单的衡量查询开销的三个指标:响应时间、扫描的行数和返回的行数。没有哪一个指标可以完美地衡量查询的开销,但它们大体反映了MySQL在内部执行查询时须要访问多少数据,并能够大概推算出查询运行的时间。这三个指标都会记录到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;
SELECT * FROM post whre post.id in (123, 456);复制代码
当向MySQL发送一个请求的时候,MySQL的工做流程:性能优化
SHOW FULL PROCESSLIST
命令查看:
查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互,这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程当中的任何错误(例如语法错误)均可能终止查询,另外在实际执行中,这几部分可能一块儿执行也可能单独执行。服务器
语法解析器和预处理:网络
查询优化器:数据结构
通过语法解析器和预处理后,语法树被认为是合法的,并将由优化器将其转化成执行计划。架构
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。
SHOW STATUS LIKE 'Last_query_cost';
来查询当前会话的当前查询的成本,其值N为MySQL的优化器认为大概须要作N个数据页的随机查找才能完成当前的查询。致使MySQL选择错误的执行计划的缘由:
优化策略:
MySQL可以处理的优化类型:
从新定义关联表的顺序:数据表的关联并不老是按照在查询中指定的顺序执行。决定关联的顺序是优化器很重要的一部分功能。
将外链接转换为内链接:并非全部的OUTER JOIN语句都必须之外链接的方式执行。例如WHERE条件,库表结构均可能会让外链接等价于一个内链接。
使用等价变换规则:MySQL使用一些等价变换来简化并规范表达式。它能够合并和减小一些比较,还能够移除一些恒成立和一些恒不成立的判断。
优化COUNT()、MIN()和MAX():索引和列是否可为空能够帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只须要查询B-Tree索引最左端的记录,MySQL能够直接获取,并在优化器生成执行计划的时候就能够利用这一点(优化器会将这个表达式做为一个常数对待,在EXPLAIN就能够看到"Select tables optimized away")。相似的,没有任何WHERE条件的COUNT(*)查询一般也可使用存储引擎提供的一些优化(MyISAM维护了一个变量来存放数据表的行数)
预估并转换为常数表达式:MySQL检测到一个表达式能够转换为常数的时候,就会一直把该表达式做为常数进行优化处理。例如:一个用户自定义变量在查询中没有发生变化、数学表达式、某些特定的查询(在索引列上执行MIN,甚至是主键或惟一键查找语句)、经过等式将常数值从一个表传到另外一个表(经过WHERE、USING或ON来限制某列取值为常数)。
覆盖索引扫描:当索引中的列包含全部查询中全部须要的列的时候,MySQL就可使用索引返回须要的数据,而无须查询对应的数据行。
子查询优化:在某些状况下能够将子查询转换成一种效率更高的形式,从而减小多个查询屡次对数据的访问。
提早终止查询:当发现已经知足查询的需求,可以马上终止查询。例如使用了LIMIT子句,或者发现一个不成立的条件(当即返回一个空结果)。当存储引擎须要检索”不一样取值“或者判断存在性的时候,例如DISTINCT,NOT EXIST()或者LEFT JOIN类型的查询,MySQL都会使用这类优化。
等值传播:若是两个列的值经过等式关联,那么就能够把其中一个列的WHERE条件传递到另外一个列上。
SELECT film.film_id
FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
WHERE film.file_id > 500;
-- 若是手动经过一些条件来告知优化器这个WHERE条件适用于两个表,在MySQL中反而让查询更难维护。
... WHERE film.file_id > 500 AND film_actor.film_id > 500;复制代码
列表IN()的比较:不一样于其它数据库IN()彻底等价于多个OR条件语句,MySQL将IN()列表中的数据先进行排序,而后经过二分查找的方式来肯定列表的值是否知足条件,前者查询复杂度为O(n),后者为O(log n)。对于有大量取值的状况,MySQL这种处理速度会更快。
数据和索引的统计信息:
MySQL如何执行关联查询:
执行计划:
关联查询优化器:
排序优化
不管如何排序都是一个成本很高的操做,因此从性能角度考虑,应尽量避免排序或者尽量避免对大量数据进行排序。
文件排序:当不能使用索引生成排序结果的时候,MySQL须要本身进行排序,若是数据量小则在内存中进行,若是数据量大则须要使用磁盘。
排序算法:
进行文件排序的时候须要使用的临时存储空间可能会比想象的要大得多。缘由在于MySQL排序时,对每个排序记录都会分配一个足够长的定长空间来存放。
在关联查询的时候排序:
若是ORDER BY子句中全部列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候进行文件排序。能够在EXPLAIN看到Extra字段有“Using filesort”
除第一种场景,MySQL都会先将关联的结果放到一个临时表中,而后在全部的关联都结束后,再进行文件排序操做。用EXPLAIN可看到“Using temporary;Using filesort”。若是查询中有LIMIT的话,LIMIT也会在排序以后应用,因此即便须要返回较少的数据,临时表和须要排序的数据仍然很是大。
5.6后版本在这里作了些改进:当只须要返回部分排序结果的时候,例如使用了LIMIT子句,MySQL再也不对全部的结果进行排序,而是根据实际状况,选择抛弃不知足条件的结果,而后在进行排序。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。
查询执行阶段不是那么复杂,MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程当中,又大量的操做须要调用存储引擎实现的“handle API”接口来完成。
MySQL的万能“嵌套循环”并非对每种查询都是最优的,但只对少部分查询不适用,咱们每每能够经过改写查询让MySQL高效地完成工做。另外,5.6版本会消除不少本来的限制,让更多的查询可以已尽量高的效率完成。
MySQL的子查询实现得很是糟糕,最糟糕的一类查询是WHERE条件语句中包含IN()的子查询。
SELECT * FROM sakila.film
WHERE film_id IN(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1 );
-- MySQL对IN()列表中的选项有专门的优化策略,但关联子查询并非这样的,MySQL会将相关的外层表压到子查询中,它认为这样能够高效地查找到数据行。也就是说,以上查询会被MySQL更改为:
SELECT * FROM sakila.film
WHERE EXISTS(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1
AND film_actor.film_id = film.film_id);
-- 这时子查询须要根据film_id来关联外部表的film,由于须要film_id字段,因此MySQL认为没法先执行这个子查询。经过EXPLIAN能够看到子查询是一个相关子查询(DEPENDENT SUBQUERY),而且能够看到对film表进行全表扫描,而后根据返回的film_id逐个进行子查询。若是外层是一个很大的表,查询性能会很糟糕。
-- 优化重写方式1:
SELECT film.* FROM sakila.film
INNER JOIN sakil.film_actor USING(film_id)
WHERE actor_id =1;
-- 优化重写方式2:使用函数GROUP_CONCAT()在IN()中构造一个逗号分割的列表。
-- 优化重写方式3,使用EXISTS()等效的改写查询:
SELECT * FROM sakila.film
WHERE EXISTS(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1
AND film_actor.film_id = film.film_id);复制代码
有时,MySQL没法将限制条件从外层“下推”到内层,这使得原表可以限制部分返回结果的条件没法应用到内层查询的优化上。
若是但愿UNION的各个子句可以根据LIMIT只取部分结果集,或者但愿可以先排好序再合并结果集的话,就须要在UNION的各个子句中分别使用这些子句。另外,从临时表取出数据的顺序是不必定的,若是要得到正确的顺序,还须要加上一个全局的ORDER BY 和 LIMIT
(SELECT first_name, last_name
FROM sakila.actor
ORDER BY last_name)
UNION ALL
(SELECT first_name, last_name
FROM sakila.customer
ORDER BY last_name)
LIMIT 20;
-- 在UNION子句分别使用LIMIT
(SELECT first_name, last_name
FROM sakila.actor
ORDER BY last_name
LIMIT 20)
UNION ALL
(SELECT first_name, last_name
FROM sakila.customer
ORDER BY last_name
LIMIT 20)
LIMIT 20;复制代码
MySQL并不支持松散索引扫描,也就没法按照不连续的方式扫描一个索引。一般,MySQL的索引扫描须要先定义一个起点和终点,即便须要的数据只是这段索引中不多数的几个,MySQL仍须要扫描这段索引中每个字段。
示例:假设咱们有索引(a,b),有如下查询SELECT ... FROM tb1 WHERE b BETEWEEN 2 AND 3;
,由于只使用了字段b而不符合索引的最左前缀,MySQL没法使用这个索引,从而只能经过全表扫描找到匹配的行。
了解索引结构的话,会发现还有一个更快的办法执行上面的查询。索引的物理结构(不是存储引擎API)使得能够先扫描a列第一个值对应的b列的范围,而后在跳到a列第二个只扫描对应的b列的范围,即松散索引扫描。这时就无须再使用WHERE过滤,由于已经跳过了全部不须要的记录。MySQL并不支持松散索引扫描
MySQL5.0 之后的版本,某些特殊的场景下是可使用松散索引扫描的。例如,在一个分组查询中须要找到分组的最大值和最小值:
-- 在Extra字段显示“Using index for group-by”,表示使用松散索引扫描
EXPLAIN SELECT actor_id, MAX(film_id)
FROM sakila.film_actor
GROUP BY actor\G;复制代码
在MySQL很好地支持松散索引扫描以前,一个简单的绕过办法就是给前面的列加上可能的常数值。5.6以后的版本,关于松散索引扫描的一些限制将会经过“索引条件下推(index condition pushdown)”的方式解决
对于MIN()和MAX()查询,MySQL的优化作得并很差。
SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';
-- 由于在first_name上没有索引,MySQL将会进行一次全表扫描。若是MySQL可以进行主键扫描,那么理论上当MySQL读到第一个知足条件的记录,就是须要找到的最小值,由于主键是严格按照actor_id字段的大小顺序排列的。
-- 曲线优化办法:移除MIN(),而后使用LIMIT
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENNLOPE' LIMIT 1;
-- 该SQL已经没法表达它的本意,通常咱们经过SQL告诉服务器须要什么数据,再由服务器决定如何最优地获取数据。但有时候为了得到更高的性能,须要放弃一些原则。复制代码
MySQL不容许对同一张表同时进行查询和更新。这其实并非优化器的限制,若是清楚MySQL是如何执行查询的,就能够避免这种状况。能够经过生成表来绕过该限制。
-- 符合标准的SQL,可是没法运行
mysql> UPDATE tbl AS outer_tbl
-> SET cnt = (
-> SELECT count(*) FROM tbl AS inner_tbl
-> WHERE inner_tbl.type = outer_tbl.type
-> );
-- 生成表来绕过该限制:
mysql> UPDATE tbl
-> INNER JOIN(
-> SELECT type, count(*) AS cnt
-> FROM tbl
-> GROUP BY type
-> ) AS der USING(type)
-> SET tbl.cnt = der.cnt;复制代码
若是对优化器选择的执行计划不满意,可使用优化器提供的几个提示(hint)来控制最终的执行计划。不过MySQL升级后可能会致使这些提示无效,须要从新审查。
部分提示类型:
HIGH_PRIORITY和LOW_PRIORITY:
告诉MySQL当多个语句同时访问某一个表的时候,这些语句的优先级。只对使用表锁的存储引擎有效,但即便是在MyISAM中也要慎重,由于这两个提示会致使并发插入被禁用,可能会致使严重下降性能
DELAYED:
STRAIGHT_JOIN:
当MySQL没能正确选择关联顺序的时候,或者因为可能的顺序太多致使MySQL没法评估全部的关联顺序的时候,STRAIGNT_JOIN都会颇有用。特别是在如下第二种状况,MySQL可能会花费大量时间在”statistics“状态,加上这个提示会大大减小优化器的搜索空间。
能够先使用EXLPAN语句来查看优化器选择的关联顺序,而后使用该提示来重写查询,肯定最优的关联顺序。可是在升级MySQL的时候,要从新审视这类查询。
SQL_SMALL_RESULT和SQL_BIG_RESULT:
SQL_BUFFER_RESULT:
SQL_CACHE和SQL_NO_CACHE
SQL_CALC_FOUND_ROWS:
FOR UPDATE和LOCK IN SHARE MODE
USING INDEX、IGONRE INDEX和FORCE INDEX:
5.0和更新版本新增用来控制优化器行为的参数:
count()的做用:
关于MyISAM的神话:
简单的优化
利用MyISAM在count(*)全表很是快的特性,来加速一些特定条件的查询。
-- 使用标准数据据worold
SELECT count(*) FROM world.city WHERE ID > 5;
-- 将条件反转,可很大程度减小扫描行数到5行之内
SELECT (SELECT count(*) FROM world.city) - COUNT(*)
FROM world.city WHERE ID <= 5;复制代码
示例:假设可能须要经过一个查询返回各类不一样颜色的商品数量
-- 使用SUM
SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0)) AS red FROM items;
-- 使用COUNT,只须要将知足条件的设置为真,不知足设置为NULL
SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULLASred FROM items;复制代码
使用近似值:
更复杂的优化:
分页的时候,另外一个经常使用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示,这样就能够得到去掉LIMIT之后知足条件的行数,所以能够做为分页的总数。加上这个提示后,MySQL无论是否须要都会扫描全部知足条件的行,而后抛弃掉不须要的行,而不是在知足LIMIT的行数后就终止扫描。因此该提示的代价可能很是高。
Percona Toolkit contains pt-query-advisor, a tool that parses a log of queries, analyzes
the query patterns, and gives annoyingly detailed advice about potentially bad practices
in them.
用户自定义变量是一个用来存储内容的临时容器,在链接MySQL的整个过程当中都存在。在查询中混合使用过程化和关系化逻辑的时候,该特性很是有用。
使用方法:
SET @one := 1;
SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
SET @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
SELECT ... WHERE col <= @last_week;
-- 具备“左值”特性,在给一个变量赋值的同时使用这个变量
SELECT actor_id, @rownum := @rownum + 1 As rownum ...复制代码
没法使用的场景:
应用场景:
优化排名语句:
-- 查询获取演过最多电影的前10位演员,而后根据出演电影次数作一个排名,若是出演次数同样,则排名相同。
mysql> 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;复制代码
避免重复查询刚刚更新的数据:
-- 在更新行的同时又但愿获取获得该行的信息。虽然看起来仍然须要两个查询和两次网络来回,但第二个查询无须访问任何数据表,速度会快不少
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 ) );复制代码
肯定取值的顺序
一个最多见的问题,没有注意到在赋值和读取变量的使用多是在查询的不一样阶段。
-- WHERE和SELECT是在查询执行的不一样阶段被执行的,而WHERE是在ORDER BY文件排序操做以前执行。
mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
-> FROM sakila.actor
-> WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt |
+----------+------+
| 1 | 1 |
| 2 | 2 |
+----------+------+复制代码
尽可能让变量的赋值和取值发生在执行查询的同一个阶段。
mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum AS rownum
-> FROM sakila.actor
-> WHERE (@rownum := @rownum + 1) <= 1;复制代码
将赋值运距放到LEAST(),这样就能够彻底不改变排序顺序的时候完成赋值操做。这个技巧在不但愿对子句的执行结果有影响却又要完成变量复制的时候颇有用。这样的函数还有GREATEST(), LENGTH(), ISNULL(), NULLIF(), IF(), 和COALESCE()。
-- LEAST()老是返回0
mysql> SET @rownum := 0;
mysql> SELECT actor_id, first_name, @rownum AS rownum
-> FROM sakila.actor
-> WHERE @rownum <= 1
-> ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);复制代码
编写偷懒的UNION:
假设须要编写一个UNION查询,其第一个子查询做为分支条件先执行,若是找到了匹配的行,则跳过第二个分支。在某些业务场景中确实会有这样的需求,好比如今一个频繁访问的表中查找“热”数据,找不到再去另一个较少访问的表中查找“冷数据“。(区分热冷数据是一个很好提升缓存命中率的办法)。
-- 在两个地方查找一个用户,一个主用户表,一个长时间不活跃的用户表,不活跃的用户表的目的是为了实现更高效的归档。
-- 旧的UNION查询,即便在users表中已经找到了记录,上面的查询仍是会去归档表中再查找一次。
SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
-- 用一个偷懒的UINON查询来抑制这样的数据返回,当第一个表中没有数据时,咱们才在第二个表中查询。一旦在第一个表中找到记录,就定义一个变量@found,经过在结果列中作一次赋值来实现,而后将赋值放在函数GREATEST中来避免返回额外的数据。为了明确结果来自哪个表,新增了一个包含表名的列。最后须要在查询的末尾将变量重置为NULL,保证遍历时不干扰后面的结果。
SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl
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;复制代码
用户自定义变量的其余用处:
其余用法:
使用MySQL来实现对列表是一个取巧的作法,不少系统在高流量、高并发的状况下表现并很差。典型的模式是一个表包含多种类型的记录:未处理记录、已处理记录、正在处理的记录等等。一个或者多个消费者线程在表中查找未处理的记录,而后声称正在处理,当处理完成后,再将记录更新为已处理状态。通常的,例如邮件发送、多命令处理、评论修改等会使用相似模式,但
原有处理方式不合适的缘由:
优化过程:
将对列表分红两部分,即将已处理记录归档或者存放到历史表,这样始终保证对列表很小。
找到未处理记录通常来讲都没问题,若是有问题则能够经过使用消息方式来通知各个消费者。
可已使用一个带有注释的SLEEP()函数作超时处理。这让线程一直阻塞,直到超时或者另外一个线程使用KILL QUERY结束当前的SLEEP。所以,当再向对列表中新增一批数据后,能够经过SHOW PROCESSLIST
,根据注释找到当前正在休眠操做的线程,并将其KILL。可使用函数GET_LOCK和RELEASE_LOCK()来实现通知,或者能够在数据库以外实现,如使用一个消息服务。
SELECT /* waiting on unsent_emails */ SLEEP(10000), col1 FROM table;复制代码
最后一个问题是如何让消费者标记正在处理的记录,而不至于让多个消费者重复处理一个记录。
尽可能避免使用SELECT FOR UPDATE,这一般是扩展性问题的根源,这会致使大量的书屋阻塞并等待。不光是队列表,任何状况下都要避免。
能够直接使用UPDATE来更新记录,而后检查是否还有其余的记录须要处理。(全部的SELECT FOR UPDATE均可以使用相似的方式改写)
-- 该表的owner用来存储当前正在处理这个记录的链接ID,即由函数CONNECTION_ID()返回额ID,若是当前记录没有被任何消费者处理,则该值为0
CREATE TABLE unsent_emails (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
-- columns for the message, from, to, subject, etc.
status ENUM('unsent', 'claimed', 'sent'),
owner INT UNSIGNED NOT NULL DEFAULT 0,
ts TIMESTAMP,
KEY (owner, status, ts)
);
-- 常见的处理办法。这里的SELECT查询使用到索引的两个列,理论上查找的效率应该更快。问题是,两个查询之间的“间隙时间”,这里的锁会让全部其余同一的查询所有被阻塞。全部这样的查询将使用相同的索引,扫描索引相同结果的部分,因此极可能被阻塞。
BEGIN;
SELECT id FROM unsent_emails
WHERE owner = 0 AND status = 'unsent'
LIMIT 10 FOR UPDATE;
-- result: 123, 456, 789
UPDATE unsent_emails
SET status = 'claimed', owner = CONNECTION_ID()
WHERE id IN(123, 456, 789);
COMMIT;
-- 改进后更高效的写法,无须使用SELECT查询去找到哪些记录尚未被处理。客户端的协议会告诉你更新了几条记录,因此能够直到此次须要处理多少条记录。
SET AUTOCOMMIT = 1;
COMMIT;
UPDATE unsent_emails
SET status = 'claimed', owner = CONNECTION_ID()
WHERE owner = 0 AND status = 'unsent'
LIMIT 10;
SET AUTOCOMMIT = 0;
SELECT id FROM unsent_emails
WHERE owner = CONNECTION_ID() AND status = 'claimed';
-- result: 123, 456, 789复制代码
最后还需处理一种特殊状况:那些正在被进程处理,而进程自己却因为某种缘由退出的状况。
只须要按期运行UPDATE语句将它都更新成原始状态,而后执行SHOW PROCESSLIST,获取当前正在工做的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程
-- 假设获取的线程ID有(十、20、30),下面的更新语句会将处理时间超过10分钟的记录状态更新成初始状态。
-- 将范围条件放在WHERE条件的末尾,这个查询刚好能勾使用索引的所有列,其它的查询也都能使用上这个索引,这样就避免了再新增一个额外的索引来知足其它的查询
UPDATE unsent_emails
SET owner = 0, status = 'unsent'
WHERE owner NOT IN(0, 10, 20, 30) AND status = 'cla
AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;复制代码
该案例中的一些基础原则:
有时,最好的办法就是将任务队列从数据库中迁移出来,Redis和memcached就是一个很好的队列容器。
不建议使用MySQL作太复杂的空间计算存储,PostgreSQL在这方面是一个不错的选择。一个典型的例子是计算以某个点为中心,必定半径内的全部点。例如查找某个点附近全部能够出租的房子,或者社交网站中”匹配“附近的用户。
假设咱们有以下表,这里经度和纬度的单位都是度:
CREATE TABLE locations (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
lat FLOAT NOT NULL,
lon FLOAT NOT NULL
);
INSERT INTO locations(name, lat, lon)
VALUES('Charlottesville, Virginia', 38.03, −78.48),
('Chicago, Illinois', 41.85, −87.65),
('Washington, DC', 38.89, −77.04);复制代码
假设地球是圆的,而后使用两点所在最大圆(半正矢)公式来计算两点之间的距离。现有坐标latA和lonA、latB和lonB,那么点A和点B的距离计算公式以下:
ACOS(
COS(latA) * COS(latB) * COS(lonA - lonB)
+ SIN(latA) * SIN(latB)
)复制代码
计算的结果是一个弧度,若是要将结果转换成英里或公里,则须要乘以地球的半径。
SELECT * FROM locations WHERE 3979 * ACOS(
COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
+ SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;复制代码
这类查询不只没法使用索引,并且还会很是消耗CPU时间,给服务器带来很大的压力,并且还得反复计算。
优化地方:
看看是否真的须要这么精确的计算。其实该算法已经有不少不精确的地方:
若是不须要过高的精度,能够认为地球是圆的。要想有更多的优化,能够将三角函数的计算放到应用中,而不要在数据库中计算。
看看是否真须要计算一个圆周,能够考虑直接使用一个正方形代替。边长为200英里的正方形,一个顶点到中心的距离大概是141英里,这和实际计算的100英里相差并不太远。根据正方形公式来计算弧度为0.0253(100英里)的中心到边长的距离:
SELECT * FROM locations
WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253);复制代码
如今看看如何用索引来优化这个查询:
新增两个列,用来存储坐标的近似值FLOOR(),而后在查询中使用IN()将全部点的整数值都放到列表中:
mysql> ALTER TABLE locations
-> ADD lat_floor INT NOT NULL DEFAULT 0,
-> ADD lon_floor INT NOT NULL DEFAULT 0,
-> ADD KEY(lat_floor, lon_floor);复制代码
如今能够根据坐标的必定范围的近似值来搜索,这个近似值包括地板值和天花板值,地理上分别对应的是南北:
-- 查询某个范围的全部点,数值须要在应用程序中计算而不是MySQL
mysql> SELECT FLOOR( 38.03 - DEGREES(0.0253)) AS lat_lb,
-> CEILING( 38.03 + DEGREES(0.0253)) AS lat_ub,
-> FLOOR(-78.48 - DEGREES(0.0253)) AS lon_lb,
-> CEILING(-78.48 + DEGREES(0.0253)) AS lon_ub;
+--------+--------+--------+--------+
| lat_lb | lat_ub | lon_lb | lon_ub |
+--------+--------+--------+--------+
| 36 | 40 | −80 | −77 |
+--------+--------+--------+--------+
-- 生成IN()列表中的整数:
SELECT * FROM locations
WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253)
AND lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77);复制代码
使用近似值会让咱们的计算结果有误差,因此咱们还须要一些额外的条件过滤在正方形以外的点,这和前面使用CRC32作哈希索引相似:先建一个索引过滤出近似值,在使用精确条件匹配全部的记录并移除不知足条件的记录。
事实上,到这时就无须根据正方形的近似来过滤数据,可使用最大圆公式或者毕达哥拉斯定理来计算:
SELECT * FROM locations
WHERE lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77)
AND 3979 * ACOS(
COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
+ SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;复制代码
这时计算精度再次回到使用一个精确的圆周,不过如今的作法更快。只要可以高效地过滤掉大部分的点,例如使用近似整数和索引,以后再作精确数学计算的代价并不大。只要不是使用大圆周的算法,不然速度会更慢。
该案例使用的优化策略:
若是把建立高性能应用程序比做是一个环环相扣的”难题“,除了前面介绍的schema、索引和查询语句设计以外,查询优化应该是解开”难题“的最后一步。
理解查询是如何被执行的以及时间都消耗在哪些地方,这依然是前面介绍的响应时间的一部分。再加上一些诸如解析和优化过程的知识,就能够额更进一步地理解上一章讨论的MySQL如何访问表和索引的内容了。这也从另外一个维度理解MySQL在访问表和索引时查询和索引的关系。
优化一般须要三管齐下:不作、少作、快速地作。