一、主键设计问题
分布式ID生成策略:
- 数据库自增ID
- UUID生成
- Redis的原子自增方式
- 雪花算法
- 百度UidGenerator算法
- 美团Leaf算法
1.1 雪花算法
1.1.1 结构
占用空间大小:64bit , 恰好跟 java 中long类型一样大,mysql中用bigInt存储
分为四个部分:
- 是1个bit:0 ,无意义
- 是41个bit:表示的是时间戳,一般起始时间为项目上线时间,可表示69年的时间
- 是10个bit:记录工作机器id。可以分为5个bit代表机房id,5个bit代表机器id。可随意调整比例
- 是12个bit:表示的序号,表示同一个机房里面,在统一毫秒内同一台机器生成的最大ID数量。根据请求数累加。
1.1.2 优缺点
属于半依赖数据源方式,原理是使用Long类型(64位),按照一定的规则进行填充:时间(毫秒级)+集群ID+机器ID+序列号,每部分占用的位数可以根据实际需要分配,其中集群ID和机器ID这两部分,在实际应用场景中要依赖外部参数配置或数据库记录。
- 优点:高性能、低延迟、去中心化、按时间有序
- 缺点:要求机器时钟同步(到秒级即可)
参考文章:
[一篇文章彻底搞懂snowflake算法及百度美团的最佳实践 | Spring For All (spring4all.com)](http://www.spring4all.com/article/17333#:~:text=snowflake算法实现原理. snowflake算法来源于Twitter,使用scala语言实现,利用Thrift框架实现RPC接口调用,最初的项目起因是数据库从mysql迁移到Cassandra,Cassandra没有现成可用,的ID生成机制,就催生了这个项目,现有的github源码有兴趣可以去看看。. snowflake算法的特性是有序、唯一,并且要求高性能,低延迟(每台机器每秒至少生成10k条数据,并且响应时间在2ms以内),要在分布式环境(多集群,跨机房)下使用,因此snowflake算法得到的ID是分段组成的:. 与指定日期的时间差(毫秒级),41位,够用69年.)
1.1.3 代码实现
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间戳 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间戳向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间戳 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间戳
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间戳
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
1.2 UUID
1.2.1 定义
UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码,UUID 的目的是让分布式系统中的所有元素都能有唯一的识别信息。
UUID 是由一组32位数的16进制数字所构成,是故 UUID 理论上的总数为1632=2128,约等于3.4 x 10123。
也就是说若每纳秒产生1百万个 UUID,要花100亿年才会将所有 UUID 用完
1.2.2 结构
UUID 的十六个八位字节被表示为 32个十六进制数字,大小为 128 bit
以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。
例如: 123e4567-e89b-12d3-a456-426655440000
1.2.3 优缺点
优点:
- 能够保证独立性,程序可以在不同得数据库间迁移,效果不受影响
- 保证生成的ID不仅是表独立的,而且是库独立的
- 出现数据拆分、合并存储的时候,能达到全局的唯一性
缺点:
- uuid之间比较大小相对数字慢不少, 影响查询速度。
- 影响插入速度,因为UUID是无序的,插入B+树中比较麻烦。
- uuid占空间大, 如果你建的索引越多, 影响越严重
1.3 数据库ID自增
1.3.1 优缺点
优点:
- 自增,可作为聚簇索引,提高效率
- 节省磁盘空间。
- 插叙,写入效率高
缺点:
- 导入旧数据时,可能会ID重复,导致导入失败
- 分布式架构,多个Mysql实力可能导致ID重复
(106条消息) 分布式–雪花算法改进版–百度的UidGenerator_IT利刃出鞘的博客-CSDN博客_百度雪花算法
二、索引失效案例
2.1 全值匹配我最爱
#数据准备
create table `tb_seller` (
`sellerid` varchar (100),
`name` varchar (100),
`nickname` varchar (50),
`password` varchar (60),
`status` varchar (1),
`address` varchar (100),
`createtime` datetime,
primary key(`sellerid`)
)engine=innodb default charset=utf8mb4;
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('alibaba','阿里巴巴','阿里小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('baidu','百度科技有限公司','百度小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('huawei','华为科技有限公司','华为小店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itcast','传智播客教育科技有限公司','传智播客','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('itheima','黑马程序员','黑马程序员','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('luoji','罗技科技有限公司','罗技小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('oppo','OPPO科技有限公司','OPPO官方旗舰店','e10adc3949ba59abbe56e057f20f883e','0','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('ourpalm','掌趣科技股份有限公司','掌趣小店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('qiandu','千度科技','千度小店','e10adc3949ba59abbe56e057f20f883e','2','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('sina','新浪科技有限公司','新浪官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00');
insert into `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('yijia','宜家家居','宜家家居旗舰店','e10adc3949ba59abbe56e057f20f883e','1','北京市','2088-01-01 12:00:00');
create index idx_seller_name_sta_addr on tb_seller(name,status,address)
explain select * from tb_seller where name='小米科技' and status='1' and address='北京市';
结果使用到索引,且效率最高
2.2 最佳左前缀法则
#最左前缀原则
DROP TABLE IF EXISTS staff;
CREATE TABLE IF NOT EXISTS staff (
id INT PRIMARY KEY auto_increment,
name VARCHAR(50),
age INT,
pos VARCHAR(50) COMMENT '职位',
salary DECIMAL(10,2)
);
INSERT INTO staff(name, age, pos, salary) VALUES('Alice', 22, 'HR', 5000);
INSERT INTO staff(name, age, pos, salary) VALUES('Bob', 22, 'RD', 10000);
INSERT INTO staff(name, age, pos, salary) VALUES('David', 22, 'Sale', 120000);
CREATE INDEX idx_nameAgePos ON staff(name, age, pos);
#使用到索引的 1-3
EXPLAIN SELECT * FROM staff WHERE name = 'Alice';
EXPLAIN SELECT * FROM staff WHERE age = 22 AND name ='Alice';
EXPLAIN SELECT * FROM staff WHERE name = 'Alice' AND age = 22 AND pos = 'HR';
#没有使用到索引的 4
EXPLAIN SELECT * FROM staff WHERE age = 22 AND pos = 'HR';
#使用部分索引的 5
EXPLAIN SELECT * FROM staff where name = 'Alice' AND pos = 'HR';
总结:
-
case 1- 3使用上了索引,不论字段在where中的顺序,符合最佳左前缀法则。
-
没有带头的索引大哥,则全表扫描。
-
5中,只有name字段使用上了索引,中间兄弟age断了,于是后面的兄弟pos挂了
2.3 主键插入顺序
可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入
2.4 计算、函数、类型转换导致索引失效
索引失效:
# 1、 以%开头的模糊查找
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE '%abc';
# 2、带有函数的条件查找
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
# 3、带有表达式的查找
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
索引优化生效:
#1、
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#2、
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#3、
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000;
2.5 类型转换导致索引失效
下列哪个sql语句可以用到索引。(假设name字段上设置有索引)
# 未使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
# 使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
2.6 范围条件右边的列索引失效
自己测试了一下,没什么区别,跟视频里讲的不同。
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 ;
2.7 不等于(!= 或者 <>) 索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc';
2.8 is null 可以使用索引,is not null 无法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;
两个都用到索引了,怎么回事?
2.9 like 以通配符%开头索引失效
拓展:Alibaba《
Java****开发手册》
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
2.10 OR前后存在非索引的列,索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
#使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = 'Abel';
这两句话结果都是一样的,怎么回事?
2.11 数据库和表的字符集要统一使用utf8mb4
三、关联查询优化
3.1 join语句原理
在实际生产中,关于join语句使用的问题,一般会集中在以下两类:
- 我们DBA不让使用join,使用join有什么问题呢?
- 如果有两个大小不同的表做join,应该用哪个表做驱动表呢?
Index Nested-Loop Join
select * from t1 straight_join t2 on (t1.a=t2.a);
如果直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析SQL语句的执行过程。所以,为了便于分析执行过程中的性能问题,我改用straight_join让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱动表。
这条语句的explain结果:
可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执行流程是这样的:
- 从表t1中读入一行数据 R;
- 从数据行R中,取出a字段到表t2里去查找;
- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
- 重复执行步骤1到3,直到表t1的末尾循环结束。
这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ。
流程图如下:
在这个流程里:
- 对驱动表t1做了全表扫描,这个过程需要扫描100行;
- 而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描100行;
- 所以,整个执行流程,总扫描行数是200。
先看第一个问题:能不能使用join?
假设不使用join,那我们就只能用单表查询。我们看看上面这条语句的需求,用单表查询怎么实现。
- 执行
select * from t1
,查出表t1的所有数据,这里有100行; - 循环遍历这100行数据:
- 从每一行R取出字段a的值$R.a;
- 执行
select * from t2 where a=$R.a
; - 把返回的结果和R构成结果集的一行。
可以看到,在这个查询过程,也是扫描了200行,但是总共执行了101条语句,比直接join多了100次交互。除此之外,客户端还要自己拼接SQL语句和结果。
显然,这么做还不如直接join好。
我们再来看看第二个问题:怎么选择驱动表?
在这个join语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。
假设被驱动表的行数是M。每次在被驱动表查一行数据,要先搜索索引a,再搜索主键索引。每次搜索一棵树近似复杂度是以2为底的M的对数,记为log2M,所以在被驱动表上查一行的时间复杂度是 2*log2M。
假设驱动表的行数是N,执行过程就要扫描驱动表N行,然后对于每一行,到被驱动表上匹配一次。
因此整个执行过程,近似复杂度是 N + N2log2M。
显然,N对扫描行数的影响更大,因此应该让小表来做驱动表。
到这里小结一下,通过上面的分析我们得到了两个结论:
- 使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;
- 如果使用join语句的话,需要让小表做驱动表。
但是,这个结论的前提是“可以使用被驱动表的索引”。
接下来,我们再看看被驱动表用不上索引的情况。
Simple Nested-Loop Join
select * from t1 straight_join t2 on (t1.a=t2.b);
由于表t2的字段b上没有索引,因此再用图2的执行流程时,每次到t2去匹配的时候,就要做一次全表扫描。
你可以先设想一下这个问题,继续使用图2的算法,是不是可以得到正确的结果呢?如果只看结果的话,这个算法是正确的,而且这个算法也有一个名字,叫做“Simple Nested-Loop Join”。
但是,这样算来,这个SQL请求就要扫描表t2多达100次,总共扫描100*1000=10万行。
这还只是两个小表,如果t1和t2都是10万行的表(当然了,这也还是属于小表的范围),就要扫描100亿行,这个算法看上去太“笨重”了。
当然,MySQL也没有使用这个Simple Nested-Loop Join算法,而是使用了另一个叫作“Block Nested-Loop Join”的算法,简称BNL。
Block Nested-Loop Join
这时候,被驱动表上没有可用的索引,算法的流程是这样的:
- 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
- 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。
流程图如下:
对应地,这条SQL语句的explain结果如下所示:
与预测结果不同,使用了hash join
如果使用Simple Nested-Loop Join算法进行查询,扫描行数也是10万行。因此,从时间复杂度上来说,这两个算法是一样的。但是,Block Nested-Loop Join算法的这10万次判断是内存操作,速度上会快很多,性能也更好。
接下来,我们来看一下,在这种情况下,应该选择哪个表做驱动表。
假设小表的行数是N,大表的行数是M,那么在这个算法里:
- 两个表都做一次全表扫描,所以总的扫描行数是M+N;
- 内存中的判断次数是M*N。
可以看到,调换这两个算式中的M和N没差别,因此这时候选择大表还是小表做驱动表,执行耗时是一样的。
然后,你可能马上就会问了,这个例子里表t1才100行,要是表t1是一个大表,join_buffer放不下怎么办呢?
join_buffer的大小是由参数join_buffer_size设定的,默认值是256k。**如果放不下表t1的所有数据话,策略很简单,就是分段放。**我把join_buffer_size改成1200,再执行:
select * from t1 straight_join t2 on (t1.a=t2.b);
执行过程就变成了:
- 扫描表t1,顺序读取数据行放入join_buffer中,放完第88行join_buffer满了,继续第2步;
- 扫描表t2,把t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回;
- 清空join_buffer;
- 继续扫描表t1,顺序读取最后的12行数据放入join_buffer中,继续执行第2步。
执行流程图为:
小结
第一个问题:能不能使用join语句?
- 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
- 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用。
所以你在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样。
第二个问题是:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?
- 如果是Index Nested-Loop Join算法,应该选择小表做驱动表;
- 如果是Block Nested-Loop Join算法:
- 在join_buffer_size足够大的时候,是一样的;
- 在join_buffer_size不够大的时候(这种情况更常见),应该选择小表做驱动表。
所以,这个问题的结论就是,总是应该使用小表做驱动表。
什么是小表?
在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
参考文章:
https://funnylog.gitee.io/mysql45/34讲到底可不可以使用join.html
MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!_哔哩哔哩_bilibili
待解问题:
什么是hash join ?
与上面这些有什么区别?
是什么时候用hash join 的?
优点?
3.2 子查询优化
子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。
子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子
查询的执行效率不高。
原因:
- mysql会为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中记录查询。查询完毕后,再撤销这些临时表。这样会消耗过多的cpu和io资源。
- 子查询创建的临时表,不会存在索引
在MySQL中,可以使用连接(JOIN)查询来替代子查询。
3.3 排序优化
在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫 描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
3.4 filesort算法
双路排序 (慢)
MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
从磁盘取排序字段,在buffer进行排序,再从 磁盘取其他字段 。
单路排序 (快)
从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输
出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空
间, 因为它把每一行都保存在内存中了。
优化策略
1. 尝试提高 sort_buffer_size
2. 尝试提高 max_length_for_sort_data
3. Order by 时select * 是一个大忌。最好只Query需要的字段。
3.5 Group By 优化
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
- group by 先排序再分组,遵照索引建的最佳左前缀法则
- where效率高于having,能写在where限定的条件就不要写在having中了
- 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
四、其他优化
4.1 覆盖索引
- 有下面两种理解:
一个索引包含了满足查询结果的数据就叫做覆盖索引
非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列
- 好处
- 避免Innodb表进行索引的二次查询(回表)
- 可以把随机IO变成顺序IO加快查询效率
- 缺点
需要建立冗余字段
4.2 索引下推
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优
化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
不使用ICP索引扫描过程:
storage层:只将满足index key条件的索引记录对应的整行记录取出,返回给server层
server 层:对返回的数据,使用后面的where条件过滤,直至返回最后一行。
使用ICP索引扫描过程:
storage层:
首先将index key条件满足的索引记录区间确定,然后在索引上使用index filter进行过滤。将满足的index filter条件的索引记录才去回表取出整行记录返回server层。不满足index filter条件的索引记录丢弃,不回表、也不会返回server层。
server 层:
对返回的数据,使用table filter条件做最后的过滤。
使用前后的成本差别
使用前,存储层多返回了需要被index filter过滤掉的整行记录
使用ICP后,直接就去掉了不满足index filter条件的记录,省去了他们回表和传递到server层的成本。
ICP的 加速效果 取决于在存储引擎内通过 ICP筛选 掉的数据的比例。
ICP的使用条件
- 只能用于二级索引(secondary index)
- explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null
- 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
- ICP可以用于MyISAM和InnnoDB存储引擎
- MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
- 当SQL使用覆盖索引时,不支持ICP优化方法