From 52b140a35b889d3b8491632cc43097f7708c1067 Mon Sep 17 00:00:00 2001 From: tangenta Date: Tue, 23 Mar 2021 20:01:37 +0800 Subject: [PATCH] cherry pick #5771 to release-5.0 Signed-off-by: ti-srebot --- clustered-indexes.md | 295 ++++++++++++++++++++++++------------------- 1 file changed, 162 insertions(+), 133 deletions(-) diff --git a/clustered-indexes.md b/clustered-indexes.md index a524befd0170..1a5b3a4fc711 100644 --- a/clustered-indexes.md +++ b/clustered-indexes.md @@ -1,190 +1,219 @@ --- title: 聚簇索引 -summary: 了解 TiDB 中的聚簇索引。 +summary: 本文档介绍了聚簇索引的概念、使用场景、使用方法、限制和兼容性。 --- # 聚簇索引 -聚簇索引是 TiDB 在 5.0.0-rc 版本中引入的实验性特性。本文档通过多个示例来说明该特性对 TiDB 查询性能的影响。如需启用此特性及查看详细操作指南,参见 [`tidb_enable_clustered_index` 系统变量介绍](/system-variables.md#tidb_enable_clustered_index-从-v500-rc-版本开始引入)。 +聚簇索引 (clustered index) 是 TiDB 从 v5.0 开始支持的特性,用于控制含有主键的表数据的存储方式。通过使用聚簇索引,TiDB 可以更好地组织数据表,从而提高某些查询的性能。有些数据库管理系统也将聚簇索引称为“索引组织表” (index-organized tables)。 -通过使用聚簇索引,TiDB 可以更好地组织数据表,从而提高某些查询的性能。有些数据库管理系统也将聚簇索引称为“索引组织表” (index-organized tables)。 +目前 TiDB 中含有主键的表分为以下两类: -TiDB 仅支持根据表的`主键`来进行聚簇操作。聚簇索引启用时,`主键`和`聚簇索引`两个术语在一些情况下可互换使用。`主键`指的是约束(一种逻辑属性),而`聚簇索引`描述的是数据存储的物理实现。 +- `NONCLUSTERED`,表示该表的主键为非聚簇索引。在非聚簇索引表中,行数据的键由 TiDB 内部隐式分配的 `_tidb_rowid` 构成,而主键本质上是唯一索引,因此非聚簇索引表存储一行至少需要两个键值对,分别为 + - `_tidb_rowid`(键)- 行数据(值) + - 主键列数据(键) - `_tidb_rowid`(值) +- `CLUSTERED`,表示该表的主键为聚簇索引。在聚簇索引表中,行数据的键由用户给定的主键列数据构成,不需要用唯一索引模拟,因此聚簇索引表存储一行至少只要一个键值对,即 + - 主键列数据(键) - 行数据(值) -## TiDB v5.0 前支持部分主键作为聚簇索引 +> **注意:** +> +> TiDB 仅支持根据表的主键来进行聚簇操作。聚簇索引启用时,“主键”和“聚簇索引”两个术语在一些情况下可互换使用。主键指的是约束(一种逻辑属性),而聚簇索引描述的是数据存储的物理实现。 -在 v5.0 之前,TiDB 对聚簇索引的支持有限,需要同时满足以下条件才能启用: +## 使用场景 -- 数据表设置了主键 -- 主键的数据类型为 `INTEGER` 或 `BIGINT` -- 主键只有一列 +相较于非聚簇索引表,聚簇索引表在以下几个场景中,性能和吞吐量都有较大优势: + +- 插入数据时会减少一次从网络写入索引数据。 +- 等值条件查询仅涉及主键时会减少一次从网络读取数据。 +- 范围条件查询仅涉及主键时会减少多次从网络读取数据。 +- 等值或范围条件查询仅涉及主键的前缀时会减少多次从网络读取数据。 + +另一方面,聚簇索引表也存在一定的劣势: + +- 批量插入大量取值相邻的主键时,可能会产生较大的写热点问题。 +- 当使用大于 64 位的数据类型作为主键时,可能导致表本身需要占用更多的存储空间。该现象在存在多个二级索引时尤为明显。 + +## 使用方法 -当其中任一条件不满足时,TiDB 会创建一个隐藏的 64 位 `handle` 值,以组织该数据表。与非聚簇索引相比,使用聚簇索引一步就能完成表查询,效率更高。下面的例子对比了两张数据表的 `EXPLAIN` 语句输出结果,其中一张表支持使用聚簇索引,而另一张不支持: +### 创建聚簇索引表 + +从 TiDB 版本 5.0 开始,要指定一个表的主键是否使用聚簇索引,可以在 `CREATE TABLE` 语句中将 `CLUSTERED` 或者 `NONCLUSTERED` 非保留关键字标注在 `PRIMARY KEY` 后面,例如: ```sql -CREATE TABLE always_clusters_in_all_versions ( - id BIGINT NOT NULL PRIMARY KEY auto_increment, - b CHAR(100), - INDEX(b) -); - -CREATE TABLE does_not_cluster_by_default ( - guid CHAR(32) NOT NULL PRIMARY KEY, - b CHAR(100), - INDEX(b) -); - -INSERT INTO always_clusters_in_all_versions VALUES (1, 'aaa'), (2, 'bbb'); -INSERT INTO does_not_cluster_by_default VALUES ('02dd050a978756da0aff6b1d1d7c8aef', 'aaa'), ('35bfbc09cb3c93d8ef032642521ac042', 'bbb'); - -EXPLAIN SELECT * FROM always_clusters_in_all_versions WHERE id = 1; -EXPLAIN SELECT * FROM does_not_cluster_by_default WHERE guid = '02dd050a978756da0aff6b1d1d7c8aef'; +CREATE TABLE t (a BIGINT PRIMARY KEY CLUSTERED, b VARCHAR(255)); +CREATE TABLE t (a BIGINT PRIMARY KEY NONCLUSTERED, b VARCHAR(255)); +CREATE TABLE t (a BIGINT KEY CLUSTERED, b VARCHAR(255)); +CREATE TABLE t (a BIGINT KEY NONCLUSTERED, b VARCHAR(255)); +CREATE TABLE t (a BIGINT, b VARCHAR(255), PRIMARY KEY(a, b) CLUSTERED); +CREATE TABLE t (a BIGINT, b VARCHAR(255), PRIMARY KEY(a, b) NONCLUSTERED); ``` -```sql -Query OK, 0 rows affected (0.09 sec) +注意,列定义中的 `KEY` 和 `PRIMARY KEY` 含义相同。 -Query OK, 0 rows affected (0.10 sec) +此外,TiDB 支持[可执行的注释语法](/comment-syntax.md): -Records: 2 Duplicates: 0 Warnings: 0 +```sql +CREATE TABLE t (a BIGINT PRIMARY KEY /*T![clustered_index] CLUSTERED */, b VARCHAR(255)); +CREATE TABLE t (a BIGINT PRIMARY KEY /*T![clustered_index] NONCLUSTERED */, b VARCHAR(255)); +CREATE TABLE t (a BIGINT, b VARCHAR(255), PRIMARY KEY(a, b) /*T![clustered_index] CLUSTERED */,); +CREATE TABLE t (a BIGINT, b VARCHAR(255), PRIMARY KEY(a, b) /*T![clustered_index] NONCLUSTERED */); +``` -Records: 2 Duplicates: 0 Warnings: 0 +### 添加、删除聚簇索引 -+-------------+---------+------+---------------------------------------+---------------+ -| id | estRows | task | access object | operator info | -+-------------+---------+------+---------------------------------------+---------------+ -| Point_Get_1 | 1.00 | root | table:always_clusters_in_all_versions | handle:1 | -+-------------+---------+------+---------------------------------------+---------------+ -1 row in set (0.00 sec) +目前 TiDB 不支持在建表之后添加或删除聚簇索引,也不支持聚簇索引和非聚簇索引的互相转换。例如: -+-------------+---------+------+--------------------------------------------------------+---------------+ -| id | estRows | task | access object | operator info | -+-------------+---------+------+--------------------------------------------------------+---------------+ -| Point_Get_1 | 1.00 | root | table:does_not_cluster_by_default, index:PRIMARY(guid) | | -+-------------+---------+------+--------------------------------------------------------+---------------+ -1 row in set (0.00 sec) +```sql +ALTER TABLE t ADD PRIMARY KEY(b, a) CLUSTERED; -- 暂不支持 +ALTER TABLE t DROP PRIMARY KEY; -- 如果主键为聚簇索引,则不支持 +ALTER TABLE t DROP INDEX `PRIMARY`; -- 如果主键为聚簇索引,则不支持 ``` -以上两个 `EXPLAIN` 语句输出结果类似,但在第二个例子中,TiDB 需要首先读取 `guid` 列上的主键索引,才能获得 `handle` 的值。 +### 添加、删除非聚簇索引 -而在下面的例子中,由于 `does_not_cluster_by_default.b` 这列并不是主键,查询效率差异体现得更为明显。TiDB 必须进行额外的扫表操作 (`└─TableFullScan_5`) 才能将 `handle` 的值转变为 `guid` 的主键值。示例如下: +TiDB 支持在建表之后添加或删除非聚簇索引。此时可以选择显式指定 `NONCLUSTERED` 关键字或省略关键字: ```sql -EXPLAIN SELECT id FROM always_clusters_in_all_versions WHERE b = 'aaaa'; -EXPLAIN SELECT guid FROM does_not_cluster_by_default WHERE b = 'aaaa'; +ALTER TABLE t ADD PRIMARY KEY(b, a) NONCLUSTERED; +ALTER TABLE t ADD PRIMARY KEY(b, a); -- 不指定关键字,则为非聚簇索引 +ALTER TABLE t DROP PRIMARY KEY; +ALTER TABLE t DROP INDEX `PRIMARY`; ``` +### 查询主键是否为聚簇索引 + +可通过以下方式来确定一张表的主键是否使用了聚簇索引: + +- `SHOW CREATE TABLE` +- `SHOW INDEX FROM` +- `information_schema.tables` + +通过 `SHOW CREATE TABLE` 查看,`PRIMARY KEY` 的属性可能为 `CLUSTERED` 或 `NONCLUSTERED`: + ```sql -+--------------------------+---------+-----------+---------------------------------------------------+-------------------------------------------------------+ -| id | estRows | task | access object | operator info | -+--------------------------+---------+-----------+---------------------------------------------------+-------------------------------------------------------+ -| Projection_4 | 0.00 | root | | test.always_clusters_in_all_versions.id | -| └─IndexReader_6 | 0.00 | root | | index:IndexRangeScan_5 | -| └─IndexRangeScan_5 | 0.00 | cop[tikv] | table:always_clusters_in_all_versions, index:b(b) | range:["aaaa","aaaa"], keep order:false, stats:pseudo | -+--------------------------+---------+-----------+---------------------------------------------------+-------------------------------------------------------+ -3 rows in set (0.01 sec) - -+---------------------------+---------+-----------+-----------------------------------+------------------------------------------------+ -| id | estRows | task | access object | operator info | -+---------------------------+---------+-----------+-----------------------------------+------------------------------------------------+ -| Projection_4 | 0.00 | root | | test.does_not_cluster_by_default.guid | -| └─TableReader_7 | 0.00 | root | | data:Selection_6 | -| └─Selection_6 | 0.00 | cop[tikv] | | eq(test.does_not_cluster_by_default.b, "aaaa") | -| └─TableFullScan_5 | 2.00 | cop[tikv] | table:does_not_cluster_by_default | keep order:false, stats:pseudo | -+---------------------------+---------+-----------+-----------------------------------+------------------------------------------------+ -4 rows in set (0.00 sec) +mysql> SHOW CREATE TABLE t; ++-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Table | Create Table | ++-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| t | CREATE TABLE `t` ( + `a` bigint(20) NOT NULL, + `b` varchar(255) DEFAULT NULL, + PRIMARY KEY (`a`) /*T![clustered_index] CLUSTERED */ +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin | ++-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +1 row in set (0.01 sec) ``` -## TiDB v5.0 起支持任意主键作为聚簇索引 - -从 v5.0 开始,TiDB 全面支持使用任意主键作为聚簇索引。下方示例沿用了上一节的数据表例子,但开启了聚簇索引特性,并列出相应的 `EXPLAIN` 语句输出结果: +通过 `SHOW INDEX FROM` 查看,`Clustered` 一列可能的结果为 `Yes` 或 `No`: ```sql -SET tidb_enable_clustered_index = 1; -CREATE TABLE will_now_cluster ( - guid CHAR(32) NOT NULL PRIMARY KEY, - b CHAR(100), - INDEX(b) -); - -INSERT INTO will_now_cluster VALUES (1, 'aaa'), (2, 'bbb'); -INSERT INTO will_now_cluster VALUES ('02dd050a978756da0aff6b1d1d7c8aef', 'aaa'), ('35bfbc09cb3c93d8ef032642521ac042', 'bbb'); - -EXPLAIN SELECT * FROM will_now_cluster WHERE guid = '02dd050a978756da0aff6b1d1d7c8aef'; -EXPLAIN SELECT guid FROM will_now_cluster WHERE b = 'aaaa'; +mysql> SHOW INDEX FROM t; ++-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+-----------+ +| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | Clustered | ++-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+-----------+ +| t | 0 | PRIMARY | 1 | a | A | 0 | NULL | NULL | | BTREE | | | YES | NULL | YES | ++-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+-----------+ +1 row in set (0.01 sec) ``` +查询 `information_schema.tables` 系统表中的 `TIDB_PK_TYPE` 列,可能的结果为 `CLUSTERED` 或 `NONCLUSTERED`: + ```sql -Query OK, 0 rows affected (0.00 sec) +mysql> SELECT TIDB_PK_TYPE FROM information_schema.tables WHERE table_schema = 'test' AND table_name = 't'; ++--------------+ +| TIDB_PK_TYPE | ++--------------+ +| CLUSTERED | ++--------------+ +1 row in set (0.03 sec) +``` -Query OK, 0 rows affected (0.11 sec) +## 限制 -Query OK, 2 rows affected (0.02 sec) -Records: 2 Duplicates: 0 Warnings: 0 +目前 TiDB 的聚簇索引具有以下两类限制: -Query OK, 2 rows affected (0.01 sec) -Records: 2 Duplicates: 0 Warnings: 0 +- 明确不支持且没有支持计划的使用限制: + - 不支持与 TiDB Binlog 一起使用。开启 TiDB Binlog 后 TiDB 不允许创建非单个整数列作为主键的聚簇索引;已创建的聚簇索引表的数据插入、删除和更新动作不会通过 TiDB Binlog 同步到下游。如需同步聚簇索引表,请使用 [TiCDC](/ticdc/ticdc-overview.md)。 + - 不支持与 [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md) 一起使用;[`PRE_SPLIT_REGIONS`](/sql-statements/sql-statement-split-region.md#pre_split_regions) 在聚簇索引表上不生效。 + - 不支持对聚簇索引表进行降级。如需降级,请使用逻辑备份工具迁移数据。 +- 另一类是尚未支持,但未来有计划支持的使用限制: + - 尚未支持通过 `ALTER TABLE` 语句增加、删除、修改聚簇索引。 -+-------------+---------+------+-------------------------------------------------------+---------------+ -| id | estRows | task | access object | operator info | -+-------------+---------+------+-------------------------------------------------------+---------------+ -| Point_Get_1 | 1.00 | root | table:will_now_cluster, clustered index:PRIMARY(guid) | | -+-------------+---------+------+-------------------------------------------------------+---------------+ -1 row in set (0.00 sec) +开启 TiDB Binlog 之后,创建非单个整数列作为主键的聚簇索引会报以下错误: -+--------------------------+---------+-----------+------------------------------------+-------------------------------------------------------+ -| id | estRows | task | access object | operator info | -+--------------------------+---------+-----------+------------------------------------+-------------------------------------------------------+ -| Projection_4 | 10.00 | root | | test.will_now_cluster.guid | -| └─IndexReader_6 | 10.00 | root | | index:IndexRangeScan_5 | -| └─IndexRangeScan_5 | 10.00 | cop[tikv] | table:will_now_cluster, index:b(b) | range:["aaaa","aaaa"], keep order:false, stats:pseudo | -+--------------------------+---------+-----------+------------------------------------+-------------------------------------------------------+ -3 rows in set (0.00 sec) +```sql +mysql> CREATE TABLE t (a VARCHAR(255) PRIMARY KEY CLUSTERED); +ERROR 8200 (HY000): Cannot create clustered index table when the binlog is ON ``` -TiDB 同样支持用复合主键进行聚簇操作: +与 `SHARD_ROW_ID_BITS` 一起使用时会报以下错误: ```sql -SET tidb_enable_clustered_index = 1; -CREATE TABLE composite_primary_key ( - key_a INT NOT NULL, - key_b INT NOT NULL, - b CHAR(100), - PRIMARY KEY (key_a, key_b) -); - -INSERT INTO composite_primary_key VALUES (1, 1, 'aaa'), (2, 2, 'bbb'); -EXPLAIN SELECT * FROM composite_primary_key WHERE key_a = 1 AND key_b = 2; +mysql> CREATE TABLE t (a VARCHAR(255) PRIMARY KEY CLUSTERED) SHARD_ROW_ID_BITS = 3; +ERROR 8200 (HY000): Unsupported shard_row_id_bits for table with primary key as row id ``` -```sql -Query OK, 0 rows affected (0.00 sec) +## 兼容性 -Query OK, 0 rows affected (0.09 sec) +### 升降级兼容性 -Query OK, 2 rows affected (0.02 sec) -Records: 2 Duplicates: 0 Warnings: 0 +TiDB 支持对聚簇索引表的升级兼容,但不支持降级兼容,即高版本 TiDB 聚簇索引表的数据在低版本 TiDB 上不可用。 -+-------------+---------+------+--------------------------------------------------------------------+---------------+ -| id | estRows | task | access object | operator info | -+-------------+---------+------+--------------------------------------------------------------------+---------------+ -| Point_Get_1 | 1.00 | root | table:composite_primary_key, clustered index:PRIMARY(key_a, key_b) | | -+-------------+---------+------+--------------------------------------------------------------------+---------------+ -1 row in set (0.00 sec) -``` +聚簇索引在 TiDB v3.0 和 v4.0 中已完成部分支持,当表中存在单个整数列作为主键时默认启用,即: + +- 表设置了主键 +- 主键只有一列 +- 主键的数据类型为整数类型 + +从 TiDB v5.0 开始,不论是单整数列主键还是其他类型主键,默认均为非聚簇索引。该行为变更可能导致默认配置下的 TiDB 在某些场景中出现性能回退,此时可以考虑显式启用聚簇索引。 + +### MySQL 兼容性 + +TiDB 支持使用可执行注释的语法来包裹 `CLUSTERED` 或 `NONCLUSTERED` 关键字,且 `SHOW CREATE TABLE` 的结果均包含 TiDB 特有的可执行注释,因此这部分 DDL 语句能被 MySQL 或低版本的 TiDB 识别并执行。 + +### TiDB 生态工具兼容性 -在 MySQL 中,InnoDB 存储引擎默认会使用任意主键作为聚簇索引,此处行为与之一致。 +聚簇索引仅与 v5.0 及以后版本的以下生态工具兼容: -## 存储需求 +- 备份与恢复工具 BR、Dumpling、TiDB Lightning。 +- 数据迁移和同步工具 DM、TiCDC。 -启用聚簇索引后,主键替代 64 位的 `handle` 值成为表中每行数据的内部指针,所以对存储空间的需求可能会上升,尤其当表中包含很多二级索引时。以下表为例: +v5.0 的 BR 不能通过备份恢复将非聚簇索引表转换成聚簇索引表,反之亦然。 + +### 与 TiDB 其他特性的兼容性 + +在非单整数列作为主键的表中,从非聚簇索引变为聚簇索引之后,在 v5.0 之前版本的 TiDB 能够执行的 `SPLIT TABLE BY/BETWEEN` 语句在 v5.0 及以后版本的 TiDB 上不再可用,原因是行数据键的构成发生了变化。在聚簇索引表上执行 `SPLIT TABLE BY/BETWEEN` 时需要依据主键列指定值,而不是指定一个整数值。例如: ```sql -CREATE TABLE t1 ( - guid CHAR(32) NOT NULL PRIMARY KEY, - b BIGINT, - INDEX(b) -); +mysql> create table t (a int, b varchar(255), primary key(a, b) clustered); +Query OK, 0 rows affected (0.01 sec) + +mysql> split table t between (0) and (1000000) regions 5; +ERROR 1105 (HY000): Split table region lower value count should be 2 + +mysql> split table t by (0), (50000), (100000); +ERROR 1136 (21S01): Column count doesn't match value count at row 0 + +mysql> split table t between (0, 'aaa') and (1000000, 'zzz') regions 5; ++--------------------+----------------------+ +| TOTAL_SPLIT_REGION | SCATTER_FINISH_RATIO | ++--------------------+----------------------+ +| 4 | 1 | ++--------------------+----------------------+ +1 row in set (0.00 sec) + +mysql> split table t by (0, ''), (50000, ''), (100000, ''); ++--------------------+----------------------+ +| TOTAL_SPLIT_REGION | SCATTER_FINISH_RATIO | ++--------------------+----------------------+ +| 3 | 1 | ++--------------------+----------------------+ +1 row in set (0.01 sec) ``` -因为 `guid` 的指针的数据类型为 `char(32)`,所以 `b` 列的每一个索引都大约需要 `8 + 32 = 40` 个字节的存储空间(一个数据类型为 `BIGINT` 的数据需要 8 个字节来存储)。而在非聚簇索引的数据表中,只需要 `8 + 8 = 16` 个字节。不过,具体的存储需求在数据经过压缩后可能会有所差异。 +[`AUTO_RANDOM`](/auto-random.md) 属性只能在聚簇索引表上使用。在非聚簇索引上使用 `AUTO_RANDOM` 会报以下错误: + +```sql +mysql> create table t (a bigint primary key nonclustered auto_random); +ERROR 8216 (HY000): Invalid auto random: column a is not the integer primary key, or the primary key is nonclustered +```