SQL 数据分析 · 指南 · 2026年10月30日 · 11 分钟阅读
SQL 基础查询语句:SELECT、WHERE、ORDER BY、GROUP BY 和 JOIN 入门

一条基础的 SQL 查询总是同一个结构:SELECT 说明要看哪些列,FROM 说明数据来自哪张表,WHERE 筛选行,GROUP BY 把行分组以计算合计,ORDER BY 给结果排序,JOIN 通过共同的列把两张表连起来。靠这六个部件,就能完成数据分析师日常工作的大部分。本文逐个讲解,所有查询都能真正运行:它们都在 Nomto SQL 课程使用的航空公司数据库上执行,下面看到的每个结果都是真实输出。
示例用的数据库
我们会用到航空公司的五张表(数据值是英文,金额单位为美元,时间是莫斯科时间 UTC+3):
| 表 | 一行代表 | 会用到的列 |
|---|---|---|
airports |
一个机场(共 104 个) | airport_code、airport_name、city、timezone |
aircrafts |
一种机型(共 9 种) | aircraft_code、model、range(航程,单位公里) |
flights |
某个航班在某一天的一次起飞(共 33,121 个) | flight_no、scheduled_departure、departure_airport、arrival_airport、status、aircraft_code、actual_departure |
bookings |
一笔订单 | book_ref、book_date、total_amount |
ticket_flights |
一张机票上的一个航段 | flight_id、fare_conditions(舱位)、amount(价格) |
写任何查询之前,先弄清楚每张表的一行代表什么。新手在 SQL 上犯的错,大多来自数的行并不是自己以为的那种东西。
SELECT 和 FROM:选择列
最简单的查询就是从一张表里取几列:
SELECT model, range
FROM aircrafts;
有几条规则最好一开始就记住:
SELECT *返回所有列。用来快速看一眼没问题,但在正式查询里要写出你需要的列名:结果更清楚,别人往表里加列时也不会出问题。SELECT里可以做计算,用AS给计算出来的列起名字。- 结尾的分号用来分隔多条查询。Nomto 的 SQL 练习场一次只运行一条查询,所以可以省略。
航程的单位是公里。如果还想换算成英里:
SELECT model,
range,
ROUND(range / 1.609) AS range_miles
FROM aircrafts
ORDER BY range DESC
LIMIT 5;
| model | range | range_miles |
|---|---|---|
| Boeing 777-300 | 11100 | 6899 |
| Boeing 767-300 | 7900 | 4910 |
| Airbus A319-100 | 6700 | 4164 |
| Airbus A320-200 | 5700 | 3543 |
| Airbus A321-200 | 5600 | 3480 |
不加 ROUND 的话,这一列会带着一长串小数(6898.69484151647)。ORDER BY 和 LIMIT 下面再讲。
WHERE:筛选行
WHERE 只放行满足条件的行。莫斯科的机场:
SELECT airport_code, airport_name, city
FROM airports
WHERE city = 'Moscow'
ORDER BY airport_code;
| airport_code | airport_name | city |
|---|---|---|
| DME | Domodedovo | Moscow |
| SVO | Sheremetyevo | Moscow |
| VKO | Vnukovo | Moscow |
注意两个细节:文本要用单引号括起来(在 PostgreSQL 里双引号是给列名用的),而且比较区分大小写:'moscow' 什么也查不到。
组合条件:AND、OR、IN、BETWEEN
8 月 1 日从谢列梅捷沃(SVO)起飞的航班:
SELECT flight_no, scheduled_departure, arrival_airport, status
FROM flights
WHERE departure_airport = 'SVO'
AND scheduled_departure >= '2026-08-01'
AND scheduled_departure < '2026-08-02'
ORDER BY scheduled_departure, flight_no
LIMIT 5;
| flight_no | scheduled_departure | arrival_airport | status |
|---|---|---|---|
| PG0120 | 2026-08-01 09:00:00 | MJZ | Arrived |
| PG0591 | 2026-08-01 09:00:00 | TOF | Arrived |
| PG0241 | 2026-08-01 09:40:00 | CSY | Arrived |
| PG0677 | 2026-08-01 09:40:00 | MCX | Arrived |
| PG0041 | 2026-08-01 09:50:00 | NFG | Arrived |
那一天 SVO 一共起飞了 50 个航班。日期筛选用的是「大于或等于当天零点,并且小于次日零点」的写法。这比 BETWEEN '2026-08-01' AND '2026-08-01' 更可靠,因为对带时间的列来说,后者只能匹配到零点那一刻。
IN 可以代替对同一列的多个 OR。已经起飞的航班(在空中或已降落):
SELECT COUNT(*)
FROM flights
WHERE status IN ('Departed', 'Arrived');
结果是 16,765。同时使用 AND 和 OR 时要加括号:AND 先计算,a OR b AND c 的意思是 a OR (b AND c)。
LIKE:按模式查找
% 代表任意数量的字符,_ 代表恰好一个字符。名字以「Novo」开头的城市:
SELECT airport_code, airport_name, city
FROM airports
WHERE city LIKE 'Novo%'
ORDER BY city;
| airport_code | airport_name | city |
|---|---|---|
| NOZ | Spichenkovo | Novokuznetsk |
| OVB | Tolmachevo | Novosibirsk |
在 PostgreSQL 里,ILIKE 的作用相同,只是不区分大小写。
IS NULL:空值
NULL 表示「没有值」。还没起飞的航班,actual_departure 是空的。这里藏着 SQL 最有名的陷阱:
SELECT COUNT(*) FROM flights WHERE actual_departure = NULL; -- 0
SELECT COUNT(*) FROM flights WHERE actual_departure IS NULL; -- 16348
第一条返回 0,尽管有上万个航班还没起飞:任何与 NULL 的比较结果都是「未知」,而 WHERE 只放行结果为真的行。判断空值时,永远用 IS NULL 或 IS NOT NULL。
ORDER BY 和 LIMIT:排序并取前几行
没有 ORDER BY 时,数据库按它觉得方便的顺序返回行,这个顺序每次运行都可能不一样。最贵的五笔订单:
SELECT book_ref, book_date, total_amount
FROM bookings
ORDER BY total_amount DESC
LIMIT 5;
| book_ref | book_date | total_amount |
|---|---|---|
| 3B54BB | 2026-07-01 17:08:00 | 12045.00 |
| 3AC131 | 2026-07-27 01:06:00 | 10871.00 |
| 65A6EA | 2026-06-29 06:28:00 | 10656.00 |
| D7E9AA | 2026-08-04 05:29:00 | 10628.00 |
| EF479E | 2026-07-29 15:58:00 | 10351.00 |
ASC(默认)从小到大排序,DESC从大到小。- 可以按多列排序:
ORDER BY scheduled_departure, flight_no在两个航班同一时间起飞时,再按航班号区分先后,就像上面 SVO 的例子。 - 没有
ORDER BY的LIMIT给你的是「随便几行」,而不是「前几行」。如果在意返回哪几行,就要排序。
聚合函数:COUNT、SUM、AVG、MIN、MAX
聚合函数把很多行变成一个数。有一个区别最好早点弄明白:
SELECT COUNT(*) AS total,
COUNT(actual_departure) AS departed,
COUNT(*) - COUNT(actual_departure) AS not_yet
FROM flights;
| total | departed | not_yet |
|---|---|---|
| 33121 | 16773 | 16348 |
COUNT(*) 数的是行数;COUNT(列名) 只数非空的值。COUNT(DISTINCT 列名) 数的是不同值的个数:SELECT COUNT(DISTINCT timezone) FROM airports 返回 17 个时区。
GROUP BY:每组一个合计
GROUP BY 把表切分成若干组,并在每组内计算聚合值。每种状态各有多少航班:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
| status | flights |
|---|---|
| Arrived | 16707 |
| Scheduled | 15383 |
| On Time | 518 |
| Cancelled | 414 |
| Departed | 58 |
| Delayed | 41 |
按舱位等级同时计算多个聚合值:
SELECT fare_conditions,
COUNT(*) AS segments,
ROUND(AVG(amount), 2) AS avg_price,
MIN(amount) AS min_price,
MAX(amount) AS max_price
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY avg_price DESC;
| fare_conditions | segments | avg_price | min_price | max_price |
|---|---|---|---|---|
| Business | 107642 | 511.43 | 91.00 | 2033.00 |
| Comfort | 17291 | 327.41 | 199.00 | 476.00 |
| Economy | 920793 | 159.6 | 30.00 | 745.00 |
黄金法则:SELECT 里的每一列,要么出现在 GROUP BY 里,要么放在聚合函数里面。如果写 SELECT status, flight_no, COUNT(*) ... GROUP BY status,PostgreSQL 会报错,因为一个分组里有几百个航班号,它不知道该显示哪一个。
HAVING:筛选分组
WHERE 在分组之前筛选行,HAVING 在分组之后筛选组。拥有不止一个机场的城市:
SELECT city, COUNT(*) AS airports
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY airports DESC;
| city | airports |
|---|---|
| Moscow | 3 |
| Ulyanovsk | 2 |
不能写 WHERE COUNT(*) > 1:计算 WHERE 的时候,分组还不存在。两者的区别在 GROUP BY 和 HAVING 的区别 里有更详细的讲解。
JOIN:连接表
在 flights 里,飞机只以代码出现(321、SU9),机型名称在 aircrafts 里。JOIN 通过共同的列把两张表连起来:
SELECT a.model, COUNT(*) AS flights
FROM flights f
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
GROUP BY a.model
ORDER BY flights DESC;
| model | flights |
|---|---|
| Cessna 208 Caravan | 9273 |
| Bombardier CRJ-200 | 9048 |
| Sukhoi Superjet-100 | 8504 |
| Airbus A321-200 | 1952 |
| Boeing 737-300 | 1274 |
| Airbus A319-100 | 1239 |
| Boeing 767-300 | 1221 |
| Boeing 777-300 | 610 |
f 和 a 是别名:表的简短名字,省得反复写全名。ON 说明哪些行应该配在一起。
INNER JOIN 和 LEFT JOIN
上面的表里有 8 种机型,可 aircrafts 里有 9 种。JOIN(也就是 INNER JOIN)只保留在两张表里都能配对的行,所以没有航班的机型消失了。LEFT JOIN 保留左表的所有行,找不到配对的地方用 NULL 填充。用它可以找出「没有配对的行」:
SELECT a.aircraft_code, a.model
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.flight_id IS NULL;
| aircraft_code | model |
|---|---|
| 320 | Airbus A320-200 |
用 LEFT JOIN 计数时,要写 COUNT(f.flight_id) 而不是 COUNT(*):这样 A320 显示为 0,而不是 1。
再提醒一句,等你进阶时会用到:如果把一张表和另一张「每行对应多行」的表连接(比如一笔订单对应多张机票),行数会成倍增加,求和结果也会虚高。SQL JOIN 详解 和 SQL 面试题 里用具体数字讲了这个问题。
组合起来:一条完整的查询
这些部件按固定顺序组合:SELECT … FROM … JOIN … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT。已降落航班至少 300 个的城市,以及它们的平均起飞延误(分钟):
SELECT ap.city,
COUNT(*) AS flights,
ROUND(AVG(EXTRACT(EPOCH FROM f.actual_departure - f.scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights f
JOIN airports ap ON ap.airport_code = f.departure_airport
WHERE f.status = 'Arrived'
GROUP BY ap.city
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC;
| city | flights | avg_delay_min |
|---|---|---|
| Yekaterinburg | 348 | 14.7 |
| Bryansk | 306 | 13.2 |
| Krasnoyarsk | 358 | 13.1 |
| St. Petersburg | 956 | 12.6 |
| Moscow | 3990 | 12.4 |
| Novosibirsk | 536 | 11.8 |
| Perm | 311 | 10.5 |
| Rostov-on-Don | 310 | 9.2 |
两个时间相减得到一个时间间隔;EXTRACT(EPOCH FROM …) 把它换算成秒,再除以 60。
查询的执行顺序
我们先写 SELECT,但数据库几乎是最后才处理它:
FROM和JOIN:拼出工作用的表。WHERE:去掉不需要的行。GROUP BY:形成分组。HAVING:去掉不需要的组。SELECT:计算各列和别名。ORDER BY:排序(这里已经可以用 SELECT 里的别名了)。LIMIT:截取。
这个顺序解释了为什么 SELECT 里的别名能在 ORDER BY 里用、在 WHERE 里却不行,也解释了为什么聚合条件要写在 HAVING 里。
新手常犯的错误
- 用
= NULL代替IS NULL。 不报错,但返回零行。 - 文本用双引号。 在 PostgreSQL 里,
WHERE city = "Moscow"会去找一个叫 Moscow 的列。 - SELECT 里有未分组的列。 不在
GROUP BY里的列,都必须放在聚合函数里面。 LIMIT不配ORDER BY。 结果不是「前几名」,而是随机的。LEFT JOIN之后用COUNT(*)。 没有配对的行会被算成 1。- 对带时间的列用
BETWEEN筛日期。 改用>=当天开始、<次日开始。
怎么练习
读查询有帮助,但真正学会靠的是自己写、自己犯错。本文所有例子都可以在 Nomto 的 SQL 练习场里用同一个数据库运行和修改:换一个城市、状态或排序方式,看看结果怎么变。课程第 1 单元正好从 SELECT 和别名讲起,每一课最后都有三道自动检查的题。掌握了基础之后,可以接着做 SQL 练习题,再学 SQL 窗口函数。
常见问题
学会 SQL 基础查询要多久?
SELECT、WHERE、ORDER BY、GROUP BY 和 JOIN,每天练一小时,两三周就能掌握。真正花时间的是读懂数据:知道一行代表什么,以及 JOIN 什么时候会让行数成倍增加。
WHERE 和 HAVING 有什么区别?
WHERE 在分组之前筛选行,不能使用聚合函数。HAVING 筛选已经形成的分组,配合 COUNT、SUM、AVG 等使用。
本文的 SQL 能用在 MySQL 或 SQL Server 上吗?
基本都可以。SELECT、WHERE、GROUP BY、HAVING、JOIN 和 ORDER BY 在所有数据库里都一样。不同的是细节:SQL Server 用 TOP 而不是 LIMIT,MySQL 和 SQL Server 都没有 ILIKE,日期函数的名字也各不相同。
练习需要安装数据库吗?
不需要。在 Nomto 的 SQL 练习场里,你直接在浏览器里写查询,在航空公司数据库上运行,什么都不用装。
其他语言: English, Português (Brasil), Tiếng Việt, Español