MySQL索引优化-创建索引(上)

98 阅读11分钟

基于MySQL8.0.41给出最新语法:

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name  
    [index_type]   
    ON tbl_name (key_part,...) 
    [index_option] 
    [algorithm_option | lock_option] ... 
    
key_part: {col_name [(length)] | (expr)} [ASC | DESC] 

index_option: { 
    KEY_BLOCK_SIZE [=] value
    | index_type
    | WITH PARSER parser_name 
    | COMMENT 'string' 
    | {VISIBLE | INVISIBLE} 
    | ENGINE_ATTRIBUTE [=] 'string' 
    | SECONDARY_ENGINE_ATTRIBUTE [=] 'string' 
} 

index_type: USING {BTREE | HASH} 

algorithm_option: 
    ALGORITHM [=] {DEFAULT | INPLACE | COPY} 

lock_option: 
    LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}

Innodb支持在虚拟列上创建二级索引,支持在JSON类型字段上创建索引。

CREATE INDEX 被映射为 ALTER TABLE 语句,用于创建索引,但是 CREATE INDEX 不能创建 PRIMARY KEY,只能使用 ALTER TABLE

key_part 部分可以使用 ASCDESC ,用来指定索引值是以升序还是降序存储,默认为升序。HASH 索引和多列索引不支持 ASCDESC。从 MySQL 8.0.12 起,SPATIAL 索引也不支持。

前缀索引

对于字符串列,可以使用 col_name(length) 语法指定索引前缀长度,创建只使用列值前导部分的索引:

  • 可以为 CHAR、VARCHAR、BINARY 和 VARBINARY 键部分指定前缀。
  • BLOB 和 TEXT 列必须指定前缀创建索引。
  • 前缀限制以字节为单位。但在 CREATE TABLEALTER TABLECREATE INDEX 语句中,索引规范的前缀长度对于非二进制字符串类型(CHAR、VARCHAR、TEXT)解释为 字符数,对于二进制字符串类型(BINARY、VARBINARY、BLOB)解释为 字节数
  • 前缀支持的长度取决于存储引擎。对于使用 REDUNDANT(冗余)COMPACT(紧凑) 行格式的 InnoDB 表,前缀长度最多为 767 字节。对于DYNAMIC(动态) 或 COMPRESSED(压缩) 表,前缀长度限制为 3072 字节。对于 MyISAM 表,前缀长度限制为 1000 字节。

如果指定的索引前缀超过了列数据类型的最大大小,CREATE INDEX 会按以下方式处理该索引:

  • 对于非唯一索引,如果启用了严格 SQL 模式,会报错,如果未启用严格 SQL 模式,索引长度会被缩减到最大列数据类型大小范围内并给出警告。
  • 对于唯一索引,无论 SQL 模式如何,都会报错,因为减少索引长度没办法保证数据唯一性。

举个栗子,给t_user_order表中的字段uid创建前缀索引,指定长度是5:

create index idx_uid_prefix on t_user_order(uid(5))

执行查询SQL:

explain select * from t_user_order where uid = '9a4aebe012384c109106504b7cff14b3';

输出结果:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEt_user_orderrefidx_uid_prefixidx_uid_prefix22const30100Using where

同样的数据表,我们改成创建正常的索引:

create index idx_uid on t_user_order (uid)

执行相同的SQL:

explain select * from t_user_order where uid = '9a4aebe012384c109106504b7cff14b3';

输出结果:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEt_user_orderrefidx_uididx_uid258const30100

对比可以发现,首先前缀索引的ken_len值小于正常索引的ken_len, ken_len表示 查询实际使用的索引部分所占的字节长度,uid字段是varchar类型,且在utf8mb4编码下占用空间是每个字符串4个字节,创建的前缀索引长度是5,再加上可变长类型的实际长度前缀(1-2字节), 4 x 5 + 1 = 22,小于正常索引的258,索引字段使用率低。

其次,前缀索引的extra列显示了using where,表示MySQL服务器层在存储引擎返回行数据之后又进行了额外的过滤;正常索引的extra列是空,则没有额外的处理工作。

所以可以得出结论,前缀索引的性能在使用等值查询时,性能明显弱于正常索引匹配,如果列中的值大部分情况下只有前 5 个字符不同,那么使用前缀索引和正常索引的性能差距就很小。此外,使用前缀索引可以使索引文件更小,从而节省大量磁盘空间,并可能加快 INSERT 操作速度。

函数索引

举个栗子,我们先正常的创建一个表,并且附带一个联合索引:

CREATE TABLE t_index (
  col1 VARCHAR(10),
  col2 VARCHAR(20),
  INDEX (col1, col2(10))
);

MySQL 8.0.13 及更高版本支持创建函数索引,可以为不直接存储在表中的值建立索引,且支持ASC和DESC,举个栗子:

CREATE TABLE t1 (col1 INT, col2 INT, INDEX func_index ((ABS(col1)))); 
CREATE INDEX idx1 ON t1 ((col1 + col2)); 
CREATE INDEX idx2 ON t1 ((col1 + col2), (col1 - col2), col1); 
ALTER TABLE t1 ADD INDEX ((col1 * 40) DESC);

创建函数索引必须遵守以下规则,否则会报错:

  • 在创建索引时,表达式必须在括号里,比如:
INDEX ((col1 + col2), (col3 - col4))

错误的栗子会报错:

INDEX (col1 + col2, col3 - col4)
  • 索引部分不能只由列名组成,错误的栗子:
INDEX ((col1), (col2))

正确的:

INDEX (col1, col2)
  • 函数索引不能使用列前缀,但是可以通过 SUBSTRING函数支持。 举个例子:
CREATE TABLE tbl ( 
    col1 LONGTEXT, 
    INDEX idx1 ((SUBSTRING(col1, 1, 10))) 
);

创建tbl表,定义了col1字段,然后使用substring函数创建函数索引,接下来给这个表录入一千条数据:

INSERT INTO tbl (col1)
SELECT
    CONCAT(
        MD5(RAND()),   -- 32 位随机哈希
        MD5(RAND()),   -- 再拼接 32 位
        MD5(RAND())    -- 总长度 96 位
    ) AS random_text
FROM (
    WITH RECURSIVE numbers (n) AS (
        SELECT 1                  -- 初始值
        UNION ALL
        SELECT n + 1 FROM numbers -- 递归增加
        WHERE n < 1000            -- 停止条件(生成 1000 行)
    )
    SELECT n FROM numbers
) AS seq;

给出两个查询的栗子:

SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 9) = '2afc069d4'; 
select * from tbl where substring(col1, 1, 10) = '2afc069d47';

上面两个SQL,只有第二个SQL能用到索引,使用函数索引时的参数必须和定义索引的参数是相同的,执行计划如下:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEtblrefidx1idx143const1100
  • 函数索引部分不能使用外键列。

函数索引是以隐藏虚拟生成列的形式实现的,因此会产生这些影响:

  • 每个函数索引部分都计算在表列总数限制内。
  • 不允许使用子查询、参数、变量、存储过程和随机值函数。
  • 虚拟生成列本身不需要存储空间。索引本身与其他索引一样占用存储空间。

unique索引可以使用函数索引来定义,但是PrimarySPATIALFULLTEXT不支持使用函数式索引创建,举个栗子:

alter table t_i add unique index uniq_func((col1 + 1))

函数式索引在JSON字段上可能比较常用,因为JSON字段值通常比较复杂,如果要针对JSON字段中的某个属性创建索引的话,需要注意语法,举个容易出错的栗子:

CREATE TABLE employees ( 
    data JSON, 
    INDEX ((data->>'$.name'))
);

语法要求:

  • ->> 操作符需要转换为 JSON_UNQUOTE(JSON_EXTRACT(...))
  • JSON_UNQUOTE() 返回的值的数据类型为 LONGTEXT,所以隐藏生成的列也是相同的数据类型。
  • 直接对返回 TEXT/BLOB 类型的 JSON 提取值创建索引时,需通过 CAST 明确转换为字符类型

所以上面的SQL语句,可以改成这样:

CREATE TABLE employees (
  data JSON,
  INDEX idx_name ((CAST(json_unquote(json_extract(data, '$.name')) AS CHAR(255))))
);

或者这样:

CREATE TABLE employees ( 
    data JSON, 
    INDEX ((CAST(data->>'$.name' AS CHAR(30))))
);

但是有两个需要注意的问题:

  • CAST() 返回的字符串是使用的 utf8mb4_0900_ai_ci(默认)排序规则。
  • JSON_UNQUOTE() 返回字符串是使用的 utf8mb4_bin(硬编码)排序规则。

所以上面定义的表,如果用下面SQL执行的话,不会命中索引:

SELECT * FROM employees WHERE data->>'$.name' = 'Sdd2cfb096f';

因为查询中的表达式与索引中的表达式不同,索引不会使用。为了支持这种情况,优化器在查找要使用的索引时会自动删除 CAST(),但前提是索引表达式的collation(排序规则)与查询表达式的校对方式一致。以下给出两种解决方案:

  • 方案1,补全SQL表达式
SELECT * FROM employees WHERE CAST(data->>'$.name' AS CHAR(30)) = 'Sdd2cfb096f';

执行计划:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesreffunctional_indexfunctional_index123const1100
  • 方案2,修改创建语句
CREATE TABLE employees ( 
    data JSON, 
    INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin)) 
);

搭配的查询语句是:

SELECT * FROM employees WHERE data->>'$.name' = 'Sdd2cfb096f';

以上两种方案本质上都是解决一个问题,保证查询和数据的collation相同。

唯一索引

使用 UNIQUE 会创建一个约束,要求索引中的所有值都必须是唯一的。如果添加了重复的值,会报错。如果在 UNIQUE 索引中为列指定前缀值,则列值必须在前缀长度范围内唯一。

注意,UNIQUE 索引列可以包含NULL值,并且允许有多个NULL值存在。

如果表的 PRIMARY KEY 或 UNIQUE NOT NULL 列由整数类型的单列组成,则可以在 SELECT 语句中使用 _rowid 来替代索引列字段。

  • 如果 PRIMARY KEY 由单个整数列组成,则 _rowid 指向 PRIMARY KEY 列。如果存在 PRIMARY KEY,但它不是由单个整数列组成,则不能使用 _rowid

举个例子:

create table t_uniq (
    id int unsigned not null primary key auto_increment,
    name varchar(20) null
);
-- 填充3条数据,主键自增
insert into t_uniq (name) values('zhang'), ('qiang'), ('lisi');

如果想查询id值是1的数据,可以这样:

-- 两个SQL的执行结果都是相同的
select * from t_uniq where id = 1;
select * from t_uniq where _rowid = 1;
  • 否则,如果第一个 UNIQUE NOT NULL 索引包含一个整数列,_rowid 将指向该索引中的列。如果第一个 UNIQUE NOT NULL 索引不包含单整数列,则不能使用 _rowid。

举个栗子:

create table t_uniq
(
    id   int unsigned ,
    name varchar(20) not null ,
    unique index uniq(name)
);

insert into t_uniq (name) values('zhang'), ('qiang'), ('lisi');

同样的三条数据,但是因为第一个非空唯一索引列不是整数列,所以没办法使用_rowid来代替name查询。

全文索引

仅 InnoDB 和 MyISAM 表支持 FULLTEXT 索引,且只能包含 CHAR、VARCHAR 和 TEXT 列。索引只能针对整个列,不支持列前缀索引,如果指定了前缀长度,则会被忽略。

多值索引

从 MySQL 8.0.17 开始,InnoDB 支持多值索引。多值索引是在存储数组值的列上定义的二级索引。普通索引的每条数据都有一条索引记录(1:1)。多值索引可以为一条数据设置多条索引记录(1:N)。

多值索引用于 JSON 数组的索引。例如,在以下 JSON 文档中的zipcode数组上定义的多值索引会为每个邮政编码创建一条索引记录,每条索引记录都会引用相同的数据记录。

{ 
    "user":"Bob", 
    "user_id":31, 
    "zipcode":[94477,94536] 
}

创建多值索引

可以使用 CREATE TABLE、ALTER TABLE 或 CREATE INDEX 语句中创建多值索引。搭配使用 CAST(... AS ... ARRAY),将 JSON 数组中的同类型标量值转换为 SQL 数据类型数组。然后,用 SQL 数据类型数组中的值透明地生成一个虚拟列;最后,在虚拟列上创建一个函数索引,举个栗子:

  • 建表定义索引
CREATE TABLE customers ( 
    id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    custinfo JSON,
    INDEX zips( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) ) 
);
  • 单独增加索引
ALTER TABLE customers ADD INDEX zips( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );
CREATE INDEX zips ON customers ( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );

同样多值索引也可以作为联合索引的一部分来定义,但是整个联合索引中,只能有一个多值索引部分。

ALTER TABLE customers ADD INDEX comp(modified, (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );

空间索引

MyISAM、InnoDB、NDB 和 ARCHIVE 存储引擎支持 POINT 和 GEOMETRY 空间列。不过,不同引擎对空间列索引的支持有所不同。空间列上的空间和非空间索引可根据以下规则使用。

空间列上的空间索引具有这些特点:

  • 仅适用于 InnoDB 和 MyISAM 表。为其他存储引擎指定 SPATIAL INDEX 会报错。
  • 从 MySQL 8.0.12 开始,空间列上的索引必须是 SPATIAL 索引。因此,在空间列上创建索引时,SPATIAL 关键字是可选的,但却是隐含的。
  • 仅适用于单个空间列。不能给多个空间列创建空间索引。
  • 索引列必须为 NOT NULL。
  • 不支持前缀索引。
  • 不允许用于主键或唯一索引。

空间列上的非空间索引(使用 INDEX、UNIQUE 或 PRIMARY KEY 创建)具有这些特征:

  • 除 ARCHIVE 外,允许用于任何支持空间列的存储引擎。
  • 除非是主键索引,否则列可以为空。
  • 非 SPATIAL 索引的索引类型取决于存储引擎。目前使用的是 B 树。
  • 仅适用于 InnoDB、MyISAM 和 MEMORY 表中可以有 NULL 值的列。

索引选项

index_option 值可以是以下任何一种:

  • KEY_BLOCK_SIZE [=] value
    对于 MyISAM 表,KEY_BLOCK_SIZE 可以指定索引键块的大小(以字节为单位)。该值被视为一个提示;如果需要,可以使用不同的大小。为单个索引定义指定的 KEY_BLOCK_SIZE 值会覆盖表级 KEY_BLOCK_SIZE 值。
  • index_type
    某些存储引擎支持在创建索引时指定索引类型。例如:
CREATE TABLE lookup (id INT) ENGINE = MEMORY; 
CREATE INDEX id_index ON lookup (id) USING BTREE;

常见存储引擎支持的索引类型:

引擎类型
InnoDBBTREE,RTREE
MyISAMBTREE
MEMORYHASH,BTREE
NDBHASH,BTREE

index_type 子句不能用于 FULLTEX INDEX 或(MySQL 8.0.12 之前的)SPATIAL INDEX 。 全文索引的实现取决于存储引擎。空间索引以 R 树索引的形式实现。

下表显示了支持 index_type 选项的存储引擎的索引特性:

  • InnoDB
索引索引数据类型可存储空值可存储多个空值IS NULL 扫描类型IS NOT NULL 扫描类型
主键索引BTREENoNo
唯一索引BTREEYesYesIndexIndex
普通索引BTREEYesYesIndexIndex
全文索引YesYesTableTable
空间索引RTREENoNo
  • MyISAM
索引索引数据类型可存储空值可存储多个空值IS NULL 扫描类型IS NOT NULL 扫描类型
主键索引BTREENoNo
唯一索引BTREEYesYesIndexIndex
普通索引BTREEYesYesIndexIndex
全文索引YesYesTableTable
空间索引NoNo
  • MEMORY
索引索引数据类型可存储空值可存储多个空值IS NULL 扫描类型IS NOT NULL 扫描类型
主键索引BTREE,HASHNoNo
唯一索引BTREE,HASHYesYesIndexIndex
普通索引BTREE,HASHYesYesIndexIndex
  • NDB
索引索引数据类型可存储空值可存储多个空值IS NULL 扫描类型IS NOT NULL 扫描类型
主键索引BTREE,HASHNoNoIndex,TableIndex,Table
唯一索引BTREE,HASHYesYesIndex,TableIndex,Table
普通索引BTREE,HASHYesYesIndex,TableIndex,Table
  • WITH PARSER parser_name
    该选项只能用于 FULLTEXT 索引。如果全文索引和搜索操作需要特殊处理,该选项会为索引关联一个解析器插件。InnoDB 和 MyISAM 支持全文分析器插件。

  • COMMENT 'string'
    COMMENT 除了可以为索引设置注释,最多支持 1024 个字符;还可以为单个索引配置索引页的 MERGE_THRESHOLD。

CREATE INDEX id_index ON t1 (id) COMMENT 'MERGE_THRESHOLD=40';

如果在删除或更新数据缩短记录时,索引页的页满百分比低于 MERGE_THRESHOLD 值,InnoDB 会尝试将当前索引页与相邻的索引页合并。默认的 MERGE_THRESHOLD 值是 50,最小值是1,最大值是50。

  • VISIBLEINVISIBLE 指定索引的可见性。索引默认是可见的。优化程序不会使用不可见索引。指定索引可见性适用于主键(显式或隐式)以外的索引。

  • ENGINE_ATTRIBUTESECONDARY_ENGINE_ATTRIBUTE 从 MySQL 8.0.21 起,可以用于指定主存储引擎和辅助存储引擎的索引属性。这些选项保留供将来使用。

这两个选项后面拼接的值是包含有效 JSON 文档的字面字符串或空字符串(''),举个栗子:

CREATE INDEX i1 ON t1 (c1) ENGINE_ATTRIBUTE='{"key":"value"}';

ENGINE_ATTRIBUTE 和 SECONDARY_ENGINE_ATTRIBUTE 值可以重复,不会出错。在这种情况下,将使用最后指定的值。服务器不会检查值,也不会在更改表的存储引擎时清除这些值。

  • ALGORITHMLOCK

可以使用 ALGORITHM 和 LOCK 子句来设置表的复制方式,以及在修改表的索引时读写表的并发程度。


参考文档:dev.mysql.com/doc/refman/…