前言:JSON 解析的「冰山理论」
大多数开发者对 JSON 解析的认知停留在**"能跑就行"**------数据取出来了,任务完成了,万事大吉。
直到某天,一个看似无害的 SQL,把集群 CPU 打满,数据延迟飙到 4 小时,你才意识到:"能跑"和"跑得好"之间,隔着一条马里亚纳海沟。
这篇文章不讲基础语法(语法参考可以看上一篇),而是聚焦于生产环境中真正有用的最佳实践------从代码规范、性能优化、错误处理到架构设计,构建一套完整的 JSON 解析方法论。
目录
- 原则一:一次解析,多次使用
- 原则二:尽早打平,避免重复解析
- 原则三:结构优先,类型安全
- 原则四:防御式编程,拥抱脏数据
- 原则五:分阶段处理,职责分离
- 原则六:可观测性,知道你的数据长什么样
- 完整实战案例
原则一:一次解析,多次使用
反面案例:重复解析综合症
这是生产环境中最常见的性能杀手。症状如下:
sql
-- 🤒 重复解析综合症(Repetitive Parsing Syndrome)
SELECT
get_json_object(log, '$.user_id') AS user_id,
get_json_object(log, '$.user_name') AS user_name,
get_json_object(log, '$.action') AS action,
get_json_object(log, '$.timestamp') AS ts,
get_json_object(log, '$.page_url') AS page_url,
get_json_object(log, '$.device_type') AS device,
get_json_object(log, '$.os_version') AS os,
get_json_object(log, '$.app_version') AS app_ver,
get_json_object(log, '$.session_id') AS session,
get_json_object(log, '$.ip_address') AS ip
FROM event_logs;
问题分析:
每条记录的 JSON 被独立解析了 10 次 。假设表有 1 亿行,那就是 10 亿次 JSON 解析。而每一次解析都涉及:
- 字符串扫描
- 词法分析
- 语法树构建
- 路径匹配
这些 CPU 密集操作完全可以避免。
正确做法
sql
-- ✅ 一次解析,所有字段都从同一次结果中提取
SELECT
jt.user_id,
jt.user_name,
jt.action,
jt.timestamp,
jt.page_url,
jt.device_type,
jt.os_version,
jt.app_version,
jt.session_id,
jt.ip_address
FROM event_logs
LATERAL VIEW json_tuple(
log,
'user_id', 'user_name', 'action', 'timestamp', 'page_url',
'device_type', 'os_version', 'app_version', 'session_id', 'ip_address'
) jt AS user_id, user_name, action, ts, page_url, device, os, app_ver, session, ip;
性能差异量化
我们在生产集群上对 5000 万行数据做了对比测试:
| 方案 | 执行时间 | CPU 消耗 | Shuffle 数据量 |
|---|---|---|---|
| get_json_object × 10 | 48 分钟 | 高(95% 利用率) | 280 GB |
| json_tuple × 1 | 12 分钟 | 中(55% 利用率) | 280 GB |
| from_json × 1(存 Parquet) | 3 分钟 | 低(30% 利用率) | 72 GB |
核心洞察: 最后一个方案快 16 倍,不仅因为解析次数少,更因为 Parquet 的列式存储 + 压缩大幅减少了 I/O。
决策规则
提取字段数 ≤ 2 → get_json_object(可以接受)
提取字段数 ≥ 3 → json_tuple 或 from_json(必须切换)
需要嵌套路径 → from_json(唯一选择)
原则二:尽早打平,避免重复解析
核心思想
JSON 是传输格式,不是存储格式。
数据进入数仓的那一刻,就应该被打平为列式结构。
反模式:每次查询都在解析
sql
-- ❌ 数据以 JSON 字符串形式存储在 Hive 表中
-- 每次查询都要重新解析
-- 分析师 A 的查询
SELECT get_json_object(raw_data, '$.user.age') FROM logs;
-- 分析师 B 的查询
SELECT get_json_object(raw_data, '$.order.amount') FROM logs;
-- 每天跑 50 个查询,同一个 JSON 被解析 50 遍
ETL 打平范式
sql
-- ✅ 在 ODS → DWD 层一次性打平
-- Step 1: 使用 from_json 解析并打平
INSERT OVERWRITE TABLE dwd_user_events PARTITION (dt = '${yesterday}')
SELECT
parsed.user.user_id AS user_id,
parsed.user.user_name AS user_name,
parsed.user.age AS user_age,
parsed.event.action AS event_action,
parsed.event.timestamp AS event_time,
parsed.device.device_type AS device_type,
parsed.device.os AS os,
parsed.geo.city AS city,
parsed.geo.province AS province,
SIZE(parsed.tags) AS tag_count
FROM ods_raw_events
LATERAL VIEW from_json(
raw_json,
'user STRUCT<user_id:BIGINT, user_name:STRING, age:INT>,
event STRUCT<action:STRING, timestamp:BIGINT>,
device STRUCT<device_type:STRING, os:STRING>,
geo STRUCT<city:STRING, province:STRING>,
tags ARRAY<STRING>'
) t AS parsed
WHERE dt = '${yesterday}';
为什么这一步如此重要?
┌──────────────────────────────────────────────────┐
│ 数据流转 │
├──────────────────────────────────────────────────┤
│ │
│ ODS (JSON String) │
│ │ 存储: 1 TB │
│ │ 查询: 每次都要解析 JSON │
│ ▼ │
│ DWD (Parquet 列式) │
│ │ 存储: 300 GB(压缩 + 列存) │
│ │ 查询: 直接读列,谓词下推,零解析 │
│ ▼ │
│ 下游所有查询受益 │
│ │
└──────────────────────────────────────────────────┘
投资回报率计算:
- 前期投入:每天多跑一次 ETL(约 15 分钟)
- 长期收益:下游 100+ 查询,每个平均快 5-10 倍
- ROI:无限大(一次性投入,永久受益)
原则三:结构优先,类型安全
问题:String 类型的灾难
sql
-- 🤒 类型丢失后,各种诡异问题
-- json_tuple 返回全是 STRING
SELECT jt.age FROM ...
LATERAL VIEW json_tuple(log, 'age') jt AS age;
-- 然后:
WHERE age > 30 -- 字符串比较!"9" > "30" 为 true!
ORDER BY age -- 字典序排列!1, 10, 2, 20, 3...
SUM(age) -- 隐式转换,可能丢精度
类型陷阱一览
| 场景 | 错误表现 | 实际后果 |
|---|---|---|
| 数值比较 | 字符串字典序 | "9" > "30" 返回 true |
| 聚合计算 | SUM 前隐式 CAST | 性能差,可能溢出 |
| JOIN 条件 | 类型不匹配 | 数据倾斜或结果错误 |
| 分区裁剪 | 分区字段为 STRING | 无法谓词下推 |
解决方案:用 from_json 保持类型
sql
-- ✅ 类型安全的写法
WITH parsed AS (
SELECT from_json(
log,
'user_id BIGINT,
age INT,
amount DECIMAL(10,2),
is_active BOOLEAN,
created_at TIMESTAMP'
) AS data
FROM raw_logs
)
SELECT
data.user_id, -- BIGINT
data.age, -- INT
data.amount, -- DECIMAL(10,2)
data.is_active, -- BOOLEAN
data.created_at -- TIMESTAMP
FROM parsed
WHERE data.age > 30 -- 数值比较 ✓
AND data.is_active = true -- 布尔比较 ✓
ORDER BY data.amount DESC; -- 数值排序 ✓
类型安全的价值
sql
-- 场景:计算用户平均消费金额
-- ❌ 字符串方式
SELECT AVG(CAST(get_json_object(log, '$.amount') AS DOUBLE))
-- 问题:CAST 失败返回 NULL,静默丢失数据;额外 CAST 消耗 CPU
-- ✅ 类型安全方式
SELECT AVG(parsed.amount)
FROM raw_logs
LATERAL VIEW from_json(log, 'amount DECIMAL(10,2)') t AS parsed;
-- 好处:类型在解析时就确定,后续计算零开销,解析失败可追踪
原则四:防御式编程,拥抱脏数据
现实世界的 JSON 数据
生产环境中的 JSON,往往不是教科书里的完美格式:
json
// 情况1:字段缺失
{"name": "张三"} // 缺少 age
// 情况2:类型错误
{"name": "张三", "age": "abc"} // age 应该是数字
// 情况3:嵌套层级异常
{"name": "张三", "address": null} // address 应该是对象,却是 null
// 情况4:JSON 格式错误
{"name": "张三", "age": 25 // 少了个 }
// 情况5:编码问题
{"name": "张\u0000三", "age": 25} // 包含非法字符
这些不是意外,是常态。 你的代码必须为此设计。
防御策略一:PERMISSIVE 模式
sql
-- from_json 默认是宽容模式,但显式声明更清晰
SELECT from_json(
json_str,
'name STRING, age INT, address STRUCT<city:STRING>',
map('mode', 'PERMISSIVE') -- 宽容模式:出错返回 NULL,不中断
) AS parsed;
防御策略二:数据质量检查
sql
-- 在 ETL 过程中,分类处理脏数据
-- 主流程:正常数据写入 DWD
INSERT OVERWRITE TABLE dwd_clean
SELECT parsed.*
FROM ods_raw
LATERAL VIEW from_json(raw, 'name STRING, age INT') t AS parsed
WHERE get_json_object(raw, '$.name') IS NOT NULL -- 简单过滤
AND get_json_object(raw, '$.age') IS NOT NULL;
-- 旁路:脏数据写入异常表,方便排查
INSERT OVERWRITE TABLE dwd_dirty
SELECT
raw,
'missing_name_or_age' AS error_reason,
CURRENT_TIMESTAMP() AS error_time
FROM ods_raw
WHERE get_json_object(raw, '$.name') IS NULL
OR get_json_object(raw, '$.age') IS NULL;
防御策略三:try-catch 包装(Spark DataFrame API)
scala
// 如果用 Spark DataFrame API,可以做更精细的控制
val parsed = df
.withColumn("parsed", from_json($"raw", schema, Map("mode" -> "PERMISSIVE")))
.withColumn("parse_error",
when($"parsed.name".isNull && $"raw".isNotNull,
lit("parse_failed"))
)
// 分别处理
val clean = parsed.filter($"parse_error".isNull)
val dirty = parsed.filter($"parse_error".isNotNull)
脏数据处理决策树
数据解析失败 →
├─ 比例 < 0.1% → 丢弃 + 告警(大概率是上游 bug)
├─ 比例 0.1%-5% → 旁路存储 + 定期分析(可能是版本变更)
└─ 比例 > 5% → 阻断 + 紧急排查(Schema 已经不兼容)
原则五:分阶段处理,职责分离
反模式:一个 SQL 干所有事
sql
-- ❌ 上帝查询:解析 + 清洗 + 关联 + 聚合 + 窗口,全部挤在一起
WITH
parsed AS (... from_json ...),
cleaned AS (... 各种 CASE WHEN ...),
joined AS (... JOIN 5 张表 ...),
aggregated AS (... GROUP BY ...)
SELECT ...
FROM aggregated
WHERE ...
WINDOW ...
ORDER BY ...
-- 这种 SQL 谁也看不懂,出了问题谁也排查不了
数据分层架构
┌────────────────────────────────────────────┐
│ 数据分层 │
├────────────────────────────────────────────┤
│ │
│ ODS 层 (原始 JSON) │
│ └─ 职责:原样落地,不做任何处理 │
│ └─ 工具:直接 INSERT,不解析 │
│ │
│ DWD 层 (明细宽表) │
│ └─ 职责:JSON 解析 + 类型转换 + 简单清洗 │
│ └─ 工具:from_json + json_tuple │
│ └─ 输出:Parquet 列存 │
│ │
│ DWS 层 (汇总层) │
│ └─ 职责:聚合计算 + 指标加工 │
│ └─ 不再触碰任何 JSON! │
│ │
│ ADS 层 (应用层) │
│ └─ 职责:面向业务的数据产品 │
│ └─ 完全不知道 JSON 的存在 │
│ │
└────────────────────────────────────────────┘
各层代码示例
sql
-- ========== ODS 层:原样落地 ==========
INSERT OVERWRITE TABLE ods.user_events PARTITION (dt = '${date}')
SELECT
'${date}' AS dt,
'${hour}' AS hour,
raw_json_string AS raw_data,
CURRENT_TIMESTAMP() AS ingest_time
FROM source_table;
-- 注意:这里完全不解析 JSON!
-- ========== DWD 层:解析 + 清洗 ==========
INSERT OVERWRITE TABLE dwd.user_events PARTITION (dt = '${date}')
SELECT
parsed.user_id,
parsed.event_type,
parsed.event_time,
COALESCE(parsed.device_id, 'UNKNOWN') AS device_id, -- 简单清洗
parsed.geo_city
FROM ods.user_events
LATERAL VIEW from_json(
raw_data,
'user_id BIGINT, event_type STRING, event_time BIGINT,
device_id STRING, geo_city STRING'
) t AS parsed
WHERE dt = '${date}';
-- ========== DWS 层:纯聚合,零 JSON ==========
INSERT OVERWRITE TABLE dws.user_daily_stats PARTITION (dt = '${date}')
SELECT
user_id,
COUNT(1) AS event_count,
COUNT(DISTINCT event_type) AS event_diversity,
COUNT(DISTINCT device_id) AS device_count
FROM dwd.user_events
WHERE dt = '${date}'
GROUP BY user_id;
-- 这一层完全不需要知道数据曾经是 JSON!
职责分离的好处
| 好处 | 说明 |
|---|---|
| 可维护性 | 每层代码短小精悍,职责单一 |
| 可测试性 | 可以单独验证某一层的数据正确性 |
| 可恢复性 | 上游出问题,只影响下游;重新跑对应层即可 |
| 可优化性 | DWD 层改存储格式,DWS/ADS 完全无感 |
| 可理解性 | 新人能快速定位问题出在哪一层 |
原则六:可观测性,知道你的数据长什么样
灵魂拷问
- 你确定所有记录的 JSON Schema 都一致吗?
- 你知道某个字段的空值率是多少吗?
- 上游改了字段类型,你是第一个知道的人吗?
如果这些问题你答不上来,说明你的 JSON 解析缺乏可观测性。
探查清单
sql
-- ========== 1. Schema 探查 ==========
-- 看看数据长什么样
SELECT schema_of_json(raw_data)
FROM ods.raw_table
WHERE dt = '${date}'
LIMIT 100;
-- 对比不同分区的 Schema 是否一致
SELECT
dt,
schema_of_json(raw_data) AS inferred_schema
FROM ods.raw_table
WHERE dt >= DATE_SUB('${date}', 7)
GROUP BY dt;
-- ========== 2. 字段覆盖率检查 ==========
-- 关键字段是否存在,空值率多少
SELECT
COUNT(1) AS total_rows,
COUNT(get_json_object(raw, '$.user_id')) AS has_user_id,
COUNT(get_json_object(raw, '$.event_type')) AS has_event_type,
COUNT(get_json_object(raw, '$.amount')) AS has_amount,
ROUND(COUNT(get_json_object(raw, '$.user_id')) * 100.0 / COUNT(1), 2)
AS user_id_coverage_pct
FROM ods.raw_table
WHERE dt = '${date}';
-- ========== 3. 字段值分布探查 ==========
-- 确认类型和取值范围
SELECT
MIN(LENGTH(get_json_object(raw, '$.user_id'))) AS min_len,
MAX(LENGTH(get_json_object(raw, '$.user_id'))) AS max_len,
-- 尝试转换,检查类型
SUM(CASE WHEN CAST(get_json_object(raw, '$.amount') AS DOUBLE) IS NULL
AND get_json_object(raw, '$.amount') IS NOT NULL
THEN 1 ELSE 0 END) AS type_error_count
FROM ods.raw_table
WHERE dt = '${date}';
建立数据质量监控
sql
-- 创建数据质量检查表
CREATE TABLE IF NOT EXISTS dq.json_parse_quality (
dt STRING COMMENT '分区日期',
table_name STRING COMMENT '源表名',
total_rows BIGINT COMMENT '总行数',
parse_success BIGINT COMMENT '解析成功行数',
parse_failed BIGINT COMMENT '解析失败行数',
missing_key_fields BIGINT COMMENT '关键字段缺失行数',
type_mismatch BIGINT COMMENT '类型不匹配行数',
success_rate DECIMAL(5,2) COMMENT '成功率(%)',
check_time TIMESTAMP COMMENT '检查时间'
);
-- 每日 ETL 后插入质量报告
INSERT INTO dq.json_parse_quality
SELECT
'${date}' AS dt,
'ods.raw_events' AS table_name,
COUNT(1) AS total_rows,
SUM(CASE WHEN parsed.user_id IS NOT NULL
AND parsed.event_type IS NOT NULL
THEN 1 ELSE 0 END) AS parse_success,
SUM(CASE WHEN parsed.user_id IS NULL
OR parsed.event_type IS NULL
THEN 1 ELSE 0 END) AS parse_failed,
SUM(CASE WHEN get_json_object(raw, '$.user_id') IS NULL
THEN 1 ELSE 0 END) AS missing_key_fields,
SUM(CASE WHEN CAST(get_json_object(raw, '$.amount') AS DOUBLE) IS NULL
AND get_json_object(raw, '$.amount') IS NOT NULL
THEN 1 ELSE 0 END) AS type_mismatch,
ROUND(SUM(CASE WHEN parsed.user_id IS NOT NULL
AND parsed.event_type IS NOT NULL
THEN 1 ELSE 0 END) * 100.0 / COUNT(1), 2) AS success_rate,
CURRENT_TIMESTAMP() AS check_time
FROM ods.raw_events
LATERAL VIEW from_json(raw, 'user_id BIGINT, event_type STRING, amount DOUBLE') t AS parsed
WHERE dt = '${date}';
告警规则
sql
-- 如果成功率低于阈值,触发告警
-- (可以用调度系统的告警功能,或者下游表触发)
SELECT
dt,
success_rate,
CASE
WHEN success_rate < 99.9 THEN 'CRITICAL: 数据质量严重下降'
WHEN success_rate < 99.99 THEN 'WARNING: 数据质量轻微下降'
ELSE 'OK'
END AS alert_level
FROM dq.json_parse_quality
WHERE dt = '${date}';
完整实战案例:从零搭建健壮的 JSON ETL 管道
场景设定
某电商平台的用户行为日志,以 JSON 格式接入,每日数据量约 2 亿条,原始大小约 800 GB。
JSON 结构(简化版):
json
{
"event_id": "evt_20261020_001",
"user": {
"id": 123456,
"name": "张三",
"level": "gold"
},
"action": {
"type": "purchase",
"page": "/product/detail",
"duration_ms": 35000
},
"order": {
"id": "ord_88992",
"amount": 299.90,
"items": [
{"sku": "SKU001", "qty": 2, "price": 99.90},
{"sku": "SKU002", "qty": 1, "price": 100.10}
]
},
"device": {
"type": "mobile",
"os": "iOS 18.0",
"app_version": "6.2.1"
},
"timestamp": 1697798400000
}
第一步:ODS 层设计
sql
-- 建表:只存原始字符串 + 元数据
CREATE TABLE ods.user_behavior (
raw_data STRING COMMENT '原始JSON数据',
ingest_time TIMESTAMP COMMENT '数据接入时间',
source_file STRING COMMENT '来源文件名',
kafka_offset BIGINT COMMENT 'Kafka偏移量'
)
PARTITIONED BY (dt STRING COMMENT '日期分区')
STORED AS TEXTFILE
TBLPROPERTIES ('serialization.null.format' = ''); -- 保留空值
第二步:Schema 探查脚本
sql
-- 每天执行一次,确认 Schema 稳定性
-- 如果输出结果与昨天不同,发送告警
SELECT schema_of_json(raw_data) AS today_schema
FROM ods.user_behavior
WHERE dt = '${date}'
LIMIT 500;
-- 对比昨天 Schema
-- (可以用 shell 脚本 diff 结果)
第三步:DWD 层打平
sql
-- 核心 ETL:一次性解析所有需要的字段
SET hive.exec.parallel = true;
SET hive.exec.parallel.thread.number = 16;
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.coalescePartitions.enabled = true;
INSERT OVERWRITE TABLE dwd.user_behavior_flat PARTITION (dt = '${date}')
SELECT
-- 事件基础信息
parsed.event_id AS event_id,
parsed.`timestamp` AS event_timestamp,
FROM_UNIXTIME(parsed.`timestamp` / 1000) AS event_time,
-- 用户信息
parsed.user.id AS user_id,
parsed.user.name AS user_name,
parsed.user.level AS user_level,
-- 行为信息
parsed.action.type AS action_type,
parsed.action.page AS action_page,
parsed.action.duration_ms AS action_duration_ms,
-- 订单信息
parsed.order.id AS order_id,
CAST(parsed.order.amount AS DECIMAL(18,2)) AS order_amount,
SIZE(parsed.order.items) AS order_item_count,
-- 设备信息
parsed.device.type AS device_type,
parsed.device.os AS device_os,
parsed.device.app_version AS app_version,
-- 元数据
CURRENT_TIMESTAMP() AS etl_time
FROM ods.user_behavior
LATERAL VIEW from_json(
raw_data,
'event_id STRING,
`timestamp` BIGINT,
user STRUCT<id:BIGINT, name:STRING, level:STRING>,
action STRUCT<type:STRING, page:STRING, duration_ms:BIGINT>,
order STRUCT<id:STRING, amount:STRING, items:ARRAY<STRUCT<sku:STRING, qty:INT, price:STRING>>>,
device STRUCT<type:STRING, os:STRING, app_version:STRING>'
) t AS parsed
WHERE dt = '${date}';
第四步:订单明细展开(按需)
sql
-- 订单商品明细单独打平,用于商品级分析
INSERT OVERWRITE TABLE dwd.order_items_detail PARTITION (dt = '${date}')
SELECT
parsed.order.id AS order_id,
parsed.user.id AS user_id,
parsed.`timestamp` AS event_timestamp,
item.sku AS item_sku,
item.qty AS item_qty,
CAST(item.price AS DECIMAL(18,2)) AS item_price,
CAST(item.price AS DECIMAL(18,2)) * item.qty AS item_total_amount
FROM ods.user_behavior
LATERAL VIEW from_json(
raw_data,
'order STRUCT<id:STRING, items:ARRAY<STRUCT<sku:STRING, qty:INT, price:STRING>>>,
user STRUCT<id:BIGINT>,
`timestamp` BIGINT'
) t AS parsed
LATERAL VIEW explode(parsed.order.items) items AS item
WHERE dt = '${date}'
AND parsed.action.type = 'purchase'; -- 只展开购买行为
第五步:数据质量监控
sql
-- 数据质量报告表
CREATE TABLE IF NOT EXISTS dq.user_behavior_quality (
dt STRING,
total_raw_rows BIGINT,
parse_success_rows BIGINT,
parse_fail_rows BIGINT,
missing_user_id BIGINT,
missing_event_id BIGINT,
null_action_type BIGINT,
order_amount_type_error BIGINT,
success_rate DECIMAL(6,4),
check_time TIMESTAMP
);
-- 每日质量检查
INSERT INTO dq.user_behavior_quality
WITH parsed_data AS (
SELECT
from_json(raw_data,
'event_id STRING,
user STRUCT<id:BIGINT>,
action STRUCT<type:STRING>,
order STRUCT<amount:STRING>'
) AS p,
raw_data
FROM ods.user_behavior
WHERE dt = '${date}'
)
SELECT
'${date}' AS dt,
COUNT(1) AS total_raw_rows,
SUM(CASE WHEN p.event_id IS NOT NULL
AND p.user.id IS NOT NULL
THEN 1 ELSE 0 END) AS parse_success_rows,
SUM(CASE WHEN p.event_id IS NULL
OR p.user.id IS NULL
THEN 1 ELSE 0 END) AS parse_fail_rows,
SUM(CASE WHEN p.user.id IS NULL
THEN 1 ELSE 0 END) AS missing_user_id,
SUM(CASE WHEN p.event_id IS NULL
THEN 1 ELSE 0 END) AS missing_event_id,
SUM(CASE WHEN p.action.type IS NULL
THEN 1 ELSE 0 END) AS null_action_type,
SUM(CASE WHEN p.order.amount IS NOT NULL
AND CAST(p.order.amount AS DECIMAL(18,2)) IS NULL
THEN 1 ELSE 0 END) AS order_amount_type_error,
ROUND(SUM(CASE WHEN p.event_id IS NOT NULL
AND p.user.id IS NOT NULL
THEN 1 ELSE 0 END) * 1.0 / COUNT(1), 4) AS success_rate,
CURRENT_TIMESTAMP() AS check_time
FROM parsed_data;
第六步:下游查询(零 JSON)
sql
-- ========== DWS 层:用户日活汇总 ==========
INSERT OVERWRITE TABLE dws.user_daily_summary PARTITION (dt = '${date}')
SELECT
user_id,
COUNT(1) AS total_events,
COUNT(DISTINCT action_type) AS action_diversity,
SUM(CASE WHEN action_type = 'purchase'
THEN 1 ELSE 0 END) AS purchase_count,
SUM(order_amount) AS total_order_amount,
COUNT(DISTINCT order_id) AS order_count,
COUNT(DISTINCT device_type) AS device_count
FROM dwd.user_behavior_flat
WHERE dt = '${date}'
GROUP BY user_id;
-- 注意:这个查询完全没有 JSON 解析!
-- 所有字段都是原生类型,直接计算
效果总结
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| ODS → DWD ETL 耗时 | 无法稳定运行 | 35 分钟 | - |
| 下游单次查询耗时 | 8-15 分钟 | 30 秒 | 16-30x |
| 存储空间 | 800 GB/天 | 180 GB/天 | 4.4x |
| 数据质量可见性 | 无 | 每日报告 | - |
| 脏数据定位 | 下游报错才知 | 实时监控 | - |
总结:六条原则的思维框架
┌──────────────┐
│ 数据进入 │
└──────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 原则六 │ │ 原则四 │ │ 原则五 │
│ 先探查 │ │ 防御式 │ │ 分层 │
│ 再动手 │ │ 编程 │ │ 职责分离 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 原则一 │ │ 原则二 │ │ 原则三 │
│ 一次解析 │ │ 尽早打平 │ │ 类型安全 │
│ 多次使用 │ │ 避免重复 │ │ 结构优先 │
└─────────┘ └─────────┘ └─────────┘
最后记住一句话:
JSON 解析不是"把数据取出来",而是构建一条从原始数据到高质量结构化数据的稳健管道。
做到了这一点,你不仅写出能跑的代码,更写出了能抗住生产环境考验的代码。
*如果这篇文章对你有帮助,欢迎分享给团队里的其他数据工程师。关于 JSON 解析的更多技术细节(语法对比、性能基准测试),可以参考上一篇文章:Hive on Spark JSON 解析终极指南:6种方法全解析,从此告别性能踩坑 *