news 2026/9/12 11:27:42

如何用 ClickHouse 的 merge() 表函数同时查询多张 schema 不一致的表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何用 ClickHouse 的 merge() 表函数同时查询多张 schema 不一致的表

如何用 ClickHouse 的 merge() 表函数同时查询多张 schema 不一致的表

【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse

当你同一个数据库里有多张结构相近但不完全一致的表——同一列在不同表里类型不同,或者部分表多了若干列——用普通 SQL 很难把它们当成一张表来查。ClickHouse 的merge()表函数就是为这个场景设计的:它创建一个临时的 Merge 表,把匹配到的表当成一个整体来读,结果列取各表列名的并集,列类型由底层表推导。当某列的类型在各表间不一致时,该列会表现为Variant,需要配合variantType/variantElement函数按行取出对应类型的值。

本文的操作路径基于官方文档中的完整示例(按年代拆分的网球比赛数据),从建表、比对 schema 到查询与验证。涉及variantType(v24.2.0 引入)和variantElement(v25.2.0 引入),按文中示例执行需要 v25.2.0 或更新的 ClickHouse。

merge() 的语法与参数

语法见 merge 表函数参考:

merge(['db_name',] 'tables_regexp')
参数说明
db_name可选,默认currentDatabase()。可以是数据库名、返回数据库名常量的表达式,或REGEXP(expression)用来匹配多个库名
tables_regexp匹配表名的正则表达式。正则基于 re2(支持 PCRE 子集),大小写敏感

临时 Merge 表不提供写入能力;读取自动并行化,且底层表已建索引时会被使用。与 Merge 引擎相同,查询时可以用到两个虚拟列:_table(数据实际读自哪张表)和_database(来自哪个库)。对_table加过滤(例如WHERE _table='xyz')时只会读取满足条件的表。

准备示例数据:四张 schema 各不相同的表

下面的建表命令来自 官方数据建模指南。它从文档给出的 Jeff Sackmann 网球数据集地址下载 1960s–1990s 四个年代的 CSV 并建表,执行前需要有外网访问权限。注意每个年代刻意用了不同的 schema:

  • winner_seed类型逐年变化:Nullable(String)Nullable(UInt8)Nullable(UInt16)
  • 1960s 的scoreString,其余年代用splitByWhitespace拆成Array(String)
  • 1990s 把surface设为Enum,并额外增加walkoverretirement两列。
CREATE OR REPLACE TABLE atp_matches_1960s ORDER BY tourney_id AS SELECT tourney_id, surface, winner_name, loser_name, winner_seed, loser_seed, score FROM url('https://raw.githubusercontent.com/JeffSackmann/tennis_atp/refs/heads/master/atp_matches_{1968..1969}.csv') SETTINGS schema_inference_make_columns_nullable=0, schema_inference_hints='winner_seed Nullable(String), loser_seed Nullable(UInt8)'; CREATE OR REPLACE TABLE atp_matches_1970s ORDER BY tourney_id AS SELECT tourney_id, surface, winner_name, loser_name, winner_seed, loser_seed, splitByWhitespace(score) AS score FROM url('https://raw.githubusercontent.com/JeffSackmann/tennis_atp/refs/heads/master/atp_matches_{1970..1979}.csv') SETTINGS schema_inference_make_columns_nullable=0, schema_inference_hints='winner_seed Nullable(UInt8), loser_seed Nullable(UInt8)'; CREATE OR REPLACE TABLE atp_matches_1980s ORDER BY tourney_id AS SELECT tourney_id, surface, winner_name, loser_name, winner_seed, loser_seed, splitByWhitespace(score) AS score FROM url('https://raw.githubusercontent.com/JeffSackmann/tennis_atp/refs/heads/master/atp_matches_{1980..1989}.csv') SETTINGS schema_inference_make_columns_nullable=0, schema_inference_hints='winner_seed Nullable(UInt16), loser_seed Nullable(UInt16)'; CREATE OR REPLACE TABLE atp_matches_1990s ORDER BY tourney_id AS SELECT tourney_id, surface, winner_name, loser_name, winner_seed, loser_seed, splitByWhitespace(score) AS score, toBool(arrayExists(x -> position(x, 'W/O') > 0, score))::Nullable(bool) AS walkover, toBool(arrayExists(x -> position(x, 'RET') > 0, score))::Nullable(bool) AS retirement FROM url('https://raw.githubusercontent.com/JeffSackmann/tennis_atp/refs/heads/master/atp_matches_{1990..1999}.csv') SETTINGS schema_inference_make_columns_nullable=0, schema_inference_hints='winner_seed Nullable(UInt16), loser_seed Nullable(UInt16), surface Enum(\'Hard\', \'Grass\', \'Clay\', \'Carpet\')';

如果你已有自己的表,无需使用这套数据,只要保证它们能被同一个正则匹配到(本例中是atp_matches*),后续查询逻辑通用。

先用 system.columns 看清各表的差异

查询前把每张表同名列的类型并排列出来,便于判断哪些列会形成Variant

SELECT * EXCEPT(position) FROM ( SELECT position, name, any(if(table = 'atp_matches_1960s', type, null)) AS 1960s, any(if(table = 'atp_matches_1970s', type, null)) AS 1970s, any(if(table = 'atp_matches_1980s', type, null)) AS 1980s, any(if(table = 'atp_matches_1990s', type, null)) AS 1990s FROM system.columns WHERE database = currentDatabase() AND table LIKE 'atp_matches%' GROUP BY ALL ORDER BY position ASC ) SETTINGS output_format_pretty_max_value_width=25;

文档示例输出:

┌─name────────┬─1960s────────────┬─1970s───────────┬─1980s────────────┬─1990s─────────────────────┐ │ tourney_id │ String │ String │ String │ String │ │ surface │ String │ String │ String │ Enum8('Hard' = 1, 'Grass'⋯│ │ winner_name │ String │ String │ String │ String │ │ loser_name │ String │ String │ String │ String │ │ winner_seed │ Nullable(String) │ Nullable(UInt8) │ Nullable(UInt16) │ Nullable(UInt16) │ │ loser_seed │ Nullable(UInt8) │ Nullable(UInt8) │ Nullable(UInt16) │ Nullable(UInt16) │ │ score │ String │ Array(String) │ Array(String) │ Array(String) │ │ walkover │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(Bool) │ │ retirement │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(Bool) │ └─────────────┴──────────────────┴─────────────────┴───────────────────────────┴───────────────────┘

可以确认三处差异:winner_seedNullable(String)一路变成Nullable(UInt16)score在 1960s 是String、其余是Array(String);1990s 独有的walkoverretirement列在其余表中为 NULL(即该表没有这列)。

用 merge() 直接查询

对于类型兼容的列,可以直接写普通查询。比如查找 John McEnroe 战胜 1 号种子的比赛:

SELECT loser_name, score FROM merge('atp_matches*') WHERE winner_name = 'John McEnroe' AND loser_seed = 1;

文档示例输出:

┌─loser_name────┬─score───────────────────────────┐ │ Bjorn Borg │ ['6-3','6-4'] │ │ Bjorn Borg │ ['7-6','6-1','6-7','5-7','6-4'] │ │ Bjorn Borg │ ['7-6','6-4'] │ │ Bjorn Borg │ ['4-6','7-6','7-6','6-4'] │ │ Jimmy Connors │ ['6-1','6-3'] │ │ Ivan Lendl │ ['6-2','4-6','6-3','6-7','7-6'] │ │ Ivan Lendl │ ['6-3','3-6','6-3','7-6'] │ │ Ivan Lendl │ ['6-1','6-3'] │ │ Stefan Edberg │ ['6-2','6-3'] │ │ Stefan Edberg │ ['7-6','6-2'] │ │ Stefan Edberg │ ['6-2','6-2'] │ │ Jakob Hlasek │ ['6-3','7-6'] │ └───────────────┴─────────────────────────────────┘

merge('atp_matches*')没有指定库名,默认在当前库中按正则atp_matches*匹配上述四张表,一次查询覆盖全部年代。

列类型不一致时:用 variantType + variantElement 分支取值

接着在上一步结果上再加条件:McEnroe 的winner_seed排在第 3 位或更后。由于winner_seed在各表类型不同,合并后是Variant,不能直接拿数值比较。文档给出的做法是用 multiIf 按行分派:先用variantType判断该行存储的具体类型,再用variantElement取出该类型的值;String分支需要先转成数值再比较:

SELECT loser_name, score, winner_seed FROM merge('atp_matches*') WHERE winner_name = 'John McEnroe' AND loser_seed = 1 AND multiIf( variantType(winner_seed) = 'UInt8', variantElement(winner_seed, 'UInt8') >= 3, variantType(winner_seed) = 'UInt16', variantElement(winner_seed, 'UInt16') >= 3, variantElement(winner_seed, 'String')::UInt16 >= 3 );

文档示例输出:

┌─loser_name────┬─score─────────┬─winner_seed─┐ │ Bjorn Borg │ ['6-3','6-4'] │ 3 │ │ Stefan Edberg │ ['6-2','6-3'] │ 6 │ │ Stefan Edberg │ ['7-6','6-2'] │ 4 │ │ Stefan Edberg │ ['6-2','6-2'] │ 7 │ └───────────────┴───────────────────────────┴───────────┘

两个函数的行为见 variantType / variantElement 参考:variantType返回每行的变体类型名(NULL 行为None);variantElement(variant, type_name[, default_value])取出指定类型的子列,第三个可选参数default_value在变体中不存在该类型时作为默认值。Variant本身的定义与子列读取方式见 Variant 数据类型。

用 _table 虚拟列确认行来自哪张表

_table加入 SELECT 即可知道每行读自哪张底表(结果与上一查询对应):

SELECT _table, loser_name, score, winner_seed FROM merge('atp_matches*') WHERE winner_name = 'John McEnroe' AND loser_seed = 1 AND multiIf( variantType(winner_seed) = 'UInt8', variantElement(winner_seed, 'UInt8') >= 3, variantType(winner_seed) = 'UInt16', variantElement(winner_seed, 'UInt16') >= 3, variantElement(winner_seed, 'String')::UInt16 >= 3 );

文档示例输出:

┌─_table────────────┬─loser_name────┬─score─────────┬─winner_seed─┐ │ atp_matches_1970s │ Bjorn Borg │ ['6-3','6-4'] │ 3 │ │ atp_matches_1980s │ Stefan Edberg │ ['6-2','6-3'] │ 6 │ │ atp_matches_1980s │ Stefan Edberg │ ['7-6','6-2'] │ 4 │ │ atp_matches_1980s │ Stefan Edberg │ ['6-2','6-2'] │ 7 │ └───────────────────┴───────────────┴───────────────────────────┴───────────┘

_table还能用来判断“哪些表缺列”。先按表统计walkover的取值分布:

SELECT _table, walkover, count() FROM merge('atp_matches*') GROUP BY ALL ORDER BY _table;

文档示例输出:

┌─_table────────────┬─walkover─┬─count()─┐ │ atp_matches_1960s │ ᴺᵁᴸᴸ │ 7542 │ │ atp_matches_1970s │ ᴺᵁᴸᴸ │ 39165 │ │ atp_matches_1980s │ ᴺᵁᴸᴸ │ 36233 │ │ atp_matches_1990s │ true │ 128 │ │ atp_matches_1990s │ false │ 37022 │ └───────────────────┴──────────┴─────────┘

可以看到只有atp_matches_1990swalkover的真实值,其余表的行全是 NULL——这正是“缺列”表在合并结果中的表现。如果要在统计中把缺列的行也算进来,需要回落到score列自行判断W/O,而score的类型同样是Variant,所以按类型分派:Array(String)行遍历数组找W/OString行直接做字符串匹配:

SELECT _table, multiIf( walkover IS NOT NULL, walkover, variantType(score) = 'Array(String)', toBool(arrayExists( x -> position(x, 'W/O') > 0, variantElement(score, 'Array(String)') )), variantElement(score, 'String') LIKE '%W/O%' ), count() FROM merge('atp_matches*') GROUP BY ALL ORDER BY _table;

文档示例输出:

┌─_table────────────┬─multiIf(isNo⋯, '%W/O%'))─┬─count()─┐ │ atp_matches_1960s │ true │ 242 │ │ atp_matches_1960s │ false │ 7300 │ │ atp_matches_1970s │ true │ 422 │ │ atp_matches_1970s │ false │ 38743 │ │ atp_matches_1980s │ true │ 92 │ │ atp_matches_1980s │ false │ 36141 │ │ atp_matches_1990s │ true │ 128 │ │ atp_matches_1990s │ false │ 37022 │ └───────────────────┴───────────────────────────┴─────────┘

此时 1960s–1980s 各表也有true行,说明缺列的行已通过score补上了判断。

验证方式与适用边界

  • 验证:按上文的顺序执行——先查system.columns确认各表实际类型差异,再跑merge()查询,对照文档示例输出核对行数与类型列的表现(缺列表为 NULL、类型列为Variant)。
  • 版本variantType自 v24.2.0、variantElement自 v25.2.0 可用;本文示例整体需要 v25.2.0+。
  • 正则匹配tables_regexp大小写敏感且基于 re2;Merge 表自身即使匹配到正则也不会被选入读取,避免循环。
  • 只读merge()生成的临时 Merge 表不支持写入;查询过滤放在WHERE中即可,对_table的过滤还能把读取范围限定到满足条件的底表。

更多上下文可继续查阅仓库中的 merge 表函数参考、Merge 表引擎 和 Merge table function 指南。

【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 11:26:40

ARM信创桌面开发效率工具适配指南:终端、编译与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 11:26:04

Lithe-IDEA:专为Java后端优化的轻量级IntelliJ IDE

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 11:24:25

语言模型如何革新决策树生成技术

1. 语言模型与决策树生成的技术背景决策树作为经典的机器学习算法,在分类和回归任务中已有数十年应用历史。传统决策树构建主要依赖信息增益、基尼系数等统计指标进行节点分裂,而现代语言模型(LLM)的出现为这一领域带来了范式变革。2023年发布的GPT-4技术…

作者头像 李华
网站建设 2026/9/12 11:24:11

SerenityOS 移植 Quake III Arena:ioquake3 八个补丁的逐项深度解析

SerenityOS 移植 Quake III Arena:ioquake3 八个补丁的逐项深度解析 【免费下载链接】serenity The Serenity Operating System 🐞 项目地址: https://gitcode.com/GitHub_Trending/se/serenity 导读:本文以 SerenityOS 仓库中 Ports/q…

作者头像 李华
网站建设 2026/9/12 11:23:28

Flutter在OpenHarmony实现动态柱状图的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 11:23:02

MATLAB NURBS工具箱深度解析:从数据结构到曲面建模实践

简介:MATLAB NURBS工具箱是一套面向MATLAB环境的专业扩展库,专注于非均匀有理B样条(NURBS)曲线与曲面的创建、编辑和可视化,适用于计算机图形学、CAD/CAM/CAE以及科研数据分析等场景,能帮助工程师、科研人员…

作者头像 李华