MySQL分区表

传统的分库分表

传统的分库分表都是通过应用层逻辑实现的,对于数据库层面来说,都是普通的表和库。

分库

分库的原因

首先,在单台数据库服务器性能足够的情况下,分库对于数据库性能是没有影响的。在数据库存储上,database只起到一个namespace的作用。database中的表文件存储在一个以database名命名的文件夹中。比如下面的employees数据库:

1
2
3
4
5
6
7
8
9
10
11
mysql> show tables in employees;
+---------------------+
| Tables_in_employees |
+---------------------+
| departments |
| dept_emp |
| dept_manager |
| employees |
| salaries |
| titles |
+---------------------+

在操作系统中看是这样的:

1
2
3
4
5
# haitian at haitian-coder.local in /usr/local/var/mysql/employees on git:master ● [21:19:47]
→ ls
db.opt dept_emp.frm dept_manager.ibd salaries.frm titles.ibd
departments.frm dept_emp.ibd employees.frm salaries.ibd
departments.ibd dept_manager.frm employees.ibd titles.frm

database不是文件,只起到namespace的作用,所以MySQLdatabase大小当然也是没有限制的,而且对里面的表数量也没有限制。

C.10.2 Limits on Number of Databases and Tables

MySQL has no limit on the number of databases. The underlying file
system may have a limit on the number of directories.

MySQL has no limit on the number of tables. The underlying file system
may have a limit on the number of files that represent tables.
Individual storage engines may impose engine-specific constraints.
InnoDB permits up to 4 billion tables.

所以,为什么要分库呢?

答案是为了解决单台服务器的性能问题,当单台数据库服务器无法支撑当前的数据量时,就需要根据业务逻辑紧密程度把表分成几撮,分别放在不同的数据库服务器中以降低单台服务器的负载。

分库一般考虑的是垂直切分,除非在垂直切分后,数据量仍然多到单台服务器无法负载,才继续水平切分。

比如一个论坛系统的数据库因当前服务器性能无法满足需要进行分库。先垂直切分,按业务逻辑把用户相关数据表比如用户信息、积分、用户间私信等放入user数据库;论坛相关数据表比如板块,帖子,回复等放入forum数据库,两个数据库放在不同服务器上。

拆分后表往往不可能完全无关联,比如帖子中的发帖人、回复人这些信息都在user数据库中。未拆分前可能一次联表查询就能获取当前帖子的回复、发帖人、回复人等所有信息,拆分后因为跨数据库无法联表查询,只能多次查询获得最终数据。

所以总结起来,分库的目的是降低单台服务器负载,切分原则是根据业务紧密程度拆分,缺点是跨数据库无法联表查询

分表

分表的原因

当数据量超大的时候,B-Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表,查询所有符合条件的记录,如果数据量巨大,这将产生大量随机I/O,随之,数据库的响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高。

垂直分表

原因:

1.根据MySQL索引实现原理及相关优化策略的内容我们知道Innodb主索引叶子节点存储着当前行的所有信息,所以减少字段可使内存加载更多行数据,有利于查询。

2.受限于操作系统中的文件大小限制。

切分原则:
把不常用或业务逻辑不紧密或存储内容比较多的字段分到新的表中可使表存储更多数据。。

水平分表

原因:

1.随着数据量的增大,table行数巨大,查询的效率越来越低。

2.同样受限于操作系统中的文件大小限制,数据量不能无限增加,当到达一定容量时,需要水平切分以降低单表(文件)的大小。

切分原则: 增量区间或散列或其他业务逻辑。

使用哪种切分方法要根据实际业务逻辑判断。

比如对表的访问多是近期产生的新数据,历史数据访问较少,可以考虑根据时间增量把数据按照一定时间段(比如每年)切分。

如果对表的访问较均匀,没有明显的热点区域,则可以考虑用范围(比如每500w一个表)或普通Hash或一致性Hash来切分。

全局主键问题:

原本依赖数据库生成主键(比如自增)的表在拆分后需要自己实现主键的生成,因为一般拆分规则是建立在主键上的,所以在插入新数据时需要确定主键后才能找到存储的表。

实际应用中也已经有了比较成熟的方案。比如对于自增列做主键的表,flickr的全局主键生成方案很好的解决了性能和单点问题,具体实现原理可以参考这个帖子。除此之外,还有类似于uuid的全局主键生成方案,比如达达参考的Instagram的ID生成器

一致性Hash:

使用一致性Hash切分比普通的Hash切分可扩展性更强,可以实现拆分表的添加和删除。一致性Hash的具体原理可以参考这个帖子,如果拆分后的表存储在不同服务器节点上,可以跟帖子一样对节点名或ip取Hash;如果拆分后的表存在一个服务器中则可对拆分后的表名取Hash。

MySQL的分区表

上面介绍的传统的分库分表都是在应用层实现,拆分后都要对原有系统进行很大的调整以适应新拆分后的库或表,比如实现一个SQL中间件、原本的联表查询改成两次查询、实现一个全局主键生成器等等。

而下面介绍的MySQL分区表是在数据库层面,MySQL自己实现的水平分表功能,在很大程度上简化了分表的难度。

介绍

对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表实现。

也就是说,对于原表分区后,对于应用层来说可以不做变化,我们无需改变原有的SQL语句,相当于MySQL帮我们实现了传统分表后的SQL中间件,当然,MySQL的分区表的实现要复杂很多。

另外,在创建分区时可以指定分区的索引文件和数据文件的存储位置,所以可以把数据表的数据分布在不同的物理设备上,从而高效地利用多个硬件设备。

一些限制:

1.在5.6.7之前的版本,一个表最多有1024个分区;从5.6.7开始,一个表最多可以有8192个分区。

2.分区表中无法使用外键约束。

3.主表的所有唯一索引列(包括主键)都必须包含分区字段。MySQL官方文档中写的是:

All columns used in the partitioning expression for a partitioned
table must be part of every unique key that the table may have.

这句话不是很好理解,需要通过例子才能明白,MySQL官方文档也为此限制特意做了举例和解释

分区表的类型

RANGE分区

根据范围分区,范围应该连续但是不重叠,使用PARTITION BY RANGE, VALUES LESS THAN关键字。不使用COLUMNS关键字时RANGE括号内必须为整数字段名或返回确定整数的函数。

根据数值范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT NOT NULL,
store_id INT NOT NULL
)
PARTITION BY RANGE (store_id) (
PARTITION p0 VALUES LESS THAN (6),
PARTITION p1 VALUES LESS THAN (11),
PARTITION p2 VALUES LESS THAN (16),
PARTITION p3 VALUES LESS THAN MAXVALUE
);

根据TIMESTAMP范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE quarterly_report_status (
report_id INT NOT NULL,
report_status VARCHAR(20) NOT NULL,
report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
PARTITION BY RANGE ( UNIX_TIMESTAMP(report_updated) ) (
PARTITION p0 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-01-01 00:00:00') ),
PARTITION p1 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-04-01 00:00:00') ),
PARTITION p2 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-07-01 00:00:00') ),
PARTITION p3 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-10-01 00:00:00') ),
PARTITION p4 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-01-01 00:00:00') ),
PARTITION p5 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-04-01 00:00:00') ),
PARTITION p6 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-07-01 00:00:00') ),
PARTITION p7 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-10-01 00:00:00') ),
PARTITION p8 VALUES LESS THAN ( UNIX_TIMESTAMP('2010-01-01 00:00:00') ),
PARTITION p9 VALUES LESS THAN (MAXVALUE)
);

添加COLUMNS关键字可定义非integer范围及多列范围,不过需要注意COLUMNS括号内只能是列名,不支持函数;多列范围时,多列范围必须呈递增趋势:

根据DATEDATETIME范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE members (
firstname VARCHAR(25) NOT NULL,
lastname VARCHAR(25) NOT NULL,
username VARCHAR(16) NOT NULL,
email VARCHAR(35),
joined DATE NOT NULL
)
PARTITION BY RANGE COLUMNS(joined) (
PARTITION p0 VALUES LESS THAN ('1960-01-01'),
PARTITION p1 VALUES LESS THAN ('1970-01-01'),
PARTITION p2 VALUES LESS THAN ('1980-01-01'),
PARTITION p3 VALUES LESS THAN ('1990-01-01'),
PARTITION p4 VALUES LESS THAN MAXVALUE
);

根据多列范围:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE rc3 (
a INT,
b INT
)
PARTITION BY RANGE COLUMNS(a,b) (
PARTITION p0 VALUES LESS THAN (0,10),
PARTITION p1 VALUES LESS THAN (10,20),
PARTITION p2 VALUES LESS THAN (10,30),
PARTITION p3 VALUES LESS THAN (10,35),
PARTITION p4 VALUES LESS THAN (20,40),
PARTITION p5 VALUES LESS THAN (MAXVALUE,MAXVALUE)
);

List分区

根据具体数值分区,每个分区数值不重叠,使用PARTITION BY LISTVALUES IN关键字。跟Range分区类似,不使用COLUMNS关键字时List括号内必须为整数字段名或返回确定整数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY LIST(store_id) (
PARTITION pNorth VALUES IN (3,5,6,9,17),
PARTITION pEast VALUES IN (1,2,10,11,19,20),
PARTITION pWest VALUES IN (4,12,13,14,18),
PARTITION pCentral VALUES IN (7,8,15,16)
);

数值必须被所有分区覆盖,否则插入一个不属于任何一个分区的数值会报错。

1
2
3
4
5
6
7
8
9
10
11
12
mysql> CREATE TABLE h2 (
-> c1 INT,
-> c2 INT
-> )
-> PARTITION BY LIST(c1) (
-> PARTITION p0 VALUES IN (1, 4, 7),
-> PARTITION p1 VALUES IN (2, 5, 8)
-> );
Query OK, 0 rows affected (0.11 sec)
mysql> INSERT INTO h2 VALUES (3, 5);
ERROR 1525 (HY000): Table has no partition for value 3

当插入多条数据出错时,如果表的引擎支持事务(Innodb),则不会插入任何数据;如果不支持事务,则出错前的数据会插入,后面的不会执行。

可以使用IGNORE关键字忽略出错的数据,这样其他符合条件的数据会全部插入不受影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> TRUNCATE h2;
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM h2;
Empty set (0.00 sec)
mysql> INSERT IGNORE INTO h2 VALUES (2, 5), (6, 10), (7, 5), (3, 1), (1, 9);
Query OK, 3 rows affected (0.00 sec)
Records: 5 Duplicates: 2 Warnings: 0
mysql> SELECT * FROM h2;
+------+------+
| c1 | c2 |
+------+------+
| 7 | 5 |
| 1 | 9 |
| 2 | 5 |
+------+------+
3 rows in set (0.00 sec)

Range分区相同,添加COLUMNS关键字可支持非整数和多列。

Hash分区

Hash分区主要用来确保数据在预先确定数目的分区中平均分布,Hash括号内只能是整数列或返回确定整数的函数,实际上就是使用返回的整数对分区数取模。

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY HASH(store_id)
PARTITIONS 4;
1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY HASH( YEAR(hired) )
PARTITIONS 4;

Hash分区也存在与传统Hash分表一样的问题,可扩展性差。MySQL也提供了一个类似于一致Hash的分区方法-线性Hash分区,只需要在定义分区时添加LINEAR关键字,如果对实现原理感兴趣,可以查看官方文档

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY LINEAR HASH( YEAR(hired) )
PARTITIONS 4;

Key分区

按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的 哈希函数是由MySQL
服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的
哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

Key分区与Hash分区很相似,只是Hash函数不同,定义时把Hash关键字替换成Key即可,同样Key分区也有对应与线性Hash的线性Key分区方法。

1
2
3
4
5
6
7
CREATE TABLE tk (
col1 INT NOT NULL,
col2 CHAR(5),
col3 DATE
)
PARTITION BY LINEAR KEY (col1)
PARTITIONS 3;

另外,当表存在主键或唯一索引时可省略Key括号内的列名,Mysql将按照主键-唯一索引的顺序选择,当找不到唯一索引时报错。

子分区

子分区是分区表中每个分区的再次分割。创建子分区方法:

1
2
3
4
5
6
7
8
CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE( YEAR(purchased) )
SUBPARTITION BY HASH( TO_DAYS(purchased) )
SUBPARTITIONS 2 (
PARTITION p0 VALUES LESS THAN (1990),
PARTITION p1 VALUES LESS THAN (2000),
PARTITION p2 VALUES LESS THAN MAXVALUE
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE( YEAR(purchased) )
SUBPARTITION BY HASH( TO_DAYS(purchased) ) (
PARTITION p0 VALUES LESS THAN (1990) (
SUBPARTITION s0
DATA DIRECTORY = '/disk0/data'
INDEX DIRECTORY = '/disk0/idx',
SUBPARTITION s1
DATA DIRECTORY = '/disk1/data'
INDEX DIRECTORY = '/disk1/idx'
),
PARTITION p1 VALUES LESS THAN (2000) (
SUBPARTITION s2
DATA DIRECTORY = '/disk2/data'
INDEX DIRECTORY = '/disk2/idx',
SUBPARTITION s3
DATA DIRECTORY = '/disk3/data'
INDEX DIRECTORY = '/disk3/idx'
),
PARTITION p2 VALUES LESS THAN MAXVALUE (
SUBPARTITION s4
DATA DIRECTORY = '/disk4/data'
INDEX DIRECTORY = '/disk4/idx',
SUBPARTITION s5
DATA DIRECTORY = '/disk5/data'
INDEX DIRECTORY = '/disk5/idx'
)
);

需要注意的是:每个分区的子分区数必须相同。如果在一个分区表上的任何分区上使用SUBPARTITION来明确定义任何子分区,那么就必须定义所有的子分区,且必须指定一个全表唯一的名字。

分区表的使用及查询优化

根据实际情况选择分区方法

对现有表分区的原则与传统分表一样。

传统的按照增量区间分表对应于分区的Range分区,比如对表的访问多是近期产生的新数据,历史数据访问较少,则可以按一定时间段(比如年或月)或一定数量(比如100万)对表分区,具体根据哪种取决于表索引结构。分区后最后一个分区即为近期产生的数据,当一段时间过后数据量再次变大,可对最后一个分区重新分区(REORGANIZE PARTITION)把一段时间(一年或一月)或一定数量(比如100万)的数据分离出去。

传统的散列方法分表对应于分区的Hash/Key分区,具体方法上面已经介绍过。

查询优化

分区的目的是为了提高查询效率,如果查询范围是所有分区那么就说明分区没有起到作用,我们用explain partitions命令来查看SQL对于分区的使用情况。

一般来说,就是在where条件中加入分区列。

比如表salaries结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mysql> show create table salaries\G;
*************************** 1. row ***************************
Table: salaries
Create Table: CREATE TABLE `salaries` (
`emp_no` int(11) NOT NULL,
`salary` int(11) NOT NULL,
`from_date` date NOT NULL,
`to_date` date NOT NULL,
PRIMARY KEY (`emp_no`,`from_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (year(from_date))
(PARTITION p1 VALUES LESS THAN (1985) ENGINE = InnoDB,
PARTITION p2 VALUES LESS THAN (1986) ENGINE = InnoDB,
PARTITION p3 VALUES LESS THAN (1987) ENGINE = InnoDB,
PARTITION p4 VALUES LESS THAN (1988) ENGINE = InnoDB,
PARTITION p5 VALUES LESS THAN (1989) ENGINE = InnoDB,
PARTITION p6 VALUES LESS THAN (1990) ENGINE = InnoDB,
PARTITION p7 VALUES LESS THAN (1991) ENGINE = InnoDB,
PARTITION p8 VALUES LESS THAN (1992) ENGINE = InnoDB,
PARTITION p9 VALUES LESS THAN (1993) ENGINE = InnoDB,
PARTITION p10 VALUES LESS THAN (1994) ENGINE = InnoDB,
PARTITION p11 VALUES LESS THAN (1995) ENGINE = InnoDB,
PARTITION p12 VALUES LESS THAN (1996) ENGINE = InnoDB,
PARTITION p13 VALUES LESS THAN (1997) ENGINE = InnoDB,
PARTITION p14 VALUES LESS THAN (1998) ENGINE = InnoDB,
PARTITION p15 VALUES LESS THAN (1999) ENGINE = InnoDB,
PARTITION p16 VALUES LESS THAN (2000) ENGINE = InnoDB,
PARTITION p17 VALUES LESS THAN (2001) ENGINE = InnoDB,
PARTITION p18 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */

则下面的查询没有利用分区,因为partitions中包含了所有的分区:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain partitions select * from salaries where salary > 100000\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: salaries
partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2835486
Extra: Using where

只有在where条件中加入分区列才能起到作用,过滤掉不需要的分区:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain partitions select * from salaries where salary > 100000 and from_date > '1998-01-01'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: salaries
partitions: p15,p16,p17,p18
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1152556
Extra: Using where

与普通搜索一样,在运算符左侧使用函数将使分区过滤失效,即使与分区函数想同也一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain partitions select * from salaries where salary > 100000 and year(from_date) > 1998\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: salaries
partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2835486
Extra: Using where

分区和分表的比较

  • 传统分表后,countsum等统计操作只能对所有切分表进行操作后之后在应用层再次计算得出最后统计数据。而分区表则不受影响,可直接统计。

Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might be SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By “parallelized,” we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions.

  • 分区对原系统改动最小,分区只涉及数据库层面,应用层不需要做出改动。

  • 分区有个限制是主表的所有唯一字段(包括主键)必须包含分区字段,而分表没有这个限制。

  • 分表包括垂直切分和水平切分,而分区只能起到水平切分的作用。

注:转自haitian blog, 基础上进行补充

1
2
3
4
5
6
7
8
9
10
11
12
(function(){
// 关闭窗口时弹出确认提示
$(window).bind('beforeunload', function(){
// 只有在标识变量is_confirm不为false时,才弹出确认提示
if(window.is_confirm !== false)
return '您可能有数据没有保存';
})
// mouseleave mouseover事件也可以注册在body、外层容器等元素上
.bind('mouseover mouseleave', function(event){
is_confirm = event.type == 'mouseleave';
});
})();

认识与开始

认识慢查询

MySQL的查询执行流程

对于一个业务查询来说, 性能问题的直观反映是响应时间,对于一个任务来说分析的主要方法就是拆分任务为子任务,找到慢的子任务。
对于一个慢查询来说,在不关注服务器当前的CPU、网络等因素的前提下,响应时间高主要分析以下几个方面:

  1. 是不是请求了过多的数据
  2. 查询是否扫描了额外不相关的数据

开始检测慢查询

MySQL较新的版本已经内置提取慢查询的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#long_query_time用于定义慢查询的响应时间阀值,单位秒,
mysql> show variables like 'long%';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.00 sec)
#查看是否打开查询日志记录与日志文件
mysql> SHOW VARIABLES LIKE 'slow%';
+---------------------+-------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------+
| slow_launch_time | 2 |
| slow_query_log | OFF |
| slow_query_log_file | /var/lib/mysql/node1-slow.log |
+---------------------+-------------------------------+
3 rows in set (0.01 sec)
#打开记录开关
SET GLOBAL slow_query_log='ON'
#测试
SELECT SLEEP(10)
#查看日志文件
cat /var/lib/mysql/node1-slow.log
usr/sbin/mysqld, Version: 5.6.21 (MySQL Community Server (GPL)). started with:
Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock
Time Id Command Argument
# Time: 170110 15:12:48
# User@Host: root[root] @ [127.0.0.1] Id: 98122
# Query_time: 10.317485 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
use db_1;
SET timestamp=1484032368;
select sleep(10);
#记录开启成功
#mysqldumpslow 命令方便的查看慢查询
mysqldumpslow -s c -t 10 /tmp/slow-log
#-s, 是表示按照何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒序;
#-t 返回记录数
#-g "exp" 按照参数字符串对查询进行过滤

开始分析慢查询

官方提供Explain命令用以分析MySQL对于SELECT的执行计划。

1
Explain select * from xxx

返回结果包含内容:

  • id: 查询编号

  • select_type: 查询的具体类型,详见点击

  • table: 该查询所引用的表

  • partitions: 该查询匹配到的分区

  • type: 表示这个表是如何组织到查询中或表是如何进行关联的

    • system: 该表只有一行数据
    • const: 最多只有一行匹配的数据,在指定使用主键或唯一索引时候,该表的数据则作为常量使用
    • eq_ref: 对于每个来自于前面的表的行组合,从该表中读取一行。

      select * from t_order a,t_order_ext b where a.order_id=b.order_id;

    • ref: 对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。

      select * from t_order a,t_order_ext b where a.order_id=b.order_id and a.order_id=100;

    • fulltext: 查询使用了一个全文索引
    • ref_or_null: 类似ref,但是MySQL特意为可以为空的字段进行了空值的查询(字段尽量不为空)

      select * from t_order where user_id=100 or user_id is null;

    • index_merge: 表示使用了索引合并优化方法
    • unique_subquery: 该类型替换了类似以下IN子查询的 eq_ref:

      value IN (SELECT primary_key FROM single_table WHERE some_expr)

    • index_subquery: 该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引:

      value IN (SELECT key_column FROM single_table WHERE some_expr)

    • range: 只检索给定范围的行,用一个索引来选择行
    • index: 类似ALL,除了索引树被扫描了:
      • i:索引覆盖了整个查询,这样的话只扫描索引树就可以返回结果而不需要扫描数据,此时Extra 显示Using index.
      • ii: 读取了索引的内容用以进行全表扫描,此时Extra没有Uses index
    • ALL: 完整的扫描全表,最慢的联接类型,尽可能的避免

      select * from xxx

  • possible_keys: 可能使用到的索引,不一定真正使用到,但如果是NULL则代表无索引可用,需要优化

  • key: MySQL决定在查询中真正使用的索引,有可能不存在于possible_keys中,原因是此时发现possible_keys中的索引无助于更快的查询,但是所查询的列却完全包含在其他索引中

  • key_len: 决定使用索引的长度

  • ref: 那些列或者常量需要和使用到的索引进行比较

  • rows: MySQL认为的需要完成查询必须检查的行数

  • filtered: 表示在该查询条件下能够过滤掉数据行数的百分比(实际行数=rows*filtered/100) 5.7.3之前版本需要使用EXTENDED参数才显示,

  • Extra: 显示关于此查询额外的信息

    • Child of ‘table’ pushed join@1: 只在集群模式下存在
    • const row not found: 查询的表为空
    • Deleting all rows: 对于DELETE语句使用,表示MySQL使用一种快速删除所有数据的方式
    • Distinct: MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行
    • FirstMatch(tbl_name): 一种semi-join简化策略用在了表上
    • Full scan on NULL key: 子查询优化退化策略,此时优化器无法使用索引查找方式
    • Impossible HAVING: HAVING语句总是FALSE,无法选择任何数据
    • Impossible WHERE: WHERE语句总是FALSE,无法选择任何数据
    • Impossible WHERE noticed after reading const tables: MySQL读取了所有的system或者const表,发现where条件永远FALSE
    • LooseScan(m..n): semi-join使用到松散扫描策略
    • No matching min/max row: select min/max类型的语句没有找到匹配的行
    • no matching row in const table: 联表查询中存在空表或没有满足针对唯一索引条件的行
    • No matching rows after partition pruning: DELETE或UPDATE,对分区进行剪枝后没有发现要删除或更新的行
    • No tables used: 查询没有FROM条件或者存在FROM DUAL
    • Not exists: MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。

      SELECT * FROM t1 LEFT JOIN t2 ON t1.id=t2.id WHERE t2.id IS NULL;
      此例中,t2的id是主键不可空,所以针对t1当中的每一行记录,MySQL只查t2一次即返回

    • Plan isn’t ready yet: 使用 EXPLAIN FOR CONNECTION 时候,查询计划还没有结束时
    • Range checked for each record(index map: N): MySQL没有发现好的索引,但是发现查询的列在之前的表中已知时有一些索引可用,比没有索引可用要快,N标识可能用到的索引,16进制的掩码(0x19 =>11001标识索引编号1、4、5可能会用到)
    • Scanned N databases: 略,关于INFORMATION_SCHEMA表
    • Select tables optimized away: 优化器确定只有一行需要返回,为了产生这一行数据需要读取确定行数的数据
    • Skip_open_table, Open_frm_only, Open_full_table: 略,关于INFORMATION_SCHEMA表
    • Start temporary, End temporary: 表示在semi-join优化中使用了去重策略(Duplicate Weedout)
    • unique row not found: 对于SELECT … FROM tbl_name,没有行满足关于unique index或者primary key条件的行
    • Using filesort: 此时mysql会根据联接类型浏览所有符合条件的记录,并保存排序关键字和行指针,然后排序关键字并按顺序检索行。
    • Using index: 只需要查找索引树就可以获取所有的行,而不需要做额外的查找
    • Using index condition: 需要读取索引进一步判断是否需要读取表所有的行
    • Using index for group-by: group by查询中使用索引即可查找到结果
    • sing join buffer (Block Nested Loop), Using join buffer (Batched Key Access): 联表查询需要读入前面表作为缓存一部分进行优化查询
    • Using MRR: Multi-Range Read优化策略
    • Using sort_union(…), Using union(…), Using intersect(…): 标识索引归并是如何进行的
    • Using temporary: 使用了中间表
    • Using where: 当有where子句时
    • Using where with pushed condition: 略
    • Zero limit: 存在LIMIT 0 限制无法选择任何数据

在执行EXPLAIN EXTENDED后紧接着执行SHOW WARNINGS可输出进过优化器优化处理的SQL提示语句,并不一定可以执行

1
2
3
4
5
6
7
8
9
10
11
12
13
EXPLAIN EXTENDED
SELECT
*
FROM
activitypin_info
JOIN activityinfo
ON
activity_id = activityinfo.id
WHERE
activity_id < 1000
AND order_id > 1000;
SHOW WARNINGS

得到的优化SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* select#1 */
select
`db1`.`aa`.`id` AS `id`
from
`db1`.`activitypin_info` `aa`
join `db1`.`activityinfo`
where
(
(
`db1`.`aa`.`activity_id` = `db1`.`activityinfo`.`id`
)
and
(
`db1`.`activityinfo`.`id` < 1000
)
and
(
`db1`.`aa`.`order_id` > 1000
)
)

在生成的结果中可能会有一些特殊的标记,见链接点击查看

简单的方式评估查询性能

查询行数与寻道数的关系:

$$N_{seek}=\frac{\log(行数)}{\log(索引大小/2*3/(索引长度 + 数据指针大小))}$$
  • 索引大小: 一般是1024 bytes
  • 数据指针: 一般是4 bytes
  • 索引长度: 500000行一般3bytes

索引存储空间问题:

1
空间=行数*(3+4)*3/2(MB)