说明
Doris 数据表模型上目前分为三类:DUPLICATE KEY, UNIQUE KEY, AGGREGATE KEY。
推荐规约
因为数据模型在建表时就已经确定,且无法修改。所以,选择一个合适的数据模型非常重要。
只指定排序列,相同的 KEY 行不会合并。
适用于数据无需提前聚合的分析业务:
最佳实践
-- 例如 允许 KEY 重复仅追加新数据的日志数据分析 CREATE TABLE session_data ( visitorid SMALLINT, sessionid BIGINT, visittime DATETIME, city CHAR(20), province CHAR(20), ip varchar(32), brower CHAR(20), url VARCHAR(1024) ) DUPLICATE KEY(visitorid, sessionid) -- 只用于指定排序列,相同的 KEY 行不会合并 DISTRIBUTED BY HASH(sessionid, visitorid) BUCKETS 10;
AGGREGATE KEY 相同时,新旧记录进行聚合,目前支持的聚合方式:
适合报表和多维分析业务:
最佳实践
-- 例如 网站流量分析 CREATE TABLE site_visit ( siteid INT, city SMALLINT, username VARCHAR(32), pv BIGINT SUM DEFAULT '0' -- PV 浏览量计算 ) AGGREGATE KEY(siteid, city, username) -- 相同的 KEY 行会合并,非 KEY 列会根据指定的聚合函数进行聚合 DISTRIBUTED BY HASH(siteid) BUCKETS 10;
UNIQUE KEY 相同时,新记录覆盖旧记录。在 1.2 版本之前,UNIQUE KEY 实现上和 AGGREGATE KEY 的 REPLACE 聚合方法一样,二者本质上相同,自 1.2 版本我们给 UNIQUE KEY 引入了 merge on write 实现,该实现有更好的聚合查询性能。
适用于有更新需求的分析业务:
最佳实践
-- 例如 订单去重分析 CREATE TABLE sales_order ( orderid BIGINT, status TINYINT, username VARCHAR(32), amount BIGINT DEFAULT '0' ) UNIQUE KEY(orderid) -- 相同的 KEY 行会合并 DISTRIBUTED BY HASH(orderid) BUCKETS 10;
说明
索引用于帮助快速过滤或查找数据。目前主要支持两类索引:
在 Aggregate、Unique 和 Duplicate 三种数据模型中。底层的数据存储,是按照各自建表语句中,AGGREGATE KEY、UNIQUE KEY 和 DUPLICATE KEY 中指定的列进行排序存储的。而前缀索引,即在排序的基础上,实现的一种根据给定前缀列,快速查询数据的索引方式。
前缀索引是稀疏索引,不能精确定位到 Key 所在的行,只能粗粒度地定位出 Key 可能存在的范围,然后使用二分查找算法精确地定位 Key 的位置。
推荐规约
ZoneMap 索引是在列存格式上,对每一列自动维护的索引信息,包括 Min/Max,null 值个数等等。在数据查询时,会根据范围条件过滤的字段按照 ZoneMap 统计信息选取扫描的数据范围。
例如对 age 字段进行过滤,查询语句如下:SELECT * FROM table WHERE age > 0 and age < 51;
在没有命中 Short Key Index 的情况下,会根据条件语句中 age 的查询条件,利用 ZoneMap 索引找到应该扫描的数据 ordinary 范围,减少要扫描的 page 数量。
从 2.0.0 版本开始,Doris 支持倒排索引,可以用来进行文本类型的全文检索、普通数值日期类型的等值范围查询,快速从海量数据中过滤出满足条件的行。
最佳实践
-- 创建示例:可以表创建时指定或者创建后新增,如下创建表时指定 CREATE TABLE table_name ( columns_difinition, INDEX idx_name1(column_name1) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese")] [COMMENT 'your comment'] INDEX idx_name2(column_name2) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese")] [COMMENT 'your comment'] INDEX idx_name3(column_name3) USING INVERTED [PROPERTIES("parser" = "chinese", "parser_mode" = "fine_grained|coarse_grained")] [COMMENT 'your comment'] INDEX idx_name4(column_name4) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese", "support_phrase" = "true|false")] [COMMENT 'your comment'] INDEX idx_name5(column_name4) USING INVERTED [PROPERTIES("char_filter_type" = "char_replace", "char_filter_pattern" = "._"), "char_filter_replacement" = " "] [COMMENT 'your comment'] INDEX idx_name5(column_name4) USING INVERTED [PROPERTIES("char_filter_type" = "char_replace", "char_filter_pattern" = "._")] [COMMENT 'your comment'] ) table_properties; -- 使用示例:全文检索关键词匹配,通过 MATCH_ANY MATCH_ALL 完成 SELECT * FROM table_name WHERE column_name MATCH_ANY | MATCH_ALL 'keyword1 ...';
推荐规约
警告
强制规约
倒排索引在不同数据模型中有不同的使用限制:
Doris 支持用户对取值区分度比较大的字段添加 BloomFilter 索引,适合在基数较高的列上进行等值查询的场景。
最佳实践
-- 创建示例:通过在建表语句的 PROPERTIES 里加上"bloom_filter_columns"="k1,k2,k3" -- 例如下面我们对表里的 saler_id,category_id 创建了 BloomFilter 索引。 CREATE TABLE IF NOT EXISTS sale_detail_bloom ( sale_date date NOT NULL COMMENT "销售时间", customer_id int NOT NULL COMMENT "客户编号", saler_id int NOT NULL COMMENT "销售员", sku_id int NOT NULL COMMENT "商品编号", category_id int NOT NULL COMMENT "商品分类", sale_count int NOT NULL COMMENT "销售数量", sale_price DECIMAL(12,2) NOT NULL COMMENT "单价", sale_amt DECIMAL(20,2) COMMENT "销售总金额" ) Duplicate KEY(sale_date, customer_id,saler_id,sku_id,category_id) DISTRIBUTED BY HASH(saler_id) BUCKETS 10 PROPERTIES ( "bloom_filter_columns"="saler_id,category_id" );
警告
强制规约
从 2.0.0 版本开始,Doris 为了提升LIKE的查询性能,增加了 NGram BloomFilter 索引。
最佳实践
-- 创建示例:表创建时指定 CREATE TABLE `nb_table` ( `siteid` int(11) NULL DEFAULT "10" COMMENT "", `citycode` smallint(6) NULL COMMENT "", `username` varchar(32) NULL DEFAULT "" COMMENT "", INDEX idx_ngrambf (`username`) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="256") COMMENT 'username ngram_bf index' ) ENGINE=OLAP AGGREGATE KEY(`siteid`, `citycode`, `username`) COMMENT "OLAP" DISTRIBUTED BY HASH(`siteid`) BUCKETS 10; -- PROPERTIES("gram_size"="3", "bf_size"="256"),分别表示 gram 的个数和 bloom filter 的字节数。 -- gram 的个数跟实际查询场景相关,通常设置为大部分查询字符串的长度,bloom filter 字节数,可以通过测试得出,通常越大过滤效果越好,可以从 256 开始进行验证测试看看效果。当然字节数越大也会带来索引存储、内存 cost 上升。 -- 如果数据基数比较高,字节数可以不用设置过大,如果基数不是很高,可以通过增加字节数来提升过滤效果。
警告
强制规约
为了加速数据查询,Doris 支持用户为某些字段添加 Bitmap 索引,适合在基数较低的列上进行等值查询或范围查询的场景。
最佳实践
-- 创建示例:在 bitmap_table 上为 siteid 创建 Bitmap 索引 CREATE INDEX [IF NOT EXISTS] bitmap_index_name ON bitmap_table (siteid) USING BITMAP COMMENT 'bitmap_siteid';
警告
强制规约
Duplicate、Uniq 数据模型的所有列和 Aggregate模型的 key 列上。TINYINTSMALLINTINTBIGINTCHARVARCHARDATEDATETIMELARGEINTDECIMALBOOLDoris 支持多种字段类型,例如精确去重 BITMAP、模糊去重 HLL、半结构化 ARRAY/MAP/JSON 和常见的数字、字符串和时间类型等。
说明
推荐规约
建表时除了要注意数据表模型、索引和字段类型的选择还需要注意分区分桶的设置。
最佳实践
-- 以 Unique 模型的 Merge-on-Write 表为例 -- Unique 模型的写时合并实现,与聚合模型就是完全不同的两种模型了,查询性能更接近于 duplicate 模型, -- 在有主键约束需求的场景上相比聚合模型有较大的查询性能优势,尤其是在聚合查询以及需要用索引过滤大量数据的查询中。 -- 非分区表 CREATE TABLE IF NOT EXISTS tbl_unique_merge_on_write ( `user_id` LARGEINT NOT NULL COMMENT "用户id", `username` VARCHAR(50) NOT NULL COMMENT "用户昵称", `register_time` DATE COMMENT "用户注册时间", `city` VARCHAR(20) COMMENT "用户所在城市", `age` SMALLINT COMMENT "用户年龄", `sex` TINYINT COMMENT "用户性别", `phone` LARGEINT COMMENT "用户电话", `address` VARCHAR(500) COMMENT "用户地址" ) UNIQUE KEY(`user_id`, `username`) -- 3-5G 的数据量 DISTRIBUTED BY HASH(`user_id`) BUCKETS 10 PROPERTIES ( -- 在 1.2.0 版本中,作为一个新的 feature,写时合并默认关闭,用户可以通过添加下面的 property 来开启 "enable_unique_key_merge_on_write" = "true" ); -- 分区表 CREATE TABLE IF NOT EXISTS tbl_unique_merge_on_write_p ( `user_id` LARGEINT NOT NULL COMMENT "用户id", `username` VARCHAR(50) NOT NULL COMMENT "用户昵称", `register_time` DATE COMMENT "用户注册时间", `city` VARCHAR(20) COMMENT "用户所在城市", `age` SMALLINT COMMENT "用户年龄", `sex` TINYINT COMMENT "用户性别", `phone` LARGEINT COMMENT "用户电话", `address` VARCHAR(500) COMMENT "用户地址" ) UNIQUE KEY(`user_id`, `username`, `register_time`) PARTITION BY RANGE(`register_time`) ( PARTITION p00010101_1899 VALUES [('0001-01-01'), ('1900-01-01')), PARTITION p19000101 VALUES [('1900-01-01'), ('1900-01-02')), PARTITION p19000102 VALUES [('1900-01-02'), ('1900-01-03')), PARTITION p19000103 VALUES [('1900-01-03'), ('1900-01-04')), PARTITION p19000104_1999 VALUES [('1900-01-04'), ('2000-01-01')), FROM ("2000-01-01") TO ("2022-01-01") INTERVAL 1 YEAR, PARTITION p30001231 VALUES [('3000-12-31'), ('3001-01-01')), PARTITION p99991231 VALUES [('9999-12-31'), (MAXVALUE)) ) -- 默认 3-5G 的数据量 DISTRIBUTED BY HASH(`user_id`) BUCKETS 10 PROPERTIES ( -- 在 1.2.0 版本中,作为一个新的 feature,写时合并默认关闭,用户可以通过添加下面的 property 来开启 "enable_unique_key_merge_on_write" = "true", -- 动态分区调度的单位。可指定为 HOUR、DAY、WEEK、MONTH、YEAR。分别表示按小时、按天、按星期、按月、按年进行分区创建或删除。 "dynamic_partition.time_unit" = "MONTH", -- 动态分区的起始偏移,为负数。根据 time_unit 属性的不同,以当天(星期/月)为基准,分区范围在此偏移之前的分区将会被删除(TTL)。如果不填写,则默认为 -2147483648,即不删除历史分区。 "dynamic_partition.start" = "-3000", -- 动态分区的结束偏移,为正数。根据 time_unit 属性的不同,以当天(星期/月)为基准,提前创建对应范围的分区。 "dynamic_partition.end" = "10", -- 动态创建的分区名前缀(必选)。 "dynamic_partition.prefix" = "p", -- 动态创建的分区所对应的分桶数量。 "dynamic_partition.buckets" = "10", "dynamic_partition.enable" = "true", -- 动态创建的分区所对应的副本数量,如果不填写,则默认为该表创建时指定的副本数量 3。 "dynamic_partition.replication_num" = "3", "replication_num" = "3" ); -- 分区创建查看 -- 实际创建的分区数需要结合 dynamic_partition.start、end 以及 PARTITION BY RANGE 的设置共同决定 show partitions from tbl_unique_merge_on_write_p;
警告
强制规约
数据库字符集指定 UTF-8,并且只支持 UTF-8。
表的副本数必须为 3(未指定副本数时,默认为 3)。
单个 Tablet(Tablet 数 = 分区数 * 桶数 * 副本数)的数据量理论上没有上下界,除小表(百兆维表)外需确保在 1G - 10G 的范围内:
5 亿以上的数据必须设置分区分桶策略:
i. 大表的单个 Tablet 存储数据大小在 1G-10G 区间,可防止过多的小文件产生。 ii. 百兆左右的维表 Tablet 数量控制在 3-5 个,保证一定的并发数也不会产生过多的小文件。
i. 如果 OLAP 表没有更新类型的字段,将表的数据分桶模式设置为 RANDOM,则可以避免严重的数据倾斜 (数据在导入表对应的分区的时候,单次导入作业每个 Batch 的数据将随机选择一个 Tablet 进行写入)。 ii. 当表的分桶模式被设置为 RANDOM 时,因为没有分桶列,无法根据分桶列的值仅对几个分桶查询,对表进行查询的时候将对命中分区的全部分桶同时扫描,该设置适合对表数据整体的聚合查询分析而不适合高并发的点查询。 iii. 如果 OLAP 表的是 Random Distribution 的数据分布,那么在数据导入的时候可以设置单分片导入模式(将 `load_to_single_tablet` 设置为 true),那么在大数据量的导入的时候,一个任务在将数据写入对应的分区时将只写入一个分片,这样将能提高数据导入的并发度和吞吐量,减少数据导入和 Compaction 导致的写放大问题,保障集群的稳定性。
对于有大量历史分区数据,但是历史数据比较少,或者不均衡,或者查询概率的情况,使用如下方式将数据放在特殊分区。
对于历史数据,如果数据量比较小我们可以创建历史分区(比如年分区,月分区),将所有历史数据放到对应分区里创建历史分区方式例如:FROM ("2000-01-01") TO ("2022-01-01") INTERVAL 1 YEAR,具体参考:
( PARTITION p00010101_1899 VALUES [('0001-01-01'), ('1900-01-01')), PARTITION p19000101 VALUES [('1900-01-01'), ('1900-01-02')), ... PARTITION p19000104_1999 VALUES [('1900-01-04'), ('2000-01-01')), FROM ("2000-01-01") TO ("2022-01-01") INTERVAL 1 YEAR, PARTITION p30001231 VALUES [('3000-12-31'), ('3001-01-01')), PARTITION p99991231 VALUES [('9999-12-31'), (MAXVALUE)) )
单表物化视图不能超过 6 个。