DuckDB 技巧 – 第 2 部分
要点:我们将继续我们的“DuckDB 技巧”系列,重点介绍用于清理、转换和汇总数据的查询。
概览
本文是 DuckDB 技巧系列的最新文章,我们将向您展示 DuckDB 中巧妙的 SQL 技巧。以下是我们即将介绍的内容的摘要:
| 操作 | SQL 指令 |
|---|---|
| 修复 CSV 文件中的时间戳 | regexp_replace() 和 strptime() |
| 填充缺失值 | CROSS JOIN, LEFT JOIN 和 coalesce() |
| 重复的转换步骤 | CREATE OR REPLACE TABLE t AS ... FROM t ... |
| 计算列的校验和 | bit_xor(md5_number(COLUMNS(*)::VARCHAR)) |
| 创建校验和的宏 | CREATE MACRO checksum(tbl) AS TABLE ... |
数据集
对于我们的示例数据集,我们将使用 schedule.csv,这是一个手写的 CSV 文件,用于编码会议日程。该日程包含时段、地点和安排的活动。
timeslot,location,event
2024-10-10 9am,room Mallard,Keynote
2024-10-10 10.30am,room Mallard,Customer stories
2024-10-10 10.30am,room Fusca,Deep dive 1
2024-10-10 12.30pm,main hall,Lunch
2024-10-10 2pm,room Fusca,Deep dive 2
修复 CSV 文件中的时间戳
与实际用例中通常一样,输入 CSV 文件很混乱,时间戳不规则,例如 2024-10-10 9am。因此,如果我们使用 DuckDB 的 CSV 读取器加载 schedule.csv 文件,CSV sniffer 会将第一列检测为 VARCHAR 字段。
CREATE TABLE schedule_raw AS
SELECT * FROM 'https://duckdb.net.cn/data/schedule.csv';
SELECT * FROM schedule_raw;
┌────────────────────┬──────────────┬──────────────────┐
│ timeslot │ location │ event │
│ varchar │ varchar │ varchar │
├────────────────────┼──────────────┼──────────────────┤
│ 2024-10-10 9am │ room Mallard │ Keynote │
│ 2024-10-10 10.30am │ room Mallard │ Customer stories │
│ 2024-10-10 10.30am │ room Fusca │ Deep dive 1 │
│ 2024-10-10 12.30pm │ main hall │ Lunch │
│ 2024-10-10 2pm │ room Fusca │ Deep dive 2 │
└────────────────────┴──────────────┴──────────────────┘
理想情况下,我们希望 timeslot 列的类型为 TIMESTAMP,以便稍后在查询中将其视为时间戳。为了实现这一点,我们可以使用我们刚刚加载的表,并通过使用基于正则表达式的搜索和替换操作来修复有问题的实体,该操作将格式统一为 hours.minutes,后跟 am 或 pm。然后,我们使用 strptime 将字符串转换为时间戳,其中 %p 格式说明符捕获字符串的 am/pm 部分。
CREATE TABLE schedule_cleaned AS
SELECT
timeslot
.regexp_replace(' (\d+)(am|pm)$', ' \1.00\2')
.strptime('%Y-%m-%d %H.%M%p') AS timeslot,
location,
event
FROM schedule_raw;
请注意,我们使用 点运算符进行函数链式调用以提高可读性。例如,regexp_replace(string, pattern, replacement) 被公式化为 string.regexp_replace(pattern, replacement)。结果是下表
┌─────────────────────┬──────────────┬──────────────────┐
│ timeslot │ location │ event │
│ timestamp │ varchar │ varchar │
├─────────────────────┼──────────────┼──────────────────┤
│ 2024-10-10 09:00:00 │ room Mallard │ Keynote │
│ 2024-10-10 10:30:00 │ room Mallard │ Customer stories │
│ 2024-10-10 10:30:00 │ room Fusca │ Deep dive 1 │
│ 2024-10-10 12:30:00 │ main hall │ Lunch │
│ 2024-10-10 14:00:00 │ room Fusca │ Deep dive 2 │
└─────────────────────┴──────────────┴──────────────────┘
填充缺失值
接下来,我们希望导出一个包含完整信息的日程表:每个时段,每个地点都应该在该表中有一行。对于没有指定事件的时段-地点组合,我们希望显式地添加一个字符串,表示 <empty>。
为了实现这一点,我们首先创建一个表 timeslot_location_combinations,其中包含使用 CROSS JOIN 的所有可能的组合。然后,我们可以使用 LEFT JOIN 将原始表连接到这些组合。最后,我们使用 coalesce 函数 将 NULL 值替换为 <empty> 字符串。
CROSS JOIN子句等同于简单地在FROM子句中列出表而不指定连接条件。通过显式地拼写出CROSS JOIN,我们表明我们打算计算笛卡尔积——这在大表上是一种昂贵的操作,在大多数用例中应避免。
CREATE TABLE timeslot_location_combinations AS
SELECT timeslot, location
FROM (SELECT DISTINCT timeslot FROM schedule_cleaned)
CROSS JOIN (SELECT DISTINCT location FROM schedule_cleaned);
CREATE TABLE schedule_filled AS
SELECT timeslot, location, coalesce(event, '<empty>') AS event
FROM timeslot_location_combinations
LEFT JOIN schedule_cleaned
USING (timeslot, location)
ORDER BY ALL;
SELECT * FROM schedule_filled;
┌─────────────────────┬──────────────┬──────────────────┐
│ timeslot │ location │ event │
│ timestamp │ varchar │ varchar │
├─────────────────────┼──────────────┼──────────────────┤
│ 2024-10-10 09:00:00 │ main hall │ <empty> │
│ 2024-10-10 09:00:00 │ room Fusca │ <empty> │
│ 2024-10-10 09:00:00 │ room Mallard │ Keynote │
│ 2024-10-10 10:30:00 │ main hall │ <empty> │
│ 2024-10-10 10:30:00 │ room Fusca │ Deep dive 1 │
│ 2024-10-10 10:30:00 │ room Mallard │ Customer stories │
│ 2024-10-10 12:30:00 │ main hall │ Lunch │
│ 2024-10-10 12:30:00 │ room Fusca │ <empty> │
│ 2024-10-10 12:30:00 │ room Mallard │ <empty> │
│ 2024-10-10 14:00:00 │ main hall │ <empty> │
│ 2024-10-10 14:00:00 │ room Fusca │ Deep dive 2 │
│ 2024-10-10 14:00:00 │ room Mallard │ <empty> │
├─────────────────────┴──────────────┴──────────────────┤
│ 12 rows 3 columns │
└───────────────────────────────────────────────────────┘
我们还可以使用 WITH 子句 将所有内容放在一个查询中
WITH timeslot_location_combinations AS (
SELECT timeslot, location
FROM (SELECT DISTINCT timeslot FROM schedule_cleaned)
CROSS JOIN (SELECT DISTINCT location FROM schedule_cleaned)
)
SELECT timeslot, location, coalesce(event, '<empty>') AS event
FROM timeslot_location_combinations
LEFT JOIN schedule_cleaned
USING (timeslot, location)
ORDER BY ALL;
重复的数据转换步骤
数据清理和转换通常作为一系列转换发生,这些转换将数据塑造成最适合后续分析的形式。这些转换通常通过使用 CREATE TABLE ... AS SELECT 语句定义越来越新的表来完成。
例如,在上面的部分中,我们创建了 schedule_raw、schedule_cleaned 和 schedule_filled。如果由于某种原因,我们想要跳过时间戳的清理步骤,我们必须重新制定计算 schedule_filled 的查询,以使用 schedule_raw 而不是 schedule_cleaned。这可能很繁琐且容易出错,并且会导致大量未使用的临时数据——这些数据可能会被我们忘记更新的查询意外地拾取!
在交互式分析中,最好通过运行 CREATE OR REPLACE 语句使用相同的表名。
CREATE OR REPLACE TABLE table_name AS
...
FROM table_name
...;
使用此技巧,我们可以按如下方式运行我们的分析:
CREATE OR REPLACE TABLE schedule AS
SELECT * FROM 'https://duckdb.net.cn/data/schedule.csv';
CREATE OR REPLACE TABLE schedule AS
SELECT
timeslot
.regexp_replace(' (\d+)(am|pm)$', ' \1.00\2')
.strptime('%Y-%m-%d %H.%M%p') AS timeslot,
location,
event
FROM schedule;
CREATE OR REPLACE TABLE schedule AS
WITH timeslot_location_combinations AS (
SELECT timeslot, location
FROM (SELECT DISTINCT timeslot FROM schedule)
CROSS JOIN (SELECT DISTINCT location FROM schedule)
)
SELECT timeslot, location, coalesce(event, '<empty>') AS event
FROM timeslot_location_combinations
LEFT JOIN schedule
USING (timeslot, location)
ORDER BY ALL;
SELECT * FROM schedule;
使用这种方法,我们可以跳过任何步骤并继续分析,而无需调整下一步。
更重要的是,我们的脚本现在可以从头开始重新运行,而无需显式删除任何表:CREATE OR REPLACE 语句将自动替换任何现有表。
计算列的校验和
计算表中每列的校验和通常是有益的,例如,查看两项操作之间列的内容是否已更改。我们可以按如下方式计算 schedule 表的校验和:
SELECT bit_xor(md5_number(COLUMNS(*)::VARCHAR))
FROM schedule;
这里发生了什么?我们首先列出列 (COLUMNS(*)) 并将所有列转换为 VARCHAR 值。然后,我们使用 md5_number 函数计算数字 MD5 哈希值,并使用 bit_xor 聚合函数聚合它们。这会为每列生成一个 HUGEINT (INT128) 值,该值可用于比较表的内容。
如果我们在上面的脚本中运行此查询,我们将得到以下结果:
┌──────────────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────────────────┐
│ timeslot │ location │ event │
│ int128 │ int128 │ int128 │
├──────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────┤
│ -134063647976146309049043791223896883700 │ 85181227364560750048971459330392988815 │ -65014404565339851967879683214612768044 │
└──────────────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────────────────┘
┌────────────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────────────────┐
│ timeslot │ location │ event │
│ int128 │ int128 │ int128 │
├────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────┤
│ 62901011016747318977469778517845645961 │ 85181227364560750048971459330392988815 │ -65014404565339851967879683214612768044 │
└────────────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────────────────┘
┌──────────────────────────────────────────┬──────────┬──────────────────────────────────────────┐
│ timeslot │ location │ event │
│ int128 │ int128 │ int128 │
├──────────────────────────────────────────┼──────────┼──────────────────────────────────────────┤
│ -162418013182718436871288818115274808663 │ 0 │ -135609337521255080720676586176293337793 │
└──────────────────────────────────────────┴──────────┴──────────────────────────────────────────┘
为校验和查询创建宏
我们可以使用新的 query_table 函数将 校验和查询转换为 表宏
CREATE MACRO checksum(table_name) AS TABLE
SELECT bit_xor(md5_number(COLUMNS(*)::VARCHAR))
FROM query_table(table_name);
这样,我们可以简单地在 schedule 表上调用它,如下所示(也利用了 DuckDB 的 FROM-first 语法):
FROM checksum('schedule');
┌──────────────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────────────────┐
│ timeslot │ location │ event │
│ int128 │ int128 │ int128 │
├──────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────┤
│ -134063647976146309049043791223896883700 │ 85181227364560750048971459330392988815 │ -65014404565339851967879683214612768044 │
└──────────────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────────────────┘
结束语
今天就到这里!我们很快会带着更多 DuckDB 技巧和案例研究回来。与此同时,如果您有想要分享的技巧,请在我们的社交媒体网站上与 DuckDB 团队分享,或将其提交到 DuckDB Snippets 网站(由我们在 MotherDuck 的朋友维护)。