文档主页
MySQL 8.4 参考手册
相关文档 下载本手册
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
手册页 (TGZ) - 258.5Kb
手册页 (Zip) - 365.5Kb
信息 (Gzip) - 4.0Mb
信息 (Zip) - 4.0Mb


MySQL 8.4 参考手册  /  ...  /  CREATE INDEX 语句

15.1.15 CREATE INDEX 语句

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}

通常,您会在创建表时使用 CREATE TABLE 语句创建表上的所有索引。请参阅 第 15.1.20 节,“CREATE TABLE 语句”。此准则对于 InnoDB 表尤其重要,因为主键决定了数据文件中行的物理布局。 CREATE INDEX 语句使您能够向现有表添加索引。

CREATE INDEX 被映射到 ALTER TABLE 语句以创建索引。请参阅 第 15.1.9 节,“ALTER TABLE 语句”。 CREATE INDEX 不能用于创建 PRIMARY KEY;请改用 ALTER TABLE。有关索引的更多信息,请参阅 第 10.3.1 节,“MySQL 如何使用索引”。

InnoDB 支持虚拟列上的二级索引。有关更多信息,请参阅“第 15.1.20.9 节 “二级索引和生成列””。

当启用innodb_stats_persistent 设置时,在为 InnoDB 表创建索引之后,对该表运行 ANALYZE TABLE 语句。

key_part 规范的 expr 还可以采用 (CAST json_expression AS type ARRAY) 形式,以便在 JSON 列上创建多值索引。请参阅多值索引。

形式为 (key_part1, key_part2, ...) 的索引规范将创建一个包含多个键部分的索引。索引键值是通过连接给定键部分的值而形成的。例如,(col1, col2, col3) 指定了一个多列索引,其索引键由 col1、col2 和 col3 中的值组成。

key_part 规范可以以 ASC 或 DESC 结尾,以指定索引值是按升序还是降序存储。如果未给出顺序说明符,则默认为升序。

HASH 索引、多值索引或 SPATIAL 索引不支持 ASC 和 DESC。

以下各节介绍 CREATE INDEX 语句的不同方面

列前缀键部分

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

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

  • 对于非唯一索引,要么发生错误(如果启用了严格 SQL 模式),要么将索引长度缩短到最大列数据类型大小以内,并生成警告(如果未启用严格 SQL 模式)。

  • 对于唯一索引,无论 SQL 模式如何,都会发生错误,因为减少索引长度可能会导致插入不符合指定唯一性要求的非唯一条目。

此处显示的语句使用 name 列的前 10 个字符创建一个索引(假设 name 的类型为非二进制字符串)

CREATE INDEX part_of_name ON customer (name(10));

如果列中的名称通常在前 10 个字符中不同,则使用此索引执行的查找速度应该不会比使用从整个 name 列创建的索引慢很多。此外,对索引使用列前缀可以使索引文件小得多,这可以节省大量磁盘空间,并且还可以加快 INSERT 操作的速度。

函数键部分

“普通” 索引会对列值或列值的前缀进行索引。例如,在下表中,给定 t1 行的索引条目包括完整的 col1 值和 col2 值的前缀(由其前 10 个字符组成)

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

对表达式值进行索引的函数键部分也可以用来代替列或列前缀值。使用函数键部分可以对未直接存储在表中的值进行索引。示例

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);

具有多个键部分的索引可以混合使用非函数键部分和函数键部分。

函数键部分支持 ASC 和 DESC。

函数键部分必须遵循以下规则。如果键部分定义包含不允许的构造,则会发生错误。

  • 在索引定义中,将表达式括在括号中,以将它们与列或列前缀区分开来。例如,这是允许的;表达式括在括号中

    INDEX ((col1 + col2), (col3 - col4))

    这将产生错误;表达式未括在括号中

    INDEX (col1 + col2, col3 - col4)
  • 函数键部分不能仅由列名组成。例如,这是不允许的

    INDEX ((col1), (col2))

    而是将键部分编写为非函数键部分,不带括号

    INDEX (col1, col2)
  • 函数键部分表达式不能引用列前缀。有关解决方法,请参阅本节后面有关 SUBSTRING() 和 CAST() 的讨论。

  • 外键规范中不允许使用函数键部分。

对于 CREATE TABLE ... LIKE,目标表保留原始表中的函数键部分。

函数索引实现为隐藏的虚拟生成列,这意味着

包含函数键部分的索引支持 UNIQUE。但是,主键不能包含函数键部分。主键要求存储生成列,但函数键部分实现为虚拟生成列,而不是存储的生成列。

SPATIAL 和 FULLTEXT 索引不能具有函数键部分。

如果表不包含主键,则 InnoDB 会自动将第一个 UNIQUE NOT NULL 索引提升为主键。具有函数键部分的 UNIQUE NOT NULL 索引不支持此功能。

如果存在重复索引,则非函数索引会引发警告。包含函数键部分的索引没有此功能。

要删除函数键部分引用的列,必须先删除该索引。否则,会发生错误。

虽然非函数键部分支持前缀长度规范,但这对于函数键部分是不可能的。解决方案是使用 SUBSTRING()(或 CAST(),如本节后面所述)。要使包含 SUBSTRING() 函数的函数键部分在查询中使用,WHERE 子句必须包含参数相同的 SUBSTRING()。在以下示例中,只有第二个 SELECT 能够使用该索引,因为这是唯一一个 SUBSTRING() 的参数与索引规范匹配的查询

CREATE TABLE tbl (
  col1 LONGTEXT,
  INDEX idx1 ((SUBSTRING(col1, 1, 10)))
);
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 9) = '123456789';
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 10) = '1234567890';

函数键部分允许对无法以其他方式索引的值(例如 JSON 值)进行索引。但是,必须正确执行此操作才能实现所需的效果。例如,以下语法不起作用

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

该语法失败,因为

要为 JSON 列建立索引,您可以尝试使用 CAST() 函数,如下所示

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

隐藏的生成列被分配了 VARCHAR(30) 数据类型,可以对其建立索引。但是,在尝试使用索引时,这种方法会产生一个新问题

  • CAST() 返回一个排序规则为 utf8mb4_0900_ai_ci(服务器默认排序规则)的字符串。

  • JSON_UNQUOTE() 返回一个排序规则为 utf8mb4_bin(硬编码)的字符串。

因此,上一个表定义中的索引表达式与以下查询中的 WHERE 子句表达式之间存在排序规则不匹配

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

由于查询和索引中的表达式不同,因此未使用索引。为了支持函数键部分的这种场景,优化器在查找要使用的索引时会自动去除 CAST(),但前提是索引表达式的排序规则与查询表达式的排序规则匹配。要使用具有函数键部分的索引,以下两种解决方案都可以使用(尽管它们的效果略有不同)

  • 解决方案 1:为索引表达式分配与 JSON_UNQUOTE() 相同的排序规则

    CREATE TABLE employees (
      data JSON,
      INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin))
    );
    INSERT INTO employees VALUES
      ('{ "name": "james", "salary": 9000 }'),
      ('{ "name": "James", "salary": 10000 }'),
      ('{ "name": "Mary", "salary": 12000 }'),
      ('{ "name": "Peter", "salary": 8000 }');
    SELECT * FROM employees WHERE data->>'$.name' = 'James';

    ->> 运算符与 JSON_UNQUOTE(JSON_EXTRACT(...)) 相同,并且 JSON_UNQUOTE() 返回一个排序规则为 utf8mb4_bin 的字符串。因此,比较区分大小写,并且只有一行匹配

    +------------------------------------+
    | data                               |
    +------------------------------------+
    | {"name": "James", "salary": 10000} |
    +------------------------------------+
  • 解决方案 2:在查询中指定完整的表达式

    CREATE TABLE employees (
      data JSON,
      INDEX idx ((CAST(data->>"$.name" AS CHAR(30))))
    );
    INSERT INTO employees VALUES
      ('{ "name": "james", "salary": 9000 }'),
      ('{ "name": "James", "salary": 10000 }'),
      ('{ "name": "Mary", "salary": 12000 }'),
      ('{ "name": "Peter", "salary": 8000 }');
    SELECT * FROM employees WHERE CAST(data->>'$.name' AS CHAR(30)) = 'James';

    CAST() 返回一个排序规则为 utf8mb4_0900_ai_ci 的字符串,因此比较不区分大小写,并且有两行匹配

    +------------------------------------+
    | data                               |
    +------------------------------------+
    | {"name": "james", "salary": 9000}  |
    | {"name": "James", "salary": 10000} |
    +------------------------------------+

请注意,尽管优化器支持使用索引生成列自动去除 CAST(),但以下方法不起作用,因为它在有索引和没有索引的情况下会产生不同的结果(Bug#27337092)

mysql> CREATE TABLE employees (
         data JSON,
         generated_col VARCHAR(30) AS (CAST(data->>'$.name' AS CHAR(30)))
       );
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> INSERT INTO employees (data)
       VALUES ('{"name": "james"}'), ('{"name": "James"}');
Query OK, 2 rows affected, 1 warning (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 1

mysql> SELECT * FROM employees WHERE data->>'$.name' = 'James';
+-------------------+---------------+
| data              | generated_col |
+-------------------+---------------+
| {"name": "James"} | James         |
+-------------------+---------------+
1 row in set (0.00 sec)

mysql> ALTER TABLE employees ADD INDEX idx (generated_col);
Query OK, 0 rows affected, 1 warning (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 1

mysql> SELECT * FROM employees WHERE data->>'$.name' = 'James';
+-------------------+---------------+
| data              | generated_col |
+-------------------+---------------+
| {"name": "james"} | james         |
| {"name": "James"} | James         |
+-------------------+---------------+
2 rows in set (0.01 sec)

唯一索引

UNIQUE 索引创建一个约束,要求索引中的所有值都必须唯一。如果尝试添加一个与现有行具有相同键值的新行,则会发生错误。如果在 UNIQUE 索引中为列指定了前缀值,则列值在该前缀长度内必须唯一。对于可以包含 NULL 的列,UNIQUE 索引允许多个 NULL 值。

如果一个表具有由单个整数类型的列组成的 PRIMARY KEY 或 UNIQUE NOT NULL 索引,则可以使用 _rowid 在 SELECT 语句中引用该索引列,如下所示

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

  • 否则,如果第一个 UNIQUE NOT NULL 索引由单个整数列组成,则 _rowid 引用该索引中的列。如果第一个 UNIQUE NOT NULL 索引不是由单个整数列组成,则不能使用 _rowid。

全文索引

FULLTEXT 索引仅支持 InnoDB 和 MyISAM 表,并且只能包含 CHAR、VARCHAR 和 TEXT 列。索引始终基于整个列进行;不支持列前缀索引,并且如果指定了任何前缀长度,则将忽略该长度。有关操作的详细信息,请参阅 第 14.9 节“全文搜索函数”。

多值索引

InnoDB 支持多值索引。多值索引是在存储值数组的列上定义的二级索引。“普通”索引对于每个数据记录都有一个索引记录 (1:1)。多值索引对于单个数据记录可以有多个索引记录 (N:1)。多值索引旨在对 JSON 数组进行索引。例如,在以下 JSON 文档中的邮政编码数组上定义的多值索引为每个邮政编码创建一个索引记录,其中每个索引记录都引用相同的数据记录。

{
    "user":"Bob",
    "user_id":31,
    "zipcode":[94477,94536]
}
创建多值索引

您可以在 CREATE TABLE、ALTER TABLE 或 CREATE INDEX 语句中创建多值索引。这需要在索引定义中使用 CAST(... AS ... ARRAY),它将 JSON 数组中的相同类型标量值转换为 SQL 数据类型数组。然后,将使用 SQL 数据类型数组中的值透明地生成一个虚拟列;最后,在该虚拟列上创建一个函数索引(也称为虚拟索引)。在由 SQL 数据类型数组中的值组成的虚拟列上定义的函数索引构成了多值索引。

以下列表中的示例展示了三种不同的方法,可以在名为 customers 的表中名为 custinfo 的 JSON 列上的数组 $.zipcode 上创建多值索引 zips。在每种情况下,JSON 数组都被转换为 UNSIGNED 整数值的 SQL 数据类型数组。

  • 仅限 CREATE TABLE

    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)) )
        );
  • CREATE TABLE 加上 ALTER TABLE

    CREATE TABLE customers (
        id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        custinfo JSON
        );
    
    ALTER TABLE customers ADD INDEX zips( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );

  • CREATE TABLE 加上 CREATE INDEX

    CREATE TABLE customers (
        id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        custinfo JSON
        );
    
    CREATE INDEX zips ON customers ( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );

多值索引也可以定义为复合索引的一部分。此示例展示了一个复合索引,它包含两个单值部分(用于 id 和 modified 列)和一个多值部分(用于 custinfo 列)

CREATE TABLE customers (
    id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    custinfo JSON
    );

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

复合索引中只能使用一个多值键部分。多值键部分可以使用相对于键的其他部分的任何顺序。换句话说,刚刚展示的 ALTER TABLE 语句可以使用 comp(id, (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY), modified))(或任何其他顺序)并且仍然有效。

使用多值索引

当在 WHERE 子句中指定以下函数时,优化器使用多值索引来获取记录

我们可以通过使用以下 CREATE TABLE 和 INSERT 语句创建并填充 customers 表来演示这一点

mysql> CREATE TABLE customers (
    ->     id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    ->     modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    ->     custinfo JSON
    ->     );
Query OK, 0 rows affected (0.51 sec)

mysql> INSERT INTO customers VALUES
    ->     (NULL, NOW(), '{"user":"Jack","user_id":37,"zipcode":[94582,94536]}'),
    ->     (NULL, NOW(), '{"user":"Jill","user_id":22,"zipcode":[94568,94507,94582]}'),
    ->     (NULL, NOW(), '{"user":"Bob","user_id":31,"zipcode":[94477,94507]}'),
    ->     (NULL, NOW(), '{"user":"Mary","user_id":72,"zipcode":[94536]}'),
    ->     (NULL, NOW(), '{"user":"Ted","user_id":56,"zipcode":[94507,94582]}');
Query OK, 5 rows affected (0.07 sec)
Records: 5  Duplicates: 0  Warnings: 0

首先,我们对 customers 表执行三个查询,每个查询分别使用 MEMBER OF()、JSON_CONTAINS() 和 JSON_OVERLAPS(),每个查询的结果如下所示

mysql> SELECT * FROM customers
    ->     WHERE 94507 MEMBER OF(custinfo->'$.zipcode');
+----+---------------------+-------------------------------------------------------------------+
| id | modified            | custinfo                                                          |
+----+---------------------+-------------------------------------------------------------------+
|  2 | 2019-06-29 22:23:12 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
|  3 | 2019-06-29 22:23:12 | {"user": "Bob", "user_id": 31, "zipcode": [94477, 94507]}         |
|  5 | 2019-06-29 22:23:12 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
+----+---------------------+-------------------------------------------------------------------+
3 rows in set (0.00 sec)

mysql> SELECT * FROM customers
    ->     WHERE JSON_CONTAINS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+---------------------+-------------------------------------------------------------------+
| id | modified            | custinfo                                                          |
+----+---------------------+-------------------------------------------------------------------+
|  2 | 2019-06-29 22:23:12 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
|  5 | 2019-06-29 22:23:12 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
+----+---------------------+-------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM customers
    ->     WHERE JSON_OVERLAPS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+---------------------+-------------------------------------------------------------------+
| id | modified            | custinfo                                                          |
+----+---------------------+-------------------------------------------------------------------+
|  1 | 2019-06-29 22:23:12 | {"user": "Jack", "user_id": 37, "zipcode": [94582, 94536]}        |
|  2 | 2019-06-29 22:23:12 | {"user": "Jill", "user_id": 22, "zipcode": [94568, 94507, 94582]} |
|  3 | 2019-06-29 22:23:12 | {"user": "Bob", "user_id": 31, "zipcode": [94477, 94507]}         |
|  5 | 2019-06-29 22:23:12 | {"user": "Ted", "user_id": 56, "zipcode": [94507, 94582]}         |
+----+---------------------+-------------------------------------------------------------------+
4 rows in set (0.00 sec)

接下来,我们对前三个查询运行 EXPLAIN

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE 94507 MEMBER OF(custinfo->'$.zipcode');
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE JSON_CONTAINS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE JSON_OVERLAPS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

刚刚展示的三个查询都无法使用任何键。为了解决这个问题,我们可以在 JSON 列(custinfo)中的 zipcode 数组上添加一个多值索引,如下所示

mysql> ALTER TABLE customers
    ->     ADD INDEX zips( (CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)) );
Query OK, 0 rows affected (0.47 sec)
Records: 0  Duplicates: 0  Warnings: 0

当我们再次运行之前的 EXPLAIN 语句时,我们现在可以观察到查询可以使用(并且确实使用了)刚刚创建的索引 zips

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE 94507 MEMBER OF(custinfo->'$.zipcode');
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | ref  | zips          | zips | 9       | const |    1 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE JSON_CONTAINS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | range | zips          | zips | 9       | NULL |    6 |   100.00 | Using where |
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> EXPLAIN SELECT * FROM customers
    ->     WHERE JSON_OVERLAPS(custinfo->'$.zipcode', CAST('[94507,94582]' AS JSON));
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | customers | NULL       | range | zips          | zips | 9       | NULL |    6 |   100.00 | Using where |
+----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

多值索引可以定义为唯一键。如果定义为唯一键,则尝试插入多值索引中已经存在的值将返回重复键错误。如果重复值已经存在,则尝试添加唯一的多个值索引将失败,如下所示

mysql> ALTER TABLE customers DROP INDEX zips;
Query OK, 0 rows affected (0.55 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> ALTER TABLE customers
    ->     ADD UNIQUE INDEX zips((CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)));
ERROR 1062 (23000): Duplicate entry '[94507, ' for key 'customers.zips'
mysql> ALTER TABLE customers
    ->     ADD INDEX zips((CAST(custinfo->'$.zipcode' AS UNSIGNED ARRAY)));
Query OK, 0 rows affected (0.36 sec)
Records: 0  Duplicates: 0  Warnings: 0
多值索引的特征

多值索引还具有以下特征

  • 影响多值索引的 DML 操作的处理方式与影响普通索引的 DML 操作相同,唯一的区别是对于单个聚簇索引记录可能会有多个插入或更新。

  • 可为空性和多值索引

    • 如果多值键部分有一个空数组,则不会向索引添加任何条目,并且无法通过索引扫描访问数据记录。

    • 如果多值键部分生成返回 NULL 值,则会向多值索引添加一个包含 NULL 的条目。如果键部分定义为 NOT NULL,则会报告错误。

    • 如果类型化数组列设置为 NULL,则存储引擎存储一个包含指向数据记录的 NULL 的记录。

    • 在索引数组中不允许使用 JSON 空值。如果任何返回的值为 NULL,则将其视为 JSON 空值并报告 无效的 JSON 值 错误。

  • 由于多值索引是虚拟列上的虚拟索引,因此它们必须遵守与虚拟生成列上的二级索引相同的规则。

  • 不会为数组添加索引记录。

多值索引的限制

多值索引受以下限制

  • 每个多值索引只允许一个多值键部分。但是,CAST(... AS ... ARRAY) 表达式可以引用 JSON 文档中的多个数组,如下所示

    CAST(data->'$.arr[*][*]' AS UNSIGNED ARRAY)

    在这种情况下,与 JSON 表达式匹配的所有值都作为单个扁平数组存储在索引中。

  • 具有多值键部分的索引不支持排序,因此不能用作主键。出于同样的原因,不能使用 ASC 或 DESC 关键字定义多值索引。

  • 多值索引不能是覆盖索引。

  • 多值索引的每个记录的最大值数量由单个撤消日志页面上可以存储的数据量决定,即 65221 字节(64K 减去 315 字节的开销),这意味着键值的最大总长度也是 65221 字节。最大键数取决于各种因素,因此无法定义特定的限制。例如,测试表明多值索引允许每个记录有多达 1604 个整数键。当达到限制时,会报告类似于以下内容的错误:错误 3905 (HY000):多值索引“idx”的每个记录的最大值数量超过 1 个值。

  • 多值键部分中唯一允许的表达式类型是 JSON 表达式。该表达式不需要引用插入到索引列的 JSON 文档中的现有元素,但其本身必须在语法上有效。

  • 因为同一聚簇索引记录的索引记录分散在整个多值索引中,所以多值索引不支持范围扫描或仅索引扫描。

  • 外键规范中不允许使用多值索引。

  • 不能为多值索引定义索引前缀。

  • 不能在转换为 BINARY 的数据上定义多值索引(请参阅 CAST() 函数的说明)。

  • 不支持在线创建多值索引,这意味着该操作使用 ALGORITHM=COPY。请参阅 性能和空间要求。

  • 多值索引不支持以下两组字符集和排序规则以外的字符集和排序规则

    1. binary 字符集,使用默认的 binary 排序规则

    2. utf8mb4 字符集,使用默认的 utf8mb4_0900_as_cs 排序规则。

  • 与 InnoDB 表的列上的其他索引一样,不能使用 USING HASH 创建多值索引;尝试这样做会导致警告:此存储引擎不支持 HASH 索引算法,已改用存储引擎默认值。(USING BTREE 像往常一样受支持。)

空间索引

MyISAM、InnoDB、NDB 和 ARCHIVE 存储引擎支持空间列,例如 POINT 和 GEOMETRY。(第 13.4 节“空间数据类型”介绍了空间数据类型。)但是,对空间列索引的支持因引擎而异。空间列上的空间索引和非空间索引可根据以下规则使用。

空间列上的空间索引具有以下特征

  • 仅适用于 InnoDB 和 MyISAM 表。为其他存储引擎指定 SPATIAL INDEX 会导致错误。

  • 空间列上的索引 必须 是 SPATIAL 索引。因此,SPATIAL 关键字是可选的,但在创建空间列上的索引时是隐式的。

  • 仅适用于单个空间列。不能在多个空间列上创建空间索引。

  • 索引列必须是 NOT NULL。

  • 禁止使用列前缀长度。将索引每列的完整宽度。

  • 不允许用于主键或唯一索引。

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

  • 允许用于支持空间列的任何存储引擎,但 ARCHIVE 除外。

  • 列可以是 NULL,除非索引是主键。

  • 非 SPATIAL 索引的索引类型取决于存储引擎。目前,使用的是 B 树。

  • 仅允许 InnoDB、MyISAM 和 MEMORY 表的可以具有 NULL 值的列使用。

索引选项

在键部分列表之后,可以给出索引选项。 index_option 值可以是以下任意一项

  • KEY_BLOCK_SIZE [=] value

    对于 MyISAM 表,KEY_BLOCK_SIZE 可选地指定用于索引键块的大小(以字节为单位)。该值被视为提示;如果需要,可以使用其他大小。为单个索引定义指定的 KEY_BLOCK_SIZE 值会覆盖表级别的 KEY_BLOCK_SIZE 值。

    在索引级别,InnoDB 表不支持 KEY_BLOCK_SIZE。请参阅 第 15.1.20 节“CREATE TABLE 语句”。

  • index_type

    某些存储引擎允许您在创建索引时指定索引类型。例如

    CREATE TABLE lookup (id INT) ENGINE = MEMORY;
    CREATE INDEX id_index ON lookup (id) USING BTREE;

    表 15.1“每个存储引擎的索引类型” 显示了不同存储引擎支持的允许索引类型值。如果列出了多个索引类型,则在未给出索引类型说明符时,第一个是默认值。表中未列出的存储引擎不支持索引定义中的 index_type 子句。

    表 15.1 每个存储引擎的索引类型

    存储引擎 允许的索引类型
    InnoDB BTREE
    MyISAM BTREE
    MEMORY/HEAP HASH、BTREE
    NDB HASH、BTREE(请参阅正文中的注释)

    FULLTEXT INDEX 规范不能使用 index_type 子句。全文索引实现取决于存储引擎。空间索引实现为 R 树索引。

    如果指定的索引类型对给定存储引擎无效,但引擎可以使用其他索引类型而不影响查询结果,则引擎将使用可用的类型。解析器将 RTREE 识别为类型名称。这仅允许用于 SPATIAL 索引。

    NDB 存储引擎将 BTREE 索引实现为 T 树索引。

    注意

    对于 NDB 表列上的索引,USING 选项只能用于唯一索引或主键。 USING HASH 会阻止创建有序索引;否则,在 NDB 表上创建唯一索引或主键会自动导致创建有序索引和哈希索引,每个索引都索引相同的列集。

    对于包含 NDB 表的一列或多列 NULL 列的唯一索引,哈希索引只能用于查找字面值,这意味着 IS [NOT] NULL 条件需要对表进行全面扫描。一种解决方法是确保始终以包含有序索引的方式创建使用此类表上的一列或多列 NULL 列的唯一索引;也就是说,在创建索引时避免使用 USING HASH。

    如果指定的索引类型对给定存储引擎无效,但引擎可以使用其他索引类型而不影响查询结果,则引擎将使用可用的类型。解析器将 RTREE 识别为类型名称,但目前不能为任何存储引擎指定它。

    注意

    不建议在 ON tbl_name 子句之前使用 index_type 选项;预计在未来的 MySQL 版本中将删除对此位置中使用该选项的支持。如果在较早和较晚的位置都给出了 index_type 选项,则应用最终选项。

    TYPE type_name 被识别为 USING type_name 的同义词。但是,USING 是首选形式。

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

    表 15.2 InnoDB 存储引擎索引特征

    索引类 索引类型 存储 NULL 值 允许多个 NULL 值 IS NULL 扫描类型 IS NOT NULL 扫描类型
    主键 BTREE 否 否 不适用 不适用
    唯一 BTREE 是 是 索引 索引
    键 BTREE 是 是 索引 索引
    FULLTEXT 不适用 是 是 表 表
    SPATIAL 不适用 否 否 不适用 不适用

    表 15.3 MyISAM 存储引擎索引特征

    索引类 索引类型 存储 NULL 值 允许多个 NULL 值 IS NULL 扫描类型 IS NOT NULL 扫描类型
    主键 BTREE 否 否 不适用 不适用
    唯一 BTREE 是 是 索引 索引
    键 BTREE 是 是 索引 索引
    FULLTEXT 不适用 是 是 表 表
    SPATIAL 不适用 否 否 不适用 不适用

    表 15.4 MEMORY 存储引擎索引特征

    索引类 索引类型 存储 NULL 值 允许多个 NULL 值 IS NULL 扫描类型 IS NOT NULL 扫描类型
    主键 BTREE 否 否 不适用 不适用
    唯一 BTREE 是 是 索引 索引
    键 BTREE 是 是 索引 索引
    主键 HASH 否 否 不适用 不适用
    唯一 HASH 是 是 索引 索引
    键 HASH 是 是 索引 索引

    表 15.5 NDB 存储引擎索引特征

    索引类 索引类型 存储 NULL 值 允许多个 NULL 值 IS NULL 扫描类型 IS NOT NULL 扫描类型
    主键 BTREE 否 否 索引 索引
    唯一 BTREE 是 是 索引 索引
    键 BTREE 是 是 索引 索引
    主键 HASH 否 否 表(请参阅注释 1) 表(请参阅注释 1)
    唯一 HASH 是 是 表(请参阅注释 1) 表(请参阅注释 1)
    键 HASH 是 是 表(请参阅注释 1) 表(请参阅注释 1)

    表注释

    1. USING HASH 会阻止创建隐式有序索引。

  • WITH PARSER parser_name

    此选项只能与 FULLTEXT 索引一起使用。如果全文索引和搜索操作需要特殊处理,它会将解析器插件与索引关联。 InnoDB 和 MyISAM 支持全文解析器插件。如果您有一个与全文解析器插件关联的 MyISAM 表,则可以使用 ALTER TABLE 将该表转换为 InnoDB。有关更多信息,请参阅 全文解析器插件 和 编写全文解析器插件。

  • COMMENT 'string'

    索引定义可以包含最多 1024 个字符的可选注释。

    可以使用 CREATE INDEX 语句的 index_option COMMENT 子句为单个索引配置索引页的 MERGE_THRESHOLD。例如

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

    如果在删除行或通过更新操作缩短行时,索引页的页满百分比低于 MERGE_THRESHOLD 值,则 InnoDB 会尝试将该索引页与其相邻的索引页合并。默认的 MERGE_THRESHOLD 值为 50,这是以前硬编码的值。

    还可以使用 CREATE TABLE 和 ALTER TABLE 语句在索引级别和表级别定义 MERGE_THRESHOLD。有关更多信息,请参阅 第 17.8.11 节“配置索引页的合并阈值”。

  • VISIBLE、INVISIBLE

    指定索引可见性。索引默认可见。优化器不使用不可见索引。索引可见性的规范适用于除主键(显式或隐式)之外的索引。有关更多信息,请参见 第 10.3.12 节“不可见索引”。

  • ENGINE_ATTRIBUTE 和 SECONDARY_ENGINE_ATTRIBUTE 用于为主要和次要存储引擎指定索引属性。这些选项保留供将来使用。

    分配给此选项的值是一个字符串字面量,其中包含有效的 JSON 文档或空字符串 ('')。无效的 JSON 将被拒绝。

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

    ENGINE_ATTRIBUTE 和 SECONDARY_ENGINE_ATTRIBUTE 值可以重复,不会出错。在这种情况下,将使用最后指定的值。

    服务器不会检查 ENGINE_ATTRIBUTE 和 SECONDARY_ENGINE_ATTRIBUTE 值,并且在更改表的存储引擎时也不会清除它们。

表复制和锁定选项

可以提供 ALGORITHM 和 LOCK 子句来影响表复制方法以及在修改表索引时读取和写入表的并发级别。它们的含义与 ALTER TABLE 语句相同。有关更多信息,请参见 第 15.1.9 节“ALTER TABLE 语句”

NDB Cluster 支持使用与标准 MySQL Server 相同的 ALGORITHM=INPLACE 语法进行在线操作。有关更多信息,请参见 第 25.6.12 节“使用 ALTER TABLE 在 NDB Cluster 中进行在线操作”。