一、先破一个误会:Doris 不是账本,是加压站
很多人把 Doris 当成“主库”,以为数就存在这儿。不是。这套体系里,真正的原始账本在湖里(Hudi);Doris 是对外供数层,报表和页面直连的就是它。复盘的部件表里写得直白:OLAP 那一栏,Doris 的角色就是“对外供数层,页面 / 报表直连”。
拿水厂打比方:它是出水口后面那个加压站。城里楼高、管线长,不增压,水到不了六楼;可加压站里一滴水都不产,它就是水的最后一站。断了上游,它自己撑不了多久。
为什么敢说“随时能重灌”?看同步配置就明白:SeaTunnel 往 Doris 灌数时写的是 schema_save_mode=RECREATE_SCHEMA、write_mode=overwrite——每跑一次,先把目标表结构重建,再整份覆盖。一个每跑一次就被整份覆盖的东西,身份很清楚:它是湖的一份副本,不是另一本账。
- 它在哪一段
- 湖(Hudi)→
SeaTunnel→ Doris(dm_gk_dws)→ 报表 / 页面 - 里面装什么
- 湖里算完的结果表(ADS 那一层),不是原始明细,更不是源系统原样数据
- 对不上先信谁
- 先信湖。Doris 侧是那半个“可以重灌”的
- 出问题先看哪
- 先看同步,再看建表,最后才怀疑数据本身
overwrite 整表覆盖,改的东西一点不剩;而湖里那份错的还在,明早原样再错一遍。真正的修法在湖里,Doris 只负责把结果快点端出去。10.160.4.111:8182,别等业务打电话过来说“页面白屏”才去翻。二、先说人话:一家仓库里的四样东西
一张 Doris 表就是一间仓库。往里放东西的时候,你只做四个决定:货架分几层(分区)、每层分几箱(分桶)、箱子上贴什么标签(索引)、同一批货留几把钥匙(副本)。放进去的一行数据,就是箱子里的一瓶货。
这张图从上到下三层货架,每层三个纸箱,每箱几瓶;右边挂的就是那三个标签。
记住一句话:摆法不影响数对不对。数对不对是同步和口径的事(上一关那条管子)。摆法只影响三件事——灌进去快不快、取出来快不快、占多大地方。
三、拆开看:每个零件是什么
3.1 门牌:库名 + 表名
像什么:仓库门口那块牌子:几号库、几号架、第几层,写死了才有人找得到货。
其实是什么:表名不是随便起的,是照公式拼出来的。库名和表名统一只用小写字母、数字、下划线,不出现中文和别的特殊字符,长度都在 64 字节以内(限的是字节,不是字数),数据库字符集指定 utf-8。
- 公式
${数据分层}_${一级}_${二级}_${三级}_${其他}_${其他}……- 分层只认五个
dwi/dwr/dim/dws/ads,写别的都不算数- 级别怎么定
- 按“词根词条”的级别处理,词根表在探源库里:
data_dqc.t_gov_asset_name - 两个真例
dm_gk_hudi.ads_pur_ps_channel(规范原文举的例子)、dm_gk_dws.ads_log_inv_kpi_wide(真实建表脚本里建的那张)
| 第几段 | 应该是 | ads_log_inv_kpi_wide 里是 |
|---|---|---|
| 1 | 数据分层,只能五选一 | ads |
| 2 | 一级词根 | log |
| 3 | 二级词根 | inv |
| 4 | 三级词根 | kpi |
| 5 起 | 其他,按实际场景加 | wide |
每一段到底该用哪个词,规范里没写,在词根表 data_dqc.t_gov_asset_name 里查。这张表就是“词根词典”,别自己造词。
dwi_gk_hudi、dwr_gk_hudi、dm_gk_hudi”——这三个其实是湖里的库(复盘的分层图里就是这么分的)。Doris 仓这边,真实建表脚本建在 dm_gk_dws。照文档念库名,会把仓念成湖。3.2 货架:分区
像什么:货架按天一层、按月一层。要找 10 月 3 号的库存,直接上那一层,不用从底层翻起。
其实是什么:分区就是按某个字段把数据切开、分开存。切开之后每一小块(tablet)最好落在 1–3 GB 之间:太小就是一堆碎箱子,管理起来净是麻烦;太大就是一次搬不动。
- 目标
- 每个 tablet 1–3 GB
- 原文的例子
- 单分区数据量 100G、按天分区 → bucket 数量 100 个。(100G ÷ 100 桶 = 每桶约 1G,正好落在 1–3G 里)
- 红线
- 2000 万行以内禁止用动态分区
- 为什么禁
- 动态分区会自己按值造分区。小表用户盯不过来,最后造出一堆没人用的分区分桶,白占地方
3.3 纸箱:分桶
像什么:同一层货架再分几个纸箱,取货时先认箱子号,一箱一箱搬。
其实是什么:分桶是把数据按规则打散到固定的桶数里,决定并行读写的颗粒度。
- 第一条
- 不要用 Auto Bucket(规范原话)。原文给的理由:Auto Bucket 会造成 tablet 数量过多,造成大量小文件的问题
- 多少量可以不分区
- 1000 万 – 2 亿行以内,为了方便可以不设分区,直接用分桶策略
- 倾斜怎么办
- 分桶字段存在 30% 以上数据倾斜 → 禁用 Hash 分桶,改 Random 分桶
- 真名
DISTRIBUTED BY HASH(分桶字段) BUCKETS n/DISTRIBUTED BY RANDOM BUCKETS n(真实脚本里写的是RANDOM BUCKETS AUTO,第四节专门说这事)
3.4 标签:索引
像什么:箱子上贴张纸条:“这里面有没有 A 客户”。取货前先看纸条,不用开箱翻。
其实是什么:索引是帮查询跳过无关数据块的辅助结构。贴错地方,等于每箱都写着“可能有”。
- BloomFilter 用在哪
- 查询条件必须是
in或=,而且列要高基(5000 以上);也适用于非前缀过滤 - Bitmap 用在哪
- 像身份证号这种基数特别高、查询又是等值(
=)的列,用 Bitmap 能极大加速 - 反例(原文原话)
- 建在低基数列上,比如“性别”列,每个 Block 几乎都会包含所有取值,导致 BloomFilter 索引失去意义
- 真名
BloomFilter/Bitmap;建表时按列挂,具体写法按建表平台模板走——这一步的实际值要现场核
3.5 钥匙:副本
像什么:仓库钥匙配三把,一把丢了还有两把能开门。
其实是什么:副本数决定同一份数据在集群里存几份。坏一块盘,还有别的份顶上。
- 硬规矩
- 表的副本数必须为 3(规范“属性规范”一条,就这一句,没有例外条款)
- 真名
"replication_allocation" = "tag.location.default: 3"- 在哪写
- 建表语句最后的
PROPERTIES(...)里
3.6 排法:DUPLICATE KEY(最容易被误解的一条)
像什么:仓库墙上贴的一张“摆放顺序单”:先按 period 分成几堆,堆里面怎么放不管。它是排队的顺序,不是查户口的名单。
其实是什么:DUPLICATE KEY 不是主键、不去重,只决定数据在磁盘上按哪几列排序存。写 DUPLICATE KEY(period) 的意思是“按 period 排序存”——同一个 period 想有多少行就有多少行,它一行都不拦。
- 真名
DUPLICATE KEY(`period`)- 顺序哪来的
- 建表脚本第一行注释就写着:列顺序与 Hudi 一致,Doris 侧 DUPLICATE KEY 取首列。所以这张表的排序键就是第一列
period - 它管什么
- 只管“按什么顺序摆”,不管“留几行”
SUM,数字正好翻倍。这时候查 SQL 是查不出问题的——SQL 没写错,是你对这张表的假设错了。以为它去重 → 数字翻倍,这条在这套体系里踩得最多。3.7 取货规矩:查询规范的几条硬线
像什么:取货先划到那一层、那一箱,别把整仓货倒在地上挑。
其实是什么:Doris 的算力和内存是大家共用的。一条不划范围的查询,占的不是你自己的时间。
- 5 亿行以上还要排序
- 先把数据范围缩小,再排序;否则大量排序会拖垮性能
- 两张 3 亿以上的表 JOIN
- 用 Colocate Join(规范原文还给了 Doris 文档链接)
- 亿级大表
- 禁止
select *:必须明确写出要哪几列,并且必须带分区分桶条件 - 高并发点查
- 建议开启行存
select *页面只用一个金额字段,SQL 却把几十列全捞出来,还不带分区分桶条件。一次查询扫全表,页面转圈三十秒;并发一上来,别人的查询跟着一起卡。最后大家统一结论:“Doris 不行”——其实是一条 SQL 的问题。四、贴一段真建表,逐行翻译
下面这段不是编的,是从真实建表脚本 create_doris_tables.py 里摘的。为了能看清骨架,把中间的列缩掉了,其余一个字没改。
CREATE TABLE IF NOT EXISTS `dm_gk_dws`.`ads_log_inv_kpi_wide` (
`period` varchar(32) NULL COMMENT '期间:期初/本期/同期',
`org_lvl` int NULL COMMENT '组织层级 0-9,0=组织映射兜底',
`invt_amt` decimal(38,6) NULL COMMENT '库存总金额(无税)',
-- …… 中间省略 34 个列(原脚本共 37 列,每列都带 COMMENT)
) ENGINE=OLAP
DUPLICATE KEY(`period`)
COMMENT '存货管理指标-宽表'
DISTRIBUTED BY RANDOM BUCKETS AUTO
PROPERTIES ("replication_allocation" = "tag.location.default: 3")
逐行翻一遍:
| 序 | 这一行说的是什么 |
|---|---|
| ① | 建表头CREATE TABLE IF NOT EXISTS `dm_gk_dws`.`ads_log_inv_kpi_wide`在 dm_gk_dws 这个库里建表,名字叫 ads_log_inv_kpi_wide;ads_ 开头说明是应用层,库在仓这边。 |
| ② | 第一列`period` varchar(32) NULL COMMENT '期间:期初/本期/同期'只能装期初、本期、同期三种值;它同时是排序键,列顺序必须和 Hudi 侧严格一致。 |
| ③ | 每列备注 每列后面都跟着 COMMENT建表平台上字段备注是硬检查项;不写,三个月后没人说得清 invt_amt 含不含税。 |
| ④ | 引擎ENGINE=OLAP这是 Doris 的表。同一个词在湖里也出现过,湖里那些表走的是另一套写法,别混。 |
| ⑤ | 排序键DUPLICATE KEY(`period`)按 period 排序存,不唯一、不去重——就是第三节 3.6 那条。 |
| ⑥ | 表备注COMMENT '存货管理指标-宽表'表本身的一句话说明,跟字段备注一样是必填项。 |
| ⑦ | 分桶DISTRIBUTED BY RANDOM BUCKETS AUTO分桶用 Random、桶数让系统估;Random 是按轮转分箱,不怕字段倾斜。 |
| ⑧ | 副本PROPERTIES ("replication_allocation" = "tag.location.default: 3")副本留 3 份。规范那条“副本数必须为 3”,就落在这。 |
DISTRIBUTED BY RANDOM BUCKETS AUTO。两句话同时是真的,不冲突——它们只是不在同一页上。对外讲架构时按实际口径说:这张表就是 RANDOM BUCKETS AUTO 建的,别照文档念“我们不用 Auto Bucket”,脚本一翻就露。要真收口,办法也是现成的:把 AUTO 换成按 1–3G / tablet 算出来的固定桶数。五、串起来:一次完整的摆货
把上面这些零件按真实顺序排一遍。顺序不是讲究:每步的产出,正好是下一步的输入。
- 定库定名。先定库(仓这边是
dm_gk_dws),再按${数据分层}_${一级}_${二级}_${三级}_${其他}拼表名;每一段去词根表data_dqc.t_gov_asset_name里查。全小写、只允许字母数字下划线、64 字节以内。 - 先量体积。这张表一天/一个分区大概多少 G?没有这个数,后面三个决定全是猜的。
- 定分区。目标是单个 tablet 1–3 GB:100G 一个分区就配 100 个桶。表不到 2000 万行,动态分区一律不开。
- 定分桶。不用 Auto Bucket,按量算桶数;1000 万–2 亿行的表可以不设分区、直接分桶;分桶字段倾斜超过 30% 就改 Random。
- 定排法。选
DUPLICATE KEY的列(这张表取首列period),心里记牢:它是排序,不是去重。 - 贴标签。只有“查得勤 + 条件是 in 或 = + 基数 5000 以上”的列才值得建 BloomFilter;身份证这类高基等值查询用 Bitmap;低基列什么都别建。
- 配钥匙。
PROPERTIES里写死副本 3。 - 灌数。SeaTunnel 从湖里灌进来(
RECREATE_SCHEMA+overwrite),灌完对一遍湖和仓。对不上先信湖。 - 交给页面。页面直连查这张表:亿级禁
select *,字段点名字,条件带分区分桶;高并发点查开行存。
九步里,第 2 步最容易被跳过——很多人上来就写 DDL。可 1–3G、桶数、要不要分区、要不要改 Random,全是从“一天多少 G”这一个数推出来的。跳过它,后面每一行参数都是抄别人的。
六、搞错了会怎样
摆错的代价分两种:一种是当场报错,还算好的;一种是悄无声息——那种才贵。
七、一句话记住
再补一句给人听:这间仓库不用你盖,也不用你守夜。你只需要在卸货前量一次尺寸,把层、箱、标签、钥匙四件事定下来;剩下的,交给每天那次覆盖写。