当您需要在不同业务场景中使用云数据库 SelectDB 版时,深入了解其数据模型对于您的业务设计具有重要帮助。本文档将指导您了解其数据模型,以帮助您设计出更优的数据存储方案。
基本概念
在云数据库 SelectDB 版中,数据以表(Table)的形式进行逻辑上的描述。 一张表包括行(Row)和列(Column),Row即您数据表中的一行数据,Column用于描述一行数据中不同的字段。
Column可以分为两大类:Key和Value。从业务角度看,Key和Value可以分别对应维度列和指标列。在SelectDB建表语句的列中,关键字DUPLICATE KEY
、AGGREGATE KEY
和UNIQUE KEY
指定的列即是Key列,其他列是Value列。
上述关键字对应SelectDB中的3种数据模型,本文将对这些数据模型进行详细介绍:
Aggregate模型
Unique模型
Duplicate模型
Aggregate模型
对于写入的数据,SelectDB会根据不同数据模型,根据建表所选不同模型的Key列中,数据相同的行(Row)进行不同的处理。对Aggregate模型而言,指定的所有Key列数据相同的行,多行数据会进行合并,Value列按照建表时字段定义中设置的AggregationType进行预聚合,最终只保留一行数据。
这意味着Aggregate模型可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合报表类统计分析场景。该模型对count(*)
查询不友好,因为固定了Value列上的聚合方式,在进行其他类型的聚合查询时,需要考虑语义正确性。
通过以下示例来阐述什么是聚合模型,以及如何正确地使用Aggregate聚合模型。
示例1:导入数据聚合
数据表example_tbl1
的结构定义表。
ColumnName | Type | AggregationType | Comment |
user_id | LARGEINT | 无 | 用户ID |
date | DATE | 无 | 数据写入日期 |
city | VARCHAR(20) | 无 | 用户所在城市 |
age | SMALLINT | 无 | 用户年龄 |
sex | TINYINT | 无 | 用户性别 |
last_visit_date | DATETIME | REPLACE | 用户最后一次访问时间 |
cost | BIGINT | SUM | 用户总消费 |
max_dwell_time | INT | MAX | 用户最大停留时间 |
min_dwell_time | INT | MIN | 用户最小停留时间 |
创建表example_tbl1
(省略建表语句中的Partition和Distribution信息),示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl1
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据写入日期时间",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16;
这是一个典型的用户信息和访问行为的事实表。在一般星型模型中,用户信息和访问行为一般分别存放在维度表和事实表中。这里我们为了更加方便的解释SelectDB的数据模型,将两部分信息统一存放在一张表中。
表中的列按照是否设置AggregationType
,分为Key(维度列)和Value(指标列)。没有设置AggregationType
的,如user_id
、date
、age
等称为Key,而设置了AggregationType
的称为Value。当我们导入数据时,对于指定的所有Key列值完全相同时,多行数据会进行合并,而Value列会按照设置的AggregationType
进行聚合,最终只保留一行数据。
AggregationType
目前有如下聚合方式。
聚合方式参数 | 参数说明 |
SUM | 求和。适用数值类型。 |
MIN | 求最小值。适合数值类型。 |
MAX | 求最大值。适合数值类型。 |
REPLACE | 替换。对于维度列相同的行,指标列会按照导入的先后顺序,后导入的替换先导入的。 |
REPLACE_IF_NOT_NULL | 非空值替换。和REPLACE的区别在于对于null值,不做替换。这里要注意的是字段默认值要给NULL,而不能是空字符串,如果是空字符串,会给你替换成空字符串。 |
HLL_UNION | HLL类型的列的聚合方式,通过HyperLogLog算法聚合。 |
BITMAP_UNION | BITMAP类型的列的聚合方式,进行位图的并集聚合。 |
向example_tbl1
表中写入如下数据。
user_id | date | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 北京 | 20 | 0 | 2017-10-01 06:00:00 | 20 | 10 | 10 |
10000 | 2017-10-01 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 15 | 2 | 2 |
10001 | 2017-10-01 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 深圳 | 35 | 0 | 2017-10-03 10:20:22 | 11 | 6 | 6 |
通过SQL导入数据,示例如下。
INSERT INTO example_db.example_tbl_agg1 VALUES
(10000,"2017-10-01","北京",20,0,"2017-10-01 06:00:00",20,10,10),
(10000,"2017-10-01","北京",20,0,"2017-10-01 07:00:00",15,2,2),
(10001,"2017-10-01","北京",30,1,"2017-10-01 17:05:45",2,22,22),
(10002,"2017-10-02","上海",20,1,"2017-10-02 12:59:12",200,5,5),
(10003,"2017-10-02","广州",32,0,"2017-10-02 11:20:00",30,11,11),
(10004,"2017-10-01","深圳",35,0,"2017-10-01 10:00:15",100,3,3),
(10004,"2017-10-03","深圳",35,0,"2017-10-03 10:20:22",11,6,6);
数据写入到SelectDB后,SelectDB中最终存储结果如下。
user_id | date | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 35 | 10 | 2 |
10001 | 2017-10-01 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 深圳 | 35 | 0 | 2017-10-03 10:20:22 | 11 | 6 | 6 |
通过最终存储结果分析,用户ID为10000的用户只剩下了一行聚合后的数据。而其余用户的数据和原始数据保持一致。用户ID为10000的用户数据聚合后的数据解释如下。
前5列没有变化,从第6列last_visit_date
开始。
2017-10-01 07:00:00
:因为last_visit_date
列的聚合方式为REPLACE,所以2017-10-01 07:00:00
替换了2017-10-01 06:00:00
保存了下来。说明在同一个导入批次中的数据,对于REPLACE这种聚合方式,替换顺序不做保证。如在这个例子中,最终保存下来的,也有可能是
2017-10-01 06:00:00
。而对于不同导入批次中的数据,可以保证,后一批次的数据会替换前一批次。35
:因为cost
列的聚合类型为SUM,所以由20+15累加获得35。10
:因为max_dwell_time
列的聚合类型为MAX,所以10和2取最大值,获得10。2
:因为min_dwell_time
列的聚合类型为MIN,所以10和2取最小值,获得2。
经过聚合,SelectDB中最终只会存储聚合后的数据。即明细数据将丢失,您将无法再查询到聚合前的明细数据。
示例2:导入数据与已有数据聚合
创建表example_tbl2
(省略建表语句中的Partition和Distribution信息),示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl2
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据写入日期时间",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16;
向example_tbl2
表中写入如下数据。
user_id | date | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 35 | 10 | 2 |
10001 | 2017-10-01 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 深圳 | 35 | 0 | 2017-10-03 10:20:22 | 11 | 6 | 6 |
通过SQL导入数据,示例如下。
INSERT INTO test.example_tbl2 VALUES
(10000,"2017-10-01","北京",20,0,"2017-10-01 06:00:00",20,10,10),
(10000,"2017-10-01","北京",20,0,"2017-10-01 07:00:00",15,2,2),
(10001,"2017-10-01","北京",30,1,"2017-10-01 17:05:45",2,22,22),
(10002,"2017-10-02","上海",20,1,"2017-10-02 12:59:12",200,5,5),
(10003,"2017-10-02","广州",32,0,"2017-10-02 11:20:00",30,11,11),
(10004,"2017-10-01","深圳",35,0,"2017-10-01 10:00:15",100,3,3),
(10004,"2017-10-03","深圳",35,0,"2017-10-03 10:20:22",11,6,6);
再向表example_tbl2
中写入如下数据。
user_id | date | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10004 | 2017-10-03 | 深圳 | 35 | 0 | 2017-10-03 11:22:00 | 44 | 19 | 19 |
10005 | 2017-10-03 | 长沙 | 29 | 1 | 2017-10-03 18:11:02 | 3 | 1 | 1 |
通过SQL导入数据,示例如下。
INSERT INTO test.example_tbl2 VALUES
(10004,"2017-10-03","深圳",35,0,"2017-10-03 11:22:00",44,19,19),
(10005,"2017-10-03","长沙",29,1,"2017-10-03 18:11:02",3,1,1);
数据写入到SelectDB后,SelectDB中最终存储结果如下。
user_id | date | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 35 | 10 | 2 |
10001 | 2017-10-01 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 深圳 | 35 | 0 | 2017-10-03 11:22:00 | 55 | 19 | 6 |
10005 | 2017-10-03 | 长沙 | 29 | 1 | 2017-10-03 18:11:02 | 3 | 1 | 1 |
通过最终存储结果分析,用户ID为10004的用户的已有数据和新导入的数据发生了聚合。同时新增了用户ID为10005用户数据。
数据的聚合,在SelectDB中发生在如下三个阶段。
每一批次数据导入的ETL阶段。该阶段会在每一批次导入的数据内部进行聚合。
计算集群进行数据Compaction的阶段。该阶段,计算集群会对已导入的不同批次的数据进行进一步的聚合。
数据查询阶段。在数据查询时,对于查询涉及到的数据,会进行对应的聚合。
数据在不同时间聚合的程度可能不一致。比如一批数据刚导入时,可能还未与之前已存在的数据进行聚合。但是对于您而言,您只能查询到聚合后的数据。即不同的聚合程度对于您的查询而言是透明的。您只需要始终认为数据以最终完成的聚合程度存在,而不需要假设某些聚合还未发生。
示例3:保留明细数据
数据表example_tbl3
的结构定义表如下。
ColumnName | Type | AggregationType | Comment |
user_id | LARGEINT | 无 | 用户ID |
date | DATE | 无 | 数据写入日期 |
timestamp | DATETIME | 无 | 数据写入时间,精确到秒 |
city | VARCHAR(20) | 无 | 用户所在城市 |
age | SMALLINT | 无 | 用户年龄 |
sex | TINYINT | 无 | 用户性别 |
last_visit_date | DATETIME | REPLACE | 用户最后一次访问时间 |
cost | BIGINT | SUM | 用户总消费 |
max_dwell_time | INT | MAX | 用户最大停留时间 |
min_dwell_time | INT | MIN | 用户最小停留时间 |
增加了一列timestamp
,记录精确到秒的数据写入时间。 同时,将AGGREGATE KEY
设置为AGGREGATE KEY(user_id, date, timestamp, city, age, sex)
创建表example_tbl3
(省略建表语句中的Partition和Distribution信息),示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl3
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据写入日期时间",
`timestamp` DATETIME NOT NULL COMMENT "数据写入时间,精确到秒",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16;
向表example_tbl3
中写入如下数据。
user_id | date | timestamp | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 2017-10-01 08:00:05 | 北京 | 20 | 0 | 2017-10-01 06:00:00 | 20 | 10 | 10 |
10000 | 2017-10-01 | 2017-10-01 09:00:05 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 15 | 2 | 2 |
10001 | 2017-10-01 | 2017-10-01 18:12:10 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 2017-10-02 13:10:00 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 2017-10-02 13:15:00 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 2017-10-01 12:12:48 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 2017-10-03 12:38:20 | 深圳 | 35 | 0 | 2017-10-03 10:20:22 | 11 | 6 | 6 |
通过SQL导入数据,示例如下。
INSERT INTO test.example_tbl3 VALUES
(10000,"2017-10-01","2017-10-01 08:00:05","北京",20,0,"2017-10-01 06:00:00",20,10,10),
(10000,"2017-10-01","2017-10-01 09:00:05","北京",20,0,"2017-10-01 07:00:00",15,2,2),
(10001,"2017-10-01","2017-10-01 18:12:10","北京",30,1,"2017-10-01 17:05:45",2,22,22),
(10002,"2017-10-02","2017-10-02 13:10:00","上海",20,1,"2017-10-02 12:59:12",200,5,5),
(10003,"2017-10-02","2017-10-02 13:15:00","广州",32,0,"2017-10-02 11:20:00",30,11,11),
(10004,"2017-10-01","2017-10-01 12:12:48","深圳",35,0,"2017-10-01 10:00:15",100,3,3),
(10004,"2017-10-03","2017-10-03 12:38:20","深圳",35,0,"2017-10-03 10:20:22",11,6,6);
数据写入到SelectDB后,SelectDB中最终存储结果如下。
user_id | date | timestamp | city | age | sex | last_visit_date | cost | max_dwell_time | min_dwell_time |
10000 | 2017-10-01 | 2017-10-01 08:00:05 | 北京 | 20 | 0 | 2017-10-01 06:00:00 | 20 | 10 | 10 |
10000 | 2017-10-01 | 2017-10-01 09:00:05 | 北京 | 20 | 0 | 2017-10-01 07:00:00 | 15 | 2 | 2 |
10001 | 2017-10-01 | 2017-10-01 18:12:10 | 北京 | 30 | 1 | 2017-10-01 17:05:45 | 2 | 22 | 22 |
10002 | 2017-10-02 | 2017-10-02 13:10:00 | 上海 | 20 | 1 | 2017-10-02 12:59:12 | 200 | 5 | 5 |
10003 | 2017-10-02 | 2017-10-02 13:15:00 | 广州 | 32 | 0 | 2017-10-02 11:20:00 | 30 | 11 | 11 |
10004 | 2017-10-01 | 2017-10-01 12:12:48 | 深圳 | 35 | 0 | 2017-10-01 10:00:15 | 100 | 3 | 3 |
10004 | 2017-10-03 | 2017-10-03 12:38:20 | 深圳 | 35 | 0 | 2017-10-03 10:20:22 | 11 | 6 | 6 |
存储的数据和导入的数据完全一样,没有发生任何聚合。因为这批数据中加入了timestamp
列,导致每一行的Key都不完全相同。只要确保导入的数据中,每一行的Key都不完全相同,即使在聚合模型下,也可以保存完整的明细数据。
Unique模型
在某些多维分析场景下,更关注的是如何保证Key的唯一性,即如何获得主键唯一性约束。因此,云数据库 SelectDB 版引入了Unique表引擎。在早期版本中,该模型本质上是聚合模型的一个特例,也是一种简化的表结构表示方式,由于聚合模型的实现方式是读时合并MOR(Merge on Read),因此在一些聚合查询上性能不佳。在3.0版本SelectDB引入了Unique模型新的实现方式,写时合并MOW(Merge on Write),通过在写入时做一些额外的工作,实现最优的查询性能。
对Unique模型而言,指定的所有Key列数据相同时多行数据会进行覆盖,仅保留最新导入的行,提供类似关系型数据库中的唯一性约束。
Unique模型针对需要唯一性约束的场景,提供了主键唯一性约束,可用于满足订单等关系型数据分析场景。对于聚合查询有较高性能需求的场景,推荐使用新版本引入的写时合并实现,但是该表引擎无法利用 ROLLUP等预聚合带来的查询优势。
在Unique数据模型中,推荐使用写时合并的实现方式。
我们将通过以下示例来阐述两种不同的实现方式。
写时合并(MOW)
Unqiue表引擎的写时合并实现,与聚合表引擎就是完全不同的两种模型,查询性能更接近于上文的Duplicate模型,在有主键约束需求的场景上相比聚合模型有较大的查询性能优势,尤其是在聚合查询以及需要用索引过滤大量数据的查询中。
写时合并在3.0版本中,作为一个新的特性,默认关闭,您可以通过如下属性property来开启。
"enable_unique_key_merge_on_write" = "true"
数据表example_tbl6
的结构定义表。
ColumnName | Type | AggregationType | Comment |
user_id | BIGINT | 无 | 用户ID |
username | VARCHAR(50) | 无 | 用户昵称 |
city | VARCHAR(20) | NONE | 用户所在城市 |
age | SMALLINT | NONE | 用户年龄 |
sex | TINYINT | NONE | 用户性别 |
phone | LARGEINT | NONE | 用户电话 |
address | VARCHAR(500) | NONE | 用户住址 |
register_time | DATETIME | NONE | 用户注册时间 |
创建表example_tbl6
(省略建表语句中的Partition和Distribution信息),修改表property属性。示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl6
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`username` VARCHAR(50) NOT NULL COMMENT "用户昵称",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`phone` LARGEINT COMMENT "用户电话",
`address` VARCHAR(500) COMMENT "用户地址",
`register_time` DATETIME COMMENT "用户注册时间"
)
UNIQUE KEY(`user_id`, `username`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES (
"enable_unique_key_merge_on_write" = "true"
);
这种建表语句所建立的表结构与聚合表引擎完全不同。
在开启了写时合并选项的Unique表上,数据在导入阶段就会去将被覆盖和被更新的数据进行标记删除,同时将新的数据写入新的文件。在查询的时候,所有被标记删除的数据都会在文件级别被过滤掉,读取出来的数据就都是最新的数据。这消除掉了读时合并中的数据聚合过程,并且能够在很多情况下支持多种谓词的下推。因此在许多场景都能带来比较大的性能提升,尤其是在有聚合查询的情况下。
新的Merge-on-Write实现默认关闭,且只能在建表时通过指定属性property的方式打开。
旧的Merge-on-Read的实现无法无缝升级到新版本的实现(数据组织方式完全不同),如果需要改为写时合并的实现,需要手动执行
INSERT INTO unique-mow-table SELECT * FROM source_table
。在Unique引擎上独有的delete sign和sequence col,在写时合并的新版实现中仍可以正常使用,用法没有变化。
读时合并(MOR)
数据表example_tbl4
的结构定义表。
ColumnName | Type | IsKey | Comment |
user_id | BIGINT | Yes | 用户ID |
username | VARCHAR(50) | Yes | 用户昵称 |
city | VARCHAR(20) | No | 用户所在城市 |
age | SMALLINT | No | 用户年龄 |
sex | TINYINT | No | 用户性别 |
phone | LARGEINT | No | 用户电话 |
address | VARCHAR(500) | No | 用户住址 |
register_time | DATETIME | No | 用户注册时间 |
这是一个典型的用户基础信息表。这类数据没有聚合需求,只需保证主键唯一性。(这里的主键为user_id+username)。
创建表example_tbl4
(省略建表语句中的Partition和Distribution信息),示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl4
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`username` VARCHAR(50) NOT NULL COMMENT "用户昵称",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`phone` LARGEINT COMMENT "用户电话",
`address` VARCHAR(500) COMMENT "用户地址",
`register_time` DATETIME COMMENT "用户注册时间"
)
UNIQUE KEY(`user_id`, `username`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16;
数据表example_tbl5
的结构定义表。
ColumnName | Type | AggregationType | Comment |
user_id | BIGINT | 无 | 用户ID |
username | VARCHAR(50) | 无 | 用户昵称 |
city | VARCHAR(20) | REPLACE | 用户所在城市 |
age | SMALLINT | REPLACE | 用户年龄 |
sex | TINYINT | REPLACE | 用户性别 |
phone | LARGEINT | REPLACE | 用户电话 |
address | VARCHAR(500) | REPLACE | 用户住址 |
register_time | DATETIME | REPLACE | 用户注册时间 |
创建表example_tbl5
(省略建表语句中的Partition和Distribution信息),示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl5
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`username` VARCHAR(50) NOT NULL COMMENT "用户昵称",
`city` VARCHAR(20) REPLACE COMMENT "用户所在城市",
`age` SMALLINT REPLACE COMMENT "用户年龄",
`sex` TINYINT REPLACE COMMENT "用户性别",
`phone` LARGEINT REPLACE COMMENT "用户电话",
`address` VARCHAR(500) REPLACE COMMENT "用户地址",
`register_time` DATETIME REPLACE COMMENT "用户注册时间"
)
AGGREGATE KEY(`user_id`, `username`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16;
上述表example_tbl4
的表结构,完全等同于使用聚合表引擎example_tbl5
的表结构。
即Unique引擎的读时合并实现完全可以用聚合模型中的REPLACE方式替代。其内部的实现方式和数据存储方式也完全一样。
Duplicate模型
在某些多维分析场景下,数据既没有主键,也没有聚合需求。因此,引入Duplicate数据模型来满足这类需求。
对Duplicate模型而言,指定的所有Key列数据相同时多行数据同时存储在系统中,互不影响,没有预聚合、唯一性约束。
Duplicate模型适合任意维度的Ad-hoc查询,可用于满足日志等明细数据分析场景。该表引擎无法利用预聚合带来的性能提升、唯一性约束带来的自动更新便利性。
我们将通过以下示例来阐述Duplicate模型的实现方式。
数据表example_tbl7
的结构定义表。
ColumnName | Type | SortKey | Comment |
timestamp | DATETIME | Yes | 日志时间 |
type | INT | Yes | 日志类型 |
error_code | INT | Yes | 错误码 |
error_msg | VARCHAR(1024) | No | 错误详细信息 |
op_id | BIGINT | No | 负责人ID |
op_time | DATETIME | No | 处理时间 |
创建表example_tbl7
(省略建表语句中的Partition和Distribution信息)。示例如下。
CREATE TABLE IF NOT EXISTS test.example_tbl7
(
`timestamp` DATETIME NOT NULL COMMENT "日志时间",
`type` INT NOT NULL COMMENT "日志类型",
`error_code` INT COMMENT "错误码",
`error_msg` VARCHAR(1024) COMMENT "错误详细信息",
`op_id` BIGINT COMMENT "负责人id",
`op_time` DATETIME COMMENT "处理时间"
)
DUPLICATE KEY(`timestamp`, `type`, `error_code`)
DISTRIBUTED BY HASH(`type`) BUCKETS 16;
这种表模型区别于Aggregate和Unique模型,数据完全按照导入文件中的数据进行存储,不会有任何聚合。即使两行数据完全相同,也都会保留。 而在建表语句中指定的DUPLICATE KEY,只是用来指明底层数据按照那些列进行排序。在DUPLICATE KEY的选择上,建议适当的选择前2-4列即可。
数据模型比较
Aggregate表引擎的局限性
在聚合表引擎中,模型对外展现的是最终聚合后的数据。也就是说,任何还未聚合的数据(比如说两个不同导入批次的数据),必须通过某种方式保证对外展示的一致性。通过以下示例来进行说明。
表example_tbl8
结构如下。
ColumnName | Type | AggregationType | Comment |
user_id | LARGEINT | 无 | 用户ID |
date | DATE | 无 | 数据写入日期 |
cost | BIGINT | SUM | 用户总消费 |
向表example_tbl8
中分两个批次写入如下数据。
第一次批量导入的数据如下。
user_id | date | cost |
10001 | 2017-11-20 | 50 |
10002 | 2017-11-21 | 39 |
第二次批量导入的数据如下。
user_id | date | cost |
10001 | 2017-11-20 | 1 |
10001 | 2017-11-21 | 5 |
10003 | 2017-11-22 | 22 |
可以看到,用户ID为10001的数据在两个批次中导入,在SelectDB内部的合并任务未执行完毕前,底层存储会包含上述5条原始数据。但是为了保证用户只能查询到如下最终聚合后的数据,我们会在查询引擎中自动加入了聚合算子,来保证数据对外的一致性。用户查询结果如下。
user_id | date | cost |
10001 | 2017-11-20 | 51 |
10001 | 2017-11-21 | 5 |
10002 | 2017-11-21 | 39 |
10003 | 2017-11-22 | 22 |
例如,执行如下SQL,得到的结果是5,而不是1。
SELECT MIN(cost) FROM example_tbl8;
同时,这种一致性保证,在某些查询中,会极大的降低查询效率。我们以最基本的count(*)
查询为例。
SELECT COUNT(*) FROM example_tbl8;
在其他数据库中,这类查询都会很快的返回结果。因为,可以通过如“导入时对行进行计数,保存count的统计信息”,或者在查询时“仅扫描某一列数据,获得count值”的方式,只需很小的开销,即可获得查询结果。但是在聚合表引擎中,这种查询的开销非常大。
以上述的数据为例。
执行SQLselect count(*) from example_tbl8;
的结果应该为4。但如果只扫描user_id
这一列,且加上查询时聚合,最终得到的结果是3(10001, 10002, 10003)。而不加查询时聚合,则得到的结果是5(两批次一共5行数据)。可见这两个结果都是不对的。
因为当聚合列非常多时,count()
查询需要扫描大量的数据。为了得到正确的结果,我们必须同时读取user_id
和date
这两列的数据,再加上查询时聚合,才能返回4这个正确的结果。即在count()
查询中,必须扫描所有的AGGREGATE KEY列(这里就是user_id
和date
),并且聚合后,才能得到正确的结果。
因此,当业务上有频繁的count(*)
查询时,我们建议通过增加一个值恒为1、聚合类型为SUM的列来模拟count(*)
。例如在example_tbl8
的表结构中添加列count
,修改后结构定义表如下。
ColumnName | Type | AggregateType | Comment |
user_id | BIGINT | 无 | 用户ID |
date | DATE | 无 | 数据写入日期 |
cost | BIGINT | SUM | 用户总消费 |
count | BIGINT | SUM | 用于计算count |
增加一个count列,并且导入数据中,该列值恒为1。则select count(*) from table;
的结果等价于select sum(count) from table;
。而后者的查询效率将远高于前者。不过这种方式也有使用限制,就是需要您自行保证,不会重复导入AGGREGATE KEY列都相同的行。否则,select sum(count) from table;
只能表述原始导入的行数,而不是select count(*) from table;
的语义。
另一种方式,就是将如上的count
列的聚合类型改为REPLACE,且依然值恒为1。那么select sum(count) from table;
和select count(*) from table;
的结果将是一致的。并且这种方式,没有导入重复行的限制。
Unique表引擎的写时合并实现
Unique引擎的写时合并实现没有聚合引擎的局限性,还是以上方的数据为例,写时合并为每次导入的rowset增加了对应的delete bitmap,来标记哪些数据被覆盖。第一批数据导入后状态如下。
第一次批量导入的结果数据如下。
user_id | date | cost | delete bit |
10001 | 2017-11-20 | 50 | false |
10002 | 2017-11-21 | 39 | false |
当第二批数据导入完成后,第一批次导入的数据中重复的行就会被标记为已删除,此时两批数据状态如下。
第一批次导入的数据被标记后结果如下。
user_id | date | cost | delete bit |
10001 | 2017-11-20 | 50 | true |
10002 | 2017-11-21 | 39 | false |
第二批次导入的数据结果如下。
user_id | date | cost | delete bit |
10001 | 2017-11-20 | 1 | false |
10001 | 2017-11-21 | 5 | false |
10003 | 2017-11-22 | 22 | false |
在查询时,所有在delete bitmap中被标记删除的数据都不会读出来,因此也无需进行做任何数据聚合,上述数据中有效的行数为4行,查询出的结果也应该是4行,也就可以采取开销最小的方式来获取结果,即前面提到的“仅扫描某一列数据,获得count值”的方式。
在测试环境中,count(*)
查询在Unique引擎的写时合并实现上的性能,相比聚合模型有10倍以上的提升。
Duplicate表引擎的局限性
Duplicate 模型没有聚合引擎的这个局限性。因为该引擎不涉及聚合语意,在做count(*)查询时,任意选择一列查询,即可得到语意正确的结果。
Key列
Duplicate、Aggregate、Unique模型在使用过程中,都会在建表时指定Key列,然而实际上有所区别。
对于Duplicate模型,表的Key列,可以认为只是 “排序列”,并非起到唯一标识的作用。
对于Aggregate、Unique模型,在这种聚合类型的表里,Key列兼顾 “排序列” 和 “唯一标识列”,是真正意义上的“Key列”。
数据模型的选择建议
因为数据模型在建表时就已经确定,且无法修改。所以,选择一个合适的数据模型非常重要。
Aggregate模型可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,从而大幅提升查询性能。适合有固定查询模式或需要聚合分析的报表类场景。该模型对count(*)查询不友好;同时因为固定了Value列的聚合方式,在进行其他类型的聚合查询时,需要考虑语义正确性。
Unique模型针对需要唯一性约束的场景,可以保证主键唯一性。适合订单、交易等关系型数据的分析场景。该模型无法利用ROLLUP等预聚合带来的查询优势。当您对于聚合查询有较高性能需求,推荐使用自1.2版本加入的写时合并实现。
Duplicate模型虽然同样无法利用预聚合的特性,但是不受聚合模型的约束,可以充分发挥列存的优势(只读取相关列,而不需要读取所有Key列)。适合针对日志数据、明细数据等进行任意维度的Ad-hoc查询。
如果有部分列更新的需求,请查阅文档部分列更新获取相关使用建议。