CREATE DATABASE
)CREATE DATABASE [IF NOT EXISTS] db_name [ENGINE=Cnch]
IF NOT EXISTS
如果db_name
数据库已经存在,则ByteHouse不会创建新数据库并且:
建库样例
CREATE DATABASE IF NOT EXISTS test;
CREATE DATABASE IF NOT EXISTS test ENGINE=Cnch;
CREATE TABLE
)CREATE TABLE [IF NOT EXISTS] [db.]table_name ( name1 [type1] [NULL|NOT NULL] [DEFAULT|ALIAS expr1 | MATERIALIZED expr1] [COMMENT 'string'][compression_codec][TTL expr1], name2 [type2] [NULL|NOT NULL] [DEFAULT|ALIAS expr2 | MATERIALIZED expr1] [COMMENT 'string'] [compression_codec] [TTL expr2], ... ) ENGINE = CnchMergeTree() ORDER BY expr [PARTITION BY expr] [CLUSTER BY (column, expression, ...) INTO value1 BUCKETS SPLIT_NUMBER value2 WITH_RANGE] [PRIMARY KEY expr] [UNIQUE KEY expr] [SAMPLE BY expr] [TTL expr] [SETTINGS name=value, ...] [COMMENT='COMMENT']
CREATE TABLE IF NOT EXISTS test.createTable( id UInt32, name String DEFAULT '', CONSTRAINT constraint1 CHECK id > 10 ) ENGINE=CnchMergeTree ORDER BY id
在列定义中,数据类型之后的 NULL 和 NOT NULL 修饰符允许或不允许该列为可空(Nullable)。
如果数据类型不是可空的,并且指定了 NULL,则该列将被视为可空;如果指定了 NOT NULL,则不会。
例如,INT NULL 与 Nullable(INT) 相同。如果数据类型是可空的,并且指定了 NULL 或 NOT NULL 修饰符,则会抛出异常。
列描述可以指定一个默认值表达式,形式为 DEFAULT expr、MATERIALIZED expr 或 ALIAS expr。示例:URLDomain String DEFAULT domain(URL)。
表达式 expr 是可选的。如果省略,则必须明确指定列类型,并且默认值对于数值列为 0,对于字符串列为 ''(空字符串),对于数组列为 [](空数组),对于日期列为 1970-01-01,对于可空列为 NULL。
默认值列的列类型可以省略,这种情况下将从 expr 的类型推断。例如,列 EventDate DEFAULT toDate(EventTime) 的类型将是 date。
如果同时指定了数据类型和默认值表达式,将插入一个隐式类型转换函数,将表达式转换为指定的类型。例如:Hits UInt32 DEFAULT 0 在内部表示为 Hits UInt32 DEFAULT toUInt32(0)。
默认值表达式 expr 可以引用任意表列和常量。ByteHouse 检查表结构的更改不会在表达式计算中引入循环。对于 INSERT 操作,它会检查表达式是否可解——即所有需要计算的列都已提供。
DEFAULT
普通默认值。如果在 INSERT 查询中未指定此类列的值,则其值将根据 expr
计算得出。
CREATE TABLE [db.]test ( id UInt64, updated_at DateTime DEFAULT now(), updated_at_date Date DEFAULT toDate(updated_at) ) ENGINE = CnchMergeTree ORDER BY id; INSERT INTO test (id) Values (1); SELECT * FROM test; ┌─id─┬──────────updated_at─┬─updated_at_date─┐ │1 │ 2023-02-24 17:06:46 │2023-02-24 │ └────┴─────────────────────┴─────────────────┘
物化表达式。此类列的值在插入行时会根据指定的物化表达式自动计算。在INSERT
操作期间,不能显式指定这些列的值。
此外,这种类型的默认值列不会包含在 SELECT *
的结果中。这是为了保持 SELECT *
的结果总是可以使用 INSERT
插入回表中的不变性。此行为可以通过设置 asterisk_include_materialized_columns
来禁用。
CREATE TABLE [db.]test ( id UInt64, updated_at DateTime MATERIALIZED now(), updated_at_date Date MATERIALIZED toDate(updated_at) ) ENGINE = CnchMergeTree ORDER BY id; INSERT INTO test Values (1); SELECT * FROM test; ┌─id─┐ │1 │ └────┘ SELECT id, updated_at, updated_at_date FROM test; ┌─id─┬──────────updated_at─┬─updated_at_date─┐ │1 │ 2023-02-24 17:08:08 │2023-02-24 │ └────┴─────────────────────┴─────────────────┘ SELECT * FROM test SETTINGS asterisk_include_materialized_columns=1; ┌─id─┬──────────updated_at─┬─updated_at_date─┐ │1 │ 2023-02-24 17:08:08 │2023-02-24 │ └────┴─────────────────────┴─────────────────┘
计算列(同义词)。这种类型的列不会存储在表中,也无法向其中插入值。
当 SELECT 查询显式引用这种类型的列时,值是从 expr
在查询时计算得出的。默认情况下,SELECT * 会排除 ALIAS 列。此行为可以通过设置 asterisk_include_alias_columns 来禁用。
使用 ALTER 查询添加新列时,这些列的旧数据不会被写入。相反,在读取没有新列值的旧数据时,默认情况下会即时计算表达式。然而,如果运行这些表达式需要查询中未指明的不同列,这些列也会被额外读取,但仅针对需要的那部分数据块。
如果向表中添加了新列,但后来更改了其默认表达式,对于旧数据使用的值也会改变(对于那些未存储在磁盘上的数据)。请注意,当运行后台合并时,缺少某些列数据的部分会在合并部分中写入这些列的数据。
无法为嵌套数据结构中的元素设置默认值。
CREATE TABLE [db.]test ( id UInt64, size_bytes Int64, size String Alias formatReadableSize(size_bytes) ) ENGINE = CnchMergeTree ORDER BY id; INSERT INTO test Values (1, 4678899); SELECT id, size_bytes, size FROM test; ┌─id─┬─size_bytes─┬─size─────┐ │1 │4678899 │ 4.46 MiB │ └────┴────────────┴──────────┘ SELECT * FROM test SETTINGS asterisk_include_alias_columns=1; ┌─id─┬─size_bytes─┬─size─────┐ │1 │4678899 │ 4.46 MiB │ └────┴────────────┴──────────┘
分区键定义分区,分区是在一个表中通过指定的规则划分而成的逻辑数据集。可以按任意标准进行分区,如按日期。为了减少需要操作的数据,每个分区都是分开存储的。查询时,ByteHouse 尽量使用这些分区的最小子集。建表时候通过 PARTITION BY expr
子句指定。分区键可以是表中列的任意表达式。例如,指定按月分区,表达式为 toYYYYMM(date)
;或者按表达元组,如(toMonday(date), EventType)
等。
需要注意,表中分区表达式计算出的取值范围不能太大(推荐不超过一万),太多分区会占用比较大的内存以及带来比较多的 IO 和计算开销。
合理的设计分区键可以极大减少查询时需要扫描的数据量,一般考虑将查询中最常用的条件同时取值范围不超过一万的列设计为分区键(如日期等)
示例1:由 columnExpr 定义的分区键
CREATE TABLE test.partitionByClause ( VisitDate Date, Hour UInt8, ClientID UUID ) ENGINE = CnchMergeTree PARTITION BY VisitDate ORDER BY Hour;
示例2:在 columnExpr 的元组中定义分区键
CREATE TABLE test.partitionByClause ( VisitDate Date, Hour UInt8, ClientID UUID ) ENGINE = CnchMergeTree PARTITION BY (VisitDate,Hour) ORDER BY Hour;
可以是一组列的元组或任意的表达式。 例如: ORDER BY (OrderID, Date)
。
如果不需要排序,可以使用 ORDER BY tuple()
,DataPart将按照数据插入的顺序存储。
语法
[columnExpr] [ASCENDING|ASC|DESCENDING|DESC] [NULLS [FIRST|LAST] [COLLATE STRING_LITERAL]
[ASCENDING|ASC|DESCENDING|DESC]
:确定排序方向。如果未指定方向,则默认为 ASC
。[NULLS [FIRST|LAST]
:确定 NaN
和 NULL
的排序顺序。NULLS LAST
修饰符时:先是值,然后是 NaN
,最后是 NULL
。NULLS FIRST
修饰符时:先是 NULL
,然后是 NaN
,最后是其他值。[COLLATE STRING_LITERAL]
:对于按 String 值排序,可以指定排序规则。COLLATE
时,排序始终不区分大小写。COLLATE
,因为使用 COLLATE
的排序效率低于按字节的正常排序。示例1:按多个 orderExpr 排序
CREATE TABLE IF NOT EXISTS test.orderByClause (id UInt32, val UInt32) ENGINE=CnchMergeTree() ORDER BY id; INSERT INTO TABLE test.orderByClause VALUES (1,5),(1,15),(2,5),(2,15); SELECT * FROM test.orderByClause ORDER BY id ASC, val DESC;
示例2:按 NULL FIRST排序
CREATE TABLE IF NOT EXISTS test.orderByClause (id UInt32, val Nullable(UInt32)) ENGINE=CnchMergeTree() ORDER BY id; INSERT INTO test.orderByClause VALUES (1,2),(2,NULL),(3,2),(3,3),(3,NULL); SELECT * FROM test.orderByClause ORDER BY val DESC NULLS FIRST;
示例3:按COLLATE排序
CREATE TABLE IF NOT EXISTS test.orderByClause (x UInt32, s Nullable(String)) ENGINE=CnchMergeTree ORDER BY x; INSERT INTO test.orderByClause VALUES (1,'bca'),(2,NULL),(3,'ABC'),(4,'123a'),(5,'abc'),(6,NULL),(7,'BCA'); SELECT * FROM test.orderByClause ORDER BY s ASC COLLATE 'ru';
默认情况不需要显式指定,ByteHouse 将使用排序键作为主键。当有特殊场景主键和排序键不一致时,主键必须为排序键的最左前缀。如排序键为(OrderID, Date),主键必须为OrderID,不能为Date。
ByteHouse 会在主键上建立以 Granule 为单位的稀疏索引,(与之对比,所谓稠密索引则是每一行都会建立索引信息)。
如果查询条件能匹配主键索引的最左前缀,通过主键索引可以快速过滤出可能需要读取的数据颗粒,相比扫描整个 DataPart,通常要高效很多。
另外需要注意,PRIMARY KEY不能保证唯一性,所以可以插入主键重复的数据行。
分区(PARTITION BY)和主键(PRIMARY KEY)是两种不同的加速数据查询的方式,定义的时候应当尽量错开使用不同的列来定义两者,来覆盖更多的查询场景。例如order by的第一个列一定不要重复放到partition by里。下面是如何选择主键的一些考虑:
示例
CREATE TABLE IF NOT EXISTS test.sampleByClause ( id UInt32 ) ENGINE=CnchMergeTree ORDER BY id SAMPLE BY id;
主键(PRIMARY KEY)不能保证去重,如果有唯一键去重的需求,需要在建表时设置唯一键索引。设置唯一键之后,ByteHouse 提供 upsert 更新写语义,可以根据唯一键高效更新数据行,或者在upsert的时候通过设置虚拟列 delete_flag
=1
,可以用来删除指定的 key。查询自动返回每个唯一键的最新值。详情可参考 Upsert 使用示例。
唯一键可以是一组列的元组或任意的表达式,如UNIQUE KEY (product_id, sipHash64(city))
。
通过唯一键查询时会用上唯一键索引过滤数据加速查询,所以通常主键可以设置和唯一键不一样列,覆盖更多的查询条件。不过如果要使用部分列更新功能的话,是需要唯一键为排序键的最左前缀。
示例1:单字段Unique Key
CREATE TABLE test.uniqueKeyClause ( `event_date` Date, `order_id` UInt64, `state` UInt32, `amount` UInt64 ) ENGINE = CnchMergeTree PARTITION BY event_date ORDER BY state UNIQUE KEY order_id; -- rows with the same key will replace previous rows INSERT INTO test.uniqueKeyClause VALUES ('2021-03-01', 1001, 1, 100), ('2021-03-01', 1002, 1, 200), ('2021-03-01', 1001, 2, 100), ('2021-03-02', 1001, 1, 400); SELECT * FROM test.uniqueKeyClause ORDER BY event_date, order_id; ┌─event_date─┬─order_id─┬─state─┬─amount─┐ │ 2021-03-01 │ 1001 │ 2 │100 │ │ 2021-03-01 │ 1002 │ 1 │200 │ │ 2021-03-02 │ 1001 │ 1 │400 │ └────────────┴──────────┴───────┴────────┘ INSERT INTO test.uniqueKeyClause VALUES ('2021-03-01', 1002, 2, 200), ('2021-03-02', 1001, 2, 400), ('2021-03-02', 1002, 1, 300); SELECT * FROM test.uniqueKeyClause ORDER BY event_date, order_id; ┌─event_date─┬─order_id─┬─state─┬─amount─┐ │ 2021-03-01 │ 1001 │ 2 │100 │ │ 2021-03-01 │ 1002 │ 2 │200 │ │ 2021-03-02 │ 1001 │ 2 │400 │ │ 2021-03-02 │ 1002 │ 1 │300 │ └────────────┴──────────┴───────┴────────┘
示例2:多字段Unique Key
CREATE TABLE test.uniqueKeyClause ( `event_date` Date, `region` UInt64, `state` String, `amount` UInt64 ) ENGINE = CnchMergeTree PARTITION BY event_date ORDER BY state UNIQUE KEY (region, state);
分桶常用于以下场景,具体请参考 应用案例。
注意
更改现有表以添加存储桶只会影响新分区,但不会影响现有分区。
分桶应用案例
假设某 ByteHouse 订阅用户启用了六个计算节点,由于单个分区的数据量较大,超过2亿条记录,应用程序经常根据c1
和c2
字段进行聚合和连接操作。 因此,决定使用桶表进行优化。 桶表的设计选项如下:
c1
和c2
列作为分桶键。-- 创建带有分桶的表 create table with bucketing create or replace table table_01 (c1 timestamp, c2 string, c3 number) cluster by (to_date(c1), c2) INTO 12 BUCKETS; -- 将桶添加到现有数据中 add bucket to existing data ALTER TABLE t CLUSTER BY (column, expression, ...) INTO 64 BUCKETS -- 按多列将桶添加到集群中 add bucket to cluster by multiple columns ALTER TABLE t CLUSTER BY sipHash(a,b,c) INTO 64 BUCKETS -- 添加或更改集群属性 Add or change the cluster properties ALTER TABLE t MODIFY CLUSTER BY (column, expression, ...) INTO 64 BUCKETS
用于抽样的表达式,该配置为可选项。
如果要用抽样表达式,主键中必须包含这个表达式。例如: SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))
。
指定行存储的持续时间并定义数据片段在硬盘和卷上的移动逻辑的规则列表,可选项。表达式中必须存在至少一个 Date
或 DateTime
类型的列,比如:TTL date + INTERVAl 1 DAY
。
CREATE TABLE test.ttlClause ( VisitDate Date, Hour UInt8, ClientID UUID ) ENGINE = CnchMergeTree ORDER BY Hour PARTITION BY VisitDate TTL VisitDate + INTERVAL 1 DAY;
compression_codec字段可以用于配置编解码器,该配置为可选项,默认值为 LZ4。
ByteHouse支持通用目的编码和特定编码,通用编解码器更像默认编解码器(LZ4, ZTSD)及其修改版本。特定编解码器是为了利用数据的特定特征使压缩更有效而设计的。
通用编码
特定编码算法
多编解码器
举例参考:
CREATE TABLE codec_example (date Date CODEC(Delta, ZSTD), ts DateTime CODEC(LZ4HC), float_value Float32 CODEC(NONE), double_value Float64 CODEC(LZ4HC(9)) ) ENGINE = CnchMergeTree PARTITION BY tuple() ORDER BY date
表引擎即表的类型,决定了:
ByteHouse 云数仓版最常用的表引擎是 CnchMergeTree,除此之外也有其他特殊类型的表引擎包括 Hive外表、Kafka表等。本文重点分享 CnchMergeTree 表引擎的原理。
CNCHMergeTree 是最常用的表引擎,核心思想和LSM-Tree类似,数据按分区键(partition by)进行分区,然后排序键(order by)进行有序存储。主要有如下特点:
如果指定了分区键的话,数据会按分区键划分成了不同的逻辑数据集(逻辑分区,Partition)。
每一个逻辑分区可以存在零到多个数据片段(DataPart)。如果查询条件可以裁剪分区,通常可以加速查询。如果没有指定分区键,全部数据都在一个逻辑分区里。
数据片段里的数据按排序键排序。每个数据片段还会存在一个min/max索引,来加速分区选择。
每个数据片段被逻辑的分割成颗粒(granule),默认的Granule为8192行(由表的index_granularity配置决定)。颗粒是 ByteHouse 中进行数据查询时的最小不可分割数据集。每个颗粒的第一行通过该行的主键值进行标记, ByteHouse 会为每个数据片段创建一个索引文件来存储这些标记。对于每列,无论它是否包含在主键当中,ByteHouse 都会存储类似标记。这些标记让您可以在列文件中直接找到数据。Granule作为ByteHouse 稀疏索引的索引目标,也是在内存中进行数据扫描的单位。
后台任务会定时对同一个分区的DataPart进行合并,并保持按排序键有序。后台的合并减少了 Part 的数目,以便更高效存储,并提升了查询性能。
注意
MySQL兼容语法,推荐在相关方言下使用。
ByteHouse CDW 2.1兼容了大部分MySQL的DDL,可以直接使用MySQL的建表语句在ByteHouse中执行。请参考以下样例
CREATE TABLE `table1` ( `uid` bigint NOT NULL AUTO_INCREMENT, `phone` varchar(255) DEFAULT NULL, `no1` bigint DEFAULT NULL, `appleid` varchar(255) DEFAULT NULL, PRIMARY KEY (`uid`) USING BTREE, UNIQUE KEY `no1` (`no1`) USING BTREE, KEY `idx_apple_id` (`appleid`) USING BTREE, KEY `phone` (`phone`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=60937910 DEFAULT CHARSET=utf8mb3;
CREATE TABLE table1 ( `uid` Int64 NOT NULL, `phone` String NULL DEFAULT NULL, `no1` Int64 NULL DEFAULT NULL, `appleid` String NULL DEFAULT NULL ) ENGINE = CnchMergeTree ORDER BY tuple(uid) UNIQUE KEY tuple(uid) SETTINGS partition_level_unique_keys = 0, storage_policy = 'cnch_default_hdfs', allow_nullable_key = 1, storage_dialect_type = 'MYSQL', index_granularity = 8192
可以注意到以下转换逻辑
MySQL | ByteHouse | 说明 | |
---|---|---|---|
列属性 | 数据类型 | 兼容 | |
AUTO_INCREMENT | 忽略 | ByteHouse目前仅在语法上支持自增,功能上忽略,依赖数据源生成相应数据。将在未来版本支持。 | |
NULL | 兼容 | ||
DEFAULT | 兼容 | ||
其余定义 | 兼容 | ByteHouse兼容大部分常用字段定义 | |
表属性 | 主键:PRIMARY KEY | 唯一键:UNIQUE KEY | MySQL语义中的primary key对应ByteHouse的unique key, 而不是primary key |
排序键:ORDER BY | 默认对MySQL的primary key排序 | ||
UNIQUE KEY | 忽略 | ByteHouse在语法上支持Constraint,功能上忽略 | |
其余定义: | 忽略 | 忽略 | |
设置 | 无 | 设置 | 在MySQL方言下建表时,ByteHouse会默认增加以下设置
|
说明
ByteHouse对于其他数据库语法也提供一定的兼容性。如您感兴趣,请联系我们。
创建一个表,该表的结构与SELECT查询的结果类似,使用指定的引擎engine,并使用SELECT的数据填充它。
CREATE TABLE [IF NOT EXISTS] [db.]table_name[(name1 [type1], name2 [type2], ...)] ENGINE = CnchMergeTree ORDER BY expr [PARTITION BY expr] AS SELECT ...
如果表已经存在并且指定了IF NOT EXISTS子句,则查询不会执行任何操作。在查询中的ENGINE子句之后可以有其他子句。
示例
CREATETABLE DB2.unique_user_events ( user_id UInt32, event_timeDateTime, event_typeString, event_dataString ) ENGINE = CnchMergeTree() ORDER BY user_id PRIMARY KEY user_id UNIQUE KEY event_time; INSERT into DB2.unique_user_events(user_id,event_time,event_type,event_data) values(10112,'2024-08-28 05:11:58','ERROR','this is a bug'); INSERT into DB2.unique_user_events(user_id,event_time,event_type) values(10112,'2024-07-28 05:11:58','ERROR'); INSERT into DB2.unique_user_events(user_id,event_time,event_data) values(10112,'2024-07-28 05:11:58','this is a bug'); CREATE TABLE IF NOT EXISTS DB2.table_name2(user_id UInt32,event_time DateTime,event_data String) ENGINE = CnchMergeTree ORDER BY user_id AS select user_id,event_time,event_data from DB2.unique_user_events limit 100; select * from DB2.table_name2;
CREATE VIEW
)创建一个新视图, 有两种类型的视图:普通视图,物化视图
普通视图不存储任何数据。 他们只是在每次访问时从另一个表执行读取。换句话说,普通视图只不过是一个保存的查询。 从视图中读取时,此保存的查询用作FROM子句中的子查询.
语法
CREATE [OR REPLACE] VIEW [IF NOT EXISTS] [tableIdentifier] [UUID uuid] AS [selectUnionStmt]
OR REPLACE
,ByteHouse 将创建一个新视图并替换具有相同名称的旧视图。IF NOT EXISTS
子句,当表已经存在时,查询不会返回错误。UUID
,表将使用用户提供的 UUID。否则,将使用生成的 UUID。创建视图
-- Step 1: create a table CREATE TABLE `example_table` ( `order_by_column` String ) ENGINE = `CnchMergeTree` ORDER BY (`order_by_column`) -- step 2: create a view based on the created table CREATE VIEW example_view AS SELECT * FROM example_table
查询视图
SELECT a, b, c FROM example_view
详见物化视图章节
CREATE DICTIONARY
)ByteHouse 数据字典分为 内置字典 和 外部扩展字典 :
本文重点介绍如何操作 ByteHouse 外部扩展字典。
您可以直接在SQL工作台使用DDL语句来新建字典表。
CREATE DICTIONARY [IF NOT EXISTS] [db.]dictionary_name ( key1 type1[DEFAULT|EXPRESSION expr1] [HIERARCHICAL|INJECTIVE|IS_OBJECT_ID], key2 type2[DEFAULT|EXPRESSION expr2] [HIERARCHICAL|INJECTIVE|IS_OBJECT_ID], attr1 type2 [DEFAULT|EXPRESSION expr3], attr2 type2 [DEFAULT|EXPRESSION expr4] ) PRIMARY KEY key1, key2 SOURCE(SOURCE_NAME([param1 value1 ... paramN valueN])) LAYOUT(LAYOUT_NAME([param_name param_value])) LIFETIME({MIN min_val MAX max_val | max_val})
通过使用dictGet / dictGetOrDefault 语句和函数获取数据字典中的数据。
dictGet('db.dict_name', 'attr_name', id_expr) dictGetOrDefault('db.dict_name', 'attr_name', id_expr, default_value_expr)
说明
dictGet函数的第一个参数的格式必须类似于 db.dict_name,因为字典缓存通过该字段来索引。
ByteHouse将删除 Catalog中该字典的记录。
DROP DICTIONARY [IF EXISTS] [db.]dictionary_name;
SHOW DICTIONARIES [FROM <db>];
如果未指定 FROM
子句,则查询将返回当前数据库中的词典列表。
SHOW CREATE DICTIONARY [db.] dictionary_name
该查询语句返回一个字符串类型的“statement”列,该列包含一个值——用于创建指定对象的CREATE查询语句。