本文是MySQL的第二讲:MySQL innoDB存储引擎中索引原理。本文从数据结构的角度切入数据库中常见的B+ 树索引和哈希索引的使用,并从内部机制上讨论了使用上述索引的环境和优化方法,以及 innoDB1.2 版本(对应MySQL 5.6)开始支持的全文索引。
使用场景:
普通索引和唯一性索引:索引列的值的唯一性
唯一索引:可以保证行数据的唯一性,不允许其中任何两行具有相同索引值的索引(允许空)
create unique index indexName ON mytable(cliumnname(length));
drop index indexname ON mytable;
单个索引和复合索引:索引列所包含的列数
单值索引:即一个索引只包含单个列,一个表可以有多个单列索引
复合索引:即一个索引包含多个列
聚簇索引与非聚簇索引:聚簇索引按照数据的物理存储进行划分的
聚集索引(主键索引):提供更快的数据访问速度(堆划分),表中行的物理顺序与键值索引顺序相同。一个表只能包含一个聚集索引
非聚集索引(非主键索引):是把一个很大的范围,转换成一个小的地图,然后你需要在这个小地图中找你要寻找的信息的位置,最后通过这个位置,再去找你所需要的记录
覆盖索引定义:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
最左前缀:联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符
索引下推:like 'hello%’and age >10
检索,MySQL5.6版本之前,会对匹配的数据进行回表查询。5.6版本后,会先过滤掉age<10
的数据,再进行回表查询,减少回表率,提升检索速度
select * from T where ID=500
,即主键查询方式,则只需要搜索ID这棵B+树;select * from T where k=5
,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。alter table T engine = InnoDB
替代重建过程。1、hash索引:给表的某一列计算hash值,排序在hash数组上,hash索引可以一次定位,效率高(等值比较,适用于redis/memcached)
2、有序数组索引:往中间插入一个记录就必须得挪动后面所有的记录,成本太高,只适用于静态存储引擎(通常不怎么变更的数据)
3、btree索引:是以B+树为存储结构实现的,B+树是为磁盘或其他存储设备设计的一种平衡的多叉树,从根节点到每个叶子结点的高度差值不超过1,而且同层级的节点间有指针相互连接。基于索引的顺序扫描时,可以利用双向指针快速左右移动,效率非常高,用于数据库,文件系统中。
hash和btree区别:
索引的常见模型 | 索引构建的位置 | 优点 | 缺点 | 适用场景 | 使用案例 |
---|---|---|---|---|---|
1、哈希表:以键值对存储数据的结构 | 内存中 | 插入数据时速度快 | 查询慢:区间查询(key不是有序的) | 等值查询 | redis,Memcached等非关系型数据库 |
2、有序数组:按顺序存储,二分法查询 | 内存中 | 查询数据数据快 | 插入慢:插入一个记录就需要挪动后面的记录 | 等值查询和范围查询,不会有插入、删除、更新操作 | 静态存储引擎 |
3、搜索树(B+树) | 磁盘中 | 1、有序性 :每个节点的左子树小于父节点,父节点小于右子树 2、速度快:使用N叉搜索树,查询过程访问尽量少的数据块,查询时间复杂度O(log(N)) , 更新时间复杂度O(log(N)) | 性能折中,占用磁盘空间相对较多 大亨无:读写性能都很优秀 | 等值,范围 | |
4、跳表(在链表中二分查找元素) | 内存中 | 支持快速添加、删除、查找数据 时间复杂度是O(logn) | 空间复杂度O(n),消耗内存 | 等值和范围查找 | Redis中的有序集合 |
5、全文索引 |
Action1:InnoDB存储引擎支持的哈希索引是自适应的,InnoDB存储引擎会根据表的使用情况自动为表生成哈希索引,不能人为干预。
B树:多路平衡查找树(结点的最大值m称为B_TREE的阶,简称为m叉树)
B+树(innoDB存储引擎使用的索引结构)
结构区别:
性能区别:
缺点:(索引维护)
解决方案:
2、使用自增主键(从性能和存储空间方面考量)
B+树中的插入必须保证插入后叶子节点中的记录依然有序,同时需要考虑插入到B+树的三种情况,每种情况都可能会导致不同的插入算法,入下表所示:
Leaf Page 满 | Index Page 满 | 操作 |
---|---|---|
No | No | 直接将记录插入到叶子节点 |
Yes | No | 1)拆分Leaf Flag 2)将中间的节点放到Index Page中 3)小于中间节点的记录放左边 4)大于等于中间节点的记录放右边 |
Yes | Yes | 1)拆分Leaf Flag 2)小于中间节点的记录放左边 3)大于等于中间节点的记录放右边 4)拆分Index Page 5)小于中间节点的记录放左边 6)大于等于中间节点的记录放右边 3)中间节点放到上一层Index Page中 |
背景: B+ 树,高度为2,每页可存放4条记录,扇出为5
场景1:插入节点 28
场景2:继续插入节点70
场景3:最后插入节点95
场景4 旋转操作
B+树中删除操作
叶子节点小于填充因子 | 中间节点小于填充因子 | 操作 |
---|---|---|
No | No | 直接将记录从叶子节点删除,如果该节点还是 Index Page的节点,用该节点的右节点代替 |
Yes | No | 合并叶子节点和它的兄弟节点,同时更新Index Page |
Yes | Yes | 1)合并叶子节点和它的兄弟节点 2)更新 Index Page 3)合并 Index Page 和它的兄弟节点 |
场景1:删除键值为70的记录
场景2:接着删除键值为25的记录
场景3:删除键值为 60的情况
# 1、创建索引
alter table t add key idx_b (b(100));
# 2、创建联合索引
alter table t add key idx_a_c (a, c);
查看索引详情
show index from t;
**********************************
table: t // 索引所在的表名
non_unique: 1 //非唯一的索引
key_name: idx_c //索引的名字
seq_in_index:1 //索引中该列的位置
column_name:c
collation:a
cardinality:2 //表示索引中唯一值的数目估计值,优化器根据这个值巨鼎是否使用该索引
sub_part:null
packed:null
null:
index_type:btree
comment:
注意事项:对应用程序的几张核心表做 analyze table操作,这能使优化器和索引更好地为你工作。
如何查看索引是否有高选择性呢?
InnoDB中cardinality的统计?
背景:用户需要查询博客内容包含单词 xxx 的文章,sql如下
- 即使是加了索引,也需要对全文进行扫描,性能极低。
select * from blog where content like '%xxx%';
全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。
innodb引擎从 1.2.x版本开始支持全文索引,对应MySQL版本为5.6
全文索引的创建
CREATE TABLE `article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`),
FULLTEXT KEY `title` (`title`,`content`)
) ENGINE = MyISAM DEFAULT CHARSET = utf8;
# 给已经存在的表的指定字段创建全文索引
ALTER TABLE article
ADD FULLTEXT INDEX fulltext_article(title,content);
全文检索的查询
# 查询单列数据
SELECT * FROM `student` WHERE MATCH(`name`) AGAINST('聪');
# 等价于
SELECT * FROM `student` WHERE MATCH(`name`) AGAINST('聪' in language mode);
# 查询多列数据
SELECT * FROM article WHERE MATCH(title,content) AGAINST (‘查询字符串’);
id: 1
select_type: simple
table:fts_a
type:fulltext
possible_keys:idx_fts
key:idx_fts
key_len:0
ref:null
rows:1
extra:using where
# A 且 B
select * from fts_a where match(body) against('+Pease +hot' in boolean mode)
# 有A无B
select * from fts_a where match(body) against('+Pease -hot' in boolean mode)
# A或B
select * from fts_a where match(body) against('Pease hot' in boolean mode)
# 模糊查询
select * from fts_a where match(body) against('po*' in boolean mode)
# 短语sql查询
select * from fts_a where match(body) against('"like hot"' in boolean mode)
select * from fts_a where match(title, body) against('database' with query expansion)
强烈注意:
如果可能,请尽量先创建表并插入所有数据后再创建全文索引,而不要在创建表时就直接创建全文索引,因为前者比后者的全文索引效率要高。
删除全文索引
1、直接使用 drop index
DROP INDEX full_idx_name ON tommy.girl;
2、使用 alter table的方式
ALTER TABLE tommy.girl DROP INDEX ft_email_abcd;
在项目中的使用:场景1:查询品牌表适用类目
查询语句如下:
SELECT * FROM db_item_standard.`parana_brands`
where MATCH( `apply_category`) AGAINST ('7eklpnznr7');
原理分析
inverted file index
,其表现形式为 {单词, 单词所在文档的ID}full inverted index
, 其表现形式为 {单词,(单词所在文档的ID,在具体文档中俄位置)}例如:全文检索表t存储的内容如表
DocumentId | Text |
---|---|
1 | Pease porridge hot,pease porridge cold |
2 | Pease porridge in the pot |
3 | Nine days old |
4 | Some like it hot, some like it cold |
5 | Some like it in the pot |
6 | Nine days old |
对于 inverted file index
的关联数据,其存储内容如表所示
Number | Text | Documents |
---|---|---|
1 | code | 1,4 |
2 | days | 3,6 |
3 | hot | 1,4 |
4 | in | 2,5 |
5 | it | 4,5 |
6 | like | 4,5 |
6 | nine | 3,6 |
对于 full inverted index
,存储的是对pair,即(DocumentId,Position),因此其存储的倒排索引如表所示:
Number | Text | Documents |
---|---|---|
1 | code | (1:6),(4:8) |
2 | days | (3:2),(6:2) |
3 | hot | (1:3),(4:4) |
4 | in | (2:3),(5:4) |
5 | it | (4:3,7),(5:3) |
6 | like | (4:2,6),(5:2) |
6 | nine | (3:1),(6:1) |
full inverted index
占用更多的空间,但能更好地定位数据,并扩充一些其他的搜索特性。聚簇索引,在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引, 即如果存在聚集索引,就不能再指定CLUSTERED 关键字
非聚集索引,数据库表中记录的物理顺序与索引顺序可以不相同。一个表中只能有一个聚集索引,但表中的每一列都可以有自己的非聚集索引
先插入数据,如下表
create table t (
a int not null,
b varchar(8000),
c int not null,
primary key (a),
key idx_c(c)
) engine = innodb;
insert into t select 1,repeat('a', 7000), -1;
insert into t select 2,repeat('a', 7000), -2;
insert into t select 3,repeat('a', 7000), -3;
insert into t select 4,repeat('a', 7000), -4;
插入的列长度为 7000,因此可以使用人为的方式使目前每个页只能存放两个行记录。可以发现数据页上存放的是完整的每行的记录。而在非数据页的索引中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录
场景1:对主键排序
explain select * from Profile order by id limit 10;
************* 1.row ***************
id : 1
select_type : simple
table: profile
type : index
possible_keys : null
key : primary
ken_len:4
ref:null
rows:10
extra:
场景2:范围查询
explain select * from Profile where id>10 and id <1000;
************* 1.row ***************
id : 1
select_type : simple
table: profile
type : range
possible_keys : primary
key : primary
ken_len:4
ref:null
rows:14868
extra:using where
对于辅助索引,叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引中还包含一个书签,这个书签告诉innoDB存储引擎哪里可以找到与索引相对应的行数据。
红黑树:
hash 索引
B树索引:
MyISAM和InnoDB都是采用的B+树作为索引结构,但是叶子节点的存储上有些不同。
MyISAM:主键索引和普通索引的叶子节点都是存放 key 和 key 对应数据行的地址。在MyISAM 中,主键索引和普通索引没有任何区别。
InnoDB:主键索引存放的是 key 和 key 对应的数据行。普通索引存放的是 key 和 key 对应的主键值。因此在使用普通索引时,通常需要检索两次索引,首先检索普通索引获得主键值,然后用主键值到主键索引中检索获得记录。
1页或页的倍数最为合适。因为如果一个节点的大小小于1页,那么读取这个节点的时候其实也会读出1页,造成资源的浪费。所以为了不造成浪费,所以最后把一个节点的大小控制在1页、2页、3页等倍数页大小最为合适。
这里说的“页”是 MySQL 自定义的单位(和操作系统类似),MySQL 的 Innodb 引擎中1页的默认大小是16k,可以使用命令SHOW GLOBAL STATUS LIKE ‘Innodb_page_size’
查看。
在 MySQL 中 B+ 树的一个节点大小为“1页”,也就是16k。
为什么一个节点为1页就够了?
所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。在查找数据时一次页的查找代表一次IO,所以通过主键索引查询通常只需要1-3次 IO 操作即可查找到数据。千万级别对于一般的业务来说已经足够了,所以一个节点为1页,也就是16k是比较合理的
联合索引
排序规则
如果查询顺序和联合索引的顺序不一致,优化器会自动做优化
Action:联合索引踩坑
以“%(表示任意0个或多个字符)”开头的LIKE语句,模糊匹配;
OR语句前后没有同时使用索引
数据类型出现隐式转化(如varchar不加单引号的话可能会自动转换为int型)
对于多列索引,必须满足最左匹配原则(eg:多列索引col1、col2和col3,则索引生效的情形包括 col1或col1,col2或col1,col2,col3)
经常作查询选择的字段
经常作表连接的字段
经常出现在order by, group by, distinct后面的字段
非空字段:应该指定列为NOT NULL,含有空值的列很难进行查询优化(你应该用0、一个特殊的值或者一个空串代替空值);
取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面;
索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
InnoDB 中,对于主键索引,只需要走一遍主键索引的查询就能在叶子节点拿到数据。
而对于普通索引,叶子节点存储的是 key + 主键值,因此需要再走一次主键索引,通过主键索引找到行记录,这就是所谓的回表查询,先定位主键值,再定位行记录。
select id, name from user where name = ‘xx’
时,通过name 的索引就能拿到 id 和 name了,因此无需再回表去查数据行了。union all:对两个结果集直接进行并集操作,记录可能有重复,不会进行排序。
union:对两个结果集进行并集操作,会进行去重,记录不会重复,按字段的默认规则排序。
因此,从效率上说,UNION ALL 要比 UNION 更快。
在innoDB引擎中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表
前置条件
查询方式 | 查询语句 | 细节 |
---|---|---|
主键查询 | select * from T where ID=500 | 需要搜索ID这棵B+树可拿到数据 |
普通索引查询 | select * from T where k=5 | 需要先搜索k索引树,拿到主键值500,再到主键索引树搜索一次。这个过程称为回表 |
同上 | select * from T where k between 3 and 5 | 查询过程读了k索引树的3条记录,回表了两次 |
1、主键自动建立唯一索引
2、频繁作为查询条件的字段(经常出现在order by, group by, distinct后面的字段)
3、查询中与其它表关联的字段,外键关系建立索引
4、单键/组合索引选择? 高并发下选择组合索引
5、查询中排序的字段,统计,分组的字段
不适合:
1、频繁增删改的字段不适合
2、where条件里用不到的不创建
3、表记录太少时
4、字段重复数据太多时,如性别表
1、局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中
2、B树的节点大小设置为一页(4k),减少层间索引次数,效率较高;
3、红黑树等高度较高,索引次数较多,即磁盘IO次数较多,性能较慢;
4、B+树所有的关键字都出现在叶子节点,内存中可以存入更多的关键字,减少磁盘I/O次数;
5、B+树叶子节点通过双向链表相连,链表中的关键字是有序的,适合范围查找(在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低);
6、哈希虽然能够提供 O(1) 的单数据行操作性能, 但是对于范围查询和排序却无法很好地支持, 最终导致全表扫描;
7、B 树能够在非叶节子点中存储数据, 但是这也导致在查询连续数据时可能会带来更多的随机 I/O;
8、B+树的所有叶节点可以通过指针相互连接, 能够减少顺序遍历时产生的额外随机 I/O;
9、B 树一个节点里存的是数据, 而 B+树存储的是索引( 地址) , 所以 B 树里一个节点存不了很多个数据, 但是 B+树一个节点能存很多索引, B+树叶子节点存所有的数据;
10、B+树的叶子节点是数据阶段用了一个链表串联起来, 便于范围查找;
Buffer Pool 是 InnoDB 维护的一个缓存区域,用来缓存数据和索引在内存中,主要用来加速数据的读写,如果 Buffer Pool 越大,那么 MySQL 就越像一个内存数据库,默认大小为 128M。
InnoDB 会将那些热点数据和一些 InnoDB 认为即将访问到的数据存在 Buffer Pool 中,以提升数据的读取性能。
InnoDB 在修改数据时,如果数据的页在 Buffer Pool 中,则会直接修改 Buffer Pool,此时我们称这个页为脏页,InnoDB 会以一定的频率将脏页刷新到磁盘,这样可以尽量减少磁盘I/O,提升性能。
InnoDB存储引擎的关键特性包括:
索引是存储在磁盘上的,所以对于索引的操作需要涉及磁盘操作。如果我们使用自增主键,那么在插入主键索引(聚簇索引)时,只需不断追加即可,不需要磁盘的随机 I/O。
create table_t {
a int auto_increment,
b varchar(30),
primary key(a)
};
但是如果我们使用的是普通索引,大概率是无序的,此时就涉及到磁盘的随机 I/O,而随机I/O的性能是比较差的(Kafka 官方数据:磁盘顺序I/O的性能是磁盘随机I/O的4000~5000倍)。
因此,InnoDB 存储引擎开创性的设计了 Insert Buffer ,对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池(Buffer pool)中,若在,则直接插入;若不在,则先放入到一个 Insert Buffer 对象中,然后再以一定的频率和情况进行 Insert Buffer 和辅助索引页子节点的 merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
插入缓冲的使用需要满足以下两个条件:
因为在插入缓冲时,数据库不会去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有随机读取的情况发生,从而导致 Insert Buffer 失去了意义。
脏页刷盘风险:InnoDB 的 page size一般是16KB,操作系统写文件是以4KB作为单位,那么每写一个 InnoDB 的 page 到磁盘上,操作系统需要写4个块。于是可能出现16K的数据,写入4K 时,发生了系统断电或系统崩溃,只有一部分写是成功的,这就是 partial page write(部分页写入)问题。这时会出现数据不完整的问题。
这时是无法通过 redo log 恢复的,因为 redo log 记录的是对页的物理修改,如果页本身已经损坏,重做日志也无能为力。
doublewrite 就是用来解决该问题的。
为了解决 partial page write 问题,当 MySQL 将脏数据刷新到磁盘的时候,会进行以下操作:
1)先将脏数据复制到内存中的 doublewrite buffer
2)之后通过 doublewrite buffer 再分2次,每次1MB写入到共享表空间的磁盘上(顺序写,性能很高)
3)完成第二步之后,马上调用 fsync 函数,将doublewrite buffer中的脏页数据写入实际的各个表空间文件(离散写)。
如果操作系统在将页写入磁盘的过程中发生崩溃,InnoDB 再次启动后,发现了一个 page 数据已经损坏,InnoDB 存储引擎可以从共享表空间的 doublewrite 中找到该页的一个最近的副本,用于进行数据恢复了。
哈希(hash)是一种非常快的查找方法,一般情况下查找的时间复杂度为 O(1)。但是由于不支持范围查询等条件的限制,InnoDB 并没有采用 hash 索引,但是如果能在一些特殊场景下使用 hash 索引,则可能是一个不错的补充,而 InnoDB 正是这么做的。
具体的,InnoDB 会监控对表上索引的查找,如果观察到某些索引被频繁访问,索引成为热数据,建立哈希索引可以带来速度的提升,则建立哈希索引,所以称之为自适应(adaptive)的。自适应哈希索引通过缓冲池的 B+ 树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB 会自动根据访问的频率和模式来为某些页建立哈希索引。
InnoDB 在 I/O 的优化上有个比较重要的特性为预读,当 InnoDB 预计某些 page 可能很快就会需要用到时,它会异步地将这些 page 提前读取到缓冲池(buffer pool)中,这其实有点像空间局部性的概念。
空间局部性(spatial locality):如果一个数据项被访问,那么与他地址相邻的数据项也可能很快被访问。
InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。
其中,线性预读以 extent(块,1个 extent 等于64个 page)为单位,而随机预读放到以 extent 中的 page 为单位。线性预读着眼于将下一个extent 提前读取到 buffer pool 中,而随机预读着眼于将当前 extent 中的剩余的 page 提前读取到 buffer pool 中。
线性预读(Linear read-ahead)
随机预读(Random read-ahead):
插入新的ID: | 700 R5的记录后面插入一个新记录 |
---|---|
插入的ID值为400 | 需要逻辑上挪动后面的数据,空出位置 1、如果R5所在的数据页已经满了,根据B+树的算法,这时候需要申请一个新的数据页,导致性能下降(页分裂);2、相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并 |
索引的删除 | 如果删除,新建主键索引,会同时去修改普通索引对应的主键索引,性能消耗比较大。删除重建普通索引影响不大 |
在建表语句定义: | NOT NULL PRIMARY KEY AUTO_INCREMENT |
---|---|
从性能角度 | 1、自增主键的插入数据模式,每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂,由业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高 |
从存储角度 | Q:表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?A:如果用身份证号做主键:分主键索引的叶子节点存储的是身份证号:占用20字节,使用int作为主键:存储只需要4字节,bigint需要8字节 原则:主键长度越小,普通索引的叶子节点就越小,占用的空间也就越小 |
有没有什么场景适合用业务字段直接做主键的呢?
从辅助索引(普通索引)中查询得到记录,而不需要通过聚族索引查询获得,MySQL 中将其称为覆盖索引。
使用关键字 force index 强制使用某个索引
select * from orderDetails force index(orderID) where orderId > 10000 and orderId <102000;
使用某个字段中字符串的前几个字符建立索引
前缀索引优化为什么需要优化? | 索引文件是存储在磁盘中的,而磁盘中最小分配单元是页,通常一个页的默认大小为 16KB,减小索引字段大小,可以增加一个页中存储的索引项,有效提高索引的查询速度 |
---|---|
使用局限 | order by 无法使用前缀索引,无法把前缀索引用作覆盖索引 |
什么是执行计划:在执行一条 SQL 语句时,要想知道这个 SQL 先后查询了哪些表,是否使用了索引,这些数据从哪里获取到,获取到数据遍历了多少行数据等等,可以通过 EXPLAIN 命令来查看这些执行信息。
排序使用到索引,在执行计划中的体现就是 key 这一列。如果没有用到索引,会在 Extra 中看到 Using filesort,代表使用了内存或磁盘进行排序。而具体走内存还是磁盘,是由sort_buffer_size 和排序数据大小决定的。
排序无法使用到索引的情况有:
如下代码所示,打开 optimizer_trace 后,再执行 SQL 就可以查询 information_schema.OPTIMIZER_TRACE 表查看执行计划了,最后可以关闭 optimizer_trace 功能:
SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
假设我们为表 person 的 NAME 和 SCORE 列建了联合索引,那么下面第二条语句应该可以走索引覆盖,而第一条语句需要回表:
explain select * from person where NAME='name1';
explain select NAME,SCORE from person where NAME='name1';
通过观察 OPTIMIZER_TRACE 的输出可以看到,索引覆盖(index_only=true)的成本是 1.21 而回表查询(index_only=false)的是 2.21,也就是索引覆盖节省了回表的成本 1。
索引覆盖:
analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "name_score",
"ranges": [
"name1 <= name <= name1"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": true,
"rows": 1,
"cost": 1.21,
"chosen": true
}
]
回表:
"range_scan_alternatives": [
{
"index": "name_score",
"ranges": [
"name1 <= name <= name1"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 1,
"cost": 2.21,
"chosen": true
}
]
背景:
/* Traceid: b119a98d7092953d8c244444dba42241 */ /* Brand.countListBySearchCriteria */ select count(1) from parana_brands WHERE status in ( 1 ) and `full_name` LIKE CONCAT('%', '多友益', '%');
应急解决方案:加了联合索引 ALTER TABLE db_item_standard.parana_brands ADD INDEX idx_status_fullname (status, full_name);
存量业务梳理
应用及负责人 | 负责人 | 相关业务 | Mapper | SQL | 治理策略 | 排期时间 | 是否完成 |
---|---|---|---|---|---|---|---|
item-standard-center | xx | 品牌查询 | IM_brandMapper.xmlbrandExtMapper.xml | id="listBySearchCriteria"``id="listBySearchCriteriaCondition共15处 |
已更改为前缀匹配后续接ES | 0830 | 是 |
xx | 属性爬虫 | ZcyCrawCategoryAttributeMapper.xmlZcyCrawCategoryAttrValueMapper.xml | id="getList" 供运营使用,无调用量id="getOne" 已废弃id="count" 已废弃``id="findByCateIdAndAttrIdAndName" 已废弃 |
id=“getList” 有引用,近半月无调用量,供运营使用。其余都已废弃。可直接去除全模糊 | 0902 | 是 | |
xx | cspu爬虫 | ZcyConvertTaskMapper.xml(抓取cspu转换任务)ZcyCrawCspuMapper.xmlZcyCrawTaskMapper.xmlZcyMergeTaskMapper.xmlIM_zcyCspuCatePropMapper.xml | `id=“count” 已废弃 id=“paging” 已废弃id=“paging” id=“count” 供运营使用,偶有调用量``id=“paging” id=“count” 供运营使用,偶有调用量id=“paging” id=“count” 供运营使用,偶有调用量id=“paging” id=“count” id=“list” id=“findByParam” 已废弃``` | 可去除全模糊,更改为前缀匹配 | 0902 | 是 | |
xx | 品牌爬虫 | ZcyCrawBrandMapper.xmlZcyCrawBrandRelationMapper.xml | id="paging" 已废弃id="count" 已废弃``id="pagingForCspu" 供运营使用,偶有调用量``id="count" 已废弃id="findByType" 无调用量,使用全模糊不合理 |
id=“pagingForCspu” 供运营使用,偶有调用量,可去除全模糊;其余无调用量 | 0902 | 是 | |
xx | 类目爬虫 | ZcyCrawCategoryMapper.xml | 1、cn.gov.zcy.cspu.service.ZcyCrawCategoryFacadeImpl#crawCategoryList[30天内无调用量,dubbo接口增加ERROR告警,10月无告警,下线接口。]2、id=“count”,无接口使用paging接口或者id=count的sql片段,可优化 | ||||
xx | 后台类目查询 | backCategoryMapper.xmlIM_ZcyBackCategoryMapper.xmlIM_backCategoryMapper.xml | 标准中心Like治理【后台类目相关】 | ||||
xx | 前台类目 | IM_frontCategoryMapper.xml | id="findCategoryList" 运营使用 场景1:复制新标签,该场景目前业务代码中未使用模糊查询逻辑场景 2:批量查询前台类目,有少量调用量,但均未涉及模糊查询场景 3:获取前台类目树,有少量调用量,但均未涉及模糊查询 |
商品运营前台类目是通过前端实现模糊查询,已向web-ymer确认也未使用categoryNameFuzzyMatch字段的模糊查询。处理方式是改成前缀匹配 | 0916 | 是 | |
xx | 前台类目标签 | IM_frontCategoryTagMapper.xml | id="findCategoryTagList" 运营使用 场景1:批量查询前台类目,有少量调用量,但均未涉及模糊查询场景2:创建前台类目, 同一父类目下会做重名检查,该场景目前业务代码中未使用模糊查询逻辑,可忽略场景3:更新前台类目,同一父类目下会做重名检查,该场景目前业务代码中未使用模糊查询逻辑,可忽略 |
后台类目标签查询接口已经观察一个月,均未涉及模糊查询,处理方式是改成前缀匹配 | 0916 | 是 | |
xx | 商品标签查询 | IM_ZcyItemFeatureMapper.xml | id="query" 运营使用查询商品特性集合,有少量调用量,但均未涉及模糊查询``id="count"``id="paging"分页查询商品特征,无调用量 |
1.query涉及的接口入参都是分页和标签编码,均未涉及标签名称的模糊查询,处理方式是改成前缀匹配2.paging涉及的接口调用量很少,对系统影响不大,并且和产品沟通不同意改动,所以暂不处理 | 0916 | 是 | |
xx | 商品节能环保证书查询 | ItemMarkCertificateMapper.xml | id="markCount"``id="markPage" |
无业务代码使用sql,直接废弃 | 0916 | 是 | |
xx | spu、cspu查询 | IM_SpuAliMapper.xmlIM_spuMapper.xml(已废弃)IM_ZcySpuAuditMapper.xmlIM_ZcySpuMapper.xml(已废弃)IM_ZcySpuSnapshotMapper.xmlspuMapper.xmlZcyCspuAttributeMapper.xmlZcyCspuMapper.xmlZcySpuAuditMapper.xmlZcySpuMapper.xmlZcySpusMapper.xml | 29处IM_SpuAliMapper (id=“whereCdt”)发布链路,选择品牌型号,全模糊查询。有用到categoryId索引IM_spuMapper (id=“criteria”)SPU列表IM_ZcySpuAuditMapper(id=“listQueriedIds”)SPU审核列表IM_ZcySpuMapper(id="zcyCount,zcyPaging,list,listSpusByBrandAndCategory)SPU列表IM_ZcySpuSnapshotMapper.xml(id=“findSingleSnapshot”)SPU审核查询spuMapper(id="criteria,findByCategoryIdAndFuzzName)spu列表ZcyCspuAttributeMapper标准商品属性管理,ZcyCspuMapper标准商品管理ZcySpuAuditMapperSPU审核列表ZcySpuMapper废弃代码,可下线ZcySpusMapper废弃代码,可下线 | IM_SpuAliMapper接ES(10月中旬)IM_spuMapper下代码(10月下旬)IM_ZcySpuAuditMapper去除全模糊匹配(10月下旬)IM_ZcySpuMapper下代码 (10月下旬)IM_ZcySpuSnapshotMapper去除全模糊匹配(10月下旬)spuMapper下代码 (10月下旬)ZcyCspuAttributeMapper下代码(10月下旬)ZcyCspuMapper 下代码(10月下旬)ZcySpuAuditMapper去除全模糊匹配(10月下旬)ZcySpuMapper下代码(10月下旬)ZcySpusMapper下代码(10月下旬) | |||
xx | 类目属性 | categoryAttributeMapper.xmlIM_categoryAttributeMapper.xml | 2处 没有使用场景,可以删除 | 排期12月v1 | |||
xx | 属性 | propertyBusinessTypeMapper.xmlpropertyMapper.xmlIM_propertyBusinessTypeMapper.xmlIM_propertyMapper.xml | 9处propertyBusinessTypeMapper中的模糊查询,可以删除了属性表数据量9647条,可以与产品和运营讨论下是否只支持后模糊匹配 | 排期12月v1 | |||
xx | 型号 | specificationMapper.xml(已废弃) | 没有型号库,做删除处理 | ||||
xx | 店铺 | shopMapper.xml | 1处 已经对外暴露了,业务方有 分销平台、 web-supplier-application-server dump 和 web-sop-member 四个应用需要先联系各应用负责人 | ||||
item-microservice-center | xx | 运费模板 | /ic/TransExpensesTemplateMapper.xmlTransExpensesTemplateMapper.xml | id="getTransExpensesTemplatesByOrgId"`` |
表总数据量45w,目前先不做处理。 目前线上业务必须支持前后模糊搜索。 | 10月中旬 | 完成(暂不解决) |
文件、文件夹 | /ic/UserFileMapper.xml/ic/UserFolderMapper.xml | 1处1处 | 1: 改为仅右匹配1: 右匹配,无需处理 | 1008 | 完成 | ||
套餐 | ZcyItemComposeSuitMainSkuMapper.xmlZcyItemComposeSuitMapper.xml | 1处4处 | 1:未用到 1/2:query里的未用到3/4:count里未用到 paging里的改为仅右匹配 | 1008 | 完成 | ||
标准 | StandardMapper.xml | 2处 | 1:count里的未用到 2:paging里的本身设计就是右匹配 | 1008 | 完成 | ||
xx | 审核 | AgAuditAppMapper.xml AgAuditDataMapper.xmlItemAuditMapper.xml/ic/ItemAuditMapper.xml | 5处3处6处6处 | 1、AgAuditAppMapper.xml 整治中2、疫苗的需要联系疫苗团队 | 9月份解决 | ||
协议 | /agreement/AgProtocolMapper.xml | 10处 | 暂无地方调用 | 不需要解决 | 完成 | ||
xx | 商品 | itemMapper.xml/ic/itemMapper.xml | 5处7处 | 相关接口4个,近30天都没有调用量接口处加error日志,有调用情况下优化,mapper中加注释,不再使用ItemReadServiceImpl.findByzcyItemReadService.findByParams AdminItemReadServiceImpl.findByStockReadServiceImpl.findVaccineRichStock | 1015 | 完成 | |
xx | 打标 | /ic/ZcyTagMapper.xml | 1处 | 运营后台查询类目标签使用场景,,偶有调用量,可去除全模糊,类目标签数据量并不大,共计120条 | 9月2号 | 完成 | |
商品合并 | ZcyChannelItemMergeMapper.xml | 1处 | 渠道合并页查看,基本没有啥调用量,当前sql为前缀匹配 | / | 完成 | ||
仓库 | WarehouseMapper.xml/ic/WarehouseMapper.xml | 1处 1处 | 仓库列表页根据名称查询查仓库,条件使用量不大,已产品沟通修改 | 9月2号 | 完成 | ||
商品发布控制 | ZcyItemControlledMapper.xml | 12处 | 敏感商品数据,当前接口为分销平台业务使用,使用场景为型号全查询,已排查业务,当前sql的全like均无使用场景,可改为前缀匹配like | 9月2号 | 完成 | ||
商品草稿 | AgGoodsDraftMapper.xml | 2处 | 草稿箱通过name和机构id查询当前用户的草稿数据,机构id有索引,数据量可控,当前场景产品认为还是要全模糊查询,暂不处理 | / | 完成 | ||
item-platform-center | xx | 无 | 完成 | ||||
item-crawler-center | 无 |
脚本如下:将品牌表的更新时间加1s,可以触发es中品牌表的自动dump
UPDATE db_item_standard.parana_brands
set `updated_at` = DATE_ADD(`updated_at`, INTERVAL 1 second)
where `status` != -3;
线上慢SQL
EXPLAIN SELECT config_id,brand_id,specification
FROM zcy_item_mark_detail
WHERE update_at >= '2022-08-26 00:00:00' and update_at <= '2022-08-29 00:00:00' and auth_invalid_date >= '2022-08-29 00:00:00';
更新时间
字段和授权失效时间
字段加索引通过阿里云监控查看MySQL慢查询
栉风沐雨,砥砺前进 --刘超