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

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

sql 复制代码
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:

sql 复制代码
create index idx_uid_prefix on t_user_order(uid(5))

执行查询SQL:

csharp 复制代码
explain select * from t_user_order where uid = '9a4aebe012384c109106504b7cff14b3';

输出结果:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t_user_order ref idx_uid_prefix idx_uid_prefix 22 const 30 100 Using where

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

csharp 复制代码
create index idx_uid on t_user_order (uid)

执行相同的SQL:

csharp 复制代码
explain select * from t_user_order where uid = '9a4aebe012384c109106504b7cff14b3';

输出结果:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t_user_order ref idx_uid idx_uid 258 const 30 100

对比可以发现,首先前缀索引的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 操作速度。

函数索引

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

sql 复制代码
CREATE TABLE t_index (
  col1 VARCHAR(10),
  col2 VARCHAR(20),
  INDEX (col1, col2(10))
);

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

sql 复制代码
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);

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

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

错误的栗子会报错:

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

正确的:

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

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

sql 复制代码
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;

给出两个查询的栗子:

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

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

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE tbl ref idx1 idx1 43 const 1 100
  • 函数索引部分不能使用外键列。

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

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

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

sql 复制代码
alter table t_i add unique index uniq_func((col1 + 1))

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

sql 复制代码
CREATE TABLE employees ( 
    data JSON, 
    INDEX ((data->>'$.name'))
);

语法要求:

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

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

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

或者这样:

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

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

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

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

sql 复制代码
SELECT * FROM employees WHERE data->>'$.name' = 'Sdd2cfb096f';

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

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

执行计划:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE employees ref functional_index functional_index 123 const 1 100
  • 方案2,修改创建语句
sql 复制代码
CREATE TABLE employees ( 
    data JSON, 
    INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin)) 
);

搭配的查询语句是:

sql 复制代码
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

举个例子:

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

举个栗子:

perl 复制代码
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数组上定义的多值索引会为每个邮政编码创建一条索引记录,每条索引记录都会引用相同的数据记录。

json 复制代码
{ 
    "user":"Bob", 
    "user_id":31, 
    "zipcode":[94477,94536] 
}

创建多值索引

可以使用 CREATE TABLE、ALTER TABLE 或 CREATE INDEX 语句中创建多值索引。搭配使用 CAST(... AS ... ARRAY),将 JSON 数组中的同类型标量值转换为 SQL 数据类型数组。然后,用 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)) ) 
);
  • 单独增加索引
sql 复制代码
ALTER TABLE customers ADD INDEX zips( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );
sql 复制代码
CREATE INDEX zips ON customers ( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );

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

sql 复制代码
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
    某些存储引擎支持在创建索引时指定索引类型。例如:
sql 复制代码
CREATE TABLE lookup (id INT) ENGINE = MEMORY; 
CREATE INDEX id_index ON lookup (id) USING BTREE;

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

引擎 类型
InnoDB BTREE,RTREE
MyISAM BTREE
MEMORY HASH,BTREE
NDB HASH,BTREE

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

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

  • InnoDB
索引 索引数据类型 可存储空值 可存储多个空值 IS NULL 扫描类型 IS NOT NULL 扫描类型
主键索引 BTREE No No
唯一索引 BTREE Yes Yes Index Index
普通索引 BTREE Yes Yes Index Index
全文索引 Yes Yes Table Table
空间索引 RTREE No No
  • MyISAM
索引 索引数据类型 可存储空值 可存储多个空值 IS NULL 扫描类型 IS NOT NULL 扫描类型
主键索引 BTREE No No
唯一索引 BTREE Yes Yes Index Index
普通索引 BTREE Yes Yes Index Index
全文索引 Yes Yes Table Table
空间索引 No No
  • MEMORY
索引 索引数据类型 可存储空值 可存储多个空值 IS NULL 扫描类型 IS NOT NULL 扫描类型
主键索引 BTREE,HASH No No
唯一索引 BTREE,HASH Yes Yes Index Index
普通索引 BTREE,HASH Yes Yes Index Index
  • NDB
索引 索引数据类型 可存储空值 可存储多个空值 IS NULL 扫描类型 IS NOT NULL 扫描类型
主键索引 BTREE,HASH No No Index,Table Index,Table
唯一索引 BTREE,HASH Yes Yes Index,Table Index,Table
普通索引 BTREE,HASH Yes Yes Index,Table Index,Table
  • WITH PARSER parser_name

    该选项只能用于 FULLTEXT 索引。如果全文索引和搜索操作需要特殊处理,该选项会为索引关联一个解析器插件。InnoDB 和 MyISAM 支持全文分析器插件。

  • COMMENT 'string'

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

sql 复制代码
CREATE INDEX id_index ON t1 (id) COMMENT 'MERGE_THRESHOLD=40';

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

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

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

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

sql 复制代码
CREATE INDEX i1 ON t1 (c1) ENGINE_ATTRIBUTE='{"key":"value"}';

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

  • ALGORITHM, LOCK

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


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

相关推荐
lizhongxuan4 分钟前
NVIDIA k8s-device-plugin
后端
没逻辑18 分钟前
深入 Python 性能分析:工具与实战指南
后端·python
liyanchao201818 分钟前
压缩css到war包中,添加支持es6语法的js压缩器
javascript·后端
未完结小说19 分钟前
Thrift入门:用IDL定义你的第一个RPC服务
后端
Java_baby21 分钟前
使用 Go 和 Gin 实现高可用负载均衡代理服务器
后端
这里有鱼汤21 分钟前
一篇文章搞定Python数据分析用到的所有库
后端·python·程序员
CaliXz22 分钟前
宝塔面板安装docker flarum失败,请先安装依赖应用: [‘mysql‘]:5/8
mysql·docker·容器
加瓦点灯23 分钟前
IM入门之:万人群聊系统该如何设计?
后端
Aixbox23 分钟前
宝塔Webhook: 轻松实现自动化部署
后端·ci/cd
helloworld_工程师23 分钟前
SpringBoot整合高德地图完成天气预报功能
java·前端·后端