mysqladmin -uroot -p ext -i1|awk '
/Queries/{q=$4-qp;qp=$4}
#把上次的值存到qp里,然后本次值减去qp的,就是一秒内的query数量
/Threads_connected/{tc=$4}
/Threads_running/{printf "] ]
]
",q,tc,$4}'
b.
show processlist;
mysql -e "show
processlistG"|grep State: |sort|uniq -c |sort -rn
结果:
744
State:
67
State: Sending data
36
State: freeing item
8
State: NULL
6
State: end
4
State: Updating
...
... ... ...
c.使用查询日志
统计每秒钟的慢查询数量,排序输出
awk '/^# Time:/{print $3, $4, c;c=0}/^# User/{c++}' mysql_slow.log
|sort -k 3 -nr
结果类似这样:
140106 7:00:01 278
140106 10:20:01 269
140106 7:40:01 266
140106 12:20:01 264
140106 6:40:01 261
140106 7:10:01 254
3.4.2捕获诊断数据
系统状态,CPU利用率,磁盘空间使用率,ps输出,mysql信息:show
status,processlist,show innodb status.
linux内部诊断工具有oprofile,strace,GDB,pt-pmp
3.5其它剖析工具
user_statistics表
strace会使mysql变慢,不适合在生产环境中使用
pt-ioprofile
4.1选择优化的数据类型
4.1.6
Mysql在内部使用整数存储ENUM和SET,在做比较操作时转换为字符串。
使用unsigned
int存储IP地址,如果要显示成4段样式,可以用inet_ntoa和inet_aton来回转换
4.2常见的schema设计陷阱
4.3范式和反范式
4.4缓存表和汇总表
字符串可以直接转成datetime:
select
concat(left(now(),14),'00:00') - interval 1 hour; 取上一个小时的整点。
flexviews是第三方的物化视图工具
计数器表:
一般只有一行作为计数器,这样锁竞争太激烈。可以多插入一些行,比如100,每行都是一个计数器,同时新加一个slot列,作为槽位,使用如下语句递增计数器
update hit_counter set cnt=cnt+1 where slot = rand() * 100;
当要获取新的序号时,这样获取:
select sum(cnt) from hit_counter;
这样就把写竞争拆分成100个小锁,大大增加了并发性。但100个定时器的sum还是顺序地往上走,却不会锁(因为只是select)。
还可以再新加一个date列,一天100个计数器,如下表结构:
create table daily_hit_counter (
day date not null,
slot tinyint unsigned
not null,
cnt int unsigned not
null,
primary
key(day,slot)
) engine=innodb;
day 版的增加计数器是这样:
insert into
daily_hit_counter(day,slot,cnt)
values(current_date,rand()*100,1)
on duplicate key update cnt = cnt
+1;
如果希望减少表中的行数,可以把所有计数器都加到0号slot上,然后把其余的都删掉:
update daily_hit_counter as c
inner
join (
select
day,sum(cnt) as cnt,min(slot) as
mslot
from
daily_hit_counter
group
by day
)
as x using(day)
set
c.cnt=if(c.slot=x.mslot,x.cnt,0),
c.slot=if(c.slot=x.mslot,0,c.slot);
#
delete from daily_hit_counter where slot <> 0 and cnt=
0;
结果演示:
mysql> select * from daily_hit_counter;
+------------+------+-----+
| day
|
slot | cnt |
+------------+------+-----+
| 2014-01-06 |
0
| 5 |
| 2014-01-06 |
1
| 13 |
| 2014-01-06 |
2
| 10 |
| 2014-01-06 |
3
| 15 |
| 2014-01-06 |
4
| 11 |
| 2014-01-06 | 12 |
11 |
| 2014-01-06 | 13 |
8 |
| 2014-01-06 | 14 |
14 |
| 2014-01-06 | 15 |
14 |
| 2014-01-06 | 16 |
9 |
| 2014-01-07 |
0
| 3 |
| 2014-01-07 |
1
| 11 |
| 2014-01-07 |
2
| 7 |
| 2014-01-07 |
3
| 8 |
update之后就成为
+------------+------+------+
| day
|
slot | cnt |
+------------+------+------+
| 2014-01-06 |
0
| 1026 |
| 2014-01-06 |
1
|
0
|
| 2014-01-06 |
2
|
0
|
| 2014-01-06 |
3
|
0
|
| 2014-01-07 |
0
| 1040 |
| 2014-01-07 |
1
|
0
|
| 2014-01-07 |
2
|
0
|
| 2014-01-07 |
3
|
0
|
| 2014-01-07 |
4
|
0
|
| 2014-01-07 |
5
|
0
|
| 2014-01-07 |
6
|
0
|
删掉后就只剩下两行:
| day
|
slot | cnt |
+------------+------+------+
| 2014-01-06 |
0
| 1026 |
| 2014-01-07 |
0
| 1040 |
以下是自动插入day 版的存储过程:
delimiter //
create procedure a()
begin
declare
i int default 0;
while
i < 1000 do
insert
into daily_hit_counter(day,slot,cnt)
values(current_date-
interval 1 day,rand()*100,1)
on duplicate key update
cnt = cnt +1;
insert
into daily_hit_counter(day,slot,cnt)
values(current_date,rand()*100,1)
on duplicate key update
cnt = cnt +1;
set
i=i+1;
end
while;
end;//
delimiter ;
4.5加快alter table的速度
alter table ... alter
column colname set default 5; 用alter
column 子句修改列的默认值是很快的,比change column ,modify colum 都要快。
有一些取巧的办法去快速修改表结构:
如果 a.
去掉列的auto_increment属性,b.增加、移除、更改ENUM或SET的常量,可以新建一个新结构的表,然后flush
tables with read lock,交换frm文件,最后释放锁。
快速创建myisam表的索引:
新建两个表,一个是原结构(含数据)一个是新结构,然后交换新结构表的frm和MYI文件给正式的表,别忘了加锁,最后用repaire
table重建索引。
5.3.5 聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,它是连索引带数据一起的一种数据存储方式,不光是索引。在oracle里它叫索引组织表,其实就是一种表,数据呀,索引呀啥的,都有了,啥也没剩下了。
innodb和myisam的索引区别有两点:
一个是innodb的叶子节点包括了主键值、事务id、回滚指针以及所有的剩余列。如果主键是一个列前缀索引,innodb也会包含完整的主键列和剩下的其它列。
myisam只是记录索引列和行号。
第二个区别是,二级索引很不一样。对于myisam,二级索引和主键没什么区别,主键就是非空唯一的索引,对于innodb要复杂一些,他的二级索引的叶节点不是指向“行指针”,而是指向主键的,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对I/O密集型的应用。例如,从性能的角度考虑,使用UUID作为聚簇索引会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使数据完全没有聚集特性。
5.3.6 索引覆盖
如果一个索引包含所有要查询的字段的值,就成为“索引覆盖”。在explain结果的extra列里有“using
index”就表示用到了索引覆盖。
索引覆盖要求select的列和where列出的列都要在索引中列出。这个条件有点高,有时where列出的列都包含在索引里,但select就不一定,这种情况可以通过“deferred
join”的技巧来部分使用索引覆盖。
思路是把索引包含的列组成一个子查询,得出部分结果(比如要查询的行的主键),然后再根据这个部分结果(主键)去获取全部结果。
例子:
select * from products
where actor='SEAN CARREY' and title like '%APPOLO%'
type:ref
key:
ACTOR
ref:
const
Extra:
Using where
重新设计索引,将索引扩展至覆盖三个数据列(actor,title,prod_id),重写sql:
expain select * from
products join (
select
prod_id from products where actor='SEAN CARREY' and
title
like '%APOLLO%'
)
as t1 on (t1.prod_id=products.prod_id)
子查询select部分是prod_id列,
where部分是actor,title,这三个列刚好全部被索引包含,可以用到索引覆盖。返回的prod_id给外层查询根据主键查询,减少了总成本。
******************************
1.row *****************************************
id:
1
select_type:
PRIMARY
table:
...
omitted ...
******************************
1.row *****************************************
id:
1
select_type:
PRIMARY
table:
products
...
omitted ...
******************************
1.row *****************************************
id:
2
select_type:DERIVED
table:
products
type:
ref
possible_key:
ACTOR,ACTOR_2,IX_PROD_ACTOR
key:
ACTOR_2
key_len:
52
ref:
rows:11
extra:
Using where; Using index
5.3.7 是用索引扫描来做排序
mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描。如果explain结果的type列值是"index",则说明mysql使用了索引扫描来排序。
只有当索引的列顺序和order
by子句的顺序完全一致,并且所有列的排序方向都一样时,才能使用索引对结果进行排序。如果查询需要关联多张表,则只有当order
by子句引用的字段全部为第一个表时,才能使用索引排序。order
by子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则只会执行排序操作而不会使用索引排序。
有一种情况下order by子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果where
子句或join子句中对这些列制指定了常量,就可以弥补索引的不足。
索引列(a,b,c)
则
... where a = 'xxx' order by b,c 是可以使用索引排序的
则... where a = 'xxx' order by b 也可以
5.3.9冗余和重复索引
一般来讲,重复的索引是不需要的,这也包括左前缀索引是重复的。但有一些例外情况:
第一:比如select count(*) from a where colx = xx; 在colx上建有index,
对下面的语句:
select
cola,colb from a where
colx=xx来说可能索引(colx,cola,colb)会更有效率,会使用到索引覆盖。而这两个索引是左前缀重复的,然后却都有用武之地。光保留(colx)
query2变慢,光保留(colx,cola,colb) query1会稍稍慢一些,以下是测试数据
only
index1
only
index2
both
myisam , q1
114.96
25.4
112.19
myisam, q2
9.97
16.34
16.37
innodb, q1
108.55
100.33
107.97
innodb, q2
12.12
28.04
28.06
第二:因为Inoodb的二级索引的叶节点是指向主键的,索引对于一个二级索引(colx)实际上是(colx,pkey),
所以(colx)和(colx,pkey)同时存在,就是重复的了。
5.3.11 索引和锁
这是一个很重要的内容,但书上写得不多,仅仅举了一个例子,是间隙锁的一个例子。
actorid < 5 and actor <> 1
返回2,3,4但也会把1锁住。
5.4.1 支持多种过滤条件
对于一些使用得非常频繁的列,虽然这些列的选择性不高,但由于使用很频繁,所以如果能把它们都用到索引,还是会快一些。比如sex,
country。那么我们建一个(sex,country,lname,compny,...)的索引,希望能尽量多地适用于不同的sql。
对于那些不使用这两列的查询也可以用一种办法用到这个索引。方法简称IN方法,具体是:建立一个复合索引,然后sql里的选择加上
and sex in
('m','f')让mysql选择这个索引。这样就不用改索引了。
5.4.2避免多个范围条件
所谓多范围查询,比如: and col1 > 100 and col2 between 'ab' and 'yz';
mysql无法同时使用(col1)和(col2)索引,只能使用其中之一。松散索引扫描就可以实现一个索引上使用多个范围条件,也就不需要上面的IN方法了。
5.4.3 优化排序
对于翻页逻辑会很慢,比如
select from profiles where sex='M' order by rating limit
100000,10
一种优化方法是deferred join,改写如下:
select from profiles inner join (
select
from profiles
where
x.sex='M' order by rating limit 100000,10
)
as x using ();
5.5.2 更新索引统计信息
innodb会在表首次打开,或者执行naalye table,抑或表的大小发生非常大的变化的时候计算索引的统计信息。
inndb在打开某些informaction_schema表,或者使用show table staus 和hsow
index,或在mysql客户端开启自动补全功能的时候都会触发索引统计信息的更新。如果服务器上有大量数据,这可能就是个很严重的问题。可以关闭innodb_stats_on_metadata参数来避免上面提到的问题。percona里通过innodb_stats_auto_update参数来禁止通过自动采集的方式更新索引统计信息,这是需要手动执行analyze
table命令来更新统计信息。
另外,mysql还能使用系统表来持久化这些索引统计信息。percona
5.1中控制参数是innodb_use_sys_stats_table,
mysql5.6是innodb_analyze_is_persistent
6.2慢查询基础
6.2.1 确定只向mysql申请需要的数据
1. 如果只取有限行,用limit限制
2. 如果只需要某些列,不要用select *
6.2.2 为得到上面预申请的数据,mysql需要扫描的行数
有三个指标:响应时间,扫描的行数和返回的行数。后两者比例越接近1:1就越好,但通常达不到。关键就是where条件的利用情况:
最好的情况: 由存储引擎层完成,在索引中使用where条件国旅不匹配的记录。这个应该是ICP(Index
Condition Pushdown)
较好的情况: 索引覆盖(extra: Using index)返回记录,也是依赖索引就可以滤过不要的并返回结果,不过它
是在mysql 服务层实现的(与上面的情况相区别),也不需要回表查询。
次些的情况: 从数据表中返回数据,然后过滤不满足条件的(extra: Using where).
如果尝试去提高这个比例,可以
1. 尽量按上面的三种情况使用where的列索引
2. 改变库表结构,使用汇总表
3. 把复杂sql改写成简单的,比如巨型delete改成一次delete一点,join拆成单表查询,不过这都要具体分析
6.4 查询执行的基础
查询执行路径
6.4.1 mysql server/client通信协议
这个协议是半双工的。任何一个时刻要么服务器向客服端发数据,要么是反过来。
多数连接mysql的库函数都可以获得全部结果集并缓存在内存里,也可以不缓存,一行一行获得。通常是缓存的,服务区在数据都到达客户端的缓存后才能断开连接。注意这个时候数据仅仅是在客户端驱动的缓存,还没到真正的客户代码。这样有助于少占用服务端时间,尽早释放服务端资源。但问题是如果返回一个很大的结果集,库函数会花很多时间和内存去处理这些数据,这种情况就宁可不要缓存,直接从服务器拿数据处理。
对于php来讲,mysql_query是缓存版的,还有一个mysql_unbuffered_query不缓存版。
perl的不缓存是在连接的时候指定mysql_use_result=1
6.4.2查询缓存
6.4.3 查询优化处理
一. 语法解析器和预处理
二.查询优化器
show status like
'last_query_cost';可以查看上条语句的查询成本:结果是多少个数据页的随机查找才能完成上述查询。
mysql能处理的优化类型:
重新定义关联表的顺序
将外连接转为内连接
使用等价变换规则
优化count(),min(),max():
min就找b-tree的最左节点,max就最右,这个时候explain里面的可以看到“select tables optimized
away”。所谓count()优化就是myisam预存了一个变量保存总数。
预估并转化为常数表达式
索引覆盖扫描
等值传播
子查询优化
提前终止查询(limit)
列表IN的比较:
mysql将IN()列表中的数据先进行排序,然后通过二分查找的方法来确定列表中的值是否满足条件,这是一个O(logn)复杂度操作,而其它很多数据库把IN换成OR的复杂度是O(n),这一点mysql还是比较快的。
三. mysql如何执行关联查询
主要就是Nested
Loop(嵌套循环关联),选择驱动表是个技术活。
四.执行计划
explain extended 后查看show
warnings就可以看到mysql重构的查询。
五.关联查询优化:主要就是驱动表的选择
六.排序优化:两个版本的order by
算法。当查询需要所有列的长度不超过参数max_length_for_sort_data
时,mysql使用优化后的排序方法。
七.查询执行引擎
八.返回结果给客户端
6.5 mysql查询优化器的局限性
6.5.1 关联子查询:where
id in (select ...) 这类的语句
一般来讲mysql实现子查询的效率不高,一般是把它换成join来做。但也不是绝对,具体还是要具体分析。
6.5.2UNION的限制:在每个select后面加上limit
6.5.3索引和并优化
6.5.4等值传递:mysql会把一个表上面的in列表数据传给其它表。如果这个列表很大,就会导致优化和执行都很慢。不过这个问题很罕见。
6.5.5 无并行执行
6.5.6 无哈希关联
6.5.7 关于松散索引扫描(loose index scan): 在mysql
5.6里面已经支持
select ... from tb1 where b
between 2 and 3;
索引的前导字段是a,
但查询中只指定了字段b,mysql无法使用这个索引。但可以考虑松散索引扫描:
先扫描a列的第一个值对应的b列的范围,然后再跳到a列第二个不同值扫描对应的b列的范围
6.5.8
有时最大最小值的优化不够智能,虽然b-tree的最左列就是最小的,但如果查询没用到索引,那他还是会全表扫描。这个时候可以改写sql,加上limit
1;
first_name
无索引。
6.5.9 在同一个表上查询和更新
下面的查询希望把两个表中相似行(type相等)的数量记录到字段cnt中
update
tbl
inner join (
select type,count(*) as cnt
from tbl
group by type
) as der
USING(type)
set
tbl.cnt=der.cnt;
6.7 优化特定类型的查询
6.7.1 优化count()查询
count(*)用于统计行数
count(colname 或列的表达式)用于统计列植的数量,此种情况是把NULL排除的。
下面是一个计算满足某种条件行数的示例:通过一个查询返回各种不同颜色商品的数量,此时不能用or语句(select
count(color='blue' or color='red') from
items),因为这样无法区分不同颜色的商品数量;也不能使用and,比如color = 'blue' and color 'red'
,两个and条件相互排斥。可以这样:
mysql>select sum(if(color='bule',1,0)) as
blue, sum(if(color='red',1,0)) as red from items;
也可以用count
mysql>select count(color='blue' or NULL) as blue,
count(color='red' or NULL) as red from items;
更进一步的优化count()是一个很难的问题,因为这意味这要访问大量数据。一般来讲,mysql层面能做的就是尽量使用索引覆盖扫描。如果这个不行,就要考虑修改应用的架构,增加汇总表或使用memcached这样的外部缓存机制。“快速,精准和实现简单”很难都瞒住,必须舍掉其一。
6.7.2优化关联操作
两大信条:
a.
确保ON或USING子句中的列上有索引。在创建索引的时候只需要在关联顺序中的第二个表的相应列上创建索引。如果优化器的关联顺序是B,A,那么就只需要在A表上的对应列上建索引就够了。
b. 确保任何的group by 和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化。
6.7.3优化子查询
一般来讲是使用关联查询代替,当然最详尽的方案是具体分析。不过在mysql5.6或mariaDB,就不需要用关联查询代替了。
6.7.5 优化limit 分页
1. 使用delayed
join,改写成临时表的子查询。内查询使用索引覆盖,得到主键,再和外查询关联,得到索要找的列数据。
2. 获取上次分页的最后记录的主键id0,下次使用这个id0:
where id > id0 limit offset.
(假设从小到大的顺序获取记录)
6.7.9 使用用户自定义变量
一例: 打印出某个group分组总量的排序
表:create table film_actor(actor_id int ,film_id int);
数据:insert into film_actor values
(1,100),(2,109),(3,102),(4,190),(4,123),(4,132),(4,129),(5,194),(5,190),(6,193),(6,187),(7,167),(7,105),(7,119),(7,121),(7,153),(7,149),(7,148),(7,147),(7,142),(7,141),(7,159),(7,158),(7,152);
解决方案:
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 count(*) as cnt
,actor_id
from film_actor
group by actor_id
order by cnt desc
limit 10
) as der;
结果:
+----------+-----+------+-------+
| actor_id | cnt | rank |
dummy |
+----------+-----+------+-------+
|
7 |
13 | 1 |
13 |
|
4 |
4 | 2 |
4 |
|
5 |
2 | 3 |
2 |
|
6 |
2 | 3 |
2 |
|
1 |
1 | 4 |
1 |
|
2 |
1 | 4 |
1 |
|
3 |
1 | 4 |
1 |
+----------+-----+------+-------+
分析:
子查询得到前两列的内容
select colxxx
字段列表的排列是有前后顺序的,放在后面的最后执行。
set
是初始化三个局部变量全零,
select的三个计算: 第一个是得到该行的cnt,
第二个计算rank, 第三个把本次的cnt保留供下一行比较。这三个计算是按顺序的,不能调换。
统计insert on duplicate key update的update数量
insert into t(c1,c2) values (4,4),(2,2),(3,1)
on duplicate key update
c1=values(c1) + (0*(@x:=@x+1));
表达式0*(@x:=@x+1)总是0,所以不会改变整个sql的结果,但每update一次@x变量就增加一次。最后,mysql会返回@x的值。