这里我们概述了如何在 SQL 中执行简单操作。本教程旨在为您提供一个入门,绝不是一个完整的 SQL 教程。本教程改编自 PostgreSQL 教程。
DuckDB 的 SQL 方言密切遵循 PostgreSQL 方言的约定。少数例外列在 PostgreSQL 兼容性页面上。
在接下来的示例中,我们假设您已经安装了 DuckDB 命令行界面 (CLI) shell。有关如何安装 CLI 的信息,请参阅安装页面。
概念
DuckDB 是一个关系型数据库管理系统 (RDBMS)。这意味着它是一个用于管理存储在关系中的数据的系统。关系本质上是表的数学术语。
每个表都是行的命名集合。给定表的每一行都具有相同的命名列集,并且每个列都具有特定的数据类型。表本身存储在模式中,而模式的集合构成了您可以访问的整个数据库。
创建新表
您可以通过指定表名以及所有列名及其类型来创建新表
CREATE TABLE weather (
city VARCHAR,
temp_lo INTEGER, -- minimum temperature on a day
temp_hi INTEGER, -- maximum temperature on a day
prcp FLOAT,
date DATE
);
您可以在 shell 中输入此内容,并使用换行符。命令直到分号才终止。
SQL 命令中可以自由使用空白符(即空格、制表符和换行符)。这意味着您可以以与上面不同的对齐方式输入命令,甚至可以将所有内容输入到一行上。两个破折号字符(--)引入注释。其后的任何内容都将被忽略直到行尾。SQL 对关键字和标识符不区分大小写。返回标识符时,它们原始的大小写会保留。
在 SQL 命令中,我们首先指定要执行的命令类型:CREATE TABLE。之后是命令的参数。首先给出表名 weather。然后是列名和列类型。
city VARCHAR 指定表有一个名为 city 的列,其类型为 VARCHAR。VARCHAR 指定了一种可以存储任意长度文本的数据类型。温度字段存储在 INTEGER 类型中,这是一种存储整数(即不带小数点的整数)的类型。FLOAT 列存储单精度浮点数(即带小数点的数字)。DATE 存储日期(即年、月、日的组合)。DATE 只存储具体的日期,不存储与该日期关联的时间。
DuckDB 支持标准 SQL 类型 INTEGER、SMALLINT、FLOAT、DOUBLE、DECIMAL、CHAR(n)、VARCHAR(n)、DATE、TIME 和 TIMESTAMP。
第二个示例将存储城市及其关联的地理位置
CREATE TABLE cities (
name VARCHAR,
lat DECIMAL,
lon DECIMAL
);
最后,需要提及的是,如果您不再需要某个表或想以不同的方式重新创建它,可以使用以下命令将其删除
DROP TABLE tablename;
用行填充表
insert 语句用于用行填充表
INSERT INTO weather
VALUES ('San Francisco', 46, 50, 0.25, '1994-11-27');
非数值常量(例如文本和日期)必须用单引号('')括起来,如示例所示。日期类型输入必须格式化为 'YYYY-MM-DD'。
我们可以用同样的方式插入到 cities 表中。
INSERT INTO cities
VALUES ('San Francisco', -194.0, 53.0);
到目前为止使用的语法要求您记住列的顺序。另一种语法允许您显式列出列
INSERT INTO weather (city, temp_lo, temp_hi, prcp, date)
VALUES ('San Francisco', 43, 57, 0.0, '1994-11-29');
如果您愿意,可以以不同的顺序列出列,甚至可以省略某些列,例如,如果 prcp 未知
INSERT INTO weather (date, city, temp_hi, temp_lo)
VALUES ('1994-11-29', 'Hayward', 54, 37);
提示 许多开发人员认为显式列出列比隐式依赖顺序是更好的风格。
请输入上面显示的所有命令,以便您在以下部分中拥有一些数据可供操作。
或者,您可以使用 COPY 语句。这对于大量数据来说更快,因为 COPY 命令针对批量加载进行了优化,同时灵活性低于 INSERT。一个使用 weather.csv 的示例如下
COPY weather
FROM 'weather.csv';
其中源文件的文件名必须在运行该进程的机器上可用。将数据加载到 DuckDB 中还有许多其他方法,有关更多信息,请参阅相应的文档部分。
查询表
要从表中检索数据,需要查询该表。SQL SELECT 语句用于此目的。该语句分为选择列表(列出要返回的列的部分)、表列表(列出从中检索数据的表的部分)和可选的限定条件(指定任何限制的部分)。例如,要检索 weather 表的所有行,请键入
SELECT *
FROM weather;
此处 * 是“所有列”的简写。因此,使用以下方式将获得相同的结果
SELECT city, temp_lo, temp_hi, prcp, date
FROM weather;
输出应为
| 城市 | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
| San Francisco | 43 | 57 | 0.0 | 1994-11-29 |
| Hayward | 37 | 54 | NULL | 1994-11-29 |
您可以在选择列表中写入表达式,而不仅仅是简单的列引用。例如,您可以这样做
SELECT city, (temp_hi + temp_lo) / 2 AS temp_avg, date
FROM weather;
这将给出
| 城市 | temp_avg | date |
|---|---|---|
| San Francisco | 48.0 | 1994-11-27 |
| San Francisco | 50.0 | 1994-11-29 |
| Hayward | 45.5 | 1994-11-29 |
请注意 AS 子句如何用于重新标记输出列。(AS 子句是可选的。)
通过添加 WHERE 子句来指定所需的行,可以“限定”查询。WHERE 子句包含一个布尔(真值)表达式,并且只返回布尔表达式为真的行。通常的布尔运算符(AND、OR 和 NOT)在限定条件中是允许的。例如,以下查询检索旧金山在下雨天的天气
SELECT *
FROM weather
WHERE city = 'San Francisco'
AND prcp > 0.0;
结果
| 城市 | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
您可以请求以排序顺序返回查询结果
SELECT *
FROM weather
ORDER BY city;
| 城市 | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| Hayward | 37 | 54 | NULL | 1994-11-29 |
| San Francisco | 43 | 57 | 0.0 | 1994-11-29 |
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
在此示例中,排序顺序未完全指定,因此您可能会以任意顺序获得旧金山的行。但如果您这样做,您将始终获得上面显示的结果
SELECT *
FROM weather
ORDER BY city, temp_lo;
您可以请求从查询结果中删除重复行
SELECT DISTINCT city
FROM weather;
| 城市 |
|---|
| San Francisco |
| Hayward |
这里再次,结果行的顺序可能会有所不同。您可以通过同时使用 DISTINCT 和 ORDER BY 来确保结果的一致性
SELECT DISTINCT city
FROM weather
ORDER BY city;
表之间的连接
到目前为止,我们的查询一次只访问一个表。查询可以一次访问多个表,或者以同时处理表中多行的方式访问同一个表。一次访问同一个或不同表的多个行的查询称为连接查询。例如,假设您希望列出所有天气记录及其关联城市的地理位置。为此,我们需要比较 weather 表中每行的城市列与 cities 表中所有行的名称列,并选择这些值匹配的行对。
这可以通过以下查询实现
SELECT *
FROM weather, cities
WHERE city = name;
| 城市 | temp_lo | temp_hi | prcp | date | name | lat | lon |
|---|---|---|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | -194.000 | 53.000 |
| San Francisco | 43 | 57 | 0.0 | 1994-11-29 | San Francisco | -194.000 | 53.000 |
观察结果集中的两件事
- 没有海沃德市的结果行。这是因为
cities表中没有与海沃德匹配的条目,因此连接会忽略weather表中不匹配的行。我们很快就会看到如何解决这个问题。 - 有两列包含城市名称。这是正确的,因为
weather和cities表的列列表是连接在一起的。然而,在实践中这是不理想的,因此您可能希望显式列出输出列而不是使用*
SELECT city, temp_lo, temp_hi, prcp, date, lon, lat
FROM weather, cities
WHERE city = name;
| 城市 | temp_lo | temp_hi | prcp | date | lon | lat |
|---|---|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 | 53.000 | -194.000 |
| San Francisco | 43 | 57 | 0.0 | 1994-11-29 | 53.000 | -194.000 |
由于所有列的名称都不同,解析器自动找到了它们所属的表。如果两个表中存在重复的列名,您需要限定列名以表明您指的是哪一个,例如
SELECT weather.city, weather.temp_lo, weather.temp_hi,
weather.prcp, weather.date, cities.lon, cities.lat
FROM weather, cities
WHERE cities.name = weather.city;
在连接查询中限定所有列名被广泛认为是良好的风格,这样即使后来向其中一个表添加了重复的列名,查询也不会失败。
到目前为止看到的连接查询也可以用这种替代形式编写
SELECT *
FROM weather
INNER JOIN cities ON weather.city = cities.name;
这种语法不像上面那种那样常用,但我们在此展示它以帮助您理解以下主题。
现在我们将弄清楚如何找回海沃德的记录。我们希望查询做的是扫描 weather 表,并为每一行找到匹配的 cities 行。如果没有找到匹配的行,我们希望用一些“空值”替换 cities 表的列。这种查询称为外部连接(outer join)。(到目前为止我们看到的连接是内部连接。)命令如下所示
SELECT *
FROM weather
LEFT OUTER JOIN cities ON weather.city = cities.name;
| 城市 | temp_lo | temp_hi | prcp | date | name | lat | lon |
|---|---|---|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 | San Francisco | -194.000 | 53.000 |
| San Francisco | 43 | 57 | 0.0 | 1994-11-29 | San Francisco | -194.000 | 53.000 |
| Hayward | 37 | 54 | NULL | 1994-11-29 | NULL | NULL | NULL |
此查询称为左外部连接(left outer join),因为连接运算符左侧提到的表的每一行都将在输出中至少出现一次,而右侧的表将只输出与左侧表的某些行匹配的行。当输出没有右侧表匹配的左侧表行时,将用空(null)值替换右侧表的列。
聚合函数
像大多数其他关系型数据库产品一样,DuckDB 支持聚合函数。聚合函数从多行输入中计算单个结果。例如,有聚合函数可以计算一组行的 count(计数)、sum(求和)、avg(平均值)、max(最大值)和 min(最小值)。
例如,我们可以用以下方式找到任何地方的最高低温读数
SELECT max(temp_lo)
FROM weather;
| max(temp_lo) |
|---|
| 46 |
如果我们想知道该读数发生在哪个城市(或哪些城市),我们可能会尝试
SELECT city
FROM weather
WHERE temp_lo = max(temp_lo);
但这不起作用,因为聚合函数 max 不能用于 WHERE 子句中
Binder Error:
WHERE clause cannot contain aggregates!
存在此限制的原因是 WHERE 子句决定了哪些行将包含在聚合计算中;因此显然它必须在聚合函数计算之前进行评估。然而,通常情况下,查询可以重新表述以实现所需的结果,这里通过使用子查询实现
SELECT city
FROM weather
WHERE temp_lo = (SELECT max(temp_lo) FROM weather);
| 城市 |
|---|
| San Francisco |
这是可以的,因为子查询是一个独立的计算,它独立于外部查询中发生的事情计算自己的聚合。
聚合函数与 GROUP BY 子句结合使用也非常有用。例如,我们可以用以下方式获取每个城市观察到的最高低温
SELECT city, max(temp_lo)
FROM weather
GROUP BY city;
| 城市 | max(temp_lo) |
|---|---|
| San Francisco | 46 |
| Hayward | 37 |
这为我们提供了每个城市的一行输出。每个聚合结果都在与该城市匹配的表行上计算。我们可以使用 HAVING 过滤这些分组行
SELECT city, max(temp_lo)
FROM weather
GROUP BY city
HAVING max(temp_lo) < 40;
| 城市 | max(temp_lo) |
|---|---|
| Hayward | 37 |
这只为所有 temp_lo 值低于 40 的城市提供相同的结果。最后,如果我们只关心名称以 S 开头的城市,我们可以使用 LIKE 运算符
SELECT city, max(temp_lo)
FROM weather
WHERE city LIKE 'S%' -- (1)
GROUP BY city
HAVING max(temp_lo) < 40;
有关 LIKE 运算符的更多信息,请参阅模式匹配页面。
理解聚合函数与 SQL 的 WHERE 和 HAVING 子句之间的交互作用至关重要。WHERE 和 HAVING 的根本区别在于:WHERE 在计算分组和聚合之前选择输入行(因此,它控制哪些行进入聚合计算),而 HAVING 在计算分组和聚合之后选择分组行。因此,WHERE 子句不得包含聚合函数;尝试使用聚合函数来确定哪些行将作为聚合函数的输入是没有意义的。另一方面,HAVING 子句总是包含聚合函数。
在前面的示例中,我们可以在 WHERE 中应用城市名称限制,因为它不需要聚合。这比将限制添加到 HAVING 更有效率,因为我们避免了对所有未能通过 WHERE 检查的行进行分组和聚合计算。
更新
您可以使用 UPDATE 命令更新现有行。假设您发现 11 月 28 日之后的所有温度读数都偏离了 2 度。您可以按如下方式更正数据
UPDATE weather
SET temp_hi = temp_hi - 2, temp_lo = temp_lo - 2
WHERE date > '1994-11-28';
查看数据的新状态
SELECT *
FROM weather;
| 城市 | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
| San Francisco | 41 | 55 | 0.0 | 1994-11-29 |
| Hayward | 35 | 52 | NULL | 1994-11-29 |
删除
可以使用 DELETE 命令从表中删除行。假设您不再关心海沃德的天气。那么您可以执行以下操作从表中删除这些行
DELETE FROM weather
WHERE city = 'Hayward';
所有属于海沃德的天气记录都被删除。
SELECT *
FROM weather;
| 城市 | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
| San Francisco | 41 | 55 | 0.0 | 1994-11-29 |
在发布以下形式的语句时应谨慎
DELETE FROM table_name;
警告 如果没有限定条件,
DELETE将删除给定表中的所有行,使其变为空。系统在执行此操作之前不会请求确认。