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

聚合函数: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,但数据库几乎是最后才处理它:

  1. FROM 和 JOIN:拼出工作用的表。
  2. WHERE:去掉不需要的行。
  3. GROUP BY:形成分组。
  4. HAVING:去掉不需要的组。
  5. SELECT:计算各列和别名。
  6. ORDER BY:排序(这里已经可以用 SELECT 里的别名了)。
  7. LIMIT:截取。

这个顺序解释了为什么 SELECT 里的别名能在 ORDER BY 里用、在 WHERE 里却不行,也解释了为什么聚合条件要写在 HAVING 里。

新手常犯的错误

怎么练习

读查询有帮助,但真正学会靠的是自己写、自己犯错。本文所有例子都可以在 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