目录
高性能索引使用
示例SQL
-- 示例表:
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'manager',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('HanMeimei', 23,'dev',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());
使用策略
不在索引列上做任何操作
例如计算、函数、(自动or手动)类型转换等,会导致索引失效而转向全表扫描
正例:
explain select * from employees where name = 'LiLei';
反例:
explain select * from employees where left(name, 3) = 'LiL';
尽量全值匹配
对于复合索引来说,尽量做到全值匹配,这样才能充分的利用索引,从而提高查询的命中率
正例:
explain select * from employees where name = 'LiLei' and age = 22 and position = 'manager';
反例:
explain select * from employees where name = 'LiLei';
最左前缀法则
对于复合索引来说,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列
正例:
explain select * from employees where name = 'LiLei' and age = 22 and position = 'manager';
反例:
explain select * from employees where position = 'manager';
范围条件放最后
对于复合索引来说,如果存在范围查询,那么尽量将其放到最后,其原因有两点:
- 索引范围查询越后,就可以过滤掉更多的数据
- 索引范围查询越前,就会导致索引命中越低,甚至不使用索引
正例:
explain select * from employees where name = 'LiLei' and age = 22 and position > 'a';
反例:
explain select * from employees where name > 'LiLei' and age = 22 and position = 'manager';
尽量使用覆盖索引
只访问索引的查询(索引列包含查询列),减少回表,sql中防止select *
正例:
explain select name, age from employees where name = 'LiLei' and age = 22 and position = 'manager';
反例:
explain select * from employees where name = 'LiLei' and age = 22 and position = 'manager';
慎用取非操作
mysql 在使用不等于(!= 或者<>),not in,not exists的时候可能会导致近似于全表扫描而无法使用索引
需要大量数据,并且通过成本计算才能判断是否走索引
慎用空值
is not null 容易导致索引失效,is null 则会区分被检索的列是否为 null,如果是 null 则会走 ref 类型的索引访问,如果不为 null,也是全表扫描
explain select * from employees where name is null;
explain select * from employees where name is not null;
总的来说,在设计表时列尽可能的不要声明为 null
注意like操作
一般来说,like 以通配符开头(‘%abc…’),mysql 索引失效会变成全表扫描的操作
正例:
explain select * from employees where name like 'Lei%';
反例:
explain select * from employees where name like '%Lei';
解决方式:
- 使用覆盖索引,查询字段必须是建立覆盖索引字段
explain select name from employees where name like '%Lei';
- 使用专业搜索引擎代替,例如es等
字符类型注意引号
字符串不加单引号索引失效
explain select * from employees where name = 1;
注意使用or
一般情况下,使用or也会导致索引失效,因为根据索引拿到数据之后,由于另一个条件的存在,还需要全表扫描,因此,没必要走索引
explain select * from employees where name = 'aa' or age = 22;
当然,覆盖索引也能解决这个问题:
explain select name, age from employees where name = 'aa' or age = 22;
排序和分组尽量命中索引
只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL 才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当 0RDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序
正例:
explain select * from employees where name = 'aa' order by name;
反例:
explain select * from employees where name = 'aa' order by position;
注意:
- 排序的顺序要和索引的顺序一致,尽量避免部分升序,部分降序的情况
- 排序的索引不能包含多个索引,这样也会导致利用索引排序失效
主键顺序性
尽可能保证主键是有序插入的,避免随机插入导致频繁的页分裂,数据移动等问题
注意count使用
COUNT()是一个特殊的函数,有两种非常不同的作用:
- 统计某个列值的数量:count(column)
- 统计返回结果集的行数:count(*)/count(1)/count(主键)
limit分页优化
根据自增且连续的主键排序的分页查询
对于下面sql,需要扫描10010条数据才能返回10条数据,这无疑给了mysql巨大的压力:
select * from employees limit 10000,10;
解决方案:
select * from employees where id > 10000 limit 10;
注意:
- 这条改写的SQL 在很多场景并不实用,因为表中可能某些记录被删后,主键空缺,导致结果不一致
- 如果原 SQL 是 order by 非主键的字段,按照上面说的方法改写会导致两条 SQL 的结果不一致
需要满足的条件:
- 主键自增且连续
- 结果是按主键排序的
根据非主键字段排序的分页查询
根据非主键字段排序的分页查询,SQL 如下:
select * from employees order by name limit 10000,5;
解决方案:
select e.* from employees e inner join (select id from employees order by name limit 10000,5) ed on e.id = ed.id;
实例巩固
假设存在索引index(a,b,c)
where 语句 | 索引是否被使用 |
---|---|
where a = 3 | Y,使用到a |
where a = 3 and b = 5 | Y,使用到a,b |
where a = 3 and b = 5 and c = 4 | Y,使用到a,b,c |
where b = 3 或者 where b = 3 and c=4 或者 where c = 4 | N |
where a = 3 and c = 5 | Y,使用到a,但是c不可以,b中间断了 |
where a = 3 and b > 4 and c = 5 | Y,使用到a和b,c不能用在范围之后,b断了 |
where a = 3 and b like ‘kk%’ and c = 4 | Y,使用到a,b,c |
where a = 3 and b like ‘%kk’ and c = 4 | Y,只用到a |
where a = 3 and b like ‘%kk%’ and c = 4 | Y,只用到a |
where a = 3 and b like ‘k%kk%’ and c = 4 | Y,使用到a,b,c |
口诀速记
全值匹配我最爱,最左前缀要遵守
带头大哥不能死,中间兄弟不能断
索引列上少计算,范围之后全失效
LIKE 百分写最右,覆盖索引不写*
不等空值还有 OR,索引影响要注意
VARCHAR 引号不可丢, SQL 优化有诀窍
了解分区表
简介
分区是指根据一定的规则,数据库把一个表分解成多个更小的、更容易管理的部分。就访问数据库的应用而言,逻辑上只有一个表或一个索引,但是实际上这个表可能由数 10 个物理分区对象组成,每个分区都是一个独立的对象,可以独自处理,可以作为表的一部分进行处理。分区对应用来说是完全透明的,不影响应用的业务逻辑
分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的的封装。对分区表的请求,都会转化成对存储引擎的接口调用。所以分区对于 SQL 层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是如果从底层的文件系统来看就会发现,每一个分区表都有一个使用#分隔命名的表文件
MySQL 在创建表时使用 PARTITION BY 子句定义每个分区存放的数据。在执行查询的时候,优化器会根据分区定义过滤那些没有所需数据的分区,这样查询就无须扫描所有分区一—只需要查找包含需要数据的分区就可以了
作用
分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。在下面的场景中,分区可以起到非常大的作用:
- 表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他均是历史数据
- 分区表的数据更容易维护。例如,想批量删除大量数据可以使用清除整个分区的方式。另外,还可以对一个独立分区进行优化、检查、修复等操作
- 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。可以使用分区表来避免某些特殊的瓶颈,例如 InnoDB 的单个索引的互斥访问、ext3 文件系统的 inode 锁竞争等
- 如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好
分区表本身也有一些限制,下面是其中比较重要的几点:
- 一个表最多只能有 1024 个分区
- 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
- 分区表中无法使用外键约束
分区表的原理
分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handlerobject)表示,所以也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。分区表上的操作按照下面的操作逻辑进行:
虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如 InnoDB,则会在分区层释放对应表锁。这个加锁和解锁过程与普通 InnoDB 上的查询类似
分区表的类型
MySQL 支持多种分区表,如下所示:
- RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区
- LIST 分区:类似于按 RANGE 分区,区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择
- HASH 分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含 MySQL 中有效的、产生非负整数值的任何表达式
- KEY 分区:类似于按 HASH 分区,区别在于 KEY 分区只支持计算一列或多列,且 MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值
- 复合分区/子分区:目前只支持 RANGE 和 LIST 的子分区,且子分区的类型只能为 HASH 和 KEY
分区表的基本语法
RANGE
create table test_range_partition (
id int not null primary key,
first_name varchar(30),
last_name varchar(30)
)
partition by range(id)(
partition p0 values less than (11),
partition p1 values less than (21),
partition p2 values less than (31),
partition p3 values less than (41)
);
上述例子代表的含义是:定义一个员工表,根据员工ID分区,110号员工一个分区,1120号员工一个分区,依次类推,共建立4个分区
LIST
create table test_list_partition (
c1 int,
c2 int
)
partition by list (c1) (
partition p0 values in (1, 4, 7),
partition p1 values in (2, 5, 8)
);
意味着 insert 时,c1 的值只能在 1, 4, 7,2, 5, 8 中,不在这个范围内插入就会报错
insert into test_list_partition(c1, c2) value (3,3);
HASH
create table test_hash_partition (
id int not null,
ename varchar (30),
hired date not null default '1970-01-01',
separated date not null default '9999-12-31',
job varchar (30) not null,
store_id int not null
) partition by hash ( store_id ) partitions 4;
上面是一个基于 store_id 列 HASH 分区的表,表被分成了 4 个分区,如果我们插入的记录 store_id=234,则 234 mod 4 = 2,这条记录就会保存到第二个分区。虽然我们在 HASH()中直接使用的 store_id 列,但是 MySQL 是允许基于某列值返回一个整数值的表达式或者 MySQL 中有效的任何函数或者其他表达式都是可以的
KEY
create table test_key_partition (
id int not null,
ename varchar (30),
hired date not null default '1970-01-01',
separated date not null default '9999-12-31',
job varchar (30) not null,
store_id int not null
) partition by key ( job ) partitions 4;
上面是一个基于 job 字段进行 Key 分区的表,表被分成了 4 个分区。KEY ()里只允许出现表中的字段
不建议使用分区表
在实际的互联网中,MySQL 分区表用的极少,更多的是手动实现分库分表或者借助其他的分库分表的框架,例如mycat,shardingshere相关的产品
分库分表除了支持 MySQL 分区表的水平切分以外,还支持垂直切分,把一个很大的库(表)的数据分到几个库(表)中,每个库(表)的结构都相同,但他们可能分布在不同的 mysql 实例,甚至不同的物理机器上,以达到降低单库(表)数据量,提高访问性能的目的
分区表与分库分表的对比
- 分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁
- 一旦数据量并发量上来,如果在分区表实施关联,就是一个灾难
- 底层关联方式属于黑盒,对于维护有一定的困难
- 分区表无论怎么分,都是在一台机器上,天然就有性能的上限