图片

本文字数:10706;估计阅读时间:27 分钟

作者:Mark Needham

本文在公众号【ClickHouseInc】首发

图片

原标题:2025 年你可能忽略的新函数

我和同事 Tom Schreiber 会在每次 ClickHouse 版本发布之后撰写一篇博客文章,重点介绍该版本中的重要变化,例如新增的数据湖目录或 join 性能方面的改进。

不过,在每个版本中,往往还会包含一些我们没有机会详细介绍的新函数。在这篇博客文章中,我将挑选一些 2025 年引入、但你可能没有注意到的新函数进行介绍。

2025 年一共引入了多少个函数?

首先,你是否知道,可以通过运行下面的查询来统计 2025 年一共新增了多少个函数:

select count() 
FROM system.functions 
WHERE introduced_in LIKE '25%';
┌─count()─┐
│     119 │
└─────────┘

我们也可以进一步统计每个版本分别引入了多少个函数。为此,我们需要借用一个用于对语义版本号进行排序的用户自定义函数:

CREATE FUNCTION sortableSemVer AS version -> 
  arrayMap(
    x -> toUInt32OrZero(x), 
    splitByChar('.', extract(version, '(d+(.d+)+)'))
  );

接下来,我们就可以编写如下查询:

SELECT introduced_in, count()
FROM system.functions
WHERE introduced_in LIKE '25%'
GROUP BY ALL
ORDER BY sortableSemVer(introduced_in);
┌─introduced_in─┬─count()─┐
│ 25.1          │       2 │
│ 25.2          │       2 │
│ 25.3          │       3 │
│ 25.4          │       7 │
│ 25.5          │      18 │
│ 25.6          │      17 │
│ 25.7          │      25 │
│ 25.8          │      18 │
│ 25.9          │       8 │
│ 25.10         │      10 │
│ 25.11         │       7 │
│ 25.12         │       2 │
└───────────────┴─────────┘

如果想要获取函数的完整列表,可以运行下面的查询:

SELECT
    name,
    introduced_in AS version,
    if(length(description) > 80,
       substring(description, 1, 80) || '...',
       description) AS description
FROM system.functions
WHERE introduced_in LIKE '25%'
ORDER BY sortableSemVer(introduced_in);

为了便于展示,这里的结果仅保留了前十行:

Row 1:
──────
name:        variantElement
version:     25.2
description:
Extracts a column with specified type from a `Variant` column.


Row 2:
──────
name:        numericIndexedVectorPointwiseMultiply
version:     25.7
description:
Performs pointwise multiplication between a numericIndexedVector and either ano...

Row 3:
──────
name:        __patchPartitionID
version:     25.5
description:
Internal function. Receives the name of a part and a hash of patch part's colum...

Row 4:
──────
name:        readWKBPolygon
version:     25.5
description:
                Parses a Well-Known Binary (WKB) representation of a Polygon ge...

Row 5:
──────
name:        initialQueryStartTime
version:     25.4
description:
Returns the start time of the initial current query.
`initialQueryStartTime` re...

下面让我们来看看其中一些新函数!

mapContainsValueLike 

mapContainsValueLike 是在 ClickHouse 25.5 中引入的,用于检查某个 map 中是否存在一个值,该值可以通过 LIKE 运算符匹配指定的模式。

例如,假设我们有下面这样一个查询,它会返回公司名称以及对应的用例详情:

SELECT
        'Netflix' AS company,
        map('use_case', 'streaming analytics', 'scale', '5 petabytes daily') AS details
    UNION ALL
    SELECT
        'Tesla',
        map('use_case', 'observability platform', 'scale', 'quadrillion rows', 'feature', 'vector search')
    UNION ALL
    SELECT
        'Anthropic',
        map('use_case', 'AI observability', 'scale', 'billions of events')
    UNION ALL
    SELECT
        'Uber',
        map('use_case', 'ride analytics', 'scale', 'petabyte scale')
FORMAT Vertical;
Row 1:
──────
company: Netflix
details: {'use_case':'streaming analytics','scale':'5 petabytes daily'}

Row 2:
──────
company: Tesla
details: {'use_case':'observability platform','scale':'quadrillion rows','feature':'vector search'}

Row 3:
──────
company: Anthropic
details: {'use_case':'AI observability','scale':'billions of events'}

Row 4:
──────
company: Uber
details: {'use_case':'ride analytics','scale':'petabyte scale'}

接着,我们可以编写如下查询,用来检查 map 中是否有任意值包含 obser、petabyte 或 vector 这些关键词:

WITH useCases AS (
    SELECT
        'Netflix' AS company,
        map('use_case', 'streaming analytics', 'scale', '5 petabytes daily') AS details
    UNION ALL
    SELECT
        'Tesla',
        map('use_case', 'observability platform', 'scale', 'quadrillion rows', 'feature', 'vector search')
    UNION ALL
    SELECT
        'Anthropic',
        map('use_case', 'AI observability', 'scale', 'billions of events')
    UNION ALL
    SELECT
        'Uber',
        map('use_case', 'ride analytics', 'scale', 'petabyte scale')
)
SELECT
    company,
    details,
    mapContainsValueLike(details, '%observ%') AS is_observability,
    mapContainsValueLike(details, '%petabyte%') AS petabyte_scale,
    mapContainsValueLike(details, '%vector%') AS has_vector_search
FROM useCases
FORMAT Vertical;

执行后,我们会得到如下结果:

Row 1:
──────
company:           Netflix
details:           {'use_case':'streaming analytics','scale':'5 petabytes daily'}
is_observability:  0
petabyte_scale:    1
has_vector_search: 0

Row 2:
──────
company:           Tesla
details:           {'use_case':'observability platform','scale':'quadrillion rows','feature':'vector search'}
is_observability:  1
petabyte_scale:    0
has_vector_search: 1

Row 3:
──────
company:           Anthropic
details:           {'use_case':'AI observability','scale':'billions of events'}
is_observability:  1
petabyte_scale:    0
has_vector_search: 0

Row 4:
──────
company:           Uber
details:           {'use_case':'ride analytics','scale':'petabyte scale'}
is_observability:  0
petabyte_scale:    1
has_vector_search: 0

perimeterCartesian 

perimeterCartesian 是在 ClickHouse 25.10 中新增的函数,用于在笛卡尔 (平面) 坐标系中计算指定 Geometry 对象的周长。

下面我们来看一个示例,演示它在计算正方形周长时的效果:

SELECT perimeterCartesian(readWKT('POLYGON((0 0,1 0,1 1,0 1,0 0))'));
┌─perimeterCar⋯ 1,0 0))'))─┐
│                        4 │
└──────────────────────────┘

    此外,还有 perimeterSpherical,它用于计算位于球面上的 Geometry 对象的周长。因此,如果我们想要计算环绕伦敦的 M25 高速公路的周长,就可以使用 perimeterSpherical:

    WITH
        readWKT('POLYGON((0.13870239257812503 51.2968127854147, 0.16342163085937503 51.37403072457134, 0.212860107421875 51.41516045575089, 0.27053833007812506 51.483627853536014, 0.27328491210937506 51.54686881000932, 0.25405883789062506 51.633894901713354, 0.13870239257812503 51.67308742846449, 0.08102416992187501 51.695224736990404, -0.023345947265625003 51.68500886266592, -0.12222290039062501 51.69352225137908, -0.29525756835937506 51.71224607096211, -0.37490844726562506 51.71905281158759, -0.44631958007812506 51.68330599278565, -0.49850463867187506 51.64412230646439, -0.5259704589843751 51.55028473901506, -0.5039978027343751 51.51440469156115, -0.5369567871093751 51.44255973575031, -0.5177307128906251 51.37403072457134, -0.41061401367187506 51.30883300776494, -0.29525756835937506 51.30539897974217, -0.15243530273437503 51.272762896039936, 0.04531860351562501 51.272762896039936, 0.13870239257812503 51.2968127854147))') AS m25,
        perimeterSpherical(m25) AS per_rad
    SELECT
        per_rad,
        per_rad * 6371000 AS per_meters,
        per_rad * 6371 AS per_km;

    该函数返回的是单位球面上的弧度长度,因此需要再乘以地球半径,才能得到以米或千米为单位的实际长度:

    ┌──────────────per_rad─┬─────────per_meters─┬─────────────per_km─┐
    │ 0.027954722202348813 │ 178099.53515116428 │ 178.09953515116428 │
    └──────────────────────┴────────────────────┴────────────────────┘

    事实上,这条高速公路的直径约为 188 km,因此计算结果已经相当接近了 —— 那点误差就归咎于我画的多边形不够精确吧。

    HMAC

    HMAC (Hash-based Message Authentication Code) 是一种加密构造,用于同时验证消息的完整性与真实性。ClickHouse 25.12 新增了这一函数。

    下面我们通过为单词 ‘ClickHouse’ 生成签名,来看一下它的使用方式。返回结果采用十六进制格式,因此我们会使用 hex 函数将其转换为字符串形式:

    SELECT hex(HMAC('sha256', 'ClickHouse', 'mySecretKey'))
    ┌─hex(HMAC('sha256', 'ClickHouse', 'mySecretKey'))─────────────────┐
    │ 5A79F3AA2874164CFD9811F9D1DBCEBE428C9BC52A7F57303EC6BAFCD6C9377B │
    └──────────────────────────────────────────────────────────────────┘

      消息 ‘ClickHouse’ 及其对应的签名随后可以发送给另一方。接收方可以使用自己持有的密钥副本计算 HMAC,并将计算结果与收到的签名进行比对,从而验证消息的真实性。

      argAndMin and argAndMax 

      ClickHouse 25.11 引入了 argAndMax 和 argAndMin 函数。下面我们通过英国房产价格数据集来了解这两个函数的用法。

      假设我们想要找出 2025 年成交价格最高的一套房产,可以编写如下查询:

      SELECT max(price) 
      FROM uk_price_paid 
      WHERE toYear(date) = 2025;
      ┌─max(price)─┐
      │  127700000 │ -- 127.70 million
      └────────────┘

      如果我们还想知道这个最高价格对应的是哪个城镇,该怎么做呢?这时可以使用 argMax 函数:

      SELECT argMax(town, price) 
      FROM uk_price_paid 
      WHERE toYear(date) = 2025;
      ┌─argMax(town, price)─┐
      │ PURFLEET-ON-THAMES  │
      └─────────────────────┘

      argAndMax 函数则可以一次性返回城镇名称以及对应的最高成交价格:

      SELECT argAndMax(town, price) 
      FROM uk_price_paid 
      WHERE toYear(date) = 2025;
      ┌─argAndMax(town, price)───────────┐
      │ ('PURFLEET-ON-THAMES',127700000) │
      └──────────────────────────────────┘

      同样地,我们也可以使用 argAndMin 来获取城镇以及对应的最低成交价格:

      SELECT argAndMin(town, price) 
      FROM uk_price_paid 
      WHERE toYear(date) = 2025;
      ┌─argAndMin(town, price)─┐
      │ ('CAMBRIDGE',100)      │
      └────────────────────────┘

      这个结果看起来并不合理,因为在 2025 年以 100 英镑成交一套房产的可能性非常低。

      sparseGrams

      sparseGrams 是在 ClickHouse 25.5 中引入的函数,用于查找给定字符串中所有长度至少为 n 的子字符串。这些子字符串需要满足一个条件:其边界处的 (n-1)-gram 的哈希值必须严格大于该子字符串内部任意 (n-1)-gram 的哈希值。该函数使用 CRC32 作为哈希函数。

      下面来看一个示例,看看它是如何工作的:

      SELECT sparseGrams('ClickHouse') FORMAT Vertical;
      Row 1:
      ──────
      sparseGrams('ClickHouse'): ['Cli','lic','ick','lick','ckH','kHo','ckHo','lickHo','Hou','ous','Hous','use']

        该函数由 GitHub 团队提出,在构建搜索索引时,可以作为传统 n-grams 的一种合适替代方案。

        stringBytesUniq 

        stringBytesUniq 在 ClickHouse 25.6 中引入,用于统计一个字符串中不同字节的数量。下面来看几个示例:

        SELECT
            stringBytesUniq('ClickHouse') AS ch,
            stringBytesUniq('Alexey Milovidov') AS alexey,
            stringBytesUniq('AAAAA') AS a;
        ┌─ch─┬─alexey─┬─a─┐
        │ 10 │     11 │ 1 │
        └────┴────────┴───┘

        financialInternalRateOfReturn 

        financialInternalRateOfReturn 是在 ClickHouse 25.7 中新增的函数,用于计算按年度折算后的回报率 (例如某项投资的收益率)。

        例如,假设我们在 2020 年以 113 美元买入 Apple 股票,在 2021、2022、2023 和 2024 年期间没有任何操作,并在 2025 年以 231 美元卖出。可以通过运行下面的查询来计算内部收益率:

        SELECT financialInternalRateOfReturn([-113, 0, 0, 0, 0, 231])
        ┌─financialInt⋯0, 0, 231])─┐
        │      0.15373669910090634 │
        └──────────────────────────┘

          这相当于在 2020 年到 2025 年期间,每一年都获得了约 15% 的回报。我们可以通过运行下面的查询来验证这一点:

          SELECT 113 * power(1.15373669910090634, 5);
          ┌─multiply(113⋯009064, 5))─┐
          │       230.99999999999997 │
          └──────────────────────────┘

            此外,还有一个相关函数 financialInternalRateOfReturnExtended,用于在现金流发生在不规则时间点 (即具体日期) 的情况下计算回报率。

            因此,我们仍然使用 Apple 股票的示例,不过这一次改为使用买入和卖出时的具体日期:

            SELECT financialInternalRateOfReturnExtended(
              [-113, 231],
              [toDate('2020-09-11'), toDate('2025-03-05')]
            );
            ┌─financialInt⋯5-03-05')])─┐
            │      0.17295574412431242 │
            └──────────────────────────┘

            这一次计算得到的回报率略高于 17%。

            toInterval

            toInterval 是在 ClickHouse 25.4 中引入的函数,用于根据一个数值以及对应的单位字符串生成一个 interval 值。

            此前已经存在用于完成该任务的多个独立函数 (例如 toIntervalSecond、toIntervalMinute、toIntervalDay 等),而 toInterval 将这些功能统一到了一个函数之中。

            下面来看一下它的用法:

            SELECT
                toInterval(5, 'second') AS seconds,
                toTypeName(seconds) AS secType,
                toInterval(3, 'day') AS days,
                toTypeName(days) AS daysType,
                toInterval(2, 'month') AS months,
                toTypeName(months) AS monthsType;
            
            ┌─seconds─┬─secType────────┬─days─┬─daysType────┬─months─┬─monthsType────┐
            │       5 │ IntervalSecond │    3 │ IntervalDay │      2 │ IntervalMonth │
            └─────────┴────────────────┴──────┴─────────────┴────────┴───────────────┘

            我们也可以使用该函数在现有的 DateTime 值基础上增加一段时间,如下所示:

            WITH toDateTime('2025-12-17 12:32:12') AS currentTime
            SELECT
                currentTime,
                currentTime + toInterval(7, 'day') + toInterval(23, 'hour') AS nextWeek;
            ┌─────────currentTime─┬────────────nextWeek─┐
            │ 2025-12-17 12:32:12 │ 2025-12-25 11:32:12 │
            └─────────────────────┴─────────────────────

            timeSeriesRange 

            timeSeriesRange 是在 25.8 版本中引入的函数,用于生成一段时间戳范围。它的作用类似于 range 函数,但针对的是 DateTime 类型。

            该函数会返回一个数组结果,不过我们可以借助 arrayJoin 函数将数组展开为多行记录:

            SELECT arrayJoin(
              timeSeriesRange(
                '2025-06-01 00:00:00'::DateTime, 
                '2025-06-01 00:01:00'::DateTime, 
                10
            )) AS ts;
            ┌──────────────────ts─┐
            │ 2025-06-01 00:00:00 │
            │ 2025-06-01 00:00:10 │
            │ 2025-06-01 00:00:20 │
            │ 2025-06-01 00:00:30 │
            │ 2025-06-01 00:00:40 │
            │ 2025-06-01 00:00:50 │
            │ 2025-06-01 00:01:00 │
            └─────────────────────┘

            接下来,就可以计算距离这些时间点分别已经过去了多长时间

            WITH toDateTime('2025-12-17 12:32:12') AS currentTime
            SELECT arrayJoin(
              timeSeriesRange(
                '2025-06-01 00:00:00'::DateTime, 
                '2025-06-01 00:01:00'::DateTime, 
                10
            )) AS ts,
              formatReadableTimeDelta(now() - ts) AS timeAgo;
            ┌──────────────────ts─┬─timeAgo────────────────────────────────────────────────┐
            │ 2025-06-01 00:00:00 │ 6 months, 16 days, 14 hours, 10 minutes and 41 seconds │
            │ 2025-06-01 00:00:10 │ 6 months, 16 days, 14 hours, 10 minutes and 31 seconds │
            │ 2025-06-01 00:00:20 │ 6 months, 16 days, 14 hours, 10 minutes and 21 seconds │
            │ 2025-06-01 00:00:30 │ 6 months, 16 days, 14 hours, 10 minutes and 11 seconds │
            │ 2025-06-01 00:00:40 │ 6 months, 16 days, 14 hours, 10 minutes and 1 second   │
            │ 2025-06-01 00:00:50 │ 6 months, 16 days, 14 hours, 9 minutes and 51 seconds  │
            │ 2025-06-01 00:01:00 │ 6 months, 16 days, 14 hours, 9 minutes and 41 seconds  │
            └─────────────────────┴────────────────────────────────────────────────────────┘

            征稿启示

            面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

            Logo

            腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

            更多推荐