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 表示该表有一个名为 city 的 VARCHAR 列。VARCHAR 可存储任意长度文本。温度字段使用 INTEGER(整数,不含小数点)。FLOAT 存储单精度浮点数(含小数点)。DATE 存储日期(年-月-日),不包含当天具体时间。
Goose 支持标准 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';
其中源文件必须在执行进程所在机器可访问。Goose 还有多种数据加载方式,详见相关文档。
查询表
要从表中取数,需要执行查询,通常使用 SQL SELECT。该语句由选择列(select list)、来源表(table list)和可选过滤条件(qualification)组成。例如,要取出 weather 表所有行:
SELECT *
FROM weather;
这里 * 是“所有列”的简写。所以下面语句结果相同:
SELECT city, temp_lo, temp_hi, prcp, date
FROM weather;
输出应为:
| city | 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 list 中可以写表达式,而不仅是列名。例如:
SELECT city, (temp_hi + temp_lo) / 2 AS temp_avg, date
FROM weather;
结果应为:
| city | 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 包含布尔表达式,仅返回表达式为 true 的行。常见布尔运算符(AND、OR、NOT)都可使用。例如,下面查询旧金山下雨天的天气:
SELECT *
FROM weather
WHERE city = 'San Francisco'
AND prcp > 0.0;
结果:
| city | temp_lo | temp_hi | prcp | date |
|---|---|---|---|---|
| San Francisco | 46 | 50 | 0.25 | 1994-11-27 |
你还可以要求结果按顺序返回:
SELECT *
FROM weather
ORDER BY city;
| 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 |
在这个例子中,排序键不完整,因此两行 San Francisco 的先后顺序可能变化。若改成下面写法,则结果顺序固定:
SELECT *
FROM weather
ORDER BY city, temp_lo;
你也可以去除重复行:
SELECT DISTINCT city
FROM weather;
| city |
|---|
| San Francisco |
| Hayward |
同样,结果顺序仍可能变化。可将 DISTINCT 与 ORDER BY 一起使用来保证稳定顺序:
SELECT DISTINCT city
FROM weather
ORDER BY city;
表之间的 Join
到目前为止,我们每次只查询一张表。查询也可以同时访问多张表,或以同一时刻处理同表多行的方式运行,这类查询称为 join 查询。比如你想同时列出天气记录与对应城市坐标,就需要比较 weather.city 与 cities.name,并选择匹配的行对。
可通过以下查询实现:
SELECT *
FROM weather, cities
WHERE city = name;
| city | 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 匹配项,join 会忽略weather中未匹配行。稍后会说明如何修复。 - 结果中出现两列城市名。这是正常的,因为
weather与cities的列会拼接输出。实际中这通常不理想,因此建议显式列出输出列,而非*:
SELECT city, temp_lo, temp_hi, prcp, date, lon, lat
FROM weather, cities
WHERE city = name;
| city | 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;
在 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;
| city | 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,是因为 join 运算符左侧表的每一行都至少会输出一次;右侧表仅输出与左侧匹配的行。若左侧某行在右侧无匹配,则右侧列以 null 填充。
聚合函数
与多数关系型数据库一样,Goose 支持聚合函数。聚合函数会把多行输入计算成单个结果。例如可计算 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);
| city |
|---|
| San Francisco |
这可行,因为子查询是独立计算,其聚合与外层查询分开执行。
聚合函数与 GROUP BY 结合也非常常见。例如,按城市计算最低温最大值:
SELECT city, max(temp_lo)
FROM weather
GROUP BY city;
| 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;
| city | 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 更高效,因为可提前排除无关行,避免无意义的分组与聚合计算。
更新
你可以使用 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;
| city | 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;
| city | 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会删除目标表全部行,使其变为空表。系统不会进行二次确认。