跳到主要内容

SQL 入门

本页概述如何在 SQL 中执行基础操作。 本教程仅用于入门,不是完整 SQL 教程。 内容改编自 PostgreSQL tutorial

Goose 的 SQL 方言总体遵循 PostgreSQL 约定。 少量差异见 PostgreSQL compatibility 页面

以下示例默认你已安装 Goose Command Line Interface(CLI)shell。安装方式见 installation page

基本概念

Goose 是关系型数据库管理系统(RDBMS)。它用于管理存储在“关系”中的数据,而 relation 本质上就是表的数学术语。

每张表是具名的行集合。同一表中的每一行都有相同的一组列名,且每列都有特定数据类型。表存储在 schema 中,多个 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 命令中可自由使用空白字符(空格、tab、换行)。因此你可以使用不同排版,甚至写成单行。两个短横线(--)用于注释,从其后到行尾都会被忽略。SQL 对关键字和标识符大小写不敏感。返回标识符时会保留原始大小写

在 SQL 命令中,先写命令类型 CREATE TABLE,然后是参数:先给表名 weather,再给列名和列类型。

city VARCHAR 表示该表有一个名为 cityVARCHAR 列。VARCHAR 可存储任意长度文本。温度字段使用 INTEGER(整数,不含小数点)。FLOAT 存储单精度浮点数(含小数点)。DATE 存储日期(年-月-日),不包含当天具体时间。

Goose 支持标准 SQL 类型:INTEGERSMALLINTFLOATDOUBLEDECIMALCHAR(n)VARCHAR(n)DATETIMETIMESTAMP

第二个例子用于存储城市及其地理位置:

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';

其中源文件必须在执行进程所在机器可访问。Goose 还有多种数据加载方式,详见相关文档

查询表

要从表中取数,需要执行查询,通常使用 SQL SELECT。该语句由选择列(select list)、来源表(table list)和可选过滤条件(qualification)组成。例如,要取出 weather 表所有行:

SELECT *
FROM weather;

这里 * 是“所有列”的简写。所以下面语句结果相同:

SELECT city, temp_lo, temp_hi, prcp, date
FROM weather;

输出应为:

citytemp_lotemp_hiprcpdate
San Francisco46500.251994-11-27
San Francisco43570.01994-11-29
Hayward3754NULL1994-11-29

在 select list 中可以写表达式,而不仅是列名。例如:

SELECT city, (temp_hi + temp_lo) / 2 AS temp_avg, date
FROM weather;

结果应为:

citytemp_avgdate
San Francisco48.01994-11-27
San Francisco50.01994-11-29
Hayward45.51994-11-29

注意 AS 子句用于重命名输出列。(AS 可省略。)

可通过 WHERE 子句为查询添加筛选条件。WHERE 包含布尔表达式,仅返回表达式为 true 的行。常见布尔运算符(ANDORNOT)都可使用。例如,下面查询旧金山下雨天的天气:

SELECT *
FROM weather
WHERE city = 'San Francisco'
AND prcp > 0.0;

结果:

citytemp_lotemp_hiprcpdate
San Francisco46500.251994-11-27

你还可以要求结果按顺序返回:

SELECT *
FROM weather
ORDER BY city;
citytemp_lotemp_hiprcpdate
Hayward3754NULL1994-11-29
San Francisco43570.01994-11-29
San Francisco46500.251994-11-27

在这个例子中,排序键不完整,因此两行 San Francisco 的先后顺序可能变化。若改成下面写法,则结果顺序固定:

SELECT *
FROM weather
ORDER BY city, temp_lo;

你也可以去除重复行:

SELECT DISTINCT city
FROM weather;
city
San Francisco
Hayward

同样,结果顺序仍可能变化。可将 DISTINCTORDER BY 一起使用来保证稳定顺序:

SELECT DISTINCT city
FROM weather
ORDER BY city;

表之间的 Join

到目前为止,我们每次只查询一张表。查询也可以同时访问多张表,或以同一时刻处理同表多行的方式运行,这类查询称为 join 查询。比如你想同时列出天气记录与对应城市坐标,就需要比较 weather.citycities.name,并选择匹配的行对。

可通过以下查询实现:

SELECT *
FROM weather, cities
WHERE city = name;
citytemp_lotemp_hiprcpdatenamelatlon
San Francisco46500.251994-11-27San Francisco-194.00053.000
San Francisco43570.01994-11-29San Francisco-194.00053.000

结果中有两点值得注意:

  • Hayward 没有结果行。因为 cities 表中没有 Hayward 匹配项,join 会忽略 weather 中未匹配行。稍后会说明如何修复。
  • 结果中出现两列城市名。这是正常的,因为 weathercities 的列会拼接输出。实际中这通常不理想,因此建议显式列出输出列,而非 *
SELECT city, temp_lo, temp_hi, prcp, date, lon, lat
FROM weather, cities
WHERE city = name;
citytemp_lotemp_hiprcpdatelonlat
San Francisco46500.251994-11-2753.000-194.000
San Francisco43570.01994-11-2953.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;

在 join 查询中为所有列名加表前缀通常被视为良好实践,这样即使将来某表新增同名列,查询也不易出错。

上述 join 查询也可写成下面的形式:

SELECT *
FROM weather
INNER JOIN cities ON weather.city = cities.name;

这种写法不如前者常见,这里展示是为了帮助理解后续内容。

接下来看看如何让 Hayward 回到结果里。我们希望查询扫描 weather,并为每行查找 cities 中匹配项;若无匹配,则以“空值”填充 cities 的列。这类查询称为 outer join。(前面看到的都是 inner join。)语句如下:

SELECT *
FROM weather
LEFT OUTER JOIN cities ON weather.city = cities.name;
citytemp_lotemp_hiprcpdatenamelatlon
San Francisco46500.251994-11-27San Francisco-194.00053.000
San Francisco43570.01994-11-29San Francisco-194.00053.000
Hayward3754NULL1994-11-29NULLNULLNULL

之所以称为 left outer join,是因为 join 运算符左侧表的每一行都至少会输出一次;右侧表仅输出与左侧匹配的行。若左侧某行在右侧无匹配,则右侧列以 null 填充。

聚合函数

与多数关系型数据库一样,Goose 支持聚合函数。聚合函数会把多行输入计算成单个结果。例如可计算 countsumavg(平均)、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);
city
San Francisco

这可行,因为子查询是独立计算,其聚合与外层查询分开执行。

聚合函数与 GROUP BY 结合也非常常见。例如,按城市计算最低温最大值:

SELECT city, max(temp_lo)
FROM weather
GROUP BY city;
citymax(temp_lo)
San Francisco46
Hayward37

这会为每个城市返回一行。每个聚合结果都基于该城市对应行计算。可通过 HAVING 过滤分组结果:

SELECT city, max(temp_lo)
FROM weather
GROUP BY city
HAVING max(temp_lo) < 40;
citymax(temp_lo)
Hayward37

这样仅保留 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 中 WHEREHAVING 的关系很重要。核心区别是:WHERE 在分组和聚合之前筛选输入行(决定哪些行参与聚合);HAVING 在分组和聚合之后筛选分组结果。因此 WHERE 不应包含聚合函数;而 HAVING 通常用于聚合条件。

在前例中,城市名过滤可放在 WHERE,因为它不依赖聚合。这比放到 HAVING 更高效,因为可提前排除无关行,避免无意义的分组与聚合计算。

更新

你可以使用 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;
citytemp_lotemp_hiprcpdate
San Francisco46500.251994-11-27
San Francisco41550.01994-11-29
Hayward3552NULL1994-11-29

删除

可使用 DELETE 从表中删除行。假设你不再关心 Hayward 的天气数据,可执行:

DELETE FROM weather
WHERE city = 'Hayward';

Hayward 相关天气记录会被删除。

SELECT *
FROM weather;
citytemp_lotemp_hiprcpdate
San Francisco46500.251994-11-27
San Francisco41550.01994-11-29

执行以下形式语句时要特别谨慎:

DELETE FROM ⟨table_name⟩;

警告:若无过滤条件,DELETE 会删除目标表全部行,使其变为空表。系统不会进行二次确认。