审核
什么是业务审核
- 类似与code review
- 评审业务Schema和SQL设计
- 偏重关注性能
- 是业务优化的主要入口之一
审核提前发现问题,进行优化
上线后通过监控或巡检发现问题,进行优化
Schema设计审核
- 表和字段命名是否合规
- 字段类型,长度设计是否适当
- 表关联关系是否合理
- 主键,更新时间保留字段等是否符合要求
- 约束,默认值等配置是否恰当
- 了解业务,表数据量,增长模式
- 数据访问模式,均衡度
- 根据业务需求,表是否需要分区,是否有数据什么周期
SQL语句审核
- SQL语句的执行频率
- 表上是否有合适的索引
- 单次执行的成本
- 执行模式,锁情况分析
- 关注事务上下文
什么时候需要审核
- 业务开发阶段,上线前
- 业务版本变更,线上更新前
-
- 新表和SQL上线
- SQL查询条件变化
- SQL查询频率变化
- 业务逻辑导致现有表数据量规模变化
业务发布流程
- SQL审核需要开发与应用运维支持
- 充分沟通,做好必要性说明和教育工作
- 指定业务发布流程,嵌入DBA审核环节
- 积累经验,不断完善评审方法
慢查询
查询优化,索引优化,库表结构优化需要齐头并进。
慢查询两个步骤分析:
- 确认应用程序是否向数据库请求了大量超过需要的数据
- 确认mysql服务器层是否在处理大量超过需要的数据记录
是否向数据库请求了不需要的数据
典型案例:
- 查询不需要的记录
- 多表关联时返回全部列
- 总是取出全部列
- 重复查询相同的数据
mysql是否在扫描额外的记录
在确定查询只返回需要的数据后,接下来应该看看查询为了返回结果是否扫描了过多的数据。
mysql查询开销的三个指标:
- 响应时间
- 扫描的行数
- 返回的行数
这三个指标都会记录到mysql的慢日志中,索引检查慢日志记录是找出扫描行数过多的查询的好办。
响应时间:执行时间和等待时间;
判断一个响应时间是否是合理的值,可以使用"快速上限估计"。
扫描的行数和返回的行数
分析查询时,查看该查询扫描的行数是非常有帮助的。它一定程度上说明该查询找到需要的数据的效率高不高。
如果发现查询需要扫描大量的数据但只返回少数的行,优化方法:
- 使用索引覆盖扫描,把所有需要用的列都放到索引中。
- 改变库表结构。例如使用单独的汇总表
- 重写这个复杂的查询,让mysql优化器能够以更优化的方式执行这个查询。
有的时候将大查询分解为多个小查询是有必要的。
查询执行的基础
mysql查询执行路径
- 客服端发送一条查询给服务器
- 服务器先检查缓存。如果命中缓存,则立刻返回结果。否则进入下一阶段。
- 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划。
- mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
- 将结果返回给客户端
mysql客户端/服务器通信协议
mysql客户端和服务器之间的通信协议是"半双工"。任何时候只能一方发;不能同时发送;
mysql连接时线程状态
mysql> show full processlist; +----+------+-----------+--------+---------+------+-------+------------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------+--------+---------+------+-------+------------------------+ | 39 | root | localhost | sakila | Sleep | 4 | | NULL | | 40 | root | localhost | sakila | Query | 0 | NULL | show full processlist | +----+------+-----------+--------+---------+------+-------+------------------------+ 2 rows in set (0.00 sec)
查询优化器
一条查询可以有很多种执行方式,最后都返回相同的结果。
优化器的作用就是找到这其中最好的执行计划。
mysql使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。
通过查询当前会话的last_query_cost的值来得知Mysql计算的当前查询的成本。
mysql> select count(*) from film_actor; +----------+ | count(*) | +----------+ | 5462 | +----------+ 1 row in set (0.00 sec) mysql> show status like 'last_query_cost'; +-----------------+-------------+ | Variable_name | Value | +-----------------+-------------+ | Last_query_cost | 1040.599000 | +-----------------+-------------+
这个结果表示mysql优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数,索引的基数(索引中不同值的数量),索引和数据行的长度,索引分布情况。
优化器在评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘I/O。
mysql优化器选错执行计划的原因:
- 统计信息不准确
- 执行计划中的成本估算不等同于实际执行的成本。
-
- 有的计划虽然要读取更多页,但是这些页在缓存中。
- mysql的最有可能和你想的最优不一样。
-
- 比如你希望执行时间尽可能的短,而mysql只是基于成本模型选择的最优执行计划。
- mysql从不考虑其他并发执行的查询,这可能会影响到当前查询速度。
- mysql不会考虑不受其控制的操作的成本。
-
- 如执行存储过程或者用户自定义函数的成本
优化策略:
- 静态优化
-
- 直接对解析树进行分析,并完成优化。优化器通过一些简单的代数变换将where条件转换成另一种等价形式。静态优化在第一次完成后一直有效。可以认为这是一种"编译时优化"
- 动态优化
-
- 动态优化和查询的上下文有关。也和其他很多因素有关,例如where中的取值,索引中条目,等等。每次查询的时候都重新评估,可以认为这是一种"运行时优化"
mysql能够处理的优化类型
- 重新定义关联表的顺序。
- 将外联结转成内连接
- 使用等价变化规则
-
- 合并和减少一些比较,移除一些恒成立和一些恒不成立的判断
- 优化count(),min(),max(),min()就直接拿BTree树最左端数据行
- 预估并转换为常数表达式
- 覆盖索引扫描
- 子查询优化
- 提前终止查询
- 等值传播
在查询中添加hint,提示优化器,
优化器的局限性
1 关联子查询
mysql的子查询实现得非常糟糕;最糟糕的一类查询是where条件中包含IN()的子查询语句。
例如,我们希望找到sakila数据库中,演员actor_id为1,参演过的所有影片信息。很自然的,我们会按照下面的方式
mysql> select * from film where film_id in ( select film_id from film_actor where actor_id =1) \G;
我们一般认为,mysql会首先将子查询的actor_id=1的所有film_id都找到,然后再去做外部查询,如
select * from film where film_id in (1,23,25,106,140);
然而,mysql不是这样做的。
mysql会将相关的外层表压到子查询中,它认为这样可以更高效率地查找数据行。
当然我们可以使用连接替代子查询重写这个SQL,来优化;
mysql> explain select * from film f inner join film_actor fa where f.film_id=fa.film_id and actor_id =1; +----+-------------+-------+--------+------------------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+------------------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | fa | ref | PRIMARY,idx_fk_film_id | PRIMARY | 2 | const | 19 | | | 1 | SIMPLE | f | eq_ref | PRIMARY | PRIMARY | 2 | sakila.fa.film_id | 1 | | +----+-------------+-------+--------+------------------------+---------+---------+-------------------+------+-------+ 2 rows in set (0.00 sec)
如何用好关联子查询,很多时候,关联子查询也是一种非常合理,自然,甚至是性能最好的写法。
where in()肯定是不行的,但是 where exists()有时是可以的;
2 union的限制
有时,mysql无法将限制条件从外层"下推"到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。
如果希望union的各个子句能够根据limit只取部分结果集,或者希望能够先拍下再合并结果集的话,就需要在union的各个子句中分别使用这些子句。
如:
(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;
会将actor中200条记录和customer中599条记录放在一个临时表中,然后在从临时表中取出前20条;
而
(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;
现在中间的临时表中只会包含40条记录。
3 最大值和最小值优化
对于min()和max()查询,mysql的优化做得并不好。
mysql> explain select min(actor_id) from actor where first_name='PENELOPE'; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | actor | ALL | NULL | NULL | NULL | NULL | 200 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec)
因为在first_name字段上没有索引,因此mysql将会进行一次全表扫描。
如果mysql能够进行主键扫描,那么理论上,mysql读到第一个满足条件的记录的时候,就是我们需要找的最小值了,因为主键时严格按照actor_id字段的大小顺序排序的。但这仅仅是如果,mysql这时只会做全表扫描。
优化min(),使用limit重写SQL:
mysql> explain select actor_id from actor USE INDEX(PRIMARY) where first_name='PENELOPE' LIMIT 1; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | actor | ALL | NULL | NULL | NULL | NULL | 200 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec)
看着实验结果,似乎没有使用 主键索引,不知道是什么原因导致.欢迎交流。
4 在同一个表上查询和更新
mysql不允许,对同一张表进行查询和更新:
mysql> update tbl AS outer_tbl set cnt = ( select count(*) from tbl AS inner_tbl where inner_tbl.type = outer_tbl.type ); error:you can't specify target table 'outer_tbl' for update in from clause
可以使用内连接来绕过这个限制。实际上,这执行了两个查询:一个是子查询中的select语句,另一个是多表关联update,只是关联的表是一个临时表。
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)
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。
- HIGH_PRIORITY,LOW_PRIORITY
-
- 这个提示告诉mysql,当多个语句同时访问某一个表的时候,哪些语句的优先级相对高些,哪些语句的优先级相对低些。
- 只对使用表锁的存储引擎有效,不要在innodb或者其他有细粒度锁机制和并发控制的引擎中使用。
- DELAYED
-
- 这个提示对insert,replace有效。mysql会将使用该提示的语句立即返回给客户端,并将插入的行数据放入到缓冲区,然后在表空间时批量将数据写入。
- 日志系统使用这样的提示非常有效,或者是其他需要写入大量数据但是客户端却不需要等待单条语句完成I/O的应用。这个用法有一些限制:并不是所有的存储引擎都支持这样的做法;并且该提示会导致函数LAST_INSERT_ID无法正常工作。
- USE INDEX,IGNORE INDEX ,FORCE INDEX
慢查询分析
1 show status 了解各SQL的执行频率
默认使用参数为,session;可以使用global;
mysql> show status like 'com%'; +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | Com_admin_commands | 0 | | Com_assign_to_keycache | 0 | | Com_alter_db | 0 | | Com_alter_procedure | 0 | | Com_alter_server | 0 | | Com_alter_table | 0 |
com_xxx表示每个xxx语句执行的次数:
com_select: 执行select操作的次数,一次查询只累加一次;
com_insert: 执行insert操作的次数,对于批量插入的insert操作,只累加一次;
com_update: 执行update操作的次数
com_delete: 执行delete操作的次数
上面这些参数对于所有存储引擎的表操作都会进行累计。下面几个参数只是针对innodb存储引擎,累加算法也略有不同。
innodb_rows_read: select查询返回的行数
innodb_rows_inserted: 执行insert操作插入的行数
innodb_rows_updated: 执行update操作更新的行数
innodb_rows_deleted: 执行delete操作删除的行数
通过以上参数,很容易了解当前数据库的应用是以插入更新为主还是以查询操作为主,大致的读写比例是多少;
可以通过com_commit 和 com_rollback 可以知道,事务回滚的比例;
如果比例过高则说明应用编写存在问题;
connections: 试图连接mysql服务器的次数
uptime: 服务器工作时间
slow_queries: 慢查询的次数;
2 定位低效 SQL
- 慢查询日志,定位低效SQL;long_query_time,慢查询的标准时间;
- 慢查询是,查询结束之后才记录;因此他不是实时的;show processlist 查看mysql在进行的线程,查看线程的一些状态,可以实时地查看SQL的执行情况;
3 explain分析低效查询SQL的执行计划
mysql> explain select b from t where a =1; +----+-------------+-------+------+---------------+------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+-------+------+-------------+ | 1 | SIMPLE | t | ref | a | a | 5 | const | 1 | Using where | +----+-------------+-------+------+---------------+------+---------+-------+------+-------------+ 1 row in set (0.00 sec)
当然explain也可以来查询使用了什么索引;
- select_type
-
- simple:简单表,即不使用表链接或者子查询
- Primary:主查询,即外层的查询
- union:union中的第二个或者后面的查询语句
- subquery: 子查询中的第一个select
- table:输出结果集的表
- type:访问类型
-
- all,全表扫描
- index,索引全扫描
- range,索引范围扫描,常见于< , >,between
- ref,使用非唯一索引扫描或唯一索引的前缀扫描,返回匹配某个单独值的记录行
- eq_ref,类似ref,区别在于使用了唯一索引;
- const/system,表中最多有一个匹配行;Primary key 或 unique index;
- null,不用访问表或者索引就可以得到结果
- possible_keys:表示查询时可能使用的索引
- key: 表示实际使用的索引
- key_len: 使用到索引字段的长度
- rows:扫描行的数量
- extra:执行情况的说明和描述;
使用explain extended,可以得到更清晰易读的SQL,多出了warning,可以进一步分析SQL;
当然如果表 有分区,那么使用explain partition 可以找到select到底是在哪个分区查询的;
4 show profile 分析SQL
查看mysql是否支持profile;
mysql> select @@have_profiling; +------------------+ | @@have_profiling | +------------------+ | YES | +------------------+
查看profiling是否开启,默认关闭:
mysql> select @@profiling; +-------------+ | @@profiling | +-------------+ | 0 | +-------------+ 1 row in set (0.00 sec)
开启profiling:
mysql> set profiling=1; Query OK, 0 rows affected (0.00 sec)
通过profile,我们能够更清楚地了解SQL执行的过程。
如何使用:
mysql> select count(*) from payment; +----------+ | count(*) | +----------+ | 16049 | +----------+ 1 row in set (0.02 sec)
通过show profiles,找到对应SQL的 query id;
mysql> show profiles; +----------+------------+------------------------------+ | Query_ID | Duration | Query | +----------+------------+------------------------------+ | 1 | 0.01064275 | select count(*) from payment | | 2 | 0.00048225 | show databases | | 3 | 0.00015000 | show DATABASE() | | 4 | 0.00039975 | show tables | +----------+------------+------------------------------+
通过show profile for query id ,分析具体的SQL;
能够看到执行过程中线程的每个状态和消耗的时间;
mysql> show profile for query 4; +----------------------+----------+ | Status | Duration | +----------------------+----------+ | starting | 0.000058 | | checking permissions | 0.000009 | | Opening tables | 0.000050 | | System lock | 0.000008 | | init | 0.000012 | | optimizing | 0.000005 | | statistics | 0.000012 | | preparing | 0.000010 | | executing | 0.000007 | | checking permissions | 0.000132 | | Sending data | 0.000042 | | end | 0.000007 | | query end | 0.000007 | | closing tables | 0.000005 | | removing tmp table | 0.000009 | | closing tables | 0.000006 | | freeing items | 0.000015 | | logging slow query | 0.000005 | | cleaning up | 0.000006 | +----------------------+----------+ 19 rows in set (0.00 sec)
在获取到最消耗时间的线程状态后,mysql支持进一步选择all,cpu,block io ,context switch,page faults等明细类型来查看mysql在使用什么资源上耗费了过高的时间。
例如选择查看cup的消耗时间:
mysql> show profile cpu for query 4; +----------------------+----------+----------+------------+ | Status | Duration | CPU_user | CPU_system | +----------------------+----------+----------+------------+ | starting | 0.000058 | 0.000000 | 0.000000 | | checking permissions | 0.000009 | 0.000000 | 0.000000 | | Opening tables | 0.000050 | 0.000000 | 0.000000 | | System lock | 0.000008 | 0.000000 | 0.000000 | | init | 0.000012 | 0.000000 | 0.000000 | | optimizing | 0.000005 | 0.000000 | 0.000000 | | statistics | 0.000012 | 0.000000 | 0.000000 | | preparing | 0.000010 | 0.000000 | 0.000000 | | executing | 0.000007 | 0.000000 | 0.000000 | | checking permissions | 0.000132 | 0.000000 | 0.000000 | | Sending data | 0.000042 | 0.000000 | 0.000000 | | end | 0.000007 | 0.000000 | 0.000000 | | query end | 0.000007 | 0.000000 | 0.000000 | | closing tables | 0.000005 | 0.000000 | 0.000000 | | removing tmp table | 0.000009 | 0.000000 | 0.000000 | | closing tables | 0.000006 | 0.000000 | 0.000000 | | freeing items | 0.000015 | 0.000000 | 0.000000 | | logging slow query | 0.000005 | 0.000000 | 0.000000 | | cleaning up | 0.000006 | 0.000000 | 0.000000 | +----------------------+----------+----------+------------
show profile 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了;
而mysql5.6则通过trace文件进一步向我们展示了优化器是如何选择执行计划的。
5 通过trace 分析优化器如何选择执行计划
提供了对SQL的跟踪trace,通过trace文件能够进一步了解为什么优化器选择A执行计划而不选择B执行计划,帮助我们更好地理解优化器的行为。
使用方式:
首先打开trace,设置格式为json,设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能完整显示。
然后执行select;
最后在,information_schema.optimizer_trace中查看跟踪文件;
索引问题
索引是数据库优化中最常用也是最重要的手段之一,通过索引通常可以帮助用户解决大多数的SQL性能问题。
索引的存储分类
索引是在mysql的存储引擎层中实现的,而不是在服务器层实现的。
- B-Tree 索引:大部分引擎都支持B-Tree索引
- HASH索引:只有memory引擎支持,使用场景简单。
- R-Tree索引:空间索引,Myisam引擎的一个特殊索引类型,主要用于地理空间数据类型
- Full-text:全文索引
前缀索引,大大缩小索引文件的大小,但是在order by 和 group by 操作的时候无法使用前缀索引。
查看所有使用情况
mysql> show status like 'Handler_read%'; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 1 | | Handler_read_key | 6 | | Handler_read_last | 0 | | Handler_read_next | 16050 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 297 | +-----------------------+-------+ 7 rows in set (0.00 sec)
- Handler_read_key :值高,证明索引正在工作;值低,说明增加索引得到的性能改善不高,因为索引不经常使用
- Handler_read_rnd_next:值高,意味着查询效率低,应该建立索引补救;
优化方法
定期分析表和检查表
分析表:
mysql> analyze table store; +--------------+---------+----------+----------+ | Table | Op | Msg_type | Msg_text | +--------------+---------+----------+----------+ | sakila.store | analyze | status | OK | +--------------+---------+----------+----------+ 1 row in set (0.00 sec)
本语句用于分析和存储表的关键字分布,分析的结果将可以使得系统得到准确的统计信息,是的SQL能够生成正确的执行计划。
如果用户感觉实际执行计划并不是预期的执行计划,执行一次分析表可能会解决问题。
检查表:
mysql> check table store; +--------------+-------+----------+----------+ | Table | Op | Msg_type | Msg_text | +--------------+-------+----------+----------+ | sakila.store | check | status | OK | +--------------+-------+----------+----------+ 1 row in set (0.01 sec)
检查表的作用是检查一个或多个表是否有错误。
check table 也可以检查视图是否有错误,比如在视图定义中被引用的表已经不存在;
定期优化表
优化表:
mysql> optimize table store; +--------------+----------+----------+-------------------------------------------------------------------+ | Table | Op | Msg_type | Msg_text | +--------------+----------+----------+-------------------------------------------------------------------+ | sakila.store | optimize | note | Table does not support optimize, doing recreate + analyze instead | | sakila.store | optimize | status | OK | +--------------+----------+----------+-------------------------------------------------------------------+ 2 rows in set (0.04 sec)
将表中的空间碎片进行合并,并且可以消除由于删除或者更新造成的空间浪费;
适用于:已经删除了表的一大部分,或者已经对含有可变长度行的表(含有varchar,blob,text)进行了很多更改,此时表中的空间会产生大量的空间碎片;
另外innodb表在删除大量数据后:
可以使用alter table 但是不修改引擎的方式来回收不用的空间:
mysql> alter table payment engine=innodb; Query OK, 16049 rows affected (0.62 sec) Records: 16049 Duplicates: 0 Warnings: 0
常用SQL的优化
1 大批量插入数据-load
各个引擎的优化方式是不一样的:
MyIsam 引擎,使用load 导入大批数据时:
- alter table tbl_name disable keys;
- load data infile '/home/mysql/film_test3.txt' into table film_test4;
- alter table tbl_name enable keys;
导入前,使索引失效,导入完成之后,在启用索引;
innodb引擎:
- 因为innodb的表是按照主键的顺序保存的,索引将导入的数据按照主键的顺序排序,可以有效地提高导入数据的效率;
- 在导入数据前执行 set unique_checks=0 ,关闭唯一性校验,在导入结束后执行 set unique_checks=1,恢复唯一性校验;
- 如果应用使用自动提交的方式,建议在导入前执行:set autocommit=0,关闭自动提交,在导入完成之后,再开启;
2 优化insert语句
- 使用多个值表的insert比单个insert语句快,因为多值表一起插入,缩减了客户端与数据库之间的连接,关闭等消耗;
-
- insert into test values(1,2),(2,3),(3,4).....
- 将索引文件和数据文件分在不同的磁盘上存放(利用建表中的选项)
- 当从一个文本装载一个表时,使用load data infile ,这通常比使用很多insert 语句快20倍;
- 如果从不同客户插入很多行,insert delayed 语句得到更高的速度;将数据都放入内存中,然后合并一起insert,减少客户端与数据库的交互
3 优化order by 语句
mysql中有两种排序方式
- 第一种通过有序索引顺序扫描直接返回有序数据,这种方式在使用explain 分析查询的时候显示为useing index,不需要额外的排序,操作效率较高。
- 第二种是通过返回数据进行排序,也就是filesort排序,所有不是通过索引直接返回排序结果的排序都叫filesort排序。
1.索引排序
例子:
mysql> alter table customer add index idx_email_storeid (email,store_id); Query OK, 0 rows affected (0.04 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select store_id,email, customer_id from customer order by email\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: customer type: index possible_keys: NULL key: idx_email_storeid key_len: 154 ref: NULL rows: 652 Extra: Using index 1 row in set (0.00 sec)
可以看到,此时只使用了索引顺序,没有使用filesort;
如果将索引改成:
mysql> alter table customer drop index idx_email_storeid; Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> alter table customer add index idx_storeid_email (store_id,email); Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select store_id,email, customer_id from customer order by email\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: customer type: index possible_keys: NULL key: idx_storeid_email key_len: 154 ref: NULL rows: 652 Extra: Using index; Using filesort 1 row in set (0.00 sec)
可以看到,此时使用到了filesort 这种额外排序,这显然会增加开销;
可见,索引最左端原则,order by email,那么email 这一列在索引中应该在最左端,这样才能够只使用索引排序,而不用使用filesort;
优化目标:
尽量减少额外的排序,通过索引直接返回有序数据。
到达这目的要求:where 条件和 order by 使用相同的索引,并且order by 的顺序和索引顺序相同,并且order by的字段都是升序或者降序。否则肯定需要额外的排序操作,这样就会出现filesort;
总结:
下列SQL可以使用索引:
- select * from tabname order by key_part1,key_part2,......;
- select * from tabname where key_part1=1 order by key_part1 DESC, key_part2 DESC;
- select * from tabname order by key_part1 DESC, key_part2 DESC;
下列不可以使用索引:
- select * from tabname order by key_part1 desc ,key_part2 asc;
-
- order by 的字段混合 ASC,DESC
- select * from tabname where key2 = 2 order by key1;
-
- 用于查询行的关键字与order by 中使用的不相同
- select * from tabname order by key1,key2;
2.filesort的优化
通过创建合适的索引能够减少filesort出现,但是在某些情况下,条件限制不能让filesort消失,那就需要想办法加快filesort的操作。
mysql有两种排序算法:
- 两次扫描算法:首先根据条件取出排序字段和行指针信息,之后在排序区sort buffer中排序。优点是排序的时候内存开销较小,但排序效率低;
- 一次扫描算法:一次性去除满足条件的行的所有字段,然后在排序区sort buffer中排序后直接输出结果。优点排序效率比两次扫描高,但内存开销大;
mysql通过max_length_for_sort_data的大小和query语句取出的字段总大小来判断使用哪种排序算法;
如果max_length_for_sort_data 大 则使用一次扫描算法,如果小则使用 两次扫描算法;
4 优化group by 语句
默认情况下,group by col1,col2, 的字段进行排序。这当然会造成额外消耗;要消除这种不必要的排序,可以使用 order by null 来禁止排序;
mysql> explain select payment_date,sum(amount) from payment group by payment_date \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: payment type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 16451 Extra: Using temporary; Using filesort 1 row in set (0.01 sec)
使用order by null优化group by
mysql> explain select payment_date,sum(amount) from payment group by payment_date ORDER BY NULL \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: payment type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 16451 Extra: Using temporary 1 row in set (0.00 sec)
5 优化嵌套查询
子查询可以被更有效率的连接(join)替代;
连接(join)之所以更有效率一些,因为mysql不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作;
6 mysql 如何优化or条件
对于含有or的查询子句,如果要利用索引,则or之间的每个条件列都必须用到索引;如果没有索引,则应该考虑增加索引;
mysql在处理含有or子句的查询时,实际是对or的各个字段分别查询后的结果进行了union 操作;
但是在建有符合索引的列 company_id 和moneys上面做or操作时,却不能用到索引;
7 优化分页查询
延迟关联,它让mysql扫描尽可能少的页面,索取需要访问的记录后再根据关联列回原表查询需要的所有列。
考虑下面的查询:
mysql> explain select film_id,description from film order by title limit 50,5; +----+-------------+-------+------+---------------+------+---------+------+------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+----------------+ | 1 | SIMPLE | film | ALL | NULL | NULL | NULL | NULL | 1134 | Using filesort | +----+-------------+-------+------+---------------+------+---------+------+------+----------------+ 1 row in set (0.00 sec)
使用"延迟关联":
mysql> explain select film_id,description from film inner join ( select film_id from film order by title limit 50,5) AS lim using(film_id); +----+-------------+------------+--------+---------------+-----------+---------+-------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+--------+---------------+-----------+---------+-------------+------+-------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 5 | | | 1 | PRIMARY | film | eq_ref | PRIMARY | PRIMARY | 2 | lim.film_id | 1 | | | 2 | DERIVED | film | index | NULL | idx_title | 767 | NULL | 55 | Using index | +----+-------------+------------+--------+---------------+-----------+---------+-------------+------+-------------+ 3 rows in set (0.00 sec)
首先,让film_id使用索引,找到对应film_id,然后再回表找到对应description的数据列。这样,是延迟了列的访问,所以叫延迟关联;其实是分别找出对应列的数据行;
使用书签记录
offset,它会导致mysql扫描大量不需要的行然后再抛弃掉。
可以使用书签记录上次数据的位置,那么下次就可以直接从书签记录的位置开始扫描,这样就可以避免使用offset;
首先获得第一组结果:
mysql> select * from rental order by rental_id limit 5; +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+ | rental_id | rental_date | inventory_id | customer_id | return_date | staff_id | last_update | +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+ | 1 | 2005-05-24 22:53:30 | 367 | 130 | 2005-05-26 22:04:30 | 1 | 2006-02-15 21:30:53 | | 2 | 2005-05-24 22:54:33 | 1525 | 459 | 2005-05-28 19:40:33 | 1 | 2006-02-15 21:30:53 | | 3 | 2005-05-24 23:03:39 | 1711 | 408 | 2005-06-01 22:12:39 | 1 | 2006-02-15 21:30:53 | | 4 | 2005-05-24 23:04:41 | 2452 | 333 | 2005-06-03 01:43:41 | 2 | 2006-02-15 21:30:53 | | 5 | 2005-05-24 23:05:21 | 2079 | 222 | 2005-06-02 04:33:21 | 1 | 2006-02-15 21:30:53 | +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+
之后从书签开始再找:
mysql> select * from rental where rental_id > 5 order by rental_id limit 5; +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+ | rental_id | rental_date | inventory_id | customer_id | return_date | staff_id | last_update | +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+ | 6 | 2005-05-24 23:08:07 | 2792 | 549 | 2005-05-27 01:32:07 | 1 | 2006-02-15 21:30:53 | | 7 | 2005-05-24 23:11:53 | 3995 | 269 | 2005-05-29 20:34:53 | 2 | 2006-02-15 21:30:53 | | 8 | 2005-05-24 23:31:46 | 2346 | 239 | 2005-05-27 23:33:46 | 2 | 2006-02-15 21:30:53 | | 9 | 2005-05-25 00:00:40 | 2580 | 126 | 2005-05-28 00:22:40 | 1 | 2006-02-15 21:30:53 | | 10 | 2005-05-25 00:02:21 | 1824 | 399 | 2005-05-31 22:44:21 | 2 | 2006-02-15 21:30:53 | +-----------+---------------------+--------------+-------------+---------------------+----------+---------------------+ 5 rows in set (0.00 sec)
总结
- mysql客户端和服务器间:半双工
- show processlist,实时查看SQL执行情况;
- 查询优化器:
-
- 统计信息:
-
- 每个表/索引的页面个数,
- 索引的基数,索引和数据行的长度,索引分布情况;
- 不考虑任何缓存假设数据读取需要一次IO;
- 优化策略:
-
- 静态优化,“编译时优化”
- 动态优化“运行时优化”
- 查询优化器的局限:
-
- 关联子查询,使用连接替代(5.6之后优化器已经自动优化了);
- union限制,需每个子句都limit 20控制临时表数量;
- 最大值和最小值优化,不能自动更据主键ID 选择;
- 不允许在同一表上更新和查询,可以使用内连接跳过限制;
- hint:
-
- use index
- ignore index
- force index
- 慢查询分析:
-
- show status like 'com%':
-
- 了解读写比例;
- 事务回滚比例;
- 视图连接mysql服务器的次数;
- 慢查询的次数;
- 定位低效SQL:慢查询日志
- explain分析低效SQL:
-
- explain extended 可以得到更清晰易读的SQL,多出来warning;
- explain partition 找到select到底是在哪个分区查询;
- show profile 分析SQL:
-
- show profile 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了;
- 通过show profiles 找出query id,
- 通过show profile for query id 分析具体的SQL;能够看到执行过程中线程的每个状态和消耗的时间;也能根据cpu,io,等具体参数;
- trace:5.6之后可以使用,通过trace文件能够进一步了解为什么优化器选择A执行计划而不选择B执行计划,帮助我们更好地理解优化器的行为
- 索引使用情况:
-
- show status like 'Handler_read%'
- Handler_read_key:值高,证明索引正在工作;值低,说明增加索引得到的性能改善不高,因为索引不经常使用;
- Handler_read_rnd_next:值高,意味着查询效率低,应该建立索引补救;
- 定期分析表和检查表:使系统得到准确的统计信息,使优化器更好工作;
- 常用SQL优化:
-
- load:
-
- myisam,导入前,使索引失效,导入后,开启索引;
- innodb,关闭唯一性校验
- insert:多值表插入,
- order by:
-
- 索引排序:
-
- where条件和order by使用相同索引
- order by 的顺序和索引顺序相同
- order by 字段都是升序或降序,
- filesort:两次扫描算法,一次扫描算法;
- group by:group by 默认对字段排序,使用order by null 来禁止排序;
- 子查询可以使用连接代替
- or条件使用索引需要左右都要有索引段;
- 分页查询
-
- “延迟关联”,
- “首先获得第一组,然后使用书签方式”
- 将大查询分解为多个小查询