Apache Doris 的 UNIQUE KEY 表导完两批数据,同一主键查出来还是旧值,这种「写时合并像没生效」的情况,先用 TaoToken 把排查通道搭起来最省事:打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册并创建一把 API Key,再把 Codex 的 Base URL 填成 https://taotoken.net/api,让它在对话里对照 Doris 对 Unique 模型写时合并(merge on write)的说明,帮你把建表语句、导入批次和查询版本逐项对一遍。TaoToken 在这里只做一件事——把这把 Key 和模型通道连通,它不参与 Doris 的合并计算,也不会替你去连你的库。
真正要查的是 Doris 自己的行为。原文在讲 Unique 模型时给了一个很关键的边界:同一个导入批次内的数据,对 REPLACE 这种聚合方式,替换顺序不做保证;不同导入批次之间,才可以保证后一批次替换前一批次。很多人第一次建 Unique 表,看到「旧值没被替换」就以为是写时合并失效,其实问题常常出在批次边界、建表语句里的 Key 列、以及 compaction 还没追上。下面按原文的模型脉络,把这套排查写成可落地的步骤。
1. UNIQUE KEY 表旧值没替换,先分清批次内和批次间
1.1 原文那句关于 REPLACE 的关键说明
原文用 Aggregate 模型举了一个用户访问事实表的例子:同一个用户同一天有两行数据,last_visit_date是 REPLACE 聚合,导入后只保留一行,值可能是 06:00:00 也可能是 07:00:00,因为同批次内 REPLACE 的替换顺序不保证。后面又补了一句:不同批次之间,后一批次才会替换前一批次。
这两句话几乎能解释一大半「Unique 表旧值还在」的疑问。Unique 模型在 1.2 之前本质上就是 Aggregate 模型的一个特例,把所有 Value 列都设成 REPLACE,用UNIQUE KEY简写出来。所以在读时合并(merge on read)的时代,你查 Unique 表,Doris 是查询时才把多版本按 REPLACE 规则揉成一行;到了 1.2 引入写时合并(merge on write),合并动作被挪到写入侧,查询直接读合并后的结果。
如果你的两行新数据落在同一个导入批次里,主键相同,那替换顺序本身就不保证——先导入的那条旧值留到最后,是完全符合语义的,不是故障。
1.2 写时合并 vs 读时合并,先确认你查的是哪条路径
写时合并和读时合并不是同一个东西,排查前先确认表实际走的是哪条:
- 读时合并:数据按多版本存,查询时按主键合并,
UNIQUE KEY表默认可能还是这一套; - 写时合并:写入阶段就把同主键的旧版本标记、compaction 时物理整理,查询拿到的已经是合并结果;
- 1.2 之后两者短暂共存,具体走哪条受建表属性、版本和 compaction 状态影响。
所以「旧值没被替换」有三种常见可能:新数据其实没成功进同一个副本;新数据和旧数据在同一批次内、顺序不利;写时合并属性没生效,查询侧仍在读旧版本行。这三件事都能用 Codex 生成 SQL、你在本地或 SQL 客户端执行来验证,而不是让 AI 去连你的库。
2. 把 Codex 的 Base URL 指到 TaoToken 通道
2.1 在官网拿 Key,并确认模型广场里的模型 ID
先打开 TaoToken 注册,进控制台创建一把 API Key,记下占位符替换用的真实值;顺手在模型广场看一眼当前可用的模型 ID,后面填配置时以模型广场当时列表为准,不要随手编一个带日期后缀的名字。
这一步只解决两件事:给 Codex 一把能用的 Key,以及给它一个正确的模型 ID。Doris 的合并逻辑、compaction、导入批次都由你自己的 Doris 集群决定,通道不参与其中。
2.2 ~/.codex/config.toml 里改 model_provider 和 base_url
Codex 读的是~/.codex/config.toml,别把 Claude Code 的ANTHROPIC_*环境变量套过来。自定义供应商按下面这样写:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"其中base_url结尾不要加/v1,Key 通过环境变量TAOTOKEN_API_KEY传入,值就是你在官网创建的那把。保存后新开一个终端会话,让环境变量生效,再启动 Codex。确认通道连通之后,后面的对话都围绕 Doris 的建表和导入展开,Codex 只负责解释和生成 SQL,不会自动碰你的数据库。
3. 让 Codex 生成 Unique 模型检查清单,SQL 拿到本地跑
3.1 先核对 UNIQUE KEY 的 Key 列和建表属性
原文的 Unique 例子主键是user_id + username,Value 列全部是 REPLACE 语义。你把建表语句贴进 Codex,让它按 Unique 模型的规则逐条对照:Key 列是否真的唯一约束、有没有把本该 REPLACE 的列漏在 Key 外面、分桶键是否合理、enable_unique_key_merge_on_write属性有没有打开。
一个可运行的 Unique 建表骨架可以写成这样:
CREATE TABLE IF NOT EXISTS example_db.user_profile ( `user_id` LARGEINT NOT NULL COMMENT "用户 id", `username` VARCHAR(50) NOT NULL COMMENT "用户昵称", `city` VARCHAR(20) COMMENT "所在城市", `phone` LARGEINT COMMENT "联系电话", `address` VARCHAR(500) COMMENT "联系地址", `update_time` DATETIME COMMENT "更新时间" ) UNIQUE KEY(`user_id`, `username`) DISTRIBUTED BY HASH(`user_id`) BUCKETS 4 PROPERTIES ( "replication_allocation" = "tag.location.default: 1", "enable_unique_key_merge_on_write" = "true" );把这段存成 SQL 文件,由你在自己的 Doris 环境里执行,然后把SHOW CREATE TABLE的结果、报错原文或查询输出贴回 Codex 对话,让它继续比对。不要让 Codex 直接连生产库替你执行。
3.2 查导入批次和版本,判断旧值属于哪一批
旧值没被替换,最容易忽略的就是「批次」。本地执行SHOW LOAD看最近的导入任务,把 label、状态、时间对一遍,确认新数据是不是真的进了同一个表、同一个分区。可以请 Codex 生成一组只读排查 SQL 模板:
SHOW LOAD FROM example_db ORDER BY CreateTime DESC LIMIT 20; SHOW PARTITIONS FROM example_db.user_profile; SHOW TABLET FROM example_db.user_profile;如果新数据是独立批次导入成功、批次时间晚于旧值,那跨批次替换本应生效;如果两行数据挤在同一批,那顺序不保证就是预期行为。Codex 可以帮你读这些输出、解释每一列含义,但执行和判断环境仍然在你这侧。
3.3 写时合并开关与 compaction 状态
确认写时合并是否真的在起作用,要同时看表属性和 compaction。表属性里enable_unique_key_merge_on_write为true才走写时合并路径;如果它是false或没设,表可能仍按读时合并处理。另一方面,即使开关打开,物理合并也要等 compaction 推进,短时间内查询读到的可能是尚未整理完的版本。
把SHOW CREATE TABLE的结果、以及集群当前的 compaction 相关监控贴给 Codex,让它给出「属性是否生效、还需要等多久、要不要手动触发」的对照表。这里要守住边界:Codex 只解释和对照,compaction 的触发命令由你在本地评估后执行。
4. 三种数据模型分层对照,别把 Unique 用错层
4.1 Duplicate:DWD 明细和 ADS 任意维度
Duplicate 模型完全按导入文件存储,两行完全相同也保留,不做任何聚合,列存优势能发挥出来。原文建议它在 DWD 层保存原始明细和历史,在 ADS 层做任意维度聚合。建表时不写 Unique、Aggregate 或 Duplicate 时,默认就是这个模型,并自动指定排序列。排查 Unique 之前先想清楚:这张表到底是要唯一主键,还是只是想做明细追加——用错模型,讨论「替换」本身就没意义。
4.2 Aggregate:固定报表和 REPLACE / SUM / MAX / MIN
Aggregate 模型把列显式分成 Key 和 Value,Key 相同就聚合 Value,聚合方式有 SUM、REPLACE、MAX、MIN 四种。它适合模式固定的报表查询,预聚合能大幅减少扫描量;但对COUNT(*)不友好,Key 或记录数很多时查询也会变重。原文特别点出 REPLACE 的语义:下一批数据替换之前导入的行中的值。这正好和 Unique 的读时合并同源,所以排查 Unique 时,Aggregate 的 REPLACE 规则是你理解批次行为的最好参照。
4.3 Unique:ODS 主键唯一,1.2 后走向写时合并
Unique 模型保证主键唯一,但用不了 ROLLUP 这类预聚合特性,原文因此把它定位在 ODS 层——初次全量加每天增量、需要唯一主键约束的场景。1.2 之前它等价于「全 REPLACE 的 Aggregate 表」,1.2 引入写时合并后查询性能更好,官方也把它作为未来默认方向,两者短暂共存。把这张表放在 ODS、把明细留在 DWD、把固定报表交给 Aggregate,是原文那条分层建议的落点。
5. Codex 答复与 Doris 行为对不上时的排障
5.1 Base URL 多写 /v1、模型 ID 编造这类配置错
通道侧的错比较集中:base_url写成https://taotoken.net/api/v1会直接出问题,末尾就保留https://taotoken.net/api;模型 ID 不要凭记忆拼,去模型广场确认;环境变量名要和env_key一致。还有一种情况是把别家工具的变量照搬进来,Codex 只认自己的config.toml。这些配置错会让对话根本进不去,和 Doris 的合并行为无关,先分清楚是哪一层。
5.2 把批次号、表结构、报错原文一起贴回对话
当 Codex 说「跨批次应该替换」、而你查到的旧值仍在,别只贴一句结论,把SHOW CREATE TABLE、SHOW LOAD的结果、查询语句和实际输出一起贴回去,明确告诉它这是本地执行后的结果。让它对照建表 Key 列、写时合并属性、批次时间重新推断。Codex 的价值在这里:它帮你把 Doris 文档语义和你手头的证据对上,而不是替你去集群里跑命令。
6. 排查收尾:把这次调用和结果对一下账
走到这里,你已经能用 Codex 解释 Unique 写时合并与读时合并的差异,也能生成一套只读 SQL 在自己的 Doris 里核对建表语句和导入批次。接下来回到 TaoToken 模型对话 用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 都没填错;如果之后要长期靠它读建表语句、整理排查记录,可以看一眼 Coding Plan 的额度是否够用。
Key 本身在 控制台 API Keys 创建和管理,Codex 这类命令行工具的接入参数也可以对照 Claude Code 接入文档 里的环境变量写法做迁移参考。回到 Doris 这件事本身:遇到旧值没被替换,先把批次边界、Key 列、写时合并属性和 compaction 四项排一遍,多数「写时合并失败」最后都落在同一批次内的 REPLACE 顺序不保证这条语义上。Control 住这四步,比反复重导数据有效得多。