大家好,DuckDB的SQL最初是基于PostgreSQL的,尽管这是一个不错的、可以模仿的SQL版本,但是随着时间的推移,DuckDB已经为其SQL功能引入了一些有用的补充,以使编程工作更加轻松。
对于那些没有听说过DuckDB的人来说,它是一个用C++编写的内存数据库,专为分析型SQL工作负载而设计。它的速度也非常快,性能可以与Polars相媲美。下面让我们来看看本文所涉及的一些有用的SQL命令。
1. 使用正则表达式进行动态列选择
使用正则表达式进行动态列选择,在查询表时不需要使用表中包含的确切列名,这可以为查询带来一些有用的快捷方式。
可以在列名中使用通配符表达式,搭配COLUMNS()
关键字和合适的通配符,DuckDB将只检索与COLUMNS()
表达式中的通配符匹配的列数据。
假设有以下表格:
db.sql("CREATE TABLE departments (department varchar, exmployee_count INT, average_salary INT,max_salary INT,min_salary INT)")db.sql("INSERT INTO departments VALUES ('Sales', 300, 25000,40000,19000), ('HR',50, 22000,50000,18500)");db.sql("SELECT * FROM departments")+----------|--------------|--------------|-----------|----------+
|department|exployee_count|average_salary|max_salary |min_salary|
+----------|--------------|--------------|-----------|----------+
|Sales |300 |25000 |40000 |19000 |
|HR |50 |22000 |50000 |1850 |
+----------|--------------|--------------|-----------|----------+
现在,假如我们只想显示部门以及最高/最低薪资,可以使用以下方式:
# 排除average_salary和employee_count列
db.sql("SELECT department, COLUMNS('m.*salary') FROM departments")┌────────────┬────────────┬────────────┐
│ department │ max_salary │ min_salary │
│ varchar │ int32 │ int32 │
├────────────┼────────────┼────────────┤
│ Sales │ 40000 │ 19000 │
│ HR │ 50000 │ 18500 │
└────────────┴────────────┴────────────┘
还可以在WHERE子句中使用COLUMNS
表达式,DuckDB会在每个通配符产生的表达式之间隐式地添加AND,示例如下:
db.sql("SELECT department, COLUMNS('m.*salary') FROM departments \WHERE COLUMNS('m.*salary') >= 19000")# 上述WHERE子句等价于
# WHERE max_salary > =19000 AND min_salary >= 19000
#┌────────────┬────────────┬────────────┐
│ department │ max_salary │ min_salary │
│ varchar │ int32 │ int32 │
├────────────┼────────────┼────────────┤
│ Sales │ 40000 │ 19000 │
└────────────┴────────────┴────────────┘
在对列进行计算时也可以使用这种语法。仍以部门数据集为例,如果想对列执行一些聚合,可以这样使用如下方式:
db.sql("SELECT MAX(COLUMNS('m.*salary')) FROM departments")┌─────────────────────────────┬─────────────────────────────┐
│ max(departments.max_salary) │ max(departments.min_salary) │
│ int32 │ int32 │
├─────────────────────────────┼─────────────────────────────┤
│ 50000 │ 19000 │
└─────────────────────────────┴─────────────────────────────┘
2. Pivot/Unpivot
在大多数关系型数据库系统中,对数据集进行Pivot/Unpivot通常是一件很困难的事情,但这真的可以大大简化数据分析。
让我们先设置一下测试表数据:
import duckdb as dbdb.sql("CREATE TABLE purchases (productID int, year INT, sales INT)")db.sql("INSERT INTO purchases VALUES (12345, 2019, 15000), (12345,2020, 19500), (12345, 2021, 22000), (987654, 2019, 510), (987654,2020, 1900), (987654, 2021, 2100)");db.sql("SELECT * FROM purchases")┌───────────┬───────┬───────┐
│ productID │ year │ sales │
│ int32 │ int32 │ int32 │
├───────────┼───────┼───────┤
│ 12345 │ 2019 │ 15000 │
│ 12345 │ 2020 │ 19500 │
│ 12345 │ 2021 │ 22000 │
│ 987654 │ 2019 │ 510 │
│ 987654 │ 2020 │ 1900 │
│ 987654 │ 2021 │ 2100 │
└───────────┴───────┴───────┘
希望数据看起来像这样:
┌───────────┬────────┬────────┬────────┐
│ productID │ 2019 │ 2020 │ 2021 │
│ int32 │ int128 │ int128 │ int128 │
├───────────┼────────┼────────┼────────┤
│ 12345 │ 15000 │ 19500 │ 22000 │
│ 987654 │ 510 │ 1900 │ 2100 │
└───────────┴────────┴────────┴────────┘
可以使用PIVOT
来实现这一点:
import duckdb as dbdb.sql("create table pivoted_purchases as PIVOT purchases ON year USING SUM(sales) GROUP BY productID")
db.sql("SELECT * FROM pivoted_purchases")# 如果不想根据PIVOT创建表格
# 而只是显示值,也可以这样做
# db.sql("PIVOT purchases ON year USING SUM(sales) GROUP BY productID")┌───────────┬────────┬────────┬────────┐
│ productID │ 2019 │ 2020 │ 2021 │
│ int32 │ int128 │ int128 │ int128 │
├───────────┼────────┼────────┼────────┤
│ 12345 │ 15000 │ 19500 │ 22000 │
│ 987654 │ 510 │ 1900 │ 2100 │
└───────────┴────────┴────────┴────────┘
由于在PIVOT
命令中进行的SUM(salary)聚合并没有改变任何数字数据,因此也可以使用UNPIVOT
来反转这个过程(以及之前讨论过的COLUMNS
功能)。
db.sql("UNPIVOT pivoted_purchases ON COLUMNS(* EXCLUDE productID) INTO NAME year VALUE sales")┌───────────┬─────────┬────────┐
│ productID │ year │ sales │
│ int32 │ varchar │ int128 │
├───────────┼─────────┼────────┤
│ 12345 │ 2019 │ 15000 │
│ 12345 │ 2020 │ 19500 │
│ 12345 │ 2021 │ 22000 │
│ 987654 │ 2019 │ 510 │
│ 987654 │ 2020 │ 1900 │
│ 987654 │ 2021 │ 2100 │
└───────────┴─────────┴────────┘
3. 联合数据类型
现在可以联合不同的数据类型,另一个SQL问题已经被解决了。
db.sql("SELECT 'I am a string' as col1 union select 100 union select 42.0")┌───────────────┐
│ col1 │
│ varchar │
├───────────────┤
│ I am a string │
│ 42.0 │
│ 100 │
└───────────────┘
DuckDB会将不同的数据类型强制转换为“最小公分母”来支持所有检索的类型。在这里,FLOAT和INT列的值被强制转换为VARCHARS。
4. 可重用的列别名
在传统的SQL中,当在选择语句中处理增量计算表达式时,通常需要为每一列复制整个表达式或在通用表表达式(CTE)中封装每个计算步骤。但是,通过可重用的列别名,现在可以在同一条选择语句中使用列别名,包括在where
和order by
子句中,从而简化该过程并减少冗余,示例如下:
db.sql("SELECT 'The quick brown fox jumped over the lazy dog' AS my_text,\
substring(my_text, 17,3) AS my_text_substr,\
length(my_text) AS my_text_len,\
my_text_len * my_text_len AS my_text_calc")┌──────────────────────────────────────────────┬────────────────┬─────────────┬──────────────┐
│ my_text │ my_text_substr │ my_text_len │ my_text_calc │
│ varchar │ varchar │ int64 │ int64 │
├──────────────────────────────────────────────┼────────────────┼─────────────┼──────────────┤
│ The quick brown fox jumped over the lazy dog │ fox │ 44 │ 1936 │
└──────────────────────────────────────────────┴────────────────┴─────────────┴──────────────┘
5. 列表推导式
这些是基于Python风格的列表推导式的,可用于计算列表元素上的表达式。例如有一个数字列表:
[1,2,3,4,5,6,7,8,9]
在Python中如果想输出一个新列表,使得上述列表中每个数字都平方,代码如下:
[x*x for x in [1,2,3,4,5,6,7,8,9]]# 上述Python代码的输出结果如下:
#
# [1, 4, 9, 16, 25, 36, 49, 64, 81]
在DuckDB SQL中,可以做类似操作:
db.sql("SELECT [x*x for x in nums] as squares FROM (VALUES ([1,2,3,4,5,6,7,8,9])) t(nums)")┌───────────────────────────────────┐
│ squares │
│ int32[] │
├───────────────────────────────────┤
│ [1, 4, 9, 16, 25, 36, 49, 64, 81] │
└───────────────────────────────────┘
FROM (VALUES ([1,2,3,4,5,6,7,8,9])) t(nums)
-
查询的这一部分是创建一个临时表(或派生表)
t
,其中只有一个名为nums
的列。 -
VALUES子句用于提供一个字面值。在这里,它是一个从1到9的整数列表(或数组)。
-
t(nums)是别名部分,其中派生表命名为
t
,并将包含该数组的列命名为nums
。
SELECT [x*x for x in nums] as squares
-
这是查询的主要部分,在这里进行计算。
-
查询使用了列表推导式
[x*x for x in nums]
,这是Python中常见的操作。在这种情况下,对于nums
数组中的每个元素x
,都会计算x
的平方。 -
计算结果是一个平方值数组。对于输入数组
[1,2,3,4,5,6,7,8,9]
,输出将是[1,4,9,16,25,36,49,64,81]
。 -
as squares
是将计算出的数组命名为squares
。
总的来说,查询是创建一个从1到9的数字数组,然后计算每个数字的平方,并将结果作为名为squares
的数组返回。
6. 函数链式调用
DuckDB使用点符号(.)来轻松地将单独的SQL函数串联起来,从而在一条SQL语句中有效地建立一种函数管道。
例如,在许多数据库系统中,都有一个类似于INITCAP的SQL函数,可以将文本字符串中的每个单词首字母大写。遗憾的是,DuckDB并没有内置这个函数,所以尝试使用函数链式调用(和列表推导式)来实现它。
下面是一个简单示例,想将“the quick brown fox jumped over the lazy dog”短语中的每个单词首字母大写。
db.sql("SELECT ([upper(x[1])||x[2:] for x in \
('the quick brown fox jumped over the lazy dog')\
.string_split(' ')]).list_aggr('string_agg',' ') as final_str")┌──────────────────────────────────────────────┐
│ final_str │
│ varchar │
├──────────────────────────────────────────────┤
│ The Quick Brown Fox Jumped Over The Lazy Dog │
└──────────────────────────────────────────────┘
string_split
函数会将短语拆分成单个单词,并产生一个类似于Python列表的单词列表。
[the, quick, brown, fox, jumped, over, the, lazy, dog]
接下来,对单词列表运行一个列表推导式。对于列表中的每个单词,将第一个字母设置为大写,然后将剩余的字母连接上去。这就是表达式中upper(x[1])||x[2:]
部分的作用。
这样中间单词列表就变成了这样:
[The, Quick, Brown, Fox, Jumped, Over, The, Lazy, Dog]
最后对上述单词列表运行list_agg
函数,这只是将单词列表转换回一个由空格字符分隔的单词字符串。
以上这就是本文分享的全部内容,本文介绍了DuckDB提供的六种常规SQL扩展,以帮助数据工程师更轻松地工作。