跳到主要内容

表模型最佳实践

本文从模型选型、表结构设计、模型局限性等角度,介绍 Doris 数据模型在使用过程中需要注意的事项,帮助用户选择合适的模型并规避常见性能陷阱。

数据模型选择建议

数据模型在建表时即已确定,且无法修改。因此选择合适的数据模型至关重要。

三种模型对比

数据模型适用场景优势注意事项
Duplicate任意维度的 Ad-hoc 查询不受聚合模型约束,可充分发挥列存优势(按需读列)无法利用预聚合特性 (但可以通过异步物化视图支持)
Unique需要主键唯一性约束,需要数据更新保证主键唯一性,可灵活更新无法利用预聚合特性。(但可以通过异步物化视图支持)
Aggregate固定模式的报表类查询通过预聚合显著降低查询扫描数据量与计算量count(*) 查询不友好;执行非声明聚合类型的聚合查询时需关注语义正确性

部分列更新需求

如果业务中有部分列更新的需求,请参阅以下文档获取使用建议:

建表时列类型建议

创建表时,建议遵循以下列类型与顺序规范:

  1. 列顺序:Key 列必须排在所有 Value 列之前。
  2. 优先整型:尽量选择整型类型,因为整型的计算和查找效率远高于字符串。
  3. 整型长度够用即可:在不同长度的整型类型之间选择时,遵循"够用即可"原则。
  4. 变长字符串够用即可:对于 VARCHAR 和 STRING 类型的长度,同样遵循"够用即可"原则。

Key 列在不同模型中的语义差异

Duplicate、Aggregate、Unique 三种模型在建表时都会指定 Key 列,但其语义有所区别:

模型Key 列的角色
Duplicate仅作为"排序列",不具备唯一标识作用
Aggregate、Unique兼具"排序列"与"唯一标识列",是真正意义上的 "Key 列"

Aggregate 模型的局限性

Aggregate 模型对外展示的是最终聚合后的数据。也就是说,任何还未聚合的数据(例如分属两个不同导入批次的数据),都必须通过某种方式保证对外展示的一致性。

一致性保证示例

假设表结构如下:

ColumnNameTypeAggregationTypeComment
user_idLARGEINT用户 ID
dateDATE数据灌入日期
costBIGINTSUM用户总消费

存储引擎中已存在两个导入批次的数据:

batch 1

user_iddatecost
100012017/11/2050
100022017/11/2139

batch 2

user_iddatecost
100012017/11/201
100012017/11/215
100032017/11/2222

可以看到,用户 10001 分属于两个导入批次的数据尚未聚合。但是为了保证用户只能查询到如下最终聚合后的数据:

user_iddatecost
100012017/11/2051
100012017/11/215
100022017/11/2139
100032017/11/2222

Doris 在查询引擎中加入了聚合算子,以保证数据对外的一致性。

非一致聚合查询的语义陷阱

在聚合列(Value 列)上执行与声明的聚合类型不一致的聚合查询时,需要注意语义。例如:

SELECT MIN(cost) FROM table;

得到的结果是 5,而不是 1

count(*) 查询的开销问题

聚合模型的一致性保证机制,会在某些查询中显著降低查询效率,最典型的就是 count(*) 查询:

SELECT COUNT(*) FROM table;

在其他数据库中,这类查询都会很快地返回结果。在实现上,可以通过"导入时对行进行计数,保存 count 的统计信息",或者在查询时"仅扫描某一列数据,获得 count 值"等方式,以极小的开销获得查询结果。但是在 Doris 的聚合模型中,这种查询的开销非常大。

以上述数据为例,最终聚合结果为:

user_iddatecost
100012017/11/2051
100012017/11/215
100022017/11/2139
100032017/11/2222

SELECT COUNT(*) FROM table; 的正确结果应为 4。但是:

  • 仅扫描 user_id 一列 + 查询时聚合 → 结果为 3(10001、10002、10003),错误
  • 仅扫描 user_id 一列 + 不进行查询时聚合 → 结果为 5(两批次共 5 行数据),错误

为了得到正确结果,必须同时读取 user_iddate 两列数据,再加上查询时聚合,才能返回 4 这个正确结果。也就是说,在 count(*) 查询中,Doris 必须扫描所有的 AGGREGATE KEY 列(这里就是 user_iddate),并且聚合后才能得到语义正确的结果。当聚合列非常多时,count(*) 查询需要扫描大量的数据。

count(*) 性能优化方案

当业务上有频繁的 count(*) 查询时,建议通过新增一个值恒为 1、聚合类型为 SUM 的列来模拟 count(*)。在前述示例的基础上修改表结构:

ColumnNameTypeAggregateTypeComment
user_idBIGINT用户 ID
dateDATE数据灌入日期
costBIGINTSUM用户总消费
countBIGINTSUM用于计算 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,用于标记哪些数据被覆盖。

第一批数据导入后

batch 1

user_iddatecostdelete bit
100012017/11/2050FALSE
100022017/11/2139FALSE

第二批数据导入完成后,第一批数据中重复的行就会被标记为已删除:

batch 1

user_iddatecostdelete bit
100012017/11/2050TRUE
100022017/11/2139FALSE

batch 2

user_iddatecostdelete bit
100012017/11/201FALSE
100012017/11/215FALSE
100032017/11/2222FALSE

查询时,所有在 delete bitmap 中被标记删除的数据都不会被读出,无需进行任何数据聚合。上述数据中有效行数为 4 行,查询出的结果也应为 4 行,因此可以采用开销最小的方式获取结果,即前面提到的"仅扫描某一列数据,获得 count 值"。

性能数据:在测试环境中,count(*) 查询在 Unique 模型写时合并实现上的性能,相比聚合模型有 10 倍以上的提升。

Duplicate 模型

Duplicate 模型没有聚合模型的上述局限性。由于该模型不涉及聚合语义,在执行 count(*) 查询时,任意选择一列进行扫描即可得到语义正确的结果。