From 67d72597cb7bde8eb3085aca889836698a0f7c43 Mon Sep 17 00:00:00 2001 From: tangenta Date: Wed, 18 Mar 2026 11:51:19 +0800 Subject: [PATCH 01/10] add _tidb_rowid document --- TOC-tidb-cloud-essential.md | 1 + TOC-tidb-cloud-premium.md | 1 + TOC-tidb-cloud-starter.md | 1 + TOC-tidb-cloud.md | 1 + TOC.md | 1 + clustered-indexes.md | 2 +- shard-row-id-bits.md | 10 +- .../sql-statement-show-table-next-rowid.md | 3 +- tidb-rowid.md | 173 ++++++++++++++++++ 9 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 tidb-rowid.md diff --git a/TOC-tidb-cloud-essential.md b/TOC-tidb-cloud-essential.md index d53eb7ec44d85..cb8e4f7451978 100644 --- a/TOC-tidb-cloud-essential.md +++ b/TOC-tidb-cloud-essential.md @@ -203,6 +203,7 @@ - Attributes - [AUTO_INCREMENT](/auto-increment.md) - [AUTO_RANDOM](/auto-random.md) + - [_tidb_rowid](/tidb-rowid.md) - [SHARD_ROW_ID_BITS](/shard-row-id-bits.md) - [Literal Values](/literal-values.md) - [Schema Object Names](/schema-object-names.md) diff --git a/TOC-tidb-cloud-premium.md b/TOC-tidb-cloud-premium.md index 4710c30e36c8f..a1080a1bb844d 100644 --- a/TOC-tidb-cloud-premium.md +++ b/TOC-tidb-cloud-premium.md @@ -280,6 +280,7 @@ - Attributes - [AUTO_INCREMENT](/auto-increment.md) - [AUTO_RANDOM](/auto-random.md) + - [_tidb_rowid](/tidb-rowid.md) - [SHARD_ROW_ID_BITS](/shard-row-id-bits.md) - [Literal Values](/literal-values.md) - [Schema Object Names](/schema-object-names.md) diff --git a/TOC-tidb-cloud-starter.md b/TOC-tidb-cloud-starter.md index 9b0c3194466bc..67172ccf892f3 100644 --- a/TOC-tidb-cloud-starter.md +++ b/TOC-tidb-cloud-starter.md @@ -208,6 +208,7 @@ - Attributes - [AUTO_INCREMENT](/auto-increment.md) - [AUTO_RANDOM](/auto-random.md) + - [_tidb_rowid](/tidb-rowid.md) - [SHARD_ROW_ID_BITS](/shard-row-id-bits.md) - [Literal Values](/literal-values.md) - [Schema Object Names](/schema-object-names.md) diff --git a/TOC-tidb-cloud.md b/TOC-tidb-cloud.md index 2c0959c954a6b..7b97ca11c7a6c 100644 --- a/TOC-tidb-cloud.md +++ b/TOC-tidb-cloud.md @@ -268,6 +268,7 @@ - Attributes - [AUTO_INCREMENT](/auto-increment.md) - [AUTO_RANDOM](/auto-random.md) + - [_tidb_rowid](/tidb-rowid.md) - [SHARD_ROW_ID_BITS](/shard-row-id-bits.md) - [Literal Values](/literal-values.md) - [Schema Object Names](/schema-object-names.md) diff --git a/TOC.md b/TOC.md index f880953edda66..dfa48c8fb4173 100644 --- a/TOC.md +++ b/TOC.md @@ -636,6 +636,7 @@ - Attributes - [AUTO_INCREMENT](/auto-increment.md) - [AUTO_RANDOM](/auto-random.md) + - [_tidb_rowid](/tidb-rowid.md) - [SHARD_ROW_ID_BITS](/shard-row-id-bits.md) - [Literal Values](/literal-values.md) - [Schema Object Names](/schema-object-names.md) diff --git a/clustered-indexes.md b/clustered-indexes.md index 7b35047e245c8..8e474debdd345 100644 --- a/clustered-indexes.md +++ b/clustered-indexes.md @@ -11,7 +11,7 @@ The term _clustered_ in this context refers to the _organization of how data is Currently, tables containing primary keys in TiDB are divided into the following two categories: -- `NONCLUSTERED`: The primary key of the table is non-clustered index. In tables with non-clustered indexes, the keys for row data consist of internal `_tidb_rowid` implicitly assigned by TiDB. Because primary keys are essentially unique indexes, tables with non-clustered indexes need at least two key-value pairs to store a row, which are: +- `NONCLUSTERED`: The primary key of the table is non-clustered index. In tables with non-clustered indexes, the keys for row data consist of internal [`_tidb_rowid`](/tidb-rowid.md) values implicitly assigned by TiDB. Because primary keys are essentially unique indexes, tables with non-clustered indexes need at least two key-value pairs to store a row, which are: - `_tidb_rowid` (key) - row data (value) - Primary key data (key) - `_tidb_rowid` (value) - `CLUSTERED`: The primary key of the table is clustered index. In tables with clustered indexes, the keys for row data consist of primary key data given by the user. Therefore, tables with clustered indexes need only one key-value pair to store a row, which is: diff --git a/shard-row-id-bits.md b/shard-row-id-bits.md index 38073c00b5da2..7a5cf64ffcce8 100644 --- a/shard-row-id-bits.md +++ b/shard-row-id-bits.md @@ -5,11 +5,11 @@ summary: Learn the SHARD_ROW_ID_BITS attribute. # SHARD_ROW_ID_BITS -This document introduces the `SHARD_ROW_ID_BITS` table attribute, which is used to set the number of bits of the shards after the implicit `_tidb_rowid` is sharded. +This document introduces the `SHARD_ROW_ID_BITS` table attribute, which is used to set the number of bits of the shards after the implicit [`_tidb_rowid`](/tidb-rowid.md) is sharded. ## Concept -For the tables with a non-clustered primary key or no primary key, TiDB uses an implicit auto-increment row ID. When a large number of `INSERT` operations are performed, the data is written into a single Region, causing a write hot spot. +For tables with a non-clustered primary key or no primary key, TiDB uses the hidden [`_tidb_rowid`](/tidb-rowid.md) as an implicit auto-increment row ID. When a large number of `INSERT` operations are performed, the data is written into a single Region, causing a write hot spot. To mitigate the hot spot issue, you can configure `SHARD_ROW_ID_BITS`. The row IDs are scattered and the data are written into multiple different Regions. @@ -23,9 +23,13 @@ When you set `SHARD_ROW_ID_BITS = S`, the structure of `_tidb_rowid` is as follo |--------|--------|--------------| | 1 bit | `S` bits | `63-S` bits | -- The values of the auto-increment bits are stored in TiKV and allocated sequentially. Each time a value is allocated, the next value is incremented by 1. The auto-increment bits ensure that the column values of `_tidb_rowid` are unique globally. When the value of the auto-increment bits is exhausted (that is, when the maximum value is reached), subsequent automatic allocations fail with the error `Failed to read auto-increment value from storage engine`. +- The values of the auto-increment bits are stored in TiKV and allocated sequentially. Each time a value is allocated, the next value is incremented by 1. When the value of the auto-increment bits is exhausted (that is, when the maximum value is reached), subsequent automatic allocations fail with the error `Failed to read auto-increment value from storage engine`. - The value range of `_tidb_rowid`: the maximum number of bits for the final generated value = shard bits + auto-increment bits, so the maximum value is `(2^63)-1`. +> **Warning:** +> +> `_tidb_rowid` is an internal row handle. Do not assume it is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. For details, see [`_tidb_rowid`](/tidb-rowid.md). + > **Note:** > > Selection of shard bits (`S`): diff --git a/sql-statements/sql-statement-show-table-next-rowid.md b/sql-statements/sql-statement-show-table-next-rowid.md index d145df7b8ecd6..821f8f0bd7a68 100644 --- a/sql-statements/sql-statement-show-table-next-rowid.md +++ b/sql-statements/sql-statement-show-table-next-rowid.md @@ -7,7 +7,7 @@ summary: Learn the usage of `SHOW TABLE NEXT_ROW_ID` in TiDB. `SHOW TABLE NEXT_ROW_ID` is used to show the details of some special columns of a table, including: -* [`AUTO_INCREMENT`](/auto-increment.md) column automatically created by TiDB, namely, `_tidb_rowid` column. +* [`_tidb_rowid`](/tidb-rowid.md), the hidden row-handle column automatically managed by TiDB for supported tables. * `AUTO_INCREMENT` column created by users. * [`AUTO_RANDOM`](/auto-random.md) column created by users. * [`SEQUENCE`](/sql-statements/sql-statement-create-sequence.md) created by users. @@ -65,3 +65,4 @@ This statement is a TiDB extension to MySQL syntax. * [CREATE TABLE](/sql-statements/sql-statement-create-table.md) * [AUTO_RANDOM](/auto-random.md) * [CREATE_SEQUENCE](/sql-statements/sql-statement-create-sequence.md) +* [_tidb_rowid](/tidb-rowid.md) diff --git a/tidb-rowid.md b/tidb-rowid.md new file mode 100644 index 0000000000000..3f033cc66e184 --- /dev/null +++ b/tidb-rowid.md @@ -0,0 +1,173 @@ +--- +title: _tidb_rowid +summary: Learn what `_tidb_rowid` is, when TiDB exposes it, and how to use it safely. +--- + +# _tidb_rowid + +`_tidb_rowid` is a hidden system column that TiDB uses as the row handle for tables that do not use a clustered index. You do not declare this column in the table schema, but you can reference it in SQL when the table uses `_tidb_rowid` as its handle. + +In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handle column managed by TiDB. + +> **Warning:** +> +> Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. If you need a stable unique identifier, define and use an explicit primary key instead of relying on `_tidb_rowid`. + +## When `_tidb_rowid` is available + +`_tidb_rowid` is available for tables whose row handle is not a clustered primary key. In practice, this means the following table types use `_tidb_rowid`: + +- Tables without a primary key +- Tables whose primary key is explicitly defined as `NONCLUSTERED` + +`_tidb_rowid` is not available for tables that use a clustered index, including the following: + +- Tables whose integer primary key is the clustered row handle +- Tables that use a common handle clustered primary key + +The following example shows the difference: + +{{< copyable "sql" >}} + +```sql +CREATE TABLE t1 (a INT, b VARCHAR(20)); +CREATE TABLE t2 (id BIGINT PRIMARY KEY NONCLUSTERED, a INT); +CREATE TABLE t3 (id BIGINT PRIMARY KEY CLUSTERED, a INT); +``` + +For `t1` and `t2`, you can query `_tidb_rowid`: + +{{< copyable "sql" >}} + +```sql +SELECT _tidb_rowid, a, b FROM t1; +SELECT _tidb_rowid, id, a FROM t2; +``` + +For `t3`, `_tidb_rowid` is unavailable because the clustered primary key is already the row handle: + +{{< copyable "sql" >}} + +```sql +SELECT _tidb_rowid, id, a FROM t3; +``` + +```sql +ERROR 1054 (42S22): Unknown column '_tidb_rowid' in 'field list' +``` + +## Read `_tidb_rowid` + +You can use `_tidb_rowid` in `SELECT` statements for supported tables. This is useful for tasks such as pagination, troubleshooting, and batch processing. + +Example: + +{{< copyable "sql" >}} + +```sql +CREATE TABLE t (a INT, b VARCHAR(20)); +INSERT INTO t VALUES (1, 'x'), (2, 'y'); + +SELECT _tidb_rowid, a, b FROM t ORDER BY _tidb_rowid; +``` + +```sql ++-------------+---+---+ +| _tidb_rowid | a | b | ++-------------+---+---+ +| 1 | 1 | x | +| 2 | 2 | y | ++-------------+---+---+ +``` + +To inspect the next value that TiDB will allocate for the row ID, use `SHOW TABLE ... NEXT_ROW_ID`: + +{{< copyable "sql" >}} + +```sql +SHOW TABLE t NEXT_ROW_ID; +``` + +```sql ++-----------------------+------------+-------------+--------------------+-------------+ +| DB_NAME | TABLE_NAME | COLUMN_NAME | NEXT_GLOBAL_ROW_ID | ID_TYPE | ++-----------------------+------------+-------------+--------------------+-------------+ +| update_doc_rowid_test | t | _tidb_rowid | 30001 | _TIDB_ROWID | ++-----------------------+------------+-------------+--------------------+-------------+ +``` + +## Write `_tidb_rowid` + +By default, TiDB does not allow `INSERT`, `REPLACE`, or `UPDATE` statements to write `_tidb_rowid` directly. + +```sql +INSERT INTO t(_tidb_rowid, a, b) VALUES (101, 4, 'w'); +``` + +```sql +ERROR 1105 (HY000): insert, update and replace statements for _tidb_rowid are not supported +``` + +If you need to preserve row IDs during data import or migration, enable the session variable `tidb_opt_write_row_id` first: + +{{< copyable "sql" >}} + +```sql +SET @@tidb_opt_write_row_id = ON; +INSERT INTO t(_tidb_rowid, a, b) VALUES (100, 3, 'z'); +SET @@tidb_opt_write_row_id = OFF; + +SELECT _tidb_rowid, a, b FROM t WHERE _tidb_rowid = 100; +``` + +```sql ++-------------+---+---+ +| _tidb_rowid | a | b | ++-------------+---+---+ +| 100 | 3 | z | ++-------------+---+---+ +``` + +> **Warning:** +> +> `tidb_opt_write_row_id` is intended for import and migration scenarios. It is not recommended for normal application writes. + +## Restrictions + +- You cannot create a user column named `_tidb_rowid`. +- You cannot rename an existing user column to `_tidb_rowid`. +- `_tidb_rowid` is an internal row handle. Do not treat it as a long-term business key. +- On partitioned non-clustered tables, `_tidb_rowid` values are not guaranteed to be unique across partitions. After `EXCHANGE PARTITION`, different partitions can contain rows with the same `_tidb_rowid` value. +- Whether `_tidb_rowid` exists depends on the table layout. If a table uses a clustered index, use the primary key instead. + +## Hotspot considerations + +For tables that use `_tidb_rowid`, TiDB allocates row IDs in increasing order by default. In write-intensive workloads, this can create write hotspots. + +To mitigate this issue for tables that rely on implicit row IDs, consider using [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md) and, if needed, [`PRE_SPLIT_REGIONS`](/sql-statements/sql-statement-split-region.md#pre_split_regions). + +Example: + +{{< copyable "sql" >}} + +```sql +CREATE TABLE t ( + id BIGINT PRIMARY KEY NONCLUSTERED, + c INT +) SHARD_ROW_ID_BITS = 4; +``` + +`SHARD_ROW_ID_BITS` applies only to tables that use the implicit row ID path. It does not apply to clustered-index tables. + +## Related statements and variables + +- [`SHOW TABLE NEXT_ROW_ID`](/sql-statements/sql-statement-show-table-next-rowid.md): shows the next row ID that TiDB will allocate +- [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md): shards implicit row IDs to reduce hotspots +- [`Clustered Indexes`](/clustered-indexes.md): explains when a table uses the primary key instead of `_tidb_rowid` +- [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id): controls whether writes to `_tidb_rowid` are allowed + +## See also + +- [CREATE TABLE](/sql-statements/sql-statement-create-table.md) +- [AUTO_INCREMENT](/auto-increment.md) +- [Non-transactional DML](/non-transactional-dml.md) From 88e0620bbf9f4e357d8e26aaee14925907d13903 Mon Sep 17 00:00:00 2001 From: tangenta Date: Wed, 18 Mar 2026 12:20:08 +0800 Subject: [PATCH 02/10] Update tidb-rowid.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tidb-rowid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index 3f033cc66e184..6995c5ad64ee5 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -1,6 +1,6 @@ --- title: _tidb_rowid -summary: Learn what `_tidb_rowid` is, when TiDB exposes it, and how to use it safely. +summary: Learn what `_tidb_rowid` is, when it is available, and how to use it safely. --- # _tidb_rowid From ca38a35fea9c54d41a41ea961cbc1228d8dab060 Mon Sep 17 00:00:00 2001 From: tangenta Date: Wed, 18 Mar 2026 12:20:36 +0800 Subject: [PATCH 03/10] Update tidb-rowid.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tidb-rowid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index 6995c5ad64ee5..b6fa05a3d6a37 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -23,7 +23,7 @@ In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handl `_tidb_rowid` is not available for tables that use a clustered index, including the following: - Tables whose integer primary key is the clustered row handle -- Tables that use a common handle clustered primary key +- Tables with a clustered index on a composite primary key The following example shows the difference: From dc7a12ae725dbc2bd4b3e53f251504384b4b2e8f Mon Sep 17 00:00:00 2001 From: tangenta Date: Wed, 18 Mar 2026 12:20:48 +0800 Subject: [PATCH 04/10] Update tidb-rowid.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tidb-rowid.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index b6fa05a3d6a37..9255423062ffd 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -161,10 +161,10 @@ CREATE TABLE t ( ## Related statements and variables -- [`SHOW TABLE NEXT_ROW_ID`](/sql-statements/sql-statement-show-table-next-rowid.md): shows the next row ID that TiDB will allocate -- [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md): shards implicit row IDs to reduce hotspots -- [`Clustered Indexes`](/clustered-indexes.md): explains when a table uses the primary key instead of `_tidb_rowid` -- [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id): controls whether writes to `_tidb_rowid` are allowed +- [`SHOW TABLE NEXT_ROW_ID`](/sql-statements/sql-statement-show-table-next-rowid.md): Shows the next row ID that TiDB will allocate +- [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md): Shards implicit row IDs to reduce hotspots +- [`Clustered Indexes`](/clustered-indexes.md): Explains when a table uses the primary key instead of `_tidb_rowid` +- [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id): Controls whether writes to `_tidb_rowid` are allowed ## See also From 5e88209a1d5071b64f1d5cab0a8ab12949276a22 Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Wed, 18 Mar 2026 15:42:00 +0800 Subject: [PATCH 05/10] Apply suggestions from code review --- tidb-rowid.md | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index 9255423062ffd..ffdd7c8836c53 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -27,8 +27,6 @@ In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handl The following example shows the difference: -{{< copyable "sql" >}} - ```sql CREATE TABLE t1 (a INT, b VARCHAR(20)); CREATE TABLE t2 (id BIGINT PRIMARY KEY NONCLUSTERED, a INT); @@ -37,8 +35,6 @@ CREATE TABLE t3 (id BIGINT PRIMARY KEY CLUSTERED, a INT); For `t1` and `t2`, you can query `_tidb_rowid`: -{{< copyable "sql" >}} - ```sql SELECT _tidb_rowid, a, b FROM t1; SELECT _tidb_rowid, id, a FROM t2; @@ -46,8 +42,6 @@ SELECT _tidb_rowid, id, a FROM t2; For `t3`, `_tidb_rowid` is unavailable because the clustered primary key is already the row handle: -{{< copyable "sql" >}} - ```sql SELECT _tidb_rowid, id, a FROM t3; ``` @@ -62,8 +56,6 @@ You can use `_tidb_rowid` in `SELECT` statements for supported tables. This is u Example: -{{< copyable "sql" >}} - ```sql CREATE TABLE t (a INT, b VARCHAR(20)); INSERT INTO t VALUES (1, 'x'), (2, 'y'); @@ -82,8 +74,6 @@ SELECT _tidb_rowid, a, b FROM t ORDER BY _tidb_rowid; To inspect the next value that TiDB will allocate for the row ID, use `SHOW TABLE ... NEXT_ROW_ID`: -{{< copyable "sql" >}} - ```sql SHOW TABLE t NEXT_ROW_ID; ``` @@ -110,8 +100,6 @@ ERROR 1105 (HY000): insert, update and replace statements for _tidb_rowid are no If you need to preserve row IDs during data import or migration, enable the session variable `tidb_opt_write_row_id` first: -{{< copyable "sql" >}} - ```sql SET @@tidb_opt_write_row_id = ON; INSERT INTO t(_tidb_rowid, a, b) VALUES (100, 3, 'z'); @@ -130,7 +118,7 @@ SELECT _tidb_rowid, a, b FROM t WHERE _tidb_rowid = 100; > **Warning:** > -> `tidb_opt_write_row_id` is intended for import and migration scenarios. It is not recommended for normal application writes. +> `tidb_opt_write_row_id` is intended for import and migration scenarios. It is not recommended for regular application writes. ## Restrictions @@ -148,8 +136,6 @@ To mitigate this issue for tables that rely on implicit row IDs, consider using Example: -{{< copyable "sql" >}} - ```sql CREATE TABLE t ( id BIGINT PRIMARY KEY NONCLUSTERED, @@ -161,10 +147,10 @@ CREATE TABLE t ( ## Related statements and variables -- [`SHOW TABLE NEXT_ROW_ID`](/sql-statements/sql-statement-show-table-next-rowid.md): Shows the next row ID that TiDB will allocate -- [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md): Shards implicit row IDs to reduce hotspots -- [`Clustered Indexes`](/clustered-indexes.md): Explains when a table uses the primary key instead of `_tidb_rowid` -- [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id): Controls whether writes to `_tidb_rowid` are allowed +- [`SHOW TABLE NEXT_ROW_ID`](/sql-statements/sql-statement-show-table-next-rowid.md): shows the next row ID that TiDB will allocate +- [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md): shards implicit row IDs to reduce hotspots +- [`Clustered Indexes`](/clustered-indexes.md): explains when a table uses the primary key instead of `_tidb_rowid` +- [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id): controls whether writes to `_tidb_rowid` are allowed ## See also From 091a47318c7da51584004691275128f093f9ab1b Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Thu, 19 Mar 2026 13:19:08 +0800 Subject: [PATCH 06/10] Apply suggestions from code review --- tidb-rowid.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index ffdd7c8836c53..f5eefcb579e58 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -11,19 +11,20 @@ In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handl > **Warning:** > -> Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. If you need a stable unique identifier, define and use an explicit primary key instead of relying on `_tidb_rowid`. +> - Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. +> - If you need a stable unique identifier, define and use an explicit primary key instead of relying on `_tidb_rowid`. ## When `_tidb_rowid` is available `_tidb_rowid` is available for tables whose row handle is not a clustered primary key. In practice, this means the following table types use `_tidb_rowid`: -- Tables without a primary key -- Tables whose primary key is explicitly defined as `NONCLUSTERED` +- Tables without primary keys +- Tables with primary keys that are explicitly defined as `NONCLUSTERED` `_tidb_rowid` is not available for tables that use a clustered index, including the following: -- Tables whose integer primary key is the clustered row handle -- Tables with a clustered index on a composite primary key +- Tables with integer primary keys that are clustered row handles +- Tables with clustered indexes on composite primary keys The following example shows the difference: @@ -98,7 +99,7 @@ INSERT INTO t(_tidb_rowid, a, b) VALUES (101, 4, 'w'); ERROR 1105 (HY000): insert, update and replace statements for _tidb_rowid are not supported ``` -If you need to preserve row IDs during data import or migration, enable the session variable `tidb_opt_write_row_id` first: +If you need to preserve row IDs during data import or migration, enable the system variable [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id) first: ```sql SET @@tidb_opt_write_row_id = ON; @@ -125,7 +126,7 @@ SELECT _tidb_rowid, a, b FROM t WHERE _tidb_rowid = 100; - You cannot create a user column named `_tidb_rowid`. - You cannot rename an existing user column to `_tidb_rowid`. - `_tidb_rowid` is an internal row handle. Do not treat it as a long-term business key. -- On partitioned non-clustered tables, `_tidb_rowid` values are not guaranteed to be unique across partitions. After `EXCHANGE PARTITION`, different partitions can contain rows with the same `_tidb_rowid` value. +- On partitioned non-clustered tables, `_tidb_rowid` values are not guaranteed to be unique across partitions. After you execute `EXCHANGE PARTITION`, different partitions can contain rows with the same `_tidb_rowid` value. - Whether `_tidb_rowid` exists depends on the table layout. If a table uses a clustered index, use the primary key instead. ## Hotspot considerations From 731dfe95b5cc9f19f6f3ec11401afc8610ce3d91 Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Thu, 19 Mar 2026 13:56:42 +0800 Subject: [PATCH 07/10] Apply suggestions from code review --- tidb-rowid.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index f5eefcb579e58..08171905e492e 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -11,7 +11,7 @@ In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handl > **Warning:** > -> - Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. +> - Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, executing `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. > - If you need a stable unique identifier, define and use an explicit primary key instead of relying on `_tidb_rowid`. ## When `_tidb_rowid` is available @@ -155,6 +155,6 @@ CREATE TABLE t ( ## See also -- [CREATE TABLE](/sql-statements/sql-statement-create-table.md) -- [AUTO_INCREMENT](/auto-increment.md) +- [`CREATE TABLE`](/sql-statements/sql-statement-create-table.md) +- [`AUTO_INCREMENT`](/auto-increment.md) - [Non-transactional DML](/non-transactional-dml.md) From f02825081c42437974e8dcf6d5ce825178730bbe Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Thu, 19 Mar 2026 15:03:54 +0800 Subject: [PATCH 08/10] Update tidb-rowid.md --- tidb-rowid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index 08171905e492e..320edf2be3903 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -5,7 +5,7 @@ summary: Learn what `_tidb_rowid` is, when it is available, and how to use it sa # _tidb_rowid -`_tidb_rowid` is a hidden system column that TiDB uses as the row handle for tables that do not use a clustered index. You do not declare this column in the table schema, but you can reference it in SQL when the table uses `_tidb_rowid` as its handle. +`_tidb_rowid` is a hidden system column that TiDB uses as the row handle for tables that do not use a clustered index. You cannot declare this column in the table schema, but you can reference it in SQL when the table uses `_tidb_rowid` as its handle. In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handle column managed by TiDB. From 0226e1a5152d7e8adafe1156067bc0fada189c0c Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Mon, 23 Mar 2026 17:01:05 +0800 Subject: [PATCH 09/10] Update tidb-rowid.md --- tidb-rowid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidb-rowid.md b/tidb-rowid.md index 320edf2be3903..974cb3a905484 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -129,7 +129,7 @@ SELECT _tidb_rowid, a, b FROM t WHERE _tidb_rowid = 100; - On partitioned non-clustered tables, `_tidb_rowid` values are not guaranteed to be unique across partitions. After you execute `EXCHANGE PARTITION`, different partitions can contain rows with the same `_tidb_rowid` value. - Whether `_tidb_rowid` exists depends on the table layout. If a table uses a clustered index, use the primary key instead. -## Hotspot considerations +## Address hotspot issues For tables that use `_tidb_rowid`, TiDB allocates row IDs in increasing order by default. In write-intensive workloads, this can create write hotspots. From 06323d5edf69da128c7c12b6102e6dfb2f1f8993 Mon Sep 17 00:00:00 2001 From: xixirangrang Date: Fri, 27 Mar 2026 15:45:49 +0800 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: Grace Cai --- shard-row-id-bits.md | 4 +-- .../sql-statement-show-table-next-rowid.md | 2 +- tidb-rowid.md | 33 +++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/shard-row-id-bits.md b/shard-row-id-bits.md index 7a5cf64ffcce8..ef12c3d1987e0 100644 --- a/shard-row-id-bits.md +++ b/shard-row-id-bits.md @@ -9,7 +9,7 @@ This document introduces the `SHARD_ROW_ID_BITS` table attribute, which is used ## Concept -For tables with a non-clustered primary key or no primary key, TiDB uses the hidden [`_tidb_rowid`](/tidb-rowid.md) as an implicit auto-increment row ID. When a large number of `INSERT` operations are performed, the data is written into a single Region, causing a write hot spot. +For tables with a non-clustered primary key or no primary key, TiDB uses the automatically generated [`_tidb_rowid`](/tidb-rowid.md) as an implicit auto-increment row ID. When a large number of `INSERT` operations are performed, the data is written into a single Region, causing a write hot spot. To mitigate the hot spot issue, you can configure `SHARD_ROW_ID_BITS`. The row IDs are scattered and the data are written into multiple different Regions. @@ -28,7 +28,7 @@ When you set `SHARD_ROW_ID_BITS = S`, the structure of `_tidb_rowid` is as follo > **Warning:** > -> `_tidb_rowid` is an internal row handle. Do not assume it is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. For details, see [`_tidb_rowid`](/tidb-rowid.md). +> `_tidb_rowid` is an internal row ID implicitly assigned by TiDB. Do not assume it is globally unique in all cases. For partitioned tables that do not use clustered indexes, `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. For details, see [`_tidb_rowid`](/tidb-rowid.md). > **Note:** > diff --git a/sql-statements/sql-statement-show-table-next-rowid.md b/sql-statements/sql-statement-show-table-next-rowid.md index 821f8f0bd7a68..46ea6483a5bf2 100644 --- a/sql-statements/sql-statement-show-table-next-rowid.md +++ b/sql-statements/sql-statement-show-table-next-rowid.md @@ -7,7 +7,7 @@ summary: Learn the usage of `SHOW TABLE NEXT_ROW_ID` in TiDB. `SHOW TABLE NEXT_ROW_ID` is used to show the details of some special columns of a table, including: -* [`_tidb_rowid`](/tidb-rowid.md), the hidden row-handle column automatically managed by TiDB for supported tables. +* [`_tidb_rowid`](/tidb-rowid.md), the hidden row ID column automatically managed by TiDB for supported tables. * `AUTO_INCREMENT` column created by users. * [`AUTO_RANDOM`](/auto-random.md) column created by users. * [`SEQUENCE`](/sql-statements/sql-statement-create-sequence.md) created by users. diff --git a/tidb-rowid.md b/tidb-rowid.md index 974cb3a905484..48a30eec7ee45 100644 --- a/tidb-rowid.md +++ b/tidb-rowid.md @@ -3,28 +3,25 @@ title: _tidb_rowid summary: Learn what `_tidb_rowid` is, when it is available, and how to use it safely. --- -# _tidb_rowid +# `_tidb_rowid` -`_tidb_rowid` is a hidden system column that TiDB uses as the row handle for tables that do not use a clustered index. You cannot declare this column in the table schema, but you can reference it in SQL when the table uses `_tidb_rowid` as its handle. +`_tidb_rowid` is a hidden system column automatically generated by TiDB. For tables that do not use a clustered index, it serves as the internal row ID of the table. You cannot declare or modify this column in the table schema, but you can reference it in SQL when the table uses `_tidb_rowid` as its internal row ID. -In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` handle column managed by TiDB. +In the current implementation, `_tidb_rowid` is an extra `BIGINT NOT NULL` column automatically managed by TiDB. > **Warning:** > -> - Do not assume `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, executing `ALTER TABLE ... EXCHANGE PARTITION` can leave different partitions with the same `_tidb_rowid` value. +> - Do not assume that `_tidb_rowid` is globally unique in all cases. For partitioned tables that do not use clustered indexes, executing `ALTER TABLE ... EXCHANGE PARTITION` might result in duplicate `_tidb_rowid` values across different partitions. > - If you need a stable unique identifier, define and use an explicit primary key instead of relying on `_tidb_rowid`. ## When `_tidb_rowid` is available -`_tidb_rowid` is available for tables whose row handle is not a clustered primary key. In practice, this means the following table types use `_tidb_rowid`: +TiDB uses `_tidb_rowid` to identify each row when a table does not use a clustered primary key as the unique row identifier. In practice, this means that the following types of tables use `_tidb_rowid`: - Tables without primary keys - Tables with primary keys that are explicitly defined as `NONCLUSTERED` -`_tidb_rowid` is not available for tables that use a clustered index, including the following: - -- Tables with integer primary keys that are clustered row handles -- Tables with clustered indexes on composite primary keys +`_tidb_rowid` is not available for tables that use a clustered index (that is, tables whose primary key is defined as `CLUSTERED`, regardless of whether it is a single-column or composite primary key). The following example shows the difference: @@ -34,14 +31,14 @@ CREATE TABLE t2 (id BIGINT PRIMARY KEY NONCLUSTERED, a INT); CREATE TABLE t3 (id BIGINT PRIMARY KEY CLUSTERED, a INT); ``` -For `t1` and `t2`, you can query `_tidb_rowid`: +For `t1` and `t2`, you can query `_tidb_rowid` because these tables do not use a clustered index as the row identifier: ```sql SELECT _tidb_rowid, a, b FROM t1; SELECT _tidb_rowid, id, a FROM t2; ``` -For `t3`, `_tidb_rowid` is unavailable because the clustered primary key is already the row handle: +For `t3`, `_tidb_rowid` is unavailable because the clustered primary key is already the row identifier: ```sql SELECT _tidb_rowid, id, a FROM t3; @@ -53,7 +50,7 @@ ERROR 1054 (42S22): Unknown column '_tidb_rowid' in 'field list' ## Read `_tidb_rowid` -You can use `_tidb_rowid` in `SELECT` statements for supported tables. This is useful for tasks such as pagination, troubleshooting, and batch processing. +For tables that use `_tidb_rowid`, you can query `_tidb_rowid` in `SELECT` statements. This is useful for tasks such as pagination, troubleshooting, and batch processing. Example: @@ -73,7 +70,7 @@ SELECT _tidb_rowid, a, b FROM t ORDER BY _tidb_rowid; +-------------+---+---+ ``` -To inspect the next value that TiDB will allocate for the row ID, use `SHOW TABLE ... NEXT_ROW_ID`: +To view the next value that TiDB will allocate for the row ID, use `SHOW TABLE ... NEXT_ROW_ID`: ```sql SHOW TABLE t NEXT_ROW_ID; @@ -99,7 +96,7 @@ INSERT INTO t(_tidb_rowid, a, b) VALUES (101, 4, 'w'); ERROR 1105 (HY000): insert, update and replace statements for _tidb_rowid are not supported ``` -If you need to preserve row IDs during data import or migration, enable the system variable [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id) first: +If you need to preserve the original row IDs during data import or migration, enable the [`tidb_opt_write_row_id`](/system-variables.md#tidb_opt_write_row_id) system variable first: ```sql SET @@tidb_opt_write_row_id = ON; @@ -125,15 +122,15 @@ SELECT _tidb_rowid, a, b FROM t WHERE _tidb_rowid = 100; - You cannot create a user column named `_tidb_rowid`. - You cannot rename an existing user column to `_tidb_rowid`. -- `_tidb_rowid` is an internal row handle. Do not treat it as a long-term business key. +- `_tidb_rowid` is an internal column in TiDB. Do not treat it as a business primary key or a long-term identifier. - On partitioned non-clustered tables, `_tidb_rowid` values are not guaranteed to be unique across partitions. After you execute `EXCHANGE PARTITION`, different partitions can contain rows with the same `_tidb_rowid` value. -- Whether `_tidb_rowid` exists depends on the table layout. If a table uses a clustered index, use the primary key instead. +- Whether `_tidb_rowid` exists depends on the table schema. For tables with clustered indexes, use the primary key as the row identifier. ## Address hotspot issues For tables that use `_tidb_rowid`, TiDB allocates row IDs in increasing order by default. In write-intensive workloads, this can create write hotspots. -To mitigate this issue for tables that rely on implicit row IDs, consider using [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md) and, if needed, [`PRE_SPLIT_REGIONS`](/sql-statements/sql-statement-split-region.md#pre_split_regions). +To mitigate this issue (for tables that rely on `_tidb_rowid` as the row ID), consider using [`SHARD_ROW_ID_BITS`](/shard-row-id-bits.md) to distribute row IDs more evenly, and use [`PRE_SPLIT_REGIONS`](/sql-statements/sql-statement-split-region.md#pre_split_regions) to pre-split Regions when necessary. Example: @@ -144,7 +141,7 @@ CREATE TABLE t ( ) SHARD_ROW_ID_BITS = 4; ``` -`SHARD_ROW_ID_BITS` applies only to tables that use the implicit row ID path. It does not apply to clustered-index tables. +`SHARD_ROW_ID_BITS` applies only to tables that use `_tidb_rowid` and does not apply to tables with clustered indexes. ## Related statements and variables