Duplicate模型
在某些多维分析场景下,数据既没有主键,也没有聚合需求。因此,我们引入Duplicate数据模型来满足这类需求。
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 |
处理时间 |
建表语句。
CREATE TABLE IF NOT EXISTS example_db.expamle_tbl ( `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`) DISTRIBUTED BY HASH(`type`) BUCKETS 1 PROPERTIES ( "replication_allocation" = "tag.location.default: 1" );
这种数据模型区别于Aggregate和Unique模型。数据完全按照导入文件中的数据进行存储,不会有任何聚合。即使两行数据完全相同,也都会保留。 而在建表语句中指定的DUPLICATE KEY,只是用来指明底层数据按照那些列进行排序。(更贴切的名称应该为“Sorted Column”,这里取名“DUPLICATE KEY”只是用以明确表示所用的数据模型。关于“Sorted Column”的更多解释,请参见 前缀索引)。在DUPLICATE KEY的选择上,我们建议适当的选择前2-4列就可以。
这种数据模型适用于既没有聚合需求,又没有主键唯一性约束的原始数据的存储。更多使用场景,可参见聚合模型的局限性。
聚合模型的局限性
ColumnName |
Type |
AggregationType |
Comment |
---|---|---|---|
user_id |
LARGEINT |
- |
用户 ID |
date |
DATE |
- |
数据导入日期 |
cost |
BIGINT |
SUM |
用户总消费 |
假设存储引擎中有如下两个已经导入完成的批次的数据。
batch1
batch2
user_id |
date |
cost |
---|---|---|
10001 |
2017-11-20 |
1 |
10001 |
2017-11-21 |
5 |
10003 |
2017-11-22 |
22 |
可以看到,用户10001分属在两个导入批次中的数据还没有聚合。但是为了保证用户只能查询到如下最终聚合后的数据。
user_id |
date |
cost |
---|---|---|
10001 |
2017-11-20 |
51 |
10001 |
2017-11-21 |
5 |
10002 |
2017-11-21 |
39 |
10003 |
2017-11-22 |
22 |
建数据表。
CREATE TABLE IF NOT EXISTS example_db.expamle_tb2 ( `user_id` LARGEINT NOT NULL COMMENT "用户id", `date` DATE NOT NULL COMMENT "数据导入日期时间", `cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费" ) AGGREGATE KEY(`user_id`, `date`) DISTRIBUTED BY HASH(`user_id`) BUCKETS 1 PROPERTIES ( "replication_allocation" = "tag.location.default: 1" );
INSERT INTO example_db.expamle_tb2 (user_id,date,cost) VALUES('10001','2017-11-20','50'),('10002','2017-11-21','39'),('10001','2017-11-20','1'),('10001','2017-11-21','5'),('10003','2017-11-22','22');
我们在查询引擎中加入了聚合算子,来保证数据对外的一致性。
另外,在聚合列(Value)上,执行与聚合类型不一致的聚合类查询时,要注意语意。例如我们在如上示例中执行如下查询:
mysql> SELECT MIN(cost) FROM example_db.expamle_tb2; +-------------+ | min(`cost`) | +-------------+ | 5 | +-------------+ 1 row in set (0.02 sec)
得到的结果是5,而不是1。
同时,这种一致性保证,在某些查询中,会极大的降低查询效率。
我们以最基本的count(*)查询为例:
SELECT COUNT(*) FROM table;
为在实现上,我们可以通过如导入时对行进行计数,保存count的统计信息,或者在查询时仅扫描某一列数据,获得count值的方式,只需很小的开销,即可获得查询结果。但是在Doris的聚合模型中,这种查询的开销非常大。以刚才的数据为例。
batch1
user_id |
date |
cost |
---|---|---|
10001 |
2017-11-20 |
50 |
10002 |
2017-11-21 |
39 |
batch2
user_id |
date |
cost |
---|---|---|
10001 |
2017-11-20 |
1 |
10001 |
2017-11-21 |
5 |
10003 |
2017-11-22 |
22 |
最终聚合结果如下表所示。
user_id |
date |
cost |
---|---|---|
10001 |
2017-11-20 |
51 |
10001 |
2017-11-21 |
5 |
10002 |
2017-11-21 |
39 |
10003 |
2017-11-22 |
22 |
查询结果。
mysql> SELECT COUNT(date) FROM example_db.expamle_tb2; +---------------+ | count(`date`) | +---------------+ | 4 | +---------------+ 1 row in set (0.01 sec)
所以,select count(*) from table; 的正确结果应该为4。但如果我们只扫描user_i 这一列,如果加上查询时聚合,最终得到的结果是3(10001, 10002, 10003)。而如果不加查询时聚合,则得到的结果是5(两批次一共5行数据)。可见这两个结果都是不对的。
为了得到正确的结果,我们必须同时读取user_id和date这两列的数据,再加上查询时聚合,才能返回4这个正确的结果。也就是说,在count( * ) 查询中,Doris必须扫描所有的AGGREGATE KEY列(这里就是user_id 和 date),并且聚合后,才能得到语意正确的结果。当聚合列非常多时,count( * )查询需要扫描大量的数据。
因此,当业务上有频繁的count( * )查询时,我们建议用户通过增加一个值恒为1的,聚合类型为SUM的列来模拟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; 的结果将是一致的。并且这种方式,没有导入重复行的限制。
Duplicate
Duplicate模型没有聚合模型的这个局限性。因为该模型不涉及聚合语意,在做count(*)查询时,任意选择一列查询,即可得到语意正确的结果。