报表需求 · 极简 ADS 开发
把「接到一个报表需求 → 出一张能喂 BI 的宽表」压成最短路径:不建 DWD、不建 DIM,报表即模型。
适用于 ODS 已就绪、报表口径相对独立的场景,规则均已在 Apache Doris 上实跑验证。
开工前硬门槛(先过这一关,命中即拒绝)
在取数、建表、写 SQL 之前,拿需求样例列 + 待落地字段逐条对照。命中任一条,不接单,直接转分层建模。
| # | 命中条件(任一即触发) | 判定依据 |
|---|---|---|
| 1 | 宽表预计 超 60 列 | 需求样例的列数(含占位列) |
| 2 | 需要 两种以上粒度 | 同一张表既要「按业务键汇总」又要「按单据/明细」;或总览表与明细表并存 |
| 3 | 同一口径会被第二个报表复用 | 业务方已明确另有第 2 个报表要同一个数 |
| 4 | 出现 「一列两口径」 | 同名字段在不同行 / 不同来源含义不同 |
命中后的动作:
- 不建 ADS 宽表、不写加工 SQL、不拆 CTE —— 做到一半才发现撑不住,返工成本最高;
- 回复用户:「本需求命中极简路线退出条件 N(写明哪条),建议改走分层建模」,等确认后再开工。
何时用 / 何时不用
用:单张报表、口径相对独立、预算 1~2 人日出第一版、BI 直连 ADS。
不用(改走分层建模):
- 同一口径已被 ≥2 个报表复用 → 该口径该下沉 DWD;
- 宽表超过 ~60 列且出现「一列两口径」→ 拆事实表;
- 需要按不同粒度(按单据 / 按供应商 / 按月)做多维分析 → 建 DWD 事实表再派生。
> 回补是有成本的,所以第一步就把「将来会不会被复用」问清楚,别等宽表长到 80 列再返工。
开发期交付物(3 件)
| # | 交付物 | 落位 | 说明 |
|---|---|---|---|
| 1 | Mapping 文档 | 交付目录 | 报表列 ⇄ 源表.字段,逐行 |
| 2 | <主题>-ADS建表DDL.sql |
交付目录 | 只含 HIS 存量重建段 |
| 3 | <主题>-加工SQL.sql |
交付目录 | WITH + INSERT 一条语句 |
上线交付包另计,见 Step 5。
过程件(迭代版、临时脚本、探测结果)一律不进交付目录。
Step 0 · 解析需求样例
报表需求通常给一份 xlsx,两级表头:第 2 行是分组(如 总预算、执行情况),第 3 行才是列名;可能还有一行「数据来源」标注(某列写「业务系统」「当前无线上字段」之类)。
做法:按 sheet 逐列抽出 (列号, 分组, 列名, 数据来源) 四元组,得到列全集的唯一事实源。后续 DDL 与 SQL 都以这份清单为准,不要凭记忆补列。
★ 高频陷阱:报表样例没有业务主键
报表往往只有「序号 + 名称」,没有可做唯一键的业务编码。处理方式:
1. 自补一个业务键列(如 OBJECT_NO),放主键第二位;
2. 同时列进待确认项(源端哪个字段是该编码、名称是否可能重名),不静默拍板。
★ 高频陷阱:某些列标注「当前无线上字段」
这类列照样建列、照样进 SQL,但取值先占位 NULL,并在待确认清单里写明「需业务线下补录或另找系统」。不要因为取不到就删列——删了 BI 报表会少一列。
Step 1 · 定表结构
- 主键:
CALENDAR_DATE(报表截止期,每月整体替换取值)+ 业务键(如OBJECT_NO)。
Doris 的UNIQUE KEY必须取列前缀,所以主键列天然置顶。 - 列顺序:
CALENDAR_DATE→ 业务键 → 报表列原序 →ETL_TIMESTAMP。
保持报表列序,BI 侧拖字段时能跟报表逐列对上,验收对数快。 - 分区:极简模式不建动态分区,
CALENDAR_DATE作主键首列即可;数据量上来再加PARTITION BY RANGE。 - 分桶:
DISTRIBUTED BY HASH(<业务键>) BUCKETS 3(宽表行数 = 业务对象数 × 期数,通常万级以内,3 桶够用)。
Step 2 · 出建表 DDL
规则详见 references/ddl-conventions.md,模板见 assets/ads-ddl-template.sql。七条硬规范:
- 只出 HIS 存量重建段:
DROP TABLE IF EXISTS+CREATE TABLE,不生成 INC 段。 - 类型全部大写:
DATE/INT/VARCHAR(100)/DECIMAL(38,10)/DATETIME,不写小写。 - 金额一律
DECIMAL(38,10),精度统一 10 位,不按列分别定 4 位 / 6 位。 - COMMENT 不带列号:写
COMMENT "序号",不写COMMENT "A 序号";两级表头按报表语义合成一句。 - 表 COMMENT 取纯业务名:写
COMMENT '项目预算执行情况',不加「XX 报表 R1 ·」这类自拟前缀。 VARCHAR(n)的 n 按「字节」计(中文 UTF-8 占 3 字节):长中文文本列按「字符数 × 3」定宽,描述/备注类直接VARCHAR(4000)。
否则超长行会在 strict mode 下被静默过滤(报Insert has filtered data),一百多行里只丢几行,极易误判成脏数据。- 文件头只留执行必需信息(标题 / 生成依据 / 目标库 / 编码 / 规模 / 用法 + VARCHAR 警示),
不写「设计约定」段;表前注释压到一行短名(-- <对象>主数据),不写「N 列 / 来源 / 粒度」三行块。
Step 3 · 出加工 SQL
规则详见 references/etl-naming-and-skeleton.md,模板见 assets/etl-skeleton-template.sql。
3.1 按数据来源拆 CTE(基础表)
一个数据来源 = 一个 CTE,粒度统一到「一业务键一行」。典型三源:
| CTE | 数据来源 | 承载列 |
|---|---|---|
PROJECT_INFOS |
业务系统主数据 | 对象主数据 + 属性 + 里程碑节点 |
ERP_VOUCHER_AMOUNTS |
ERP 凭证 | 实际发生额、执行数量 |
PURCHASE_ORDER_AMOUNTS |
采购订单 | 预算额、预算数量 |
临时表命名规范:业务对象英文复数,不用 tmp_ / t1 之类前缀 —— 看表名就知道装了什么。
反例 tmp_pm / tmp_erp / tmp_po,正例 PROJECT_INFOS / ERP_VOUCHER_AMOUNTS / PURCHASE_ORDER_AMOUNTS。
别名规范:t = 驱动表;t1 / t2 / t3 … 按 FROM 后的出现顺序递增,从表、维表同规则。
不要用 p. / e. / o. 这类首字母缩写——一旦来源调整,别名就和表名脱钩了。
3.2 骨架阶段全字段占位
口径未定时一律占位,绝不自拟算法:
| 类型 | 占位写法 |
|---|---|
| 文本 | '-' AS OBJECT_NO |
| 数值 / 金额 | CAST(NULL AS DECIMAL(38,10)) AS TOTAL_BUDGET |
| 日期 | CAST(NULL AS DATE) AS START_DATE |
| 整数 | CAST(NULL AS INT) AS SEQ_NO |
NULL 必须显式 CAST 类型,否则 Doris 推断不出列类型会直接报错。
每个 CTE 用 FROM (SELECT 1 AS _dummy) _d WHERE 1 = 0 收口,保证骨架跑得通、入 0 行、不污染目标表。接真源表时删掉这一行即可。
3.3 四段式 + 单语句
脚本结构:WITH + INSERT … SELECT,只有一条语句。
Doris 的 CTE 不能跨语句存在,WITH(基础表)与 INSERT(入库)必须同属一条。
驱动表选主数据(PROJECT_INFOS),LEFT JOIN 两侧事实表 —— 这样 0 发生额的对象也会留在台账里。若拿事实表驱动,会把「还没花钱的对象」整行丢掉,报表就少行。
派生列(执行率等)先占位,把公式写在注释里,等基础字段落数后再启用:
CAST(NULL AS DECIMAL(38,10)) AS `QTY_EXEC_RATE`,
-- 公式:CASE WHEN t2.`YEAR_BUD_QTY` > 0
-- THEN t1.`YEAR_EXEC_QTY` / t2.`YEAR_BUD_QTY` END
落盘前的自检:INSERT 列清单与 SELECT 输出必须逐列同序同数,连库用真实 DESC 结果比对,不靠肉眼。
Step 4 · 落盘后三件实测(不可省)
# ① 编码与残留(必须报出:字节数 / 是否 BOM / UTF-8 / 行数)
python scripts/check_sql_encoding.py "<加工SQL路径>"
# ② 实跑(连 Doris 执行整条语句,前后 COUNT(*) 应一致)
python scripts/verify_sql_exec.py "<加工SQL路径>" <库>.<表> \
--host <HOST> --port 9030 --user <USER> --password <PASSWORD>
① 编码铁律:所有 SQL 一律 UTF-8 无 BOM。落盘后用 open(p,'rb') 读首 3 字节判 EF BB BF,再 decode('utf-8') 验证可解码。不得只凭"用 UTF-8 写入"就声称合规,必须在回复里报出四元数据。
写文件用 open(p, 'w', encoding='utf-8', newline=''),禁止 utf-8-sig、禁止默认编码。
② 实跑:骨架阶段跑通即为通过(入 0 行是预期,不是失败)。
③ 残留:改名/换别名后 grep 旧名(tmp_、p.、e.)应为 0;仅命名约定注释里提及旧名可保留 1 处。
Step 5 · 上线交付包与校验
目录结构(交付根目录下建 90_上线交付包\):
90_上线交付包\
01_建表DDL\ <主题>-ADS建表DDL.sql(必要时 + <主题>-DWD中间表建表DDL.sql)
02_ETL脚本\ <主题>-01-<对象>主数据.sql / -02-<度量A>.sql / -03-<度量B>.sql / -04-组装入库.sql
03_校验脚本\ 01-建表校验.sql / 02-数据校验.sql
04_回滚脚本\ 01-回滚.sql
- 上线相关文件移动进交付包(不复制),原目录留一个
README.md指向新路径。 - ETL 超过 2 段数据来源时按「主数据 / 度量A / 度量B / 组装」拆文件,每段独立可跑、独立验证行数。
- 截止期一律
${CALENDAR_MONTH}占位符,由调度平台传入;截止期条件后挂一行-- and <业务键> = '<真实编码>'便于单对象排查。
校验脚本两件套(详见 references/verify-scripts.md):
| 文件 | 查什么 | 期望 |
|---|---|---|
01-建表校验.sql |
列数 / 表注释 / UNIQUE KEY / 分桶副本 / 关键列类型 / 临时表残留 | 全 PASS |
02-数据校验.sql |
行数盘点 / 主键唯一 / 编码集合一致 / 金额逐列对账 / 空值率 | 全 PASS |
两个脚本都要连库真跑一遍再交付,判定列直接输出 PASS / FAIL 字符串,不给原始数字让人自己判断。
硬规则
- SQL 一律 UTF-8 无 BOM,落盘实测校验并报数。
- 口径未定 → 占位符,不自拟算法、不猜字段。
- 报表样例无业务主键 → 自补一列 + 列入待确认,不静默拍板。
- DDL 只出 HIS 段;类型大写;金额
DECIMAL(38,10);COMMENT 无列号;表注释纯业务名。 - 临时表命名 = 业务对象英文复数(无
tmp_);别名t/t1/t2按 FROM 顺序。 - CTE 与 INSERT 必须同一条语句;脚本在
;结束。 NULL一律显式CAST类型。- 交付目录只放最终件;过程件留在临时目录。
VARCHAR(n)按字节计,长中文文本列按「字符数 × 3」定宽(描述/备注类VARCHAR(4000))。- DDL 文件头不写「设计约定」段、表前注释一行短名;改表注释用
MODIFY COMMENT,不是SET ("comment"=...)。 - 上线交付包按
01_建表DDL / 02_ETL脚本 / 03_校验脚本 / 04_回滚脚本四目录组织,校验脚本连库实跑后再交付。
连接方式(由使用者自填)
连接信息一律作为参数传入,不要硬编码到技能文件中:
pymysql.connect(host="<HOST>", port=<PORT>, user="<USER>", password="<PASSWORD>", charset="utf8mb4")
pyodbc.connect(r'DRIVER={SQL Server};SERVER=<HOST>,<PORT>;DATABASE=<DB>;UID=<USER>;PWD=<PASSWORD>')
依赖:pymysql(Doris/SelectDB)、pyodbc(SQL Server 源端,可选)、openpyxl(读需求样例 xlsx)。
建议装在独立虚拟环境,用该环境的 python 运行本技能脚本。
参考文件
references/ddl-conventions.md—— 建表规范细则 + 完整双表样例 + 结构变更与比对references/etl-naming-and-skeleton.md—— 命名/别名规范、占位符类型表、四段式结构、脚本头部规范、跑批参数、Doris 语法踩坑references/verify-scripts.md—— 建表校验 / 数据校验两件套 + 四个实测坑references/spec-xlsx-parsing.md—— 需求样例解析法 + 待确认项清单模板assets/ads-ddl-template.sql—— 建表 DDL 模板assets/etl-skeleton-template.sql—— 加工 SQL 骨架模板