Auto-increment column

Updated at:
Copy as MD

An auto-increment column is a feature in SelectDB that automatically generates a unique identifier without manual intervention. This feature ensures uniqueness, simplifies data ingestion, and reduces maintenance costs. It is useful for scenarios requiring a unique identifier, such as generating primary keys and managing user IDs.

Feature overview

SelectDB handles data writes to tables with an auto-increment column as follows:

  • Auto-population (column excluded): If you omit the auto-increment column from the inserted data, SelectDB automatically generates and populates a unique value for it.

  • Partial specification (column included):

    • Null value: If you provide a null value, SelectDB replaces it with a system-generated unique value.

    • Non-null value: User-provided values are inserted as-is.

    Important

    Providing a non-null value can compromise the column's uniqueness.

Uniqueness

SelectDB guarantees that system-generated values in an auto-increment column have table-level uniqueness. However:

  • System-generated values: Uniqueness is guaranteed for these values.

  • User-provided values: SelectDB does not validate the uniqueness of user-provided values, which can result in duplicate entries.

Contiguity

SelectDB typically generates dense auto-increment values, but note the following:

  • Potential gaps: Gaps may occur due to performance optimizations. Each backend node (BE) pre-allocates a block of unique values to improve efficiency. These blocks do not overlap between nodes.

  • Non-chronological values: SelectDB does not guarantee that values from later writes are greater than those from earlier writes.

    Note

    Do not use auto-increment values to infer the chronological order of writes.

Limitations

  • The instance version must be 4.0.0 or later.

  • Only Duplicate Key and Unique Key model tables support an auto-increment column.

  • A table can have at most one auto-increment column.

  • The auto-increment column must be of type BIGINT and must be NOT NULL.

  • A manually specified starting value must be 0 or greater.

Creating a table with an auto-increment column

To use an auto-increment column, add the AUTO_INCREMENT attribute to the column definition in the CREATE-TABLE statement. To specify a starting value, use the AUTO_INCREMENT(start_value) syntax. If unspecified, the starting value defaults to 1.

  • Create a Duplicate Key model table where a key column is an auto-increment column.

    CREATE TABLE `demo`.`tbl` (
          `id` BIGINT NOT NULL AUTO_INCREMENT,
          `value` BIGINT NOT NULL
    ) ENGINE=OLAP
    DUPLICATE KEY(`id`)
    DISTRIBUTED BY HASH(`id`) BUCKETS 10
    PROPERTIES (
    "replication_allocation" = "tag.location.default: 3"
    );
  • Create a Duplicate Key model table where a key column is an auto-increment column, and set the starting value to 100.

    CREATE TABLE `demo`.`tbl` (
          `id` BIGINT NOT NULL AUTO_INCREMENT(100),
          `value` BIGINT NOT NULL
    ) ENGINE=OLAP
    DUPLICATE KEY(`id`)
    DISTRIBUTED BY HASH(`id`) BUCKETS 10
    PROPERTIES (
    "replication_allocation" = "tag.location.default: 3"
    );
  • Create a Duplicate Key model table where a non-key column is an auto-increment column.

    CREATE TABLE `demo`.`tbl` (
          `uid` BIGINT NOT NULL,
          `name` BIGINT NOT NULL,
          `id` BIGINT NOT NULL AUTO_INCREMENT,
          `value` BIGINT NOT NULL
    ) ENGINE=OLAP
    DUPLICATE KEY(`uid`, `name`)
    DISTRIBUTED BY HASH(`uid`) BUCKETS 10
    PROPERTIES (
    "replication_allocation" = "tag.location.default: 3"
    );
  • Create a Unique Key model table where a key column is an auto-increment column.

    CREATE TABLE `demo`.`tbl` (
          `id` BIGINT NOT NULL AUTO_INCREMENT,
          `name` varchar(65533) NOT NULL,
          `value` int(11) NOT NULL
    ) ENGINE=OLAP
    UNIQUE KEY(`id`)
    DISTRIBUTED BY HASH(`id`) BUCKETS 10
    PROPERTIES (
    "replication_allocation" = "tag.location.default: 3"
    );
  • Create a Unique Key model table where a non-key column is an auto-increment column.

    CREATE TABLE `demo`.`tbl` (
          `text` varchar(65533) NOT NULL,
          `id` BIGINT NOT NULL AUTO_INCREMENT,
    ) ENGINE=OLAP
    UNIQUE KEY(`text`)
    DISTRIBUTED BY HASH(`text`) BUCKETS 10
    PROPERTIES (
    "replication_allocation" = "tag.location.default: 3"
    );

Auto-increment column examples

Standard ingestion

Consider the following table:

CREATE TABLE `demo`.`tbl` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `name` varchar(65533) NOT NULL,
    `value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

If you omit the id auto-increment column in an INSERT statement, SelectDB automatically populates the column with generated values.

insert into tbl(name, value) values("Bob", 10), ("Alice", 20), ("Jack", 30);

select * from tbl order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
+------+-------+-------+

Similarly, if you omit the id column during a stream load from test.csv, SelectDB automatically populates it.

test.csv:

Tom,40
John,50

Run the command:

curl --location-trusted -u user:passwd -H "columns:name,value" -H "column_separator:," -T ./test.csv http://{host}:{port}/api/{db}/tbl/_stream_load

Query result:

select * from tbl order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
|    4 | Tom   |    40 |
|    5 | John  |    50 |
+------+-------+-------+

When you use an INSERT statement and specify the id auto-increment column, any NULL provided for this column is replaced with a generated value.

insert into tbl(id, name, value) values(null, "SelectDB", 60), (null, "Nereids", 70);

select * from tbl order by id;
+------+---------+-------+
| id   | name    | value |
+------+---------+-------+
|    1 | Bob     |    10 |
|    2 | Alice   |    20 |
|    3 | Jack    |    30 |
|    4 | Tom     |    40 |
|    5 | John    |    50 |
|    6 | SelectDB   |    60 |
|    7 | Nereids |    70 |
+------+---------+-------+

Partial column update

When you perform a partial column update on a merge-on-write Unique table that contains an auto-increment column, if the auto-increment column is a key column, the target columns for the partial column update must include the auto-increment column because you must explicitly specify the key columns. The import behavior in this case is the same as that of a normal partial column update.

CREATE TABLE `demo`.`tbl2` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `name` varchar(65533) NOT NULL,
  `value` int(11) NOT NULL DEFAULT "0"
  ) ENGINE=OLAP
  UNIQUE KEY(`id`)
  DISTRIBUTED BY HASH(`id`) BUCKETS 10
  PROPERTIES (
  "replication_allocation" = "tag.location.default: 3",
  "enable_unique_key_merge_on_write" = "true"
);


insert into tbl2(id, name, value) values(1, "Bob", 10), (2, "Alice", 20), (3, "Jack", 30);

select * from tbl2 order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
+------+-------+-------+

set enable_unique_key_partial_update=true;
set enable_insert_strict=false;
insert into tbl2(id, name) values(1, "modified"), (4, "added");

select * from tbl2 order by id;
+------+----------+-------+
| id   | name     | value |
+------+----------+-------+
|    1 | modified |    10 |
|    2 | Alice    |    20 |
|    3 | Jack     |    30 |
|    4 | added    |     0 |
+------+----------+-------+

When the auto-increment column is a non-key column, its existing value is retained if you omit it during an update. If you do specify the column, any null value is replaced with a generated value, while non-null values are inserted as-is, following the semantics of a partial column update.

CREATE TABLE `demo`.`tbl3` (
  `id` BIGINT NOT NULL,
  `name` varchar(100) NOT NULL,
  `score` BIGINT NOT NULL,
  `aid` BIGINT NOT NULL AUTO_INCREMENT
  ) ENGINE=OLAP
  UNIQUE KEY(`id`)
  DISTRIBUTED BY HASH(`id`) BUCKETS 1
  PROPERTIES (
  "replication_allocation" = "tag.location.default: 3",
  "enable_unique_key_merge_on_write" = "true"
);


insert into tbl3(id, name, score) values(1, "SelectDB", 100), (2, "Nereids", 200), (3, "Bob", 300);

select * from tbl3 order by id;
+------+---------+-------+------+
| id   | name    | score | aid  |
+------+---------+-------+------+
|    1 | SelectDB   |   100 |    0 |
|    2 | Nereids |   200 |    1 |
|    3 | Bob     |   300 |    2 |
+------+---------+-------+------+


set enable_unique_key_partial_update=true;
set enable_insert_strict=false;
insert into tbl3(id, score) values(1, 999), (2, 888);

select * from tbl3 order by id;
+------+---------+-------+------+
| id   | name    | score | aid  |
+------+---------+-------+------+
|    1 | SelectDB   |   999 |    0 |
|    2 | Nereids |   888 |    1 |
|    3 | Bob     |   300 |    2 |
+------+---------+-------+------+

insert into tbl3(id, aid) values(1, 1000), (3, 500);

select * from tbl3 order by id;
+------+---------+-------+------+
| id   | name    | score | aid  |
+------+---------+-------+------+
|    1 | SelectDB   |   999 | 1000 |
|    2 | Nereids |   888 |    1 |
|    3 | Bob     |   300 |  500 |
+------+---------+-------+------+

Use cases

Dictionary encoding

In user profiling scenarios, cohort analysis using bitmaps requires a user dictionary. Each user is mapped to a unique integer value. Densely packed dictionary values can improve bitmap performance.

Take offline UV and PV analysis as an example. Assume you have the following table that stores detailed user behavior data:

CREATE TABLE `demo`.`dwd_dup_tbl` (
    `user_id` varchar(50) NOT NULL,
    `dim1` varchar(50) NOT NULL,
    `dim2` varchar(50) NOT NULL,
    `dim3` varchar(50) NOT NULL,
    `dim4` varchar(50) NOT NULL,
    `dim5` varchar(50) NOT NULL,
    `visit_time` DATE NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`user_id`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Create a dictionary table with an auto-increment column:

CREATE TABLE `demo`.`dictionary_tbl` (
    `user_id` varchar(50) NOT NULL,
    `aid` BIGINT NOT NULL AUTO_INCREMENT
) ENGINE=OLAP
UNIQUE KEY(`user_id`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3",
"enable_unique_key_merge_on_write" = "true"
);

Import existing user_id data into the dictionary table to create an integer-encoded mapping for each user_id.

insert into dictionary_tbl(user_id)
select user_id from dwd_dup_tbl group by user_id;

Alternatively, you can import only new user_id data into the dictionary table:

insert into dictionary_tbl(user_id)
select dwd_dup_tbl.user_id from dwd_dup_tbl left join dictionary_tbl
on dwd_dup_tbl.user_id = dictionary_tbl.user_id where dwd_dup_tbl.visit_time > '2023-12-10' and dictionary_tbl.user_id is NULL;

In a real-world scenario, you can also use the Flink connector to write data to SelectDB.

Assuming dim1dim3, and dim5 are the dimensions you want to analyze, create an aggregate table to store the results:

CREATE TABLE `demo`.`dws_agg_tbl` (
    `dim1` varchar(50) NOT NULL,
    `dim3` varchar(50) NOT NULL,
    `dim5` varchar(50) NOT NULL,
    `user_id_bitmap` BITMAP BITMAP_UNION NOT NULL,
    `pv` BIGINT SUM NOT NULL 
) ENGINE=OLAP
AGGREGATE KEY(`dim1`,`dim3`,`dim5`)
DISTRIBUTED BY HASH(`dim1`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Aggregate the data and insert it into the aggregate table:

insert into dws_agg_tbl
select dwd_dup_tbl.dim1, dwd_dup_tbl.dim3, dwd_dup_tbl.dim5, BITMAP_UNION(TO_BITMAP(dictionary_tbl.aid)), COUNT(1)
from dwd_dup_tbl INNER JOIN dictionary_tbl on dwd_dup_tbl.user_id = dictionary_tbl.user_id
group by dwd_dup_tbl.dim1, dwd_dup_tbl.dim3, dwd_dup_tbl.dim5;

Use the following statement to query UV and PV:

select dim1, dim3, dim5, bitmap_count(user_id_bitmap) as uv, pv from dws_agg_tbl;

Efficient pagination

When displaying data, applications often need to use pagination. Traditional pagination typically uses a combination of limit, offset, and order by in SQL queries. For example, consider the following table:

CREATE TABLE `demo`.`records_tbl` (
    `user_id` int(11) NOT NULL COMMENT "",
    `name` varchar(26) NOT NULL COMMENT "",
    `address` varchar(41) NOT NULL COMMENT "",
    `city` varchar(11) NOT NULL COMMENT "",
    `nation` varchar(16) NOT NULL COMMENT "",
    `region` varchar(13) NOT NULL COMMENT "",
    `phone` varchar(16) NOT NULL COMMENT "",
    `mktsegment` varchar(11) NOT NULL COMMENT ""
) DUPLICATE KEY (`user_id`, `name`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Assume each page displays 100 records. To get the first page of data, you can use the following SQL query:

select * from records_tbl order by user_id, name limit 100;

To get the second page, you can use the following query:

select * from records_tbl order by user_id, name limit 100 offset 100;

However, when you perform a deep pagination query with a large offset, this method is inefficient because it reads all data into memory for a full sort before further processing, even if only a few rows are required. To implement pagination more efficiently, you can use an auto-increment column to assign a unique value to each row. During a query, you can record the maximum value max_value of the unique_value column from the previous page and then use the where unique_value > max_value limit rows_per_page clause to filter a large amount of data in advance by using predicate pushdown.

To implement this more efficient method, add an auto-increment column to the table to give each row a unique identifier:

CREATE TABLE `demo`.`records_tbl2` (
    `user_id` int(11) NOT NULL COMMENT "",
    `name` varchar(26) NOT NULL COMMENT "",
    `address` varchar(41) NOT NULL COMMENT "",
    `city` varchar(11) NOT NULL COMMENT "",
    `nation` varchar(16) NOT NULL COMMENT "",
    `region` varchar(13) NOT NULL COMMENT "",
    `phone` varchar(16) NOT NULL COMMENT "",
    `mktsegment` varchar(11) NOT NULL COMMENT "",
    `unique_value` BIGINT NOT NULL AUTO_INCREMENT
) DUPLICATE KEY (`user_id`, `name`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

To get the first page of 100 records, use the following query:

select * from records_tbl2 order by unique_value limit 100;

Your application can record the maximum unique_value from the results (e.g., 99) and use it in the WHERE clause to query the next page:

select * from records_tbl2 where unique_value > 99 order by unique_value limit 100;

If you need to jump directly to a distant page (e.g., page 101), where fetching the maximum unique_value from the previous page is impractical, you can use the following query:

select user_id, name, address, city, nation, region, phone, mktsegment
from records_tbl2, (select unique_value as max_value from records_tbl2 order by unique_value limit 1 offset 9999) as previous_data
where records_tbl2.unique_value > previous_data.max_value
order by unique_value limit 100;