08瓶子与货架:Doris 怎么摆目录
Doris 里那份数不是账本,是可以随时重灌的快照;这一关讲的摆法,只决定一件事——页面打开要几秒。
仓库里卸下一批货,先别急着往里扔。老师傅先量尺寸:货架分几层、这层分几箱、箱子上贴什么标签、钥匙配几把。摆得对,取货伸手就拿;摆得错,货也在库里,可你得把整个仓库翻一遍才找得着。

一、先破一个误会:Doris 不是账本,是加压站

很多人把 Doris 当成“主库”,以为数就存在这儿。不是。这套体系里,真正的原始账本在湖里(Hudi);Doris 是对外供数层,报表和页面直连的就是它。复盘的部件表里写得直白:OLAP 那一栏,Doris 的角色就是“对外供数层,页面 / 报表直连”。

拿水厂打比方:它是出水口后面那个加压站。城里楼高、管线长,不增压,水到不了六楼;可加压站里一滴水都不产,它就是水的最后一站。断了上游,它自己撑不了多久。

为什么敢说“随时能重灌”?看同步配置就明白:SeaTunnel 往 Doris 灌数时写的是 schema_save_mode=RECREATE_SCHEMA、write_mode=overwrite——每跑一次,先把目标表结构重建,再整份覆盖。一个每跑一次就被整份覆盖的东西,身份很清楚:它是湖的一份副本,不是另一本账。

它在哪一段
湖(Hudi)→ SeaTunnel → Doris(dm_gk_dws)→ 报表 / 页面
里面装什么
湖里算完的结果表(ADS 那一层),不是原始明细,更不是源系统原样数据
对不上先信谁
先信湖。Doris 侧是那半个“可以重灌”的
出问题先看哪
先看同步,再看建表,最后才怀疑数据本身
会出事 · 把 Doris 当主库页面数字不对,有人直接连上 Doris 改数、补数。当天看着修好了,第二天同步一跑,overwrite 整表覆盖,改的东西一点不剩;而湖里那份错的还在,明早原样再错一遍。真正的修法在湖里,Doris 只负责把结果快点端出去。
注意 · 它一慢,页面就转圈页面是直连 Doris 的,中间没有缓冲垫。集群健康看 Doris 监控面板 10.160.4.111:8182,别等业务打电话过来说“页面白屏”才去翻。

二、先说人话:一家仓库里的四样东西

一张 Doris 表就是一间仓库。往里放东西的时候,你只做四个决定:货架分几层(分区)、每层分几箱(分桶)、箱子上贴什么标签(索引)、同一批货留几把钥匙(副本)。放进去的一行数据,就是箱子里的一瓶货。

这张图从上到下三层货架,每层三个纸箱,每箱几瓶;右边挂的就是那三个标签。

一张 Doris 表 = 一间仓库 标签挂在货架上 P1 分区 箱1 箱2 箱3 P2 分区 箱1 箱2 箱3 P3 分区 箱1 箱2 箱3 BloomFilter 高基 5000+ 且 in / = Bitmap 身份证这类高基等值 副本 ×3 同一批货三把钥匙 层 = 分区(partition) 箱 = 桶(bucket) 瓶 = 一行(row) 摆法只管快慢,不管对错:对错先看同步,再看建表。

记住一句话:摆法不影响数对不对。数对不对是同步和口径的事(上一关那条管子)。摆法只影响三件事——灌进去快不快、取出来快不快、占多大地方。

三、拆开看:每个零件是什么

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 里查。这张表就是“词根词典”,别自己造词。

注意 · 库名这处会念错规范 2.1.1 里写“库名分为 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 万行以内禁止用动态分区
为什么禁
动态分区会自己按值造分区。小表用户盯不过来,最后造出一堆没人用的分区分桶,白占地方
小窍门桶数是算出来的,不是拍的:先量“单分区大概多少 G”,除以目标 1–3G,再往上取整。原文那个 100G / 按天 / 100 桶的例子,就是这么算出来的。
会出事 · 小表开了动态分区一张几百万行的表,每天按值自动长出一层新分区,三个月后分区数上千、每层就几百行。没人会去删——因为没人知道哪些是活的。等你发现,删分区本身又成了一件事。

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,第四节专门说这事)
会出事 · 倾斜 30% 还用 Hash分桶字段里有个大客户、占了三成以上的行,Hash 一算全挤进同一个箱子。灌数时其它箱子闲着等它,查数时它一个人扛全部扫描——加机器也不管用,因为毛病不在机器数量上。

3.4 标签:索引

🔖 箱子上贴标签(索引)

像什么:箱子上贴张纸条:“这里面有没有 A 客户”。取货前先看纸条,不用开箱翻。

其实是什么:索引是帮查询跳过无关数据块的辅助结构。贴错地方,等于每箱都写着“可能有”。

BloomFilter 用在哪
查询条件必须是 in 或 =,而且列要高基(5000 以上);也适用于非前缀过滤
Bitmap 用在哪
像身份证号这种基数特别高、查询又是等值(=)的列,用 Bitmap 能极大加速
反例(原文原话)
建在低基数列上,比如“性别”列,每个 Block 几乎都会包含所有取值,导致 BloomFilter 索引失去意义
真名
BloomFilter / Bitmap;建表时按列挂,具体写法按建表平台模板走——这一步的实际值要现场核
注意 · 原文还有一句要核索引规范里另有一句“数据基数在一半左右”。它到底指哪张表、哪一列,材料里没给上下文,别拿这句话当通用规则用——这条要现场核。
会出事 · 低基列建 BloomFilter在“板块”“性别”这种只有几个取值的列上建,每个块都命中,索引一次都没帮上忙,还白占空间、拖慢导入。查的人以为“有索引会快”,实际一点没快。

3.5 钥匙:副本

🔑 同一批货留几把钥匙(副本)

像什么:仓库钥匙配三把,一把丢了还有两把能开门。

其实是什么:副本数决定同一份数据在集群里存几份。坏一块盘,还有别的份顶上。

硬规矩
表的副本数必须为 3(规范“属性规范”一条,就这一句,没有例外条款)
真名
"replication_allocation" = "tag.location.default: 3"
在哪写
建表语句最后的 PROPERTIES(...) 里
会出事 · 副本改成 1 省空间省下来的空间是实打实的,代价也是实打实的:那块盘一坏,这张表当场查不出来,页面上不是变慢,是直接没数。省空间该省在分区和过期数据上,不该省在钥匙上。

3.6 排法:DUPLICATE KEY(最容易被误解的一条)

🧾 排列顺序单(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”,就落在这。
会出事 · 规范和现实对不上的一处规范 2.1.3 白纸黑字写着“不要使用 Auto Bucket”,理由是 tablet 数量过多、大量小文件;可这张真实表建的时候写的是 DISTRIBUTED BY RANDOM BUCKETS AUTO。两句话同时是真的,不冲突——它们只是不在同一页上。对外讲架构时按实际口径说:这张表就是 RANDOM BUCKETS AUTO 建的,别照文档念“我们不用 Auto Bucket”,脚本一翻就露。要真收口,办法也是现成的:把 AUTO 换成按 1–3G / tablet 算出来的固定桶数。
小窍门给不懂技术的人讲这页,只讲一句就够:这张表按 period 排队存、按 Random 分箱、每份数据留三把钥匙。至于为什么不用 Hash、为什么不去重——那是下一个问题,先别在同一个句子里讲完。

五、串起来:一次完整的摆货

把上面这些零件按真实顺序排一遍。顺序不是讲究:每步的产出,正好是下一步的输入。

  1. 定库定名。先定库(仓这边是 dm_gk_dws),再按 ${数据分层}_${一级}_${二级}_${三级}_${其他} 拼表名;每一段去词根表 data_dqc.t_gov_asset_name 里查。全小写、只允许字母数字下划线、64 字节以内。
  2. 先量体积。这张表一天/一个分区大概多少 G?没有这个数,后面三个决定全是猜的。
  3. 定分区。目标是单个 tablet 1–3 GB:100G 一个分区就配 100 个桶。表不到 2000 万行,动态分区一律不开。
  4. 定分桶。不用 Auto Bucket,按量算桶数;1000 万–2 亿行的表可以不设分区、直接分桶;分桶字段倾斜超过 30% 就改 Random。
  5. 定排法。选 DUPLICATE KEY 的列(这张表取首列 period),心里记牢:它是排序,不是去重。
  6. 贴标签。只有“查得勤 + 条件是 in 或 = + 基数 5000 以上”的列才值得建 BloomFilter;身份证这类高基等值查询用 Bitmap;低基列什么都别建。
  7. 配钥匙。PROPERTIES 里写死副本 3。
  8. 灌数。SeaTunnel 从湖里灌进来(RECREATE_SCHEMA + overwrite),灌完对一遍湖和仓。对不上先信湖。
  9. 交给页面。页面直连查这张表:亿级禁 select *,字段点名字,条件带分区分桶;高并发点查开行存。

九步里,第 2 步最容易被跳过——很多人上来就写 DDL。可 1–3G、桶数、要不要分区、要不要改 Random,全是从“一天多少 G”这一个数推出来的。跳过它,后面每一行参数都是抄别人的。

六、搞错了会怎样

摆错的代价分两种:一种是当场报错,还算好的;一种是悄无声息——那种才贵。

会出事 · 以为 DUPLICATE KEY 去重最常见、也最贵。重跑一次同步、或者同一批数写了两遍,表里就是双份。SQL 没错、任务全绿、页面能打开,只有一个数字不对:翻倍了。这种错最难查,因为它长得太正常。
会出事 · 用了 Auto Bucket系统替你估桶数,估多了就是 tablet 一堆、小文件一片,导入越跑越慢。规范禁止它的原因就这一句:tablet 数量过多、大量小文件。
会出事 · 小表开动态分区分区自己长,没人删。占的地方不大,占的心力不小——每次要删,都得先弄明白哪些是活的。
注意 · 低基列建 BloomFilter每个 Block 都命中,索引形同虚设。表现是“加了索引一点没快”,然后有人开始怀疑 Doris 不行。
注意 · 副本不是 3平时看不出来。等某台机器出问题,才发现这张表只有一份,页面不是慢,是没数。
小窍门 · 排查顺序别搞反页面数字不对,按这个顺序查:①同步有没有覆盖全(SeaTunnel 那次跑成功了吗、整表覆盖了吗)→ ②建表有没有摆错(DUPLICATE KEY 被当成去重?分区桶数拍脑袋?副本只有 1?)→ ③最后才怀疑湖里的数据口径。反过来查,能查一整天。

七、一句话记住

做对了Doris 只负责“快”;它错了先看同步、再看建表,最后才怀疑数据。

再补一句给人听:这间仓库不用你盖,也不用你守夜。你只需要在卸货前量一次尺寸,把层、箱、标签、钥匙四件事定下来;剩下的,交给每天那次覆盖写。

事实来源:数据实施规范 第 2 章(Doris 物理模型)· create_doris_tables.py · 数据中台复盘这一关只管“怎么摆”下一关:预制件工厂