ClickHouse JSON处理实战:从JSON类型到行列转换全解析

发布时间:2026/9/30 10:02:09

ClickHouse JSON处理实战:从JSON类型到行列转换全解析 搞数据的人一定都有这种感受业务方扔过来的数据十个里有八个长着 JSON 的样。ClickHouse 又是出了名的强类型列存两者一撞第一个反应就是拿 String 硬存再掏出 JSONExtract 系列函数一层层剥。我之前在项目里处理埋点日志、订单快照、广告回调数据基本天天和 JSON 打交道。今天就把 JSON 字段类型和基于 JSON 函数做行列转换这套打法一次说清楚。这篇文章适合正在用 ClickHouse 处理半结构化数据、想在宽表化和明细展开之间自由切换的读者。无论你是刚接触 ClickHouse还是已经踩过几个 JSON 坑下面这些建表方式、函数细节和实战 SQL 都能直接抄去改改用。1. JSON 字段类型ClickHouse 处理半结构化数据的正确姿势1.1 先弄清问题的源头强类型列存与 JSON 的矛盾ClickHouse 的核心优势是列式存储加向量化执行每一列的数据类型在建表时就固定了这样才能做到高压缩比和极快的扫描。可 JSON 偏偏是另一个物种结构不固定、字段可嵌套、同一字段在不同行里可能是字符串也可能是数字。这种无序和结构化和列存天然相冲。早期面对这种矛盾唯一的路就是“String 一把梭”。把整个 JSON 文本塞进 String 列查询时再用 JSONExtractString、JSONExtractInt 这类函数现取现用。这个方案很稳到今天依然大量存在于生产环境缺点也很明显每次查询都在做运行时 JSON 解析字段抽取得越深、扫描行数越多性能损耗越像滚雪球。而且一旦上游变更字段名或类型你必须有兜底逻辑否则查询结果就是一片 0 和空字符串。后来 ClickHouse 从 23.6 版本开始逐步引入了原生 JSON 类型。它的底层设计思路很有意思不是把整个 JSON 当作一个大 blob 存起来而是把 JSON 里的每个叶子路径拆成一个独立的子列subcolumn每个子列再按 ClickHouse 自己的类型推断去落盘。用大白话说就是把一个“神秘莫测的文档”拆成了一个个“规矩的列”既维持了列存的性能优势又保住了 JSON 的灵活性。1.2 原生 JSON 类型怎么用要在旧版本里开启原生 JSON 类型必须显式打开开关SET allow_experimental_json_type 1;如果你的 ClickHouse 是 24.8 或更高版本这个开关基本可以不用管默认就能用。建表方式如下CREATE TABLE events_json ( id UInt64, event_time DateTime, payload JSON ) ENGINE MergeTree() ORDER BY event_time;写入时正常插入 JSON 字符串即可INSERT INTO events_json VALUES (1, 2025-01-10 10:00:00, {user_id:1001,action:click,page:/home,items:[{sku:SKU-A,price:29.9}]}), (2, 2025-01-10 10:05:00, {user_id:1002,action:view,page:/product/42,items:[]});查询时可以直接用点路径访问内部字段不用写一长串函数SELECT id, payload.user_id AS user_id, payload.action AS action, payload.page AS page FROM events_json;结果就是正常的列user_id 是 UInt64action 是 String。这就是原生 JSON 类型最直观的优势读写和普通列几乎没区别ClickHouse 会自动根据插入的内容推断子列类型并维护元数据。嵌套数组也能处理。比如想拿到 items 里的 sku 列表SELECT id, payload.items.sku AS skus FROM events_json;这种路径访问在底层对应真实存在的子列因此配合索引和压缩都比 String 硬解析高效。不过要注意JSON 类型的动态推断也有代价如果同一路径不同行写入的类型不一致ClickHouse 会尽量兼容成更宽的类型但如果类型差得太远比如一会是字符串一会是数组就可能出现类型漂移查询时需要手动 CAST 兜底。1.3 String存JSON vs 原生JSON类型怎么选我自己的经验是这两个方案不是“谁取代谁”而是分场景选择。对比项String JSON函数原生 JSON 类型查询可读性函数嵌套较多路径越长越难看直接点路径访问直观运行时解析开销每次查询都解析子列预解析扫描更快字段类型安全靠函数默认值兜底容易埋坑动态推断类型仍需注意漂移压缩率一整个文本压缩重复字段压缩率尚可独立子列按类型压缩通常更高版本稳定性全版本稳定实验性功能老版本可能受限适用场景临时分析、嵌套极深、字段极不规律高频字段访问、需要物化列、长期存储如果只是临时看一眼数据、或者上游 JSON 结构每天一个样用 String 存是最省心的。但如果你要把 JSON 里的某些字段当作核心维度天天聚合、关联我强烈建议建普通列或物化列把那几个字段提取出来而不是每次都现场解析。原生 JSON 类型适合那些字段很多但每个字段访问频率参差不齐的场景它会自动把“常用字段”变成子列查询时只扫描你用到的部分。2. JSON 相关函数把字符串变成可用数据2.1 提取函数全家桶JSONExtract 家族JSONExtract 系列是 String 存 JSON 方案里的主力军。常见的有这几个函数作用字段缺失时返回JSONExtractString(json, path...)提取字符串值空字符串JSONExtractInt(json, path...)提取整数0JSONExtractUInt(json, path...)提取无符号整数0JSONExtractFloat(json, path...)提取浮点数0JSONExtractBool(json, path...)提取布尔值falseJSONExtractArrayRaw(json, path...)提取数组每个元素为原始 JSON 字符串空数组JSONExtractKeysAndValues(json, type)提取对象的键值对数组空数组JSONExtract(json, path, type)通用提取指定返回类型类型默认值路径参数可以直接按层级写例如从订单 JSON 里取收货城市SELECT JSONExtractString(raw_json, address, city) AS city FROM orders;等价于把 JSON 当对象逐层往下走。也可以用点号路径在 ClickHouse 里JSONExtractString(raw_json, address.city)也是支持的但我个人更推荐多参数写法可读性更好也避免路径里出现点号导致歧义。如果你习惯 SQL 标准里那种$.path写法ClickHouse 也提供了 JSON_VALUE 和 JSON_QUERY。区别在于 JSON_VALUE 只能返回标量值JSON_QUERY 可以返回一段 JSON 子文档。实际项目里我大部分时间还是用 JSONExtract因为它对缺失字段的默认值处理更可预期不会抛异常。2.2 判断、检查与遍历类函数除了直接提取另外几个函数在行列转换里会频繁用到建议记牢。JSONHas 判断某个路径是否存在返回 1 或 0。这个函数特别适合处理“这个字段只有部分行才有”的场景避免把所有缺失都当默认值处理。SELECT id, JSONHas(raw_json, coupon) AS has_coupon FROM orders;JSONType 返回某个路径的值类型结果可能是 String、Int、Array、Object 等。数据清洗时用它来筛掉类型异常的行非常方便。SELECT id, JSONType(raw_json, price) AS price_type FROM orders WHERE JSONType(raw_json, price) ! Int;JSONExtractKeysAndValues 是把一个 JSON 对象拆成键值对数组的重点函数。第二个参数是指定 value 的类型比如 String、Int、Float。返回值是Array(Tuple(String, type))结构每个元素是一个元组第一个元素是 key第二个是 value。这个函数配合 arrayJoin就是“对象展开成多行”的核心工具。JSONExtractArrayRaw 则是把 JSON 数组里的每个对象抽出来变成一行一个原始 JSON 字符串后续再对这段字符串继续提取就能实现多层嵌套解析。2.3 函数解析的性能代价有一件事我必须强调在 String 列上调用 JSONExtract本质是边扫描边解析CPU 开销比直接读列大得多。你写一个查询对同一行 JSON 调了五次不同的 JSONExtractClickHouse 就要把这行 JSON 解析五遍这非常亏。所以我有几个实际建议能一次提取就不要分多次。比如要取同一个 JSON 里的多个字段写成多个 JSONExtract 没问题但更深层的逻辑是尽量让解析次数与扫描行数成正比而不是与“字段数×行数”成正比。高频查询的 JSON 字段用物化列提前提取。建表时可以加一个 MATERIALIZED 列把 JSON 里最常用的 user_id、action 直接提取成独立列配合索引和存储查询性能能有数量级提升。不要在 WHERE 里对同一路径反复调用 JSONExtract。如果确实要过滤先考虑物化列或者接受全表扫描但保证 JSON 列在内存里被高效缓存。如果 JSON 超大且路径很深考虑在导入前做白名单裁剪只保留业务真正关心的字段。数据量一上来省下的解析时间非常可观。3. 实战用JSON函数完成行列转换3.1 列转行把数组和对象展开成明细行之前我在订单中心场景里接到一个很典型的需求上游把一次订单的全部商品明细塞到了一个 JSON 数组里要按商品维度做报表。这就是标准的“一行展开成多行”ClickHouse 里核心就是 ARRAY JOIN。先建一张测试表CREATE TABLE order_cart ( order_id UInt64, order_time DateTime, cart_json String ) ENGINE MergeTree() ORDER BY order_id; INSERT INTO order_cart VALUES (1, 2025-01-10 11:00:00, {user_id:1001,items:[{sku:SKU-A,price:29.9,qty:2},{sku:SKU-B,price:49.9,qty:1}],address:{city:Hangzhou,district:Xihu}}), (2, 2025-01-10 11:05:00, {user_id:1002,items:[{sku:SKU-A,price:29.9,qty:1},{sku:SKU-C,price:99.0,qty:3}],address:{city:Shanghai,district:Pudong}});用 JSONExtractArrayRaw 把 items 数组抽出来再用 ARRAY JOIN 展开成一行一个商品SELECT order_id, JSONExtractString(item, sku) AS sku, JSONExtractFloat(item, price) AS price, JSONExtractInt(item, qty) AS qty, JSONExtractFloat(item, price) * JSONExtractInt(item, qty) AS amount FROM order_cart ARRAY JOIN JSONExtractArrayRaw(cart_json, items) AS item;这段 SQL 很值得逐句看JSONExtractArrayRaw 先取出整个 items 数组返回Array(String)ARRAY JOIN 把它当作一行行的数据源每一行 item 变量就变成了数组中的一个 JSON 字符串接下来三个 JSONExtract 继续从这段商品 JSON 里抽字段。最终结果就是把两条订单记录拆成了五条商品明细行这就是典型的“列转行”。如果 JSON 不是一个数组而是一个对象想把它“摊平”成多列或“炸开”成多行可以用 JSONExtractKeysAndValues 配合 ARRAY JOINSELECT order_id, kv.1 AS address_key, kv.2 AS address_value FROM order_cart ARRAY JOIN JSONExtractKeysAndValues(cart_json, address) AS kv;这条 SQL 的效果是把 address 对象里的 city、district 各自变成一行。需要注意 JSONExtractKeysAndValues 的第二个参数必须指定 value 的类型如果对象里的值既有字符串又有数字建议用 String 先兜底后面再按需 CAST。3.2 行转列从明细Event透视出宽表行转列Pivot在 ClickHouse 里没有那种通用的 pivot 语法基本靠聚合函数加条件计数来实现。最常见的场景是把多行不同类型的事件按某个维度聚合成一行多列。假设埋点表是典型的明细事件表CREATE TABLE user_events ( event_id UInt64, event_time DateTime, payload String ) ENGINE MergeTree() ORDER BY event_time; INSERT INTO user_events VALUES (101, 2025-01-10 10:00:00, {user_id:1001,action:click,page:/home}), (102, 2025-01-10 10:05:00, {user_id:1002,action:view,page:/product/42}), (103, 2025-01-10 10:10:00, {user_id:1001,action:buy,page:/checkout});想按天、按用户统计 view / click / buy 的次数并把三个行为做成三列SELECT toDate(event_time) AS day, JSONExtractInt(payload, user_id) AS user_id, countIf(JSONExtractString(payload, action) view) AS view_cnt, countIf(JSONExtractString(payload, action) click) AS click_cnt, countIf(JSONExtractString(payload, action) buy) AS buy_cnt FROM user_events GROUP BY day, user_id ORDER BY day, user_id;这条 SQL 的思路很直白先把 JSON 里的 user_id 和 action 都抽成普通字段再用 GROUP BY 聚合用 countIf 对每个行为各计一次数。整个查询把多行 event 合并成一行实现了“行转列”。更刁钻一点的需求是 JSON 里的 key 本身就是动态的比如每次请求带着不同的额外参数。这种情况下你可以先用 JSONExtractKeysAndValues 把对象炸开成 KV再按 KV 做透视。例如SELECT event_id, sumIf(CAST(kv.2 AS Float64), kv.1 source) AS source_value, sumIf(CAST(kv.2 AS Float64), kv.1 campaign) AS campaign_value FROM ( SELECT event_id, kv FROM user_events ARRAY JOIN JSONExtractKeysAndValues(payload, extra) AS kv ) GROUP BY event_id;这里先把 payload.extra 对象展开成 key-value 行再用 sumIf 把需要的 key 聚合成列。value 是 String 类型所以必须先 CAST 成 Float64。这是“动态 JSON key 做行转列”的通用套路理解了之后可以套到很多场景里。3.3 组合案例从购物车JSON到销售明细宽表很多需求是行转列和列转行交替出现的。我写过最典型的一个需求是这样的上游订单表把整个购物车塞成 JSON要先炸开成商品明细行再按 SKU 聚合成销售宽表。完整 SQL 可以直接参考WITH order_item AS ( SELECT order_id, order_time, JSONExtractString(item, sku) AS sku, JSONExtractFloat(item, price) AS price, JSONExtractInt(item, qty) AS qty, JSONExtractFloat(item, price) * JSONExtractInt(item, qty) AS amount FROM order_cart ARRAY JOIN JSONExtractArrayRaw(cart_json, items) AS item ) SELECT toDate(order_time) AS day, sku, count(DISTINCT order_id) AS order_count, sum(qty) AS total_qty, sum(amount) AS total_amount FROM order_item GROUP BY day, sku ORDER BY day, total_amount DESC;这段逻辑分两层CTE 里先把 each 订单的 items 数组炸开成商品明细外层再做按天、按 SKU 的聚合。底层数据从两条订单变成了五条明细再聚合成三行销售汇总。整个过程就是列转行和行转列的组合也是我在线上跑得最多的 JSON 转换 SQL 形态之一。4. 常见问题与排查技巧4.1 类型推断与空值处理JSON 解析最坑的地方就是空值和类型。JSONExtractString 在字段缺失或类型不匹配时不会报错而是悄悄返回空字符串JSONExtractInt 则返回 0。这个默认行为方便归方便但经常误导人你没法区分“这个用户真的消费了 0 元”和“这个字段根本不存在”。我的处理习惯是对关键字段先 JSONHas 判断一下再做业务逻辑。比如SELECT order_id, JSONExtractInt(payload, user_id) AS user_id, JSONHas(payload, coupon) AS has_coupon, if(has_coupon, JSONExtractFloat(payload, coupon, discount), 0) AS discount FROM orders;另一个常见问题是 JSON 里数字与字符串混用。上游有时把 id 传成字符串1001有时传成数字1001。JSONExtractInt 对数字字符串会尝试解析这是好的但如果你用 JSONExtractString 去取数字返回的就是字符串形式的数字排序和比较可能出问题。我的建议是ID 类字段统一用 JSONExtractInt展示类字段用 JSONExtractString两边口径要一致否则 join 的时候对不上排查起来非常头疼。4.2 导入报错与格式问题如果你用 JSONEachRow 格式导入数据可能遇到一个很典型的报错failed to deserialize the json body into the target type: input: missing field。这个报错的意思是你给的 JSON 请求体里缺少了目标表 schema 要求存在的字段。我遇到过的原因有两类。一类是上游漏字段比如表里有order_id列但这行 JSON 里没有order_idkey另一类是配置文件里 columns 列表写错导致字段对不上。解决办法是先核对表结构和你导入的 JSON key 是否一致确认缺失字段能否用默认值如果可以在导入设置里打开input_format_skip_unknown_fields并把缺失列设置为允许默认值。还是那句话导入前先做一层 JSON 校验用 Python 的 json.loads 或 jq 跑一遍别把脏数据直接灌进 ClickHouse。等数据进了表再发现问题修正成本会翻好几倍。4.3 性能优化与版本注意在 String 列上大量使用 JSONExtract 做聚合性能问题迟早会来找你。我的几个实操办法按优先级排序高频字段物化。这不是可选项是必选项。如果你每天有几百亿行日志每行都要提取 user_id 和 action那就在建表时加两个物化列从 JSON 里提取好再存成普通列。查询直接走普通列速度完全不是一个量级。能用 JSON 类型就用 JSON 类型。原生 JSON 类型在建表时就把叶子字段拆成子列等于变相实现了“自动物化”。前提是版本足够新。优化 JSON 本身的长度。只保留业务需要的字段别把上游一百个字段全塞进表里。JSON 越短解析越快压缩率越高。版本方面也要留个心眼原生 JSON 类型在 23.6 到 24.x 期间行为调整过不少次早期版本很多特性不稳定。如果你在生产环境用旧版本又想要原生 JSON 类型先在公司测试环境用真实数据量和查询模式跑一遍压测再上线。另外遇到 part 层面的诡异问题比如某个分区数据查不到、磁盘占用异常可以先查system.parts看看 part 名称、分区路径和 active 状态排查是不是 part 合并或损坏引起的问题而不是一头扎进 SQL 里调来调去。4.4 常见问题速查表现象原因解决方案JSONExtract 返回空字符串/0路径不存在或类型不匹配用 JSONHas 判断存在性再取值同一个字段一会是字符串一会是数字上游数据不规范统一 CAST 口径或导入前清洗JSONExtractArrayRaw 结果为空路径不是数组或数组为空先 JSONType 确认类型导入报 missing fieldJSON key 与表字段不匹配核对 schema打开 skip_unknown_fields查询慢且 JSON 函数写得多运行时解析开销大物化列 / 原生 JSON 类型 / 裁剪字段原生 JSON 类型用不了版本过旧或未开启开关升级版本SET allow_experimental_json_type1我在实际项目里用过 String 加 JSONExtract 的组合接近两年后来慢慢把高频场景切到原生 JSON 类型上整体的查询体验和性能都有了明显改善。如果你也正在做类似的转换建议先从一张小表开始验证版本行为再逐步铺开。这个方向的功能演进很快保持关注官方 changelog能少走不少弯路。
延伸阅读

更多相关文章

2026/9/30 10:02:09

视觉惯性组合导航从原理到实践:无人系统开发验证平台搭建指南

如果你最近在搞无人机、机器人或者车载平台,你应该会注意到一个趋势:以前大家做自主定位,首选RTK、激光雷达或者纯视觉方案,但现在越来越多的团队开始把“视觉 惯性”作为标配,也就是视觉惯性组合导航。我做这个方向也…

2026/9/30 11:02:45

Spring Boot + Vue家庭维修系统:源码部署与前后端联调实战

最近在帮别人整理一套“基于Spring Boot Vue的Web家庭设备维修服务系统”,光是看标题就知道,这不是一个只能跑个登录页的玩具项目,而是包含用户下单、维修工接单、管理员派单、服务评价、维修进度跟踪等完整业务流程的企业级教学项目。很多人…

2026/9/30 11:02:45

tar解压失败排查与修复:从gzip报错到完整复原

最近排查一个线上问题时,连着在三台服务器上撞见了同一种尴尬场面: tar -zxvf 刚解压到一半,终端里刷出一行 gzip: stdin: unexpected end of file ,紧接着就是 tar: Error is not recoverable: exiting now ,退…

2026/9/30 11:02:45

禅道二次开发整合Dify工作流:项目月报AI智能分析实战指南

做了这么多年项目管理和研发管理工具,我早就习惯了禅道这个老伙计。它功能扎实、部署灵活、国内团队用得多,但真要让它把项目月报这种需要"人话总结"的事情做好,还是有些力不从心。所以当看到"禅道二次开发:项目月…

2026/9/30 11:02:45

Spring Boot用户数据管理实战:从CRUD到事务、缓存与安全配置

上周帮朋友公司重构内部系统的用户管理模块,需求拆开其实不算复杂:部门树、人员列表、账号状态、登录日志,外加上一个管理后台。但业务上看着简单,真正动手之后你会发现,“用户数据管理”这五个字牵扯到的东西远不止增…

2026/9/30 10:57:41

Flink状态恢复报错StateMigrationException:成因剖析与兜底方案

1. 报错现场与根因拆解 先说说我遇到这个报错时的第一反应。那天线上作业重启,Flink SQL任务在从最近一次checkpoint恢复时直接卡死在STARTING状态,JobManager日志里反复滚出这样一段异常: Caused by: org.apache.flink.util.StateMigratio…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/30 10:28:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑