本页面概述了如何使用 SQL 执行简单操作。本教程仅旨在为您提供入门指导,绝非关于 SQL 的完整教程。本教程改编自 PostgreSQL 教程。
DuckDB 的 SQL 方言非常遵循 PostgreSQL 方言的约定。少数例外情况列在 PostgreSQL 兼容性页面上。
在接下来的示例中,我们假设您已经安装了 DuckDB 命令行界面 (CLI) shell。有关如何安装 CLI 的信息,请参阅安装页面。
概念
DuckDB 是一个关系型数据库管理系统 (RDBMS)。这意味着它是一个用于管理存储在“关系”中的数据的系统。“关系”本质上是表的数学术语。
每个表都是一个命名的行集合。给定表的每一行都具有相同的一组命名列,且每一列都有特定的数据类型。表本身存储在模式(Schema)中,模式的集合构成了您可以访问的整个数据库。
创建新表
您可以通过指定表名以及所有列名及其类型来创建新表
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');
非数值类型的常量(例如文本和日期)必须用单引号 ('') 括起来,如示例中所示。DATE 类型的输入日期必须格式化为 '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;
表之间的连接 (Joins)
到目前为止,我们的查询一次只访问一个表。查询可以一次访问多个表,或者以同时处理表的多行的方式访问同一个表。一次访问同一或不同表的多个行的查询称为连接查询。例如,假设您希望列出所有天气记录以及相关城市的位置。为此,我们需要比较 weather 表中每一行的 city 列与 cities 表中所有行的 name 列,并选择这些值匹配的行对。
这可以通过以下查询来完成
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 |
关于结果集,请观察两点
- 对于 Hayward 市,没有结果行。这是因为在
cities表中没有 Hayward 的匹配条目,所以连接会忽略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;
这种语法不如上面的常用,但我们在这里展示它是为了帮助您理解后续主题。
现在我们将弄清楚如何把 Hayward 的记录找回来。我们希望查询扫描 weather 表,并为每一行找到匹配的 cities 行。如果没有找到匹配的行,我们希望用“空值”替换 cities 表的列。这种查询称为外连接。(到目前为止我们看到的连接是内连接。)命令如下所示
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 |
此查询称为左外连接,因为连接运算符左侧提到的表中的每一行至少会出现在输出中一次,而右侧的表只会输出那些与左表某一行匹配的行。在输出没有右表匹配的左表行时,右表列将被替换为空(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 命令从表中删除行。假设您不再对 Hayward 的天气感兴趣。那么您可以执行以下操作来从表中删除这些行
DELETE FROM weather
WHERE city = 'Hayward';
所有属于 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将从给定表中删除所有行,使其变为空表。系统在执行此操作之前不会请求确认。