MYSQL-索引

565 阅读8分钟

索引

索引是提升查询速度的一种数据结构。MySQL8.0中, InnoDB存储引擎支持的索引有B+树索引, 全文索引, R树索引。

B+树索引结构

特点是: 基于磁盘的平衡树,但树特别矮,通常有3~4层,能存放千万到上亿的排序数据。

B+ 树索引由根节点(root node), 中间节点(non leaf node), 叶子节点(leaf node)组成。

所有 B+ 树都是从高度为 1 的树开始,然后根据数据的插入,慢慢增加树的高度。索引是对记录进行排序, 高度为 1 的 B+ 树索引中,存放的记录都已经排序好了,若要在一个叶子节点内再进行查询,只进行二叉查找,就能快速定位数据。

可随着插入 B+ 树索引的记录变多,1个页(16K)无法存放这么多数据,所以会发生 B+ 树的分裂,B+ 树的高度变为 2,当 B+ 树的高度大于等于 2 时,根节点和中间节点存放的是索引键对,由(索引键、指针)组成。索引键就是排序的列,而指针是指向下一层的地址,在 MySQL 的 InnoDB 存储引擎中占用 6 个字节。

优化B+ 树索引的插入性能

开销在于B+ 树索引的维护,保证数据排序,这里存在两种不同数据类型的插入情况:

  • 数据顺序(逆序)插入: 维护代价非常小,如自增ID的插入,时间的插入。
  • 数据无序插入: B+树为了维护排序,对页进行分裂,旋转灯开销较大的操作,另外,随机写性能不如顺序写,磁盘性能受到较大影响,如昵称索引

B+ 树索引的设计与管理

我们可以通过查询mysql.innodb_index_stats查看每个索引的大致情况:

SELECT table_name, index_name, stat_name, stat_value, stat_description FROM innodb_index_stat WHERE table_name='orders' and index_name ='PRIMARY'

可以通过查询sys.schema_unused_indexes查看哪些索引一直未被使用过

SELECT * FROM schema_unused_indexes WHERE object_schema != 'performance_schema'

MYSQL8.0版本推出了索引不可见(invisible)功能, 在删除废弃索引前,可以将索引设置为对优化器不可见,观察业务是否有影响

ALTER TABLE T1	
ALTER INDEX ID_NAME INVISIBLE/VISIBLE

索引组织表

数据存储有堆表和索引组织表两种方式。堆表中的数据无序存放,数据的排序完全依赖索引。而索引组织表,数据根据主键排序存放在索引中,主键索引也叫聚集索引(Clustered Index)。在索引组织表中,数据即索引,索引即数据。

二级索引通过主键索引进行再一次查询的操作叫作“回表”。索引组织表这样的二级索引设计有一个非常大的好处:若记录发生了修改,则其他索引无须进行维护,除非记录的主键发生了修改。

由于每个二级索引都包含了主键值,查询通过主键值进行回表,所以在设计表结构时让主键值尽可能的紧凑,为的就是能提升二级索引的性能.

设计主键时,有两点设计原则:

  • 要比较顺序, 对聚集索引性能友好
  • 尽可能紧凑,对二级索引性能和存储友好。

函数索引

MySQL 5.7 版本开始,MySQL 就开始支持创建函数索引 (即索引键是一个函数表达式)。函数索引有两大用处:

  • 优化业务 SQL 性能;
ALTER TABLE User ADD INDEX idx_func_register_date((DATE_FORMAT(register_date,'%Y-%m')));
EXPLAIN SELECT * FROM User WHERE DATE_FORMAT(register_date,'%Y-%m') = '2021-01'
  • 配合虚拟列(Generated Column)。
CREATE TABLE UserLogin (
    userId BIGINT,
    loginInfo JSON,
    cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone"),
    PRIMARY KEY(userId),
    UNIQUE KEY idx_cellphone(cellphone)
);

cellphone 就是一个虚拟列,它是由后面的函数表达式计算而成,本身这个列不占用任何的存储空间,而索引 idx_cellphone 实质是一个函数索引。这样做得好处是在写 SQL 时可以直接使用这个虚拟列,而不用写冗长的函数.

-- 不用虚拟列
SELECT  *  FROM UserLogin
WHERE loginInfo->>"$.cellphone" = '13918888888'
-- 使用虚拟列
SELECT  *  FROM UserLogin 
WHERE cellphone = '13918888888'

组合索引

组合索引能避免额外排序,避免回表,性能提升。

通过组合索引避免回表的优化技术也称为索引覆盖(Covering Index)。

组合索引三大优势:

  • 覆盖多个查询条件
  • 避免SQL额外排序,提升性能
  • 实现索引覆盖功能,提升查询效率

CBO工作原理

如何选择索引

优化器的选择基于成本,哪个索引低,优先使用哪个索引。

SQL 优化器会分析所有可能的执行计划,选择成本最低的执行,这种优化器称之为:CBO(Cost-based Optimizer,基于成本的优化器)。

Cost  = Server Cost + Engine Cost = CPU Cost + IO Cost

CPU Cost 表示计算的开销,比如索引键值的比较、记录值的比较、结果集的排序……这些操作都在 Server 层完成

IO Cost 表示引擎层 IO 的开销,MySQL 8.0 可以通过区分一张表的数据是否在内存中,分别计算读取内存 IO 开销以及读取磁盘 IO 的开销.

表 server_cost、engine_cost 则记录了对于各种成本的计算, 表 server_cost 记录了 Server 层优化器各种操作的成本,这里面包括了所有 CPU Cost.

  • disk_temptable_create_cost:创建磁盘临时表的成本,默认为20。
  • disk_temptable_row_cost:磁盘临时表中每条记录的成本,默认为0.5。
  • key_compare_cost:索引键值比较的成本,默认为0.05,成本最小。
  • memory_temptable_create_cost:创建内存临时表的成本:默认为1。
  • memory_temptable_row_cost:内存临时表中每条记录的成本,默认为0.1。
  • row_evaluate_cost:记录间的比较成本,默认为0.1。

表 engine_cost 记录了存储引擎层各种操作的成本,这里包含了所有的 IO Cost,具体含义如下。

  • io_block_read_cost:从磁盘读取一个页的成本,默认值为1。
  • memory_block_read_cost:从内存读取一个页的成本,默认值为0.25。

所有的成本都是可以修改的,SQL如下L

INSERT INTO engine_cost(engine_name,device_type,cost_name,cost_value,last_update,comment) 
VALUES ('InnoDB',0,'io_block_read_cost',12.5,CURRENT_TIMESTAMP,'Using HDD for InnoDB');
FLUSH OPTIMIZER_COSTS;

业务实践

字段o_orderstatus有三个值, 但是订单状态时有数据倾斜的,可以建立索引.

ALTER TABLE orders ADD INDEX idx_orderstatus(o_orderstatus)

此时, 优化器认为订单状态为P的占用1/3的数据,使用全表扫描,避免二级索引回表的效率更高。但实际上此时使用索引效率更高。这种情况下我们可以利用Mysql8.0的直方图功能, 让优化器知道数据的分布,从而更好地选择执行计划。

ANALYZE TABLE orders UPDATE HISTOGRAM ON o_orderstatus;

此时,查询就会用到索引了。

JOIN连接

算法

MySQL8.0支持两种JOIN算法用于表关联

查询数据量较小、语句相对简单,大多使用索引连接表之间的数据。这种情况下,优化器大多会用 Nested Loop Join 算法;而 查询数据量较大,关联表的数量非常多,所以用 Hash Join 算法,直接扫描全表效率会更高。

Nested Loop Join

表关联使用索引进行匹配

驱动表R 中通过 WHERE 条件过滤出的数据会在表 S 对应的索引上进行一一查询。如果驱动表 R 的数据量不大,上述算法非常高效。

对于 Left Join 来说,驱动表就是左表 R;Right Join中,驱动表就是右表 S。这是 JOIN 类型决定左表或右表的数据一定要进行查询。但对于 INNER JOIN,谁需要查询的数据量越少,谁就是驱动表。

优化器一般认为,通过索引进行查询的效率都一样,所以 Nested Loop Join 算法主要要求驱动表的数量要尽可能少。

Hash Join

用于两张表之间连接条件没有索引的情况.

Hash Join会扫描关联的两张表:

  • 首先会在扫描驱动表的过程中创建一张哈希表;
  • 接着扫描第二张表时,会在哈希表中搜索每条关联的记录,如果找到就返回记录。

较小的表作为驱动表。如果驱动表比较大,创建的哈希表超过了内存的大小,MySQL 会自动把结果转储到磁盘。

子查询

IN和EXIST 执行计划一样,效率一样。

依赖子查询问题

Mysql8.0之前,子查询会看到DEPENDENT SUBQUERY 的提示,这表示是一个依赖子查询,子查询需要依赖外部表的关联。DEPENDENT SUBQUERY 执行速度可能非常慢,大部分时候需要手动把它转化成两张表之间的连接。

对于依赖子查询的优化,就是要避免子查询由于需要对外部的依赖,而需要对子查询扫描多次的情况。所以可以通过派生表的方式,将外表和子查询的派生表进行连接,从而降低对于子查询表的扫描,从而提升 SQL 查询的性能。

分区表

分区表就是把物理表结构相同的几张表,通过一定算法,组成一张逻辑大表。MySQL 数据库支持的分区函数类型有 RANGE、LIST、HASH、KEY、COLUMNS。无论选择哪种分区函数,都要指定相关列成为分区算法的输入条件,这些列就叫“分区列”。另外,在 MySQL 分区表中,主键也必须是分区列的一部分,不然创建分区表时会失败。

CREATE TABLE t (
    a INT,
    b INT,
    c DATETIME,
    d VARCHAR(32),
    e INT,
    PRIMARY KEY (a,b,c),
    KEY idx_e (e)
)
partition by range columns(c) (
    PARTITION p0000 VALUES LESS THAN ('2019-01-01'),
    PARTITION p2019 VALUES LESS THAN ('2020-01-01'),
    PARTITION p2020 VALUES LESS THAN ('2021-01-01'),
    PARTITION p9999 VALUES LESS THAN (MAXVALUE)
);

唯一索引

在 MySQL 数据库中,分区表的索引都是局部,而非全局。也就是说,索引在每个分区文件中都是独立的,所以分区表上的唯一索引必须包含分区列信息,否则创建会报错.正因为唯一索引包含了分区列,唯一索引也就变成仅在当前分区唯一,而不是全局唯一了。

分区表技术不是用于提升 MySQL 数据库的性能,而是方便数据的管理.

ALTER TABLE orders_par TRUNCATE PARTITION p1998