动态列选择 是 ClickHouse 中一个功能强大但尚未被充分利用的特性,它允许你使用正则表达式来选择列,而无需逐个指定每一列的列名。你还可以使用 APPLY 修饰符对匹配的列应用函数,使其在数据分析和数据转换任务中非常实用。
我们将借助 纽约出租车数据集 学习如何使用这一特性,你也可以在 ClickHouse SQL playground 中找到它。
选择匹配某种模式的列
先从一个常见场景开始:只选择纽约出租车数据集中包含 _amount 的列。与其手动输入每个列名,我们可以使用带正则表达式的 COLUMNS 表达式来完成:
FROM nyc_taxi.trips
SELECT COLUMNS('.*_amount')
LIMIT 10;
在 SQL playground 中试试这个查询
此查询返回前 10 行,但只包含列名匹配模式 .*_amount(任意字符后接 _amount)的列。
┌─fare_amount─┬─tip_amount─┬─tolls_amount─┬─total_amount─┐
1. │ 9 │ 0 │ 0 │ 9.8 │
2. │ 9 │ 0 │ 0 │ 9.8 │
3. │ 3.5 │ 0 │ 0 │ 4.8 │
4. │ 3.5 │ 0 │ 0 │ 4.8 │
5. │ 3.5 │ 0 │ 0 │ 4.3 │
6. │ 3.5 │ 0 │ 0 │ 4.3 │
7. │ 2.5 │ 0 │ 0 │ 3.8 │
8. │ 2.5 │ 0 │ 0 │ 3.8 │
9. │ 5 │ 0 │ 0 │ 5.8 │
10. │ 5 │ 0 │ 0 │ 5.8 │
└─────────────┴────────────┴──────────────┴──────────────┘
假设我们还想返回包含 fee 或 tax 的列。
我们可以更新正则表达式以将它们包含进去:
SELECT COLUMNS('.*_amount|fee|tax')
FROM nyc_taxi.trips
ORDER BY rand()
LIMIT 3;
在 SQL playground 中尝试运行此查询
┌─fare_amount─┬─mta_tax─┬─tip_amount─┬─tolls_amount─┬─ehail_fee─┬─total_amount─┐
1. │ 5 │ 0.5 │ 1 │ 0 │ 0 │ 7.8 │
2. │ 12.5 │ 0.5 │ 0 │ 0 │ 0 │ 13.8 │
3. │ 4.5 │ 0.5 │ 1.66 │ 0 │ 0 │ 9.96 │
└─────────────┴─────────┴────────────┴──────────────┴───────────┴──────────────┘
选择多个匹配模式
我们可以在同一个查询中组合使用多个列匹配模式:
SELECT
COLUMNS('.*_amount'),
COLUMNS('.*_date.*')
FROM nyc_taxi.trips
LIMIT 5;
在 SQL playground 中尝试运行此查询
┌─fare_amount─┬─tip_amount─┬─tolls_amount─┬─total_amount─┬─pickup_date─┬─────pickup_datetime─┬─dropoff_date─┬────dropoff_datetime─┐
1. │ 9 │ 0 │ 0 │ 9.8 │ 2001-01-01 │ 2001-01-01 00:01:48 │ 2001-01-01 │ 2001-01-01 00:15:47 │
2. │ 9 │ 0 │ 0 │ 9.8 │ 2001-01-01 │ 2001-01-01 00:01:48 │ 2001-01-01 │ 2001-01-01 00:15:47 │
3. │ 3.5 │ 0 │ 0 │ 4.8 │ 2001-01-01 │ 2001-01-01 00:02:08 │ 2001-01-01 │ 2001-01-01 01:00:02 │
4. │ 3.5 │ 0 │ 0 │ 4.8 │ 2001-01-01 │ 2001-01-01 00:02:08 │ 2001-01-01 │ 2001-01-01 01:00:02 │
5. │ 3.5 │ 0 │ 0 │ 4.3 │ 2001-01-01 │ 2001-01-01 00:02:26 │ 2001-01-01 │ 2001-01-01 00:04:49 │
└─────────────┴────────────┴──────────────┴──────────────┴─────────────┴─────────────────────┴──────────────┴─────────────────────┘
对所有列应用函数
我们也可以使用 APPLY 修饰符,将函数应用到每一列上。
例如,如果我们想要找出这些列中每一列的最大值,可以运行以下查询:
SELECT COLUMNS('.*_amount|fee|tax') APPLY(max)
FROM nyc_taxi.trips;
在 SQL Playground 中尝试运行此查询
┌─max(fare_amount)─┬─max(mta_tax)─┬─max(tip_amount)─┬─max(tolls_amount)─┬─max(ehail_fee)─┬─max(total_amount)─┐
1. │ 998310 │ 500000.5 │ 3950588.8 │ 7999.92 │ 1.95 │ 3950611.5 │
└──────────────────┴──────────────┴─────────────────┴───────────────────┴────────────────┴───────────────────┘
或者,我们也可以改为查看平均值:
SELECT COLUMNS('.*_amount|fee|tax') APPLY(avg)
FROM nyc_taxi.trips
在 SQL playground 中尝试运行此查询
┌─avg(fare_amount)─┬───────avg(mta_tax)─┬────avg(tip_amount)─┬──avg(tolls_amount)─┬──────avg(ehail_fee)─┬──avg(total_amount)─┐
1. │ 11.8044154834777 │ 0.4555942672733423 │ 1.3469850969211845 │ 0.2256511991414463 │ 3.37600560437412e-9 │ 14.423323722271563 │
└──────────────────┴────────────────────┴────────────────────┴────────────────────┴─────────────────────┴────────────────────┘
这些值包含很多小数位,不过我们可以通过链式调用函数来处理这个问题。在这种情况下,我们先使用 avg 函数,然后再使用 round 函数:
SELECT COLUMNS('.*_amount|fee|tax') APPLY(avg) APPLY(round)
FROM nyc_taxi.trips;
在 SQL Playground 中尝试运行此查询
┌─round(avg(fare_amount))─┬─round(avg(mta_tax))─┬─round(avg(tip_amount))─┬─round(avg(tolls_amount))─┬─round(avg(ehail_fee))─┬─round(avg(total_amount))─┐
1. │ 12 │ 0 │ 1 │ 0 │ 0 │ 14 │
└─────────────────────────┴─────────────────────┴────────────────────────┴──────────────────────────┴───────────────────────┴──────────────────────────┘
但这会把平均值四舍五入为整数。如果我们希望四舍五入到比如保留 2 位小数,也可以做到。除了可以接收函数外,APPLY 修饰符还接受 lambda 表达式,这使我们可以灵活地让 round 函数将平均值四舍五入到 2 位小数:
SELECT COLUMNS('.*_amount|fee|tax') APPLY(avg) APPLY(x -> round(x, 2))
FROM nyc_taxi.trips;
在 SQL Playground 中试运行此查询
┌─round(avg(fare_amount), 2)─┬─round(avg(mta_tax), 2)─┬─round(avg(tip_amount), 2)─┬─round(avg(tolls_amount), 2)─┬─round(avg(ehail_fee), 2)─┬─round(avg(total_amount), 2)─┐
1. │ 11.8 │ 0.46 │ 1.35 │ 0.23 │ 0 │ 14.42 │
└────────────────────────────┴────────────────────────┴───────────────────────────┴─────────────────────────────┴──────────────────────────┴─────────────────────────────┘
替换列
到目前为止进展顺利。不过,假设我们只想调整其中一个值,而保持其他值不变。例如,我们可能想将总金额加倍,并将 MTA 税额除以 1.1。我们可以使用 REPLACE 修饰符来实现,它会在保留其他列不变的情况下替换某一列。
FROM nyc_taxi.trips
SELECT
COLUMNS('.*_amount|fee|tax')
REPLACE(
total_amount*2 AS total_amount,
mta_tax/1.1 AS mta_tax
)
APPLY(avg)
APPLY(col -> round(col, 2));
在 SQL playground 中尝试此查询
┌─round(avg(fare_amount), 2)─┬─round(avg(di⋯, 1.1)), 2)─┬─round(avg(tip_amount), 2)─┬─round(avg(tolls_amount), 2)─┬─round(avg(ehail_fee), 2)─┬─round(avg(mu⋯nt, 2)), 2)─┐
1. │ 11.8 │ 0.41 │ 1.35 │ 0.23 │ 0 │ 28.85 │
└────────────────────────────┴──────────────────────────┴───────────────────────────┴─────────────────────────────┴──────────────────────────┴──────────────────────────┘
排除列
我们也可以通过使用 EXCEPT 修饰符来排除某个字段。例如,要移除 tolls_amount 列,可以编写如下查询:
FROM nyc_taxi.trips
SELECT
COLUMNS('.*_amount|fee|tax') EXCEPT(tolls_amount)
REPLACE(
total_amount*2 AS total_amount,
mta_tax/1.1 AS mta_tax
)
APPLY(avg)
APPLY(col -> round(col, 2));
在 SQL Playground 中尝试运行此查询
┌─round(avg(fare_amount), 2)─┬─round(avg(di⋯, 1.1)), 2)─┬─round(avg(tip_amount), 2)─┬─round(avg(ehail_fee), 2)─┬─round(avg(mu⋯nt, 2)), 2)─┐
1. │ 11.8 │ 0.41 │ 1.35 │ 0 │ 28.85 │
└────────────────────────────┴──────────────────────────┴───────────────────────────┴──────────────────────────┴──────────────────────────┘