1. 为什么ClickHouse的函数体系值得单独花时间啃ClickHouse用久了你会发现一个很有意思的现象同样一个查询需求不同人写出来的SQL性能差距能拉到几十倍甚至上百倍。问题往往不出在表结构上也不在硬件上而是出在函数的选择和组合方式上。我见过太多人把ClickHouse当MySQL用写出来的SQL在MySQL里跑得挺欢搬到ClickHouse上直接超时然后开始怀疑是不是集群配置有问题——其实十有八九是函数没用对。ClickHouse的函数体系跟传统关系型数据库完全不是一个设计哲学。MySQL的函数是够用就行ClickHouse的函数是为分析场景而生。它内置了上千个函数覆盖字符串处理、日期时间运算、数组操作、JSON解析、聚合计算、位运算、哈希、编码转换、地理计算等几乎所有你能想到的分析场景。更关键的是很多函数是向量化执行的一次处理一整列数据而不是一行一行地算这是它查询速度能碾压传统数据库的核心原因之一。这篇文章适合谁看如果你已经在用ClickHouse但每次写SQL都要翻文档查函数用法或者你写的查询总觉得能跑但不够快那这篇内容就是给你准备的。我会把ClickHouse函数按使用频率和实战价值分成几大类每一类都讲清楚什么时候用、怎么用、有什么坑并且给出可以直接复制粘贴的SQL示例。不管你是刚接触ClickHouse的新手还是已经用了一段时间想系统梳理函数体系的老手都能从里面找到有用的东西。需要提前说明的是ClickHouse的版本迭代很快不同版本之间函数的可用性和行为可能有差异。我下面讲的内容基于近几个稳定版本22.x到24.x的通用行为如果你用的是更老的版本部分函数可能不存在或者参数不同建议先确认一下自己的版本。可以用SELECT version()查一下当前版本号做到心里有数。2. ClickHouse函数体系的整体设计思路2.1 函数分类的底层逻辑ClickHouse的函数不是随便堆在一起的它的分类背后有清晰的设计逻辑。理解这个逻辑你查文档的时候就能快速定位到需要的函数而不是漫无目的地翻。第一类是标量函数也就是对每一行独立计算的函数比如upper()、toDate()、concat()。这类函数的特点是输入一行输出一行不依赖其他行的数据。它们通常会被向量化执行引擎优化性能非常好。第二类是聚合函数比如sum()、avg()、uniq()、quantile()。这类函数把多行数据聚合成一个结果是OLAP场景的核心。ClickHouse的聚合函数比传统数据库丰富得多特别是uniqCombined、quantileTiming、topK这些近似计算函数在大数据量下能在精度和性能之间取得很好的平衡。第三类是数组函数这是ClickHouse的一大特色。传统数据库处理数组很吃力ClickHouse直接把数组当成一等公民提供了arrayMap、arrayFilter、arrayJoin、groupArray等一整套工具。很多在MySQL里需要写复杂子查询或者用临时表才能解决的问题在ClickHouse里一个数组函数就搞定了。第四类是表函数比如numbers()、remote()、url()、file()。这类函数返回的是一张表而不是一个值通常用在FROM子句里。它们在做数据生成、跨集群查询、文件导入导出时特别有用。第五类是高阶函数也就是把函数作为参数的函数比如arrayMap(x - x * 2, arr)。这类函数让ClickHouse具备了类似函数式编程的能力处理复杂数据转换时非常灵活。2.2 为什么ClickHouse的函数设计成这个样子要理解ClickHouse函数的设计得先理解它的目标场景大规模数据的实时分析。这个场景决定了几个设计原则。第一个原则是性能优先于标准兼容。ClickHouse不太在乎SQL标准怎么说它更在乎怎么算得快。比如它的join实现跟标准SQL差异很大函数命名也不完全遵循SQL标准。这不是它不专业而是它选择了为性能服务。第二个原则是近似优于精确。在大数据量下精确计算的代价可能高到无法接受。所以ClickHouse提供了大量近似计算函数比如uniq()用HyperLogLog算法估算去重数量误差在0.5%左右但速度比精确去重快几十倍。这种取舍在分析场景下是完全合理的——你做一个UV统计差个几百几千根本不影响决策。第三个原则是组合优于内置。ClickHouse不会为每个具体需求都内置一个函数而是提供基础函数让你组合。比如它没有计算环比增长率这种函数但你可以用windowFunnel或者neighbor配合算术运算轻松实现。这种设计让函数体系保持精简同时具备极强的扩展性。2.3 函数使用的三个层次我把ClickHouse函数的使用分成三个层次你可以对照看看自己在哪个层次。第一层是能用知道常用函数的名字和基本用法能写出正确的SQL。这个层次的人写出来的查询通常能跑但性能不一定好。第二层是用好知道在什么场景下该用哪个函数能根据数据特征选择最合适的函数。比如知道去重统计该用uniq还是uniqExact知道字符串匹配该用like还是match。这个层次的人写出来的查询性能通常不错。第三层是用巧能把多个函数组合起来解决复杂问题能利用高阶函数和数组函数写出简洁高效的查询。这个层次的人往往能用一条SQL解决别人需要好几步才能解决的问题。这篇文章的目标是帮你从第一层走到第三层。下面我会按函数类别逐一拆解每一类都给出实战示例和避坑经验。3. 字符串函数最常用也最容易踩坑的一类3.1 基础字符串处理函数字符串函数是日常查询里用得最多的一类。ClickHouse的字符串函数跟其他数据库大同小异但有几个细节需要注意。length()返回字符串的字节数而不是字符数这个坑很多人踩过。如果你要统计中文字符数得用lengthUTF8()。同理substring()是按字节截取的截取中文可能截出乱码要用substringUTF8()。我刚开始用ClickHouse的时候就因为这个踩过坑一个中文名字被截成了半个字排查了半天才发现是字节和字符的区别。SELECT length(hello) AS byte_len, -- 5 lengthUTF8(你好) AS char_len, -- 2 substring(hello world, 1, 5) AS sub, -- hello substringUTF8(你好世界, 1, 2) AS sub_utf8 -- 你好concat()用来拼接字符串但它有个限制如果任何一个参数是NULL整个结果就是NULL。这跟MySQL的行为不一样MySQL的concat()遇到NULL也是返回NULL但很多人会忘记这一点。如果你希望NULL被当成空字符串处理用concatAssumeInjective()或者先用ifNull()包一层。splitByChar()、splitByString()、splitByRegexp()用来分割字符串返回的是数组。这几个函数在处理日志解析、URL参数提取时特别有用。比如你有一个用逗号分隔的标签字段想统计每个标签出现的次数可以这样写SELECT arrayJoin(splitByChar(,, tags)) AS tag, count() AS cnt FROM articles GROUP BY tag ORDER BY cnt DESCarrayJoin是个很特殊的函数它会把数组展开成多行。这个函数在ClickHouse里用得极其频繁但也要小心——它会让结果集行数膨胀如果数组很大可能把内存撑爆。3.2 字符串匹配与替换字符串匹配是另一个高频场景。ClickHouse提供了like、notLike、match、extract、extractAll等函数。like支持%和_通配符跟SQL标准一致。但要注意like的性能在大数据量下可能不理想因为它无法利用索引。如果要做前缀匹配用startsWith()会快很多。如果要做正则匹配用match()它支持完整的正则表达式语法。SELECT hello world LIKE %world% AS like_match, -- 1 startsWith(hello world, hello) AS starts, -- 1 match(hello world, h.*o) AS regex_match, -- 1 extract(hello world, (\w) world) AS extracted -- helloreplaceOne()、replaceAll()、replaceRegexpOne()、replaceRegexpAll()用来做替换。这里有个性能提示replaceAll()比replaceRegexpAll()快很多能用前者就别用后者。正则替换虽然灵活但代价是编译正则表达式和回溯匹配在大数据量下差距很明显。3.3 字符串函数的实战避坑经验关于字符串函数我总结了几个实战中容易踩的坑。第一个坑是编码问题。ClickHouse默认假设字符串是UTF-8编码但如果你的数据源不是UTF-8比如GBK那所有字符串函数的结果都可能不对。导入数据前一定要确认编码必要时用convertCharset()转换。第二个坑是空字符串和NULL的区别。ClickHouse里空字符串和NULL是两个完全不同的东西。length()返回0length(NULL)返回NULL。做条件判断时要注意区分用empty()判断空字符串用isNull()判断NULL。第三个坑是大字符串的性能。ClickHouse对字符串的处理是按值传递的如果一个字段是超长文本比如几MB的JSON每次操作都会复制整个字符串性能会很差。这种情况下建议先用substring()截取需要的部分或者考虑用JSONExtract系列函数直接提取字段。提示字符串函数在ClickHouse里是大小写敏感的。upper()和lower()只处理ASCII字符处理中文或其他非ASCII字符时不会改变大小写因为本来就没有大小写概念。如果你需要Unicode大小写转换得用upperUTF8()和lowerUTF8()。4. 日期时间函数分析场景的命脉4.1 日期时间的解析与格式化日期时间函数在分析场景里用得极多因为几乎所有分析都涉及时间维度。ClickHouse的日期时间函数设计得相当完善但也有一些独特之处。toDate()、toDateTime()、toDateTime64()用来做类型转换。toDate()只保留日期部分toDateTime()精确到秒toDateTime64()可以精确到纳秒。选择哪个取决于你的精度需求精度越高占用的存储空间越大。SELECT toDate(2024-01-15 10:30:00) AS d, -- 2024-01-15 toDateTime(2024-01-15 10:30:00) AS dt, -- 2024-01-15 10:30:00 toDateTime64(2024-01-15 10:30:00.123, 3) AS dt64 -- 2024-01-15 10:30:00.123formatDateTime()用来格式化输出支持各种格式符。这个函数在做报表时特别有用比如你想把时间格式化成2024年01月这种中文格式可以这样写SELECT formatDateTime(now(), %Y年%m月) AS month_strparseDateTimeBestEffort()是个很实用的函数它能自动识别多种日期格式并解析。比如你的数据里日期格式不统一有的是2024-01-15有的是2024/01/15有的是15/01/2024用这个函数可以统一解析省去大量清洗工作。4.2 日期时间的运算与截取日期运算方面ClickHouse提供了dateAdd()、dateSub()、dateDiff()等函数。SELECT dateAdd(day, 7, today()) AS next_week, dateSub(month, 1, today()) AS last_month, dateDiff(day, 2024-01-01, 2024-01-15) AS diff_daystoStartOfDay()、toStartOfWeek()、toStartOfMonth()、toStartOfQuarter()、toStartOfYear()这一组函数用来做时间截取是做时间维度聚合的利器。比如你要按周统计订单量SELECT toStartOfWeek(order_date) AS week, count() AS order_count FROM orders GROUP BY week ORDER BY week这里有个细节要注意toStartOfWeek()默认以周一为一周的开始但有些业务场景需要以周日开始。ClickHouse支持通过第二个参数指定周起始日比如toStartOfWeek(date, 0)表示以周日开始。4.3 时间函数的性能优化技巧时间函数用得好不好对查询性能影响很大。我分享几个实战经验。第一尽量在WHERE条件里用时间字段做过滤。ClickHouse的分区通常是按时间做的如果WHERE条件里包含时间范围查询可以直接定位到相关分区跳过大量数据。比如WHERE order_date 2024-01-01 AND order_date 2024-02-01比WHERE toMonth(order_date) 1快得多因为后者无法利用分区裁剪。第二避免在时间字段上套函数。WHERE toDate(create_time) 2024-01-15这种写法会导致全表扫描因为函数让索引失效了。应该写成WHERE create_time 2024-01-15 00:00:00 AND create_time 2024-01-16 00:00:00。第三用物化视图预计算时间维度。如果你的报表经常按天、按周、按月聚合可以建物化视图把聚合结果预先算好查询时直接读结果速度能快几个数量级。注意ClickHouse的时区处理跟其他数据库不太一样。toDateTime()默认使用服务器时区如果你的数据涉及多个时区一定要显式指定时区比如toDateTime(2024-01-15 10:30:00, Asia/Shanghai)。时区搞错会导致数据对不上这种问题排查起来很痛苦。5. 数组函数ClickHouse的杀手锏5.1 数组的创建与访问数组函数是ClickHouse区别于其他数据库的核心竞争力之一。在MySQL里处理数组需要各种奇技淫巧在ClickHouse里数组是一等公民。创建数组可以用array()或者方括号[]SELECT array(1, 2, 3) AS arr1, [4, 5, 6] AS arr2, arrayConcat(arr1, arr2) AS combined访问数组元素用arr[index]注意ClickHouse的数组索引从1开始不是0。这个跟大多数编程语言不一样刚开始用容易搞错。如果索引越界ClickHouse不会报错而是返回该类型的默认值数字返回0字符串返回空串。SELECT [10, 20, 30][1] AS first, -- 10 [10, 20, 30][5] AS out_of_range -- 0arrayElement()是另一种访问方式效果跟方括号一样。has()用来判断数组是否包含某个元素indexOf()返回元素的位置countEqual()统计某个元素出现的次数。5.2 数组的高阶操作真正让数组函数强大的是高阶函数也就是arrayMap、arrayFilter、arrayReduce这一组。arrayMap()对数组每个元素应用一个函数返回新数组SELECT arrayMap(x - x * 2, [1, 2, 3]) AS doubled -- [2, 4, 6]arrayFilter()过滤数组元素SELECT arrayFilter(x - x 2, [1, 2, 3, 4]) AS filtered -- [3, 4]arrayReduce()对数组做聚合SELECT arrayReduce(sum, [1, 2, 3, 4]) AS total -- 10这几个函数组合起来能解决非常复杂的问题。举个例子假设你有一个订单表每个订单有多个商品商品价格存在数组里你想统计每个订单中价格大于100的商品总价SELECT order_id, arraySum(arrayFilter(x - x 100, prices)) AS expensive_total FROM orders如果用传统SQL写这需要展开数组、过滤、再聚合至少三步。用数组函数一步就搞定了。5.3 数组函数在实战中的典型应用数组函数在实际业务里的应用场景非常多我举几个我经常遇到的。场景一标签统计。用户表里有个tags字段是数组类型你想统计每个标签的用户数SELECT arrayJoin(tags) AS tag, count() AS user_count FROM users GROUP BY tag ORDER BY user_count DESC场景二漏斗分析。用户行为表里有个actions数组按时间顺序记录了用户的操作你想统计完成浏览-加购-下单三步的用户数SELECT count() AS funnel_users FROM user_actions WHERE hasAll(actions, [view, cart, order])场景三序列匹配。用arrayExists()判断数组中是否存在满足条件的元素用arrayAll()判断是否所有元素都满足条件SELECT arrayExists(x - x 100, [50, 150, 200]) AS has_expensive, -- 1 arrayAll(x - x 0, [1, 2, 3]) AS all_positive -- 15.4 数组函数的性能陷阱数组函数虽然强大但用不好也会踩坑。我总结几个常见的性能陷阱。第一个陷阱是数组过大导致内存爆炸。ClickHouse会把整个数组加载到内存里处理如果数组有几十万个元素单个查询就可能吃掉大量内存。这种情况下要考虑用arrayJoin展开成多行分批处理。第二个陷阱是arrayJoin导致行数膨胀。arrayJoin会把一行展开成多行如果数组平均长度是100那结果集就是原来的100倍。如果后续还有JOIN或GROUP BY性能会急剧下降。用之前一定要估算一下数据量。第三个陷阱是嵌套数组的性能。ClickHouse支持嵌套数组数组的数组但嵌套数组的操作性能比一维数组差很多。如果可能尽量把嵌套数组拍平成一维数组加一个维度字段。提示arrayJoin在SELECT子句和WHERE子句里的行为不一样。在SELECT里它会展开行在WHERE里它会被当成一个普通函数。这个差异容易让人困惑用的时候要留意。6. 聚合函数OLAP场景的核心武器6.1 基础聚合函数与组合器聚合函数是ClickHouse的看家本领。基础的sum()、count()、avg()、min()、max()跟其他数据库差不多但ClickHouse的聚合函数有一个独特的设计组合器。组合器是加在聚合函数后面的后缀用来改变聚合函数的行为。比如-If组合器让聚合函数只处理满足条件的行SELECT sumIf(amount, status paid) AS paid_amount, countIf(status cancelled) AS cancelled_count FROM orders-Array组合器让聚合函数处理数组SELECT sumArray([1, 2, 3]) AS total -- 6-State和-Merge组合器用来做中间状态存储和合并是实现物化视图增量聚合的关键。这个稍微复杂一点后面单独讲。-Distinct组合器让聚合函数只处理去重后的值SELECT sumDistinct(amount) AS distinct_sum FROM orders组合器可以叠加使用比如sumIfArray表示对满足条件的数组求和。这种设计让聚合函数的表达能力大大增强一个函数顶好几个。6.2 近似计算函数精度与性能的平衡ClickHouse最让我欣赏的设计之一就是近似计算函数。在大数据量下精确计算的代价可能高到无法接受而近似计算能在可接受的误差范围内大幅提升性能。uniq()是最常用的近似去重函数它用HyperLogLog算法估算去重数量误差约0.5%但内存占用和计算时间都远低于uniqExact()。在UV统计这种场景下0.5%的误差完全可以接受。SELECT uniq(user_id) AS approx_uv, uniqExact(user_id) AS exact_uv FROM visitsquantile()系列函数用来计算分位数quantileTiming()针对时间数据做了优化quantileExact()是精确版本。做性能监控时P95、P99这些分位数用近似函数算就够了。topK()返回出现频率最高的K个值底层用Space-Saving算法比GROUP BY ORDER BY LIMIT快得多SELECT topK(10)(page_url) AS top_pages FROM visits6.3 聚合函数的实战组合技巧聚合函数真正的威力在于组合使用。我分享几个实战中总结的组合技巧。技巧一用-If组合器做条件聚合。这比用CASE WHEN再聚合简洁得多性能也更好SELECT user_id, countIf(action click) AS clicks, countIf(action view) AS views, countIf(action purchase) AS purchases FROM events GROUP BY user_id技巧二用groupArray收集明细。有时候你既想要聚合结果又想要明细数据groupArray可以把明细收集成数组SELECT user_id, groupArray(order_id) AS order_ids, sum(amount) AS total_amount FROM orders GROUP BY user_id技巧三用-State和-Merge做增量聚合。这是物化视图的核心机制。先建一个用-State的物化视图存储中间状态查询时用-Merge合并-- 物化视图 CREATE MATERIALIZED VIEW mv_daily_stats ENGINE AggregatingMergeTree() ORDER BY (date, user_id) AS SELECT toDate(create_time) AS date, user_id, sumState(amount) AS total_amount_state FROM orders GROUP BY date, user_id; -- 查询 SELECT date, sumMerge(total_amount_state) AS total_amount FROM mv_daily_stats GROUP BY date这个模式在实时报表场景里非常有用能把查询时间从秒级降到毫秒级。6.4 聚合函数的常见问题排查聚合函数用起来简单但出问题时排查起来可能很头疼。我整理了几个常见问题。问题一内存超限。聚合操作需要在内存里维护中间状态如果GROUP BY的基数很大比如按用户ID聚合有几千万用户内存可能撑不住。解决办法是增大max_memory_usage配置或者用GROUP BY的近似替代方案比如先按时间分片再聚合。问题二结果不准确。如果用了近似函数结果有误差是正常的。但如果误差大得离谱可能是数据分布问题。比如uniq()在数据量很小时误差比例会比较大这时候用uniqExact()更合适。问题三聚合结果跟预期不符。这通常是NULL值处理的问题。ClickHouse的聚合函数默认忽略NULLcount()不统计NULL行sum()遇到NULL当0处理。如果你希望NULL参与计算得用ifNull()转换。注意count()和count(*)在ClickHouse里是等价的都统计行数。但count(column)只统计该列非NULL的行数。这个区别在统计时容易搞混写SQL时要明确自己的意图。7. JSON函数半结构化数据的处理利器7.1 JSON解析的两种方式ClickHouse处理JSON有两种方式一种是JSONExtract系列函数一种是JSON数据类型。前者适合从字符串字段里提取JSON内容后者适合整列存储JSON。JSONExtract()是最基础的函数它根据路径提取值并自动推断类型SELECT JSONExtract({name: Alice, age: 30}, name, String) AS name, JSONExtract({name: Alice, age: 30}, age, Int32) AS ageJSONExtractString()、JSONExtractInt()、JSONExtractFloat()是带类型指定的版本性能比JSONExtract()好因为不需要类型推断。JSONExtractArrayRaw()用来提取JSON数组返回的是字符串数组需要进一步解析SELECT JSONExtractArrayRaw([{id: 1}, {id: 2}]) AS raw_array7.2 JSON函数的性能考量JSON解析是CPU密集型操作在大数据量下性能可能成为瓶颈。我分享几个优化经验。第一能用类型化提取就不用通用提取。JSONExtractString()比JSONExtract()快因为省去了类型推断的开销。第二尽量在导入时就解析好JSON。如果JSON结构固定建议在导入时用物化视图把字段提取出来存成独立列查询时直接读列避免每次查询都解析JSON。第三避免在WHERE条件里用JSON函数。WHERE JSONExtractString(data, status) active这种写法无法利用索引会导致全表扫描。如果经常按JSON里的某个字段过滤应该把它提取成独立列并建索引。7.3 JSON处理的实战案例假设你有一个日志表每行是一个JSON字符串包含用户ID、事件类型、时间戳等信息。你想统计每种事件类型的数量SELECT JSONExtractString(log, event_type) AS event_type, count() AS cnt FROM logs GROUP BY event_type ORDER BY cnt DESC如果JSON里有嵌套结构可以用点号分隔的路径SELECT JSONExtractString(log, user, id) AS user_id, JSONExtractString(log, user, name) AS user_name FROM logs对于JSON数组可以结合数组函数处理SELECT arrayMap(x - JSONExtractString(x, name), JSONExtractArrayRaw(log, items)) AS item_names FROM logs8. 高阶函数与Lambda表达式函数式编程的威力8.1 Lambda表达式的基本语法ClickHouse支持Lambda表达式语法是x - expression其中x是参数expression是表达式。Lambda表达式通常配合高阶函数使用比如arrayMap、arrayFilter、arrayReduce。SELECT arrayMap(x - x * x, [1, 2, 3, 4]) AS squares, -- [1, 4, 9, 16] arrayFilter(x - x % 2 0, [1, 2, 3, 4]) AS evens -- [2, 4]Lambda表达式支持多个参数用逗号分隔SELECT arrayMap((x, y) - x y, [1, 2, 3], [10, 20, 30]) AS sums -- [11, 22, 33]8.2 高阶函数的组合使用高阶函数真正的威力在于组合。我举一个实际案例假设你有一个用户行为表每个用户有一个行为序列数组你想找出所有完成了浏览-加购-下单这个完整序列的用户。SELECT user_id, arrayExists( i - arrayExists( j - arrayExists( k - i j AND j k, arrayEnumerate(actions) ), arrayEnumerate(actions) ), arrayEnumerate(actions) ) AS completed_funnel FROM user_actions这个查询看起来复杂但逻辑很清晰用三层嵌套的arrayExists检查是否存在三个索引 i j k对应的行为分别是浏览、加购、下单。如果用传统SQL写这需要自连接三次性能差得多。8.3 高阶函数的性能与可读性权衡高阶函数虽然强大但有两个问题需要注意。第一个是可读性。嵌套的高阶函数很难读懂维护起来也麻烦。我的经验是如果嵌套超过两层就应该考虑拆分成多个步骤或者用子查询。可读性比炫技重要。第二个是性能。高阶函数会对数组每个元素执行一次Lambda表达式如果数组很大性能可能不理想。而且Lambda表达式里的操作无法被向量化优化比原生函数慢。所以能用原生函数解决的就别用高阶函数。提示ClickHouse的Lambda表达式不支持递归也不支持在Lambda里引用外部变量除了常量。这个限制在做复杂计算时要注意。9. 函数使用的性能优化与避坑总结9.1 函数选择的性能原则经过前面几类的拆解可以总结出几条函数选择的性能原则。原则一能用原生函数就别用高阶函数。原生函数是C实现的经过向量化优化比Lambda表达式快得多。原则二能用近似函数就别用精确函数。在分析场景下0.5%的误差通常可以接受但性能差距可能是几十倍。原则三能提前计算就别在查询时算。用物化视图把常用计算预先做好查询时直接读结果。原则四能避免函数就别用函数。在WHERE条件里函数会导致索引失效。能用原始字段比较的就别套函数。9.2 常见函数使用错误速查表错误写法问题正确写法WHERE toDate(dt) 2024-01-15索引失效全表扫描WHERE dt 2024-01-15 00:00:00 AND dt 2024-01-16 00:00:00length(chinese_str)返回字节数不是字符数lengthUTF8(chinese_str)concat(a, b)当a或b为NULL结果为NULLconcat(ifNull(a, ), ifNull(b, ))uniq()在数据量小时误差比例大uniqExact()arrayJoin在大数组上行数膨胀内存爆炸先过滤数组再展开JSONExtract在WHERE里无法利用索引提取成独立列sum()期望统计NULLNULL被当0处理sum(ifNull(x, 0))或countIf(x IS NOT NULL)9.3 我踩过的几个印象深刻的坑最后分享几个我实际踩过的坑都是文档里不会写的。坑一时区导致的日期错位。有一次做日报表发现每天的数据总是差几个小时。排查了半天才发现数据写入时用的是UTC时区查询时用的是本地时区导致跨天的数据被算到了前一天。解决办法是统一时区写入和查询都用同一个时区。坑二浮点数精度问题。ClickHouse的Float32和Float64在做累加时会有精度损失金额计算一定要用Decimal类型。我曾经用Float64算订单总额结果跟财务系统对不上差了0.01元排查了很久才发现是浮点精度问题。坑三arrayJoin在JOIN后的行为。arrayJoin如果在JOIN之后使用展开的行会跟JOIN的结果做笛卡尔积行数可能爆炸。这个坑我在做用户标签分析时踩过一个查询跑了半小时没出结果最后发现是arrayJoin位置放错了。坑四GROUP BY的NULL处理。ClickHouse的GROUP BY会把NULL当成一个独立的分组这跟某些数据库的行为不一样。如果你不希望NULL单独成组得用ifNull()转换。这些坑的共同特点是文档里不会专门讲但实际用起来一定会遇到。希望我的经验能帮你少走弯路。10. 函数体系的扩展与进阶方向10.1 自定义函数的可能性ClickHouse支持通过CREATE FUNCTION创建自定义函数UDF可以用SQL表达式或者可执行脚本定义。SQL类型的UDF适合封装常用表达式可执行脚本类型的UDF适合实现复杂逻辑。CREATE FUNCTION linear_equation AS (x, k, b) - k*x b; SELECT linear_equation(2, 3, 1); -- 7不过UDF的性能通常不如原生函数因为它是解释执行的。如果性能要求高还是优先用原生函数组合。10.2 函数与物化视图的结合函数和物化视图结合能发挥出很大的威力。核心思路是用聚合函数的-State组合器在物化视图里存储中间状态查询时用-Merge合并。这个模式我在前面聚合函数部分讲过这里再强调一下它的价值它能把实时聚合的查询时间从秒级降到毫秒级是做实时报表的标准方案。10.3 持续学习的方向ClickHouse的函数体系还在不断扩展每个版本都会新增函数。我的建议是关注官方文档的更新日志遇到新函数时想一想它能用在什么场景。另外多看看别人的SQL特别是那些写得简洁高效的查询往往能学到函数的巧妙用法。函数用得好不好最终还是要靠实践。我见过很多人把函数文档背得滚瓜烂熟但一到实际场景就不知道怎么组合。真正的功力在于拿到一个需求能快速判断该用哪几个函数怎么组合最简洁、最高效。这个能力只能通过大量实践来积累。我在实际使用中的体会是ClickHouse的函数体系就像一套乐高积木单个函数看起来简单但组合起来能搭出非常复杂的东西。关键是要理解每个函数的适用场景和性能特征然后在实践中不断尝试新的组合方式。踩的坑越多对函数的理解就越深写出来的SQL也就越精炼。