ByteHouse 唯一键表主要用于实现 upsert 功能。该能力是 ByteHouse 团队自研的独有特性,既能保持高效的查询性能、又支持唯一键更新。主要解决了开源 ClickHouse 不能支持高效更新操作的痛点,帮助业务更简单地开发实时分析应用。用户通过指定唯一键 UNIQUE KEY 来实现 Upsert 更新写语义,查询自动返回每个唯一键的最新值。
唯一键表即指定唯一键索引(UNIQUE KEY)的 CnchMergeTree 表,具有以下特点:
更多关于唯一键表的能力介绍、功能逻辑、使用限制等内容,请参见唯一键表。
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t1 ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t1 VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100); -- 写入相同 key 的数据可以实现更新(upsert语义) INSERT INTO t1 VALUES ('2020-10-29 23:50:00', 10002, 'Beijing', '男装', 4, 400), ('2020-10-29 23:50:00', 10003, 'Beijing', '男装', 2, 200), ('2020-10-29 23:50:00', 10004, 'Beijing', '男装', 1, 100), ('2020-10-30 00:00:05', 10001, 'Beijing', '男装', 1, 100), ('2020-10-30 00:00:05', 10002, 'Beijing', '男装', 2, 200); -- 查询自动返回每个key最新的数据 select * from t1 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 男装 │ 5 │ 500 │ │ 2020-10-29 23:50:00 │ 10002 │ Beijing │ 男装 │ 4 │ 400 │ │ 2020-10-29 23:50:00 │ 10003 │ Beijing │ 男装 │ 2 │ 200 │ │ 2020-10-29 23:50:00 │ 10004 │ Beijing │ 男装 │ 1 │ 100 │ │ 2020-10-30 00:00:05 │ 10001 │ Beijing │ 男装 │ 1 │ 100 │ │ 2020-10-30 00:00:05 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
-- UNIQUE KEY 可以包含多个字段和表达式 -- sipHash64 是一种快速且低冲突率的哈希函数,使用 sipHash64 作为 unique key 需要考虑到可能的 hash 冲突 -- sipHash64:https://clickhouse.com/docs/en/sql-reference/functions/hash-functions#siphash64 CREATE TABLE t1m ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY (product_id, sipHash64(city)); INSERT INTO t1m VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100), ('2020-10-29 23:50:00', 10002, 'Shanghai', '男装', 4, 400), ('2020-10-29 23:50:00', 10003, 'Beijing', '男装', 2, 200), ('2020-10-29 23:50:00', 10004, 'Beijing', '男装', 1, 100); select * from t1m; ┌──────────event_time─┬─product_id─┬─city─────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 男装 │ 5 │ 500 │ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ │ 2020-10-29 23:50:00 │ 10003 │ Beijing │ 男装 │ 2 │ 200 │ │ 2020-10-29 23:50:00 │ 10004 │ Beijing │ 男装 │ 1 │ 100 │ │ 2020-10-29 23:50:00 │ 10002 │ Shanghai │ 男装 │ 4 │ 400 │ └─────────────────────┴────────────┴──────────┴──────────┴────────┴─────────┘
说明
使用版本字段时有以下限制:
默认情况下,相同 unique key 后写入的数据会覆盖已有的数据。这可能会带来以下问题
为了解决上面的问题,ByteHouse 唯一键表支持将表中的某个字段指定为版本字段。引擎保证写入相同 key 的数据时,只有数据版本 >= 已有版本时,才会进行覆盖。版本字段支持所有UInt类型和Data/DateTime,且不能为 Nullable。
-- CnchMergeTree 括号内参数为可选的版本字段 CREATE TABLE t3 ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree(event_time) PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t3 VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:50:00', 10001, 'Beijing', '男装', 8, 800), ('2020-10-29 23:50:00', 10002, 'Beijing', '男装', 5, 500); -- 回溯前两条数据,由于版本 < 已有版本,写入时自动跳过 INSERT INTO t3 VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200); -- 10001 和 10002 的版本没有回退 select * from t3 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:50:00 │ 10001 │ Beijing │ 男装 │ 8 │ 800 │ │ 2020-10-29 23:50:00 │ 10002 │ Beijing │ 男装 │ 5 │ 500 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘ -- 继续回溯后两条数据,并写入两条新版本数据 INSERT INTO t3 VALUES ('2020-10-29 23:50:00', 10001, 'Beijing', '男装', 8, 800), ('2020-10-29 23:50:00', 10002, 'Beijing', '男装', 5, 500), ('2020-10-29 23:55:00', 10001, 'Beijing', '男装', 10, 1000), ('2020-10-29 23:55:00', 10002, 'Beijing', '男装', 7, 700); -- 查询自动返回最新版本的数据 select * from t3 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:55:00 │ 10001 │ Beijing │ 男装 │ 10 │ 1000 │ │ 2020-10-29 23:55:00 │ 10002 │ Beijing │ 男装 │ 7 │ 700 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
在某些应用场景下,用户希望在INSERT时加上一个字段来标识是否删除来扩展INSERT语义。
在 ByteHouse 唯一键表中,为每张表都添加了一个保留字段_delete_flag_
,类型为UInt8
, 0表示数据写入,非0表示数据删除。该字段不可在CREATE TABLE时指定,也不可查询该字段,仅可以在INSERT时指定,包括INSERT和INSERT SELECT。此外,ByteHouse 唯一键表基于保留字段_delete_flag_
,支持了 DELETE FROM
子句。
用法示例如下:
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t5 ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t5 VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100); -- 指定删除字段进行数据删除,删除字段设置非0时表示删除,设置为0时表示正常的upsert操作 INSERT INTO t5 (event_time, product_id, city, category, amount, revenue, _delete_flag_) VALUES ('2020-10-29 23:50:00', 10001, 'Beijing', '男装', 4, 400, 5), ('2020-10-29 23:50:00', 10002, 'Beijing', '男装', 2, 200, 1), ('2020-10-29 23:50:00', 10004, 'Beijing', '男装', 1, 100, 0); -- 查询结果中包含了新加入的一行数据,并删除了两行旧数据 select * from t5 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ │ 2020-10-29 23:50:00 │ 10004 │ Beijing │ 男装 │ 1 │ 100 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable -- 指定版本号 CREATE TABLE t5m ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64, `version` UInt64 ) ENGINE = CnchMergeTree(version) PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t5m VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500, 10), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200, 10), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100, 10); -- 指定删除字段并指定版本号,版本号小于查询结果中相应行的版本号,删除操作不会起作用 INSERT INTO t5m (event_time, product_id, city, category, amount, revenue, version, _delete_flag_) VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 4, 400, 5, 1), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200, 5, 1), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100, 5, 1); -- 查询结果不变,没有任何数据被删除 select * from t5m order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┬─version─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 男装 │ 5 │ 500 │ 10 │ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ 10 │ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ 10 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┴─────────┘ -- 指定删除字段进行数据删除,不指定版本号或者版本号设置为0,删除操作会跳过版本检查,直接执行 INSERT INTO t5m (event_time, product_id, city, category, amount, revenue, _delete_flag_) VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 4, 400, 1); INSERT INTO t5m (event_time, product_id, city, category, amount, revenue, version, _delete_flag_) VALUES ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100, 0, 1); -- 查询结果删除了两行旧数据 select * from t5m order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┬─version─┐ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ 10 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┴─────────┘
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t5x ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t5x VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100); -- 通过INSERT SELECT 来删除revenue >= 200的数据 INSERT INTO t5x (event_time, product_id, city, category, amount, revenue, _delete_flag_) SELECT *, 1 as _delete_flag_ from t5x where revenue >= 200; -- 查询结果中已删除revenue >= 200的数据 select * from t5x order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t5y ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t5y VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100); -- 通过 DELETE FROM 来删除revenue == 500的数据 DELETE FROM t5y WHERE revenue = 500; -- 通过 DELETE FROM 来删除revenue >= 200的数据 DELETE FROM t5y WHERE revenue >= 200; -- 查询结果中已删除revenue >= 200的数据 select * from t5y order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t6 ( `event_time` DateTime, `product_id` UInt64, `city` String, `category` String, `amount` UInt32, `revenue` UInt64 ) ENGINE = CnchMergeTree PARTITION BY toDate(event_time) ORDER BY (city, category) UNIQUE KEY product_id; INSERT INTO t6 VALUES ('2020-10-29 23:40:00', 10001, 'Beijing', '男装', 5, 500), ('2020-10-29 23:40:00', 10002, 'Beijing', '男装', 2, 200), ('2020-10-29 23:40:00', 10003, 'Beijing', '男装', 1, 100); -- 通过 UPDATE 语句来进行更新 UPDATE t6 SET amount = 10, revenue = 1000 WHERE event_time = '2020-10-29 23:40:00' and product_id=10001; -- 查询结果中 10001 数据行进行了 UPDATE 变更 select * from t6 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 男装 │ 10 │ 1000 │ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘ -- 通过 UPDATE 语句来进行更新,使用 limit 限定范围 UPDATE t6 SET category = concat('新',category) WHERE event_time = '2020-10-29 23:40:00' order by product_id limit 1; -- 查询结果中仅 10001 数据行进行了 UPDATE 变更 select * from t6 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 新男装 │ 10 │ 1000 │ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 2 │ 200 │ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 1 │ 100 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘ -- 通过 UPDATE 语句交换列 UPDATE t6 SET amount=revenue, revenue=amount WHERE event_time = '2020-10-29 23:40:00'; -- 查询结果 amount 、revenue 列进行了交换 select * from t6 order by toDate(event_time), product_id; ┌──────────event_time─┬─product_id─┬─city────┬─category─┬─amount─┬─revenue─┐ │ 2020-10-29 23:40:00 │ 10001 │ Beijing │ 新男装 │ 1000 │ 10 │ │ 2020-10-29 23:40:00 │ 10002 │ Beijing │ 男装 │ 200 │ 2 │ │ 2020-10-29 23:40:00 │ 10003 │ Beijing │ 男装 │ 100 │ 1 │ └─────────────────────┴────────────┴─────────┴──────────┴────────┴─────────┘
Bucket table 是 ByteHouse 在建表的时候的一种性能优化选项,在ByteHouse 中使用 Bucket table 时,系统会依据用户建表语句中提供的一个或者多个列、表达式整理表数据,将相同值的数据聚簇在同一个 bucket number 下,从而在查询计算中获得更好的性能。
非 bucket 级别唯一
当不使用 enable_bucket_level_unique_keys
指定 bucket 级别唯一时。ByteHouse 唯一键表使用表引擎指定的去重范围,默认为分区级别唯一,此时同分区下不同 bucket 不会存在重复数据。
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t7 ( `d` Date, `id` Int32, `s` String ) ENGINE = CnchMergeTree PARTITION BY d CLUSTER BY s INTO 10 BUCKETS ORDER BY s UNIQUE KEY id; INSERT INTO t7 VALUES ('2023-06-26', 1, '1a'), ('2023-06-26', 2, '2a'), ('2023-06-26', 3, '3a'), ('2023-06-26', 3, '3b'), ('2023-06-26', 3, '3c'); select * from t7 order by id; ┌──────────d─┬─id─┬─s──┐ │ 2023-06-26 │ 1 │ 1a │ │ 2023-06-26 │ 2 │ 2a │ │ 2023-06-26 │ 3 │ 3c │ └────────────┴────┴────┘
Bucket 级别唯一
当 cluster by 所需的列在 unique key 字段里都包含时,相同 unique key 的数据一定会落到固定的 bucket 中,可以优化为 bucket 去重;此时 partition_level_unique_keys
用于指定 bucket 的去重范围(分区级/表级)。
Bucket 唯一可以降低去重的数据量,从而提升写入 rps 和 P99 延时;**enable_bucket_level_unique_keys = 1
**可以跳过 part 的 bucket 校验,进一步提升写入效率;
-- 引擎默认保证 unique key 在分区内的唯一性 -- 注:UNIQUE KEY 不支持 Nullable CREATE TABLE t7m ( `d` Date, `id` Int32, `s` String ) ENGINE = CnchMergeTree PARTITION BY d CLUSTER BY id INTO 10 BUCKETS ORDER BY s UNIQUE KEY id SETTINGS enable_bucket_level_unique_keys = 1; INSERT INTO t7m VALUES ('2023-06-26', 1, '1a'), ('2023-06-26', 2, '2a'), ('2023-06-26', 3, '3a'), ('2023-06-26', 3, '3b'), ('2023-06-26', 3, '3c'); select * from t7m order by id; ┌──────────d─┬─id─┬─s──┐ │ 2023-06-26 │ 1 │ 1a │ │ 2023-06-26 │ 2 │ 2a │ │ 2023-06-26 │ 3 │ 3c │ └────────────┴────┴────┘ INSERT INTO t7m VALUES ('2023-06-26', 3, '3d'); select * from t7m order by id; ┌──────────d─┬─id─┬─s──┐ │ 2023-06-26 │ 1 │ 1a │ │ 2023-06-26 │ 2 │ 2a │ │ 2023-06-26 │ 3 │ 3d │ └────────────┴────┴────┘
ByteHouse云数仓版支持部分列更新模式。
在行更新模式时,缺省列采用默认值填充。而在列更新模式下,缺省列如果有原值会保留,否则填充默认值。详情请参见部分列更新。
ByteHouse 唯一键表对标并支持了 MySQL 的不同写入模式。
MySQL DML 语义 | MySQL | ByteHouse 唯一键表 |
---|---|---|
DDL 示例 |
|
|
insert into |
|
|
insert ignore |
┌─a─┬─b─┐ |
┌─a─┬─b─┐ |
replace into |
┌─a─┬─b─┐
Query OK, 2 rows affected (0.11 sec)
┌─a─┬─b─┐ |
┌─a─┬─b─┐
┌─a─┬─b─┐ |