DD 数据开发学习站DATA ENGINEERING
模块 02 · SQL 与数据库

SQL 基础

SELECT、WHERE、ORDER BY、DISTINCT、NULL,用九张 demo 小表精确取出想要的行。

难度 · 入门 约 60 分钟 必修 练习 9 项 计划内 刷题 0/5
建议先学:Python 工程化
本页目录 · 24 节

本章目标:零基础也能看懂表结构,用 SELECT、WHERE、ORDER BY、LIMIT 精确取数,并理解 NULL 和空值的区别。 前置知识:会打开命令行、运行 Python 脚本;不需要学过数据库。 预计用时:60 分钟。


1. 为什么需要它

数据开发每天面对的不是 Excel,而是几十万行的表。你要回答的问题通常很具体:上个月有多少订单被取消?哪些评价 ID 重复了?最近一周每天的销售额是多少?

这些问题的共同点是:数据太多,不可能靠眼睛看。你需要一种语言,把"我要什么"描述清楚,让数据库去执行。这种语言就是 SQL(Structured Query Language,结构化查询语言)。

SQL 是声明式语言:你只描述结果,不描述步骤。比如"取已取消的订单,按下单时间倒序,只要前 10 条",你写四行 SQL,数据库自己决定先扫哪张表、用不用索引。

数据开发、数据分析、数仓、ETL、报表,几乎所有岗位的第一道面试题都是 SQL。它不是选修课,是工作语言。

本章和后面几章的 SQL 章节只讲语法和刷题,用的是一组几十行的玩具表 demo_*,不涉及电商项目数据。真实数据、完整数仓和业务指标,全部留到项目实战。


2. 核心概念

2.1 数据库、表、行、列

关系型数据库把数据放在表(table)里。一张表就是一张二维表:

customer_id customer_name city
1 张三 北京
2 李四 上海
  • 列(column):字段,比如 city。同一列的数据类型一致。
  • 行(row):一条记录,比如一个客户。
  • 主键(primary key):能唯一标识一行的字段,比如 customer_id。主键不能重复,也不能为空。

关系型数据库的意思是:多张表可以通过共同的字段关联,而不是把所有信息塞进一张大表。第 03 章会专门讲关联。

2.2 配套的九张玩具表

为了让示例结果可核对,本章准备了一组小表,每张只有几行到二十行:

表 行数 一行代表什么 关键字段
demo_customers 7 一个客户 customer_id、customer_name、city、referrer_id
demo_orders 12 一个订单 order_id、customer_id、status、created_at、delivered_at
demo_order_items 15 订单里的一行商品 order_id、item_id、product_id、quantity、unit_price
demo_products 6 一个商品 product_id、product_name、category
demo_payments 13 一笔支付流水 payment_id、order_id、method、amount
demo_reviews 10 一条订单评价 review_id、order_id、score
demo_departments 3 一个部门 dept_id、dept_name、city
demo_employees 7 一名员工 emp_id、dept_id、salary、hire_date、manager_id
demo_logins 20 一次登录 user_id、login_date

表关系可以先用一张图记住:

demo_customers 1--N demo_orders 1--N demo_order_items N--1 demo_products

                         |
                         +--N demo_payments
                         +--N demo_reviews

demo_departments 1--N demo_employees
demo_employees 1--N demo_employees(manager_id 自关联)

一个客户可以下多个订单,一个订单可以有多行明细,一行明细对应一个商品。注意 demo_payments 和 demo_reviews 对订单也是一对多:一个订单可能分多笔支付、有多条评价,直接 JOIN 明细会让金额和评分翻倍,第 03 章会讲怎么避免。

2.2.1 怎么把示例跑起来

玩具库由脚本生成,不需要下载任何数据:

python scripts/make_sql_demo_db.py
duckdb project/sql_demo.duckdb

没有安装 DuckDB 命令行也可以直接用 Python:

python - <<'PY'
import duckdb
con = duckdb.connect('project/sql_demo.duckdb', read_only=True)
print(con.sql("SELECT * FROM demo_orders LIMIT 5").df())
PY

把示例 SQL 粘进去回车即可。方言是 DuckDB,date_trunc、FILTER、窗口函数都能用;Hive/Spark/MySQL 专属写法会单独标注差异。

2.3 一条 SQL 的骨架

本章只用到下面几个关键字:

SELECT   要哪些列
FROM     从哪张表
WHERE    保留哪些行
ORDER BY 按什么排序
LIMIT    只要前几行

书写顺序是 SELECT → FROM → WHERE → ORDER BY → LIMIT,但执行顺序是 FROM → WHERE → SELECT → ORDER BY → LIMIT。先找到表,再筛行,再决定输出哪些列,最后排序和截断。所以 WHERE 里不能用 SELECT 刚起的别名,那时别名还不存在;ORDER BY 里可以,因为它执行得更晚。

2.4 数据类型与 NULL

类型 例子 说明
字符串 'paid'、'北京' 必须用单引号
整数 15、3 数量、序号
浮点数 58.90 金额、价格
日期 / 时间 DATE '2024-03-01'、TIMESTAMP '2024-03-01 10:30:00' 年月日与时分秒
布尔 TRUE、FALSE 条件的结果

NULL 不是 0,也不是空字符串。它表示未知、缺失、没有值。后文会单独讲它,因为它是 SQL 里最容易出错的地方。


3. 动手示例

3.1 先看表结构

python scripts/make_sql_demo_db.py

然后用 DuckDB 打开 project/sql_demo.duckdb。写查询前先 DESCRIBE:

DESCRIBE demo_orders;

字段名拼错是最常见的报错原因,先看一眼能省很多时间。

3.2 SELECT * 与选列

-- 所有列,只要前 5 行
SELECT * FROM demo_orders LIMIT 5;

-- 只要订单号、状态、下单时间
SELECT order_id, status, created_at FROM demo_orders LIMIT 5;

* 表示所有列。练习时用它快速看数据没问题,但工作中要只选需要的列,原因见本章面试延伸。

3.3 别名与表达式

SELECT order_id AS 订单号,
       quantity * unit_price AS 明细金额
FROM demo_order_items
LIMIT 5;

AS 起列别名,让结果更好读。quantity * unit_price 是表达式,只影响查询结果,不会修改原表;支持 +、-、*、/、%。别名可以在 ORDER BY 里直接使用,例如 ORDER BY 明细金额 DESC。

3.4 DISTINCT:去重

SELECT DISTINCT status FROM demo_orders ORDER BY status;

结果有 3 行:canceled、paid、refunded。

注意:DISTINCT 作用于整行。SELECT DISTINCT city, customer_name 是"城市和姓名的组合不重复",不是"城市不重复"。

3.5 WHERE:过滤行

WHERE 后面跟一个条件,只有结果为 TRUE 的行才会保留。

比较运算:

SELECT order_id, status
FROM demo_orders
WHERE status = 'canceled';

结果只有 1007。= 是等于,<> 或 != 是不等于。字符串要用单引号,数据值区分大小写:写 'Canceled' 一条也查不到。

区间 BETWEEN:

SELECT COUNT(*) AS 三月订单数
FROM demo_orders
WHERE created_at BETWEEN TIMESTAMP '2024-03-01 00:00:00'
                     AND TIMESTAMP '2024-03-31 23:59:59';

BETWEEN a AND b 等价于 >= a AND <= b,两头都包含。结果是 8。

组合条件 AND / OR / NOT:

-- 三月里,取消或退款的订单
SELECT COUNT(*)
FROM demo_orders
WHERE (status = 'canceled' OR status = 'refunded')
  AND created_at BETWEEN TIMESTAMP '2024-03-01 00:00:00'
                     AND TIMESTAMP '2024-03-31 23:59:59';

结果是 2(1005 退款、1007 取消)。AND 的优先级高于 OR,不加括号会变成"所有取消订单,加上三月的退款订单",含义完全不同。

多选一 IN:WHERE status IN ('canceled', 'refunded') 等价于两个 OR 的简写,全量结果是 2。

取反 NOT:WHERE NOT status = 'paid' 会把条件翻转;这种场景通常直接写 <> 更清楚,但遇到 NOT IN、NOT LIKE 时要留意 NULL。

模糊匹配 LIKE:

SELECT customer_id, customer_name, city
FROM demo_customers
WHERE city LIKE '%京%'
ORDER BY customer_id;

% 代表任意多个字符,_ 代表恰好一个字符。结果是 3 位北京客户(1、3、7)。

判空 IS NULL:

SELECT COUNT(*) AS 没有推荐人的客户
FROM demo_customers
WHERE referrer_id IS NULL;

结果 3(客户 1、5、7 不是被推荐来的)。空值不一定是脏数据:这里"没有推荐人"就是正常业务含义。

3.6 ORDER BY 与 LIMIT

SELECT order_id, created_at
FROM demo_orders
ORDER BY created_at DESC
LIMIT 5;
  • ASC 升序,是默认值,可以不写;DESC 是降序。
  • 多列排序:ORDER BY status ASC, created_at DESC,先按状态,再按时间。
  • LIMIT 5 取前 5 行。

注意:只想看"前 5 条"时,ORDER BY 和 LIMIT 要一起写。没有排序的 LIMIT 只保证"随便给你 5 行"。

3.7 NULL 的三值逻辑

SQL 的条件不只有真、假,还有第三种结果:未知(NULL)。TRUE AND NULL 是 NULL,FALSE AND NULL 是 FALSE,NOT NULL 是 NULL,NULL = NULL 也是 NULL。

WHERE 只保留结果为 TRUE 的行,结果为 NULL 的行会被丢弃。于是有了两个经典坑。

坑一:= NULL 永远查不到数据。

-- 错误写法:结果是 0 行
SELECT COUNT(*) FROM demo_customers WHERE referrer_id = NULL;

-- 正确写法:结果 3
SELECT COUNT(*) FROM demo_customers WHERE referrer_id IS NULL;

NULL 表示"不知道"。"不知道"等于"不知道"吗?SQL 的答案是"不知道",不是"相等"。

坑二:NOT IN 的列表里如果有 NULL,整个条件会失效。

-- 想找"推荐人不是 1 号"的客户,结果却是 0 行
SELECT COUNT(*) FROM demo_customers WHERE referrer_id NOT IN (1, NULL);

因为 referrer_id NOT IN (1, NULL) 会展开成 referrer_id <> 1 AND referrer_id <> NULL,后半句永远是 NULL。稳妥写法是用 NOT EXISTS,第 04 章会讲。

3.8 用 SQL 做数据探查

数据开发的第一项工作不是写报表,而是先看数据长什么样。下面几个查询就是常见的体检动作。

1. 一个订单有多条评价

SELECT order_id, COUNT(*) AS 评价条数
FROM demo_reviews
GROUP BY order_id
HAVING COUNT(*) > 1;

只有订单 1002 出现 2 条评价。这里提前用了 GROUP BY,下一章会系统讲。清洗时先把评价聚合到订单粒度,再 JOIN 订单,否则评分会被重复计算。

2. 已取消但仍有支付

SELECT o.order_id, p.amount
FROM demo_orders o
JOIN demo_payments p ON p.order_id = o.order_id
WHERE o.status = 'canceled';

订单 1007 已取消,却有一笔 89 元的支付。这类记录通常先告警,不直接删除。

3. 支付金额与明细金额对不上

SELECT *
FROM (
    SELECT o.order_id,
           (SELECT ROUND(SUM(quantity * unit_price), 2)
            FROM demo_order_items i WHERE i.order_id = o.order_id) AS item_amount,
           (SELECT ROUND(SUM(amount), 2)
            FROM demo_payments p WHERE p.order_id = o.order_id) AS pay_amount
    FROM demo_orders o
) t
WHERE item_amount IS NOT NULL
  AND ABS(item_amount - pay_amount) > 0.01;

订单 1002 明细 258.00、支付 257.90,差 0.10。

4. 员工没有上级

SELECT emp_id, emp_name
FROM demo_employees
WHERE manager_id IS NULL;

结果是 3 位部门负责人。NULL 在这里同样有明确业务含义。

提示:能区分"业务空值"和"脏数据",是数据开发的基本功。判断标准不是字段是否为空,而是状态和事实是否矛盾。


4. 常见坑

  1. 字符串用了双引号。"paid" 在 SQL 里是列名,会报找不到列。字符串一律单引号。
  2. 用 = NULL 判空。永远匹配不到,必须用 IS NULL / IS NOT NULL。
  3. OR 不加括号。AND 优先级更高,条件含义会悄悄改变。
  4. 以为 BETWEEN 不包含边界。它是闭区间,两头都包含。
  5. NOT IN 遇到 NULL。列表里有 NULL 时,结果可能为空;优先考虑 NOT EXISTS。
  6. LIMIT 不配 ORDER BY。返回顺序不稳定,换一次执行可能换一批数据。
  7. 字段名拼错。先 DESCRIBE 表名; 看真实字段,不要凭印象写。

5. 在线练习

SQL 题不在本地贴答案。按顺序在力扣完成,提交通过后再对照下面的自测点。

题目 考点
595. 大的国家 WHERE + OR
584. 寻找用户推荐人 NULL 与 !=
183. 从不订购的客户 NOT IN / 反连接
620. 有趣的电影 WHERE + ORDER BY
596. 超过 5 名学生的课 GROUP BY + HAVING

自测:

  • 能用 WHERE、ORDER BY、LIMIT 组合取数。
  • 能区分 = NULL 和 IS NULL。
  • 能用 NOT IN 找出“没有关联记录”的行。

6. 面试延伸

面试题:SQL 的书写顺序和执行顺序为什么不一样?

是什么:书写顺序是 SELECT → FROM → WHERE → ORDER BY → LIMIT,逻辑执行顺序是 FROM → WHERE → SELECT → ORDER BY → LIMIT。数据库要先知道数据来自哪张表,才能筛行,筛完才知道输出哪些列。

解决什么问题:理解执行顺序,才能解释"为什么 WHERE 里不能用 SELECT 的别名,而 ORDER BY 可以"。前者执行时别名还没产生,后者已经产生。

我的实操边界:平时按书写顺序写,排查问题时按执行顺序想;聚合和窗口阶段会在后续章节补充。

面试题:为什么工作中不建议写 SELECT *?

是什么:SELECT * 会返回表里的所有列,而不是你真正需要的列。

解决什么问题:只选需要的列能减少 IO 和内存;表结构变化时,下游结果不会因为多出几列而错位;列名写清楚,代码也更容易维护。

我的实操边界:练习和临时看数据时会用 *,写正式查询、ETL 和报表时会显式列出字段。

面试题:= NULL 为什么查不到数据?

是什么:NULL 代表未知,任何与它比较的结果都是 NULL,而 WHERE 只保留结果为 TRUE 的行。

解决什么问题:判空必须用 IS NULL / IS NOT NULL。同理,NOT IN 的子查询结果里如果有 NULL,整个条件可能失效,此时应改用 NOT EXISTS。

我的实操边界:数据探查时用 IS NULL 统计空值;写关联前会确认字段是否允许为空,避免把业务空值误当脏数据。


7. 本章验收

  • 能用自己的话解释表、行、列、主键,以及玩具库里九张表各存什么。
  • 能用 make_sql_demo_db.py 生成玩具库,并用 DESCRIBE 查到字段名。
  • 能独立写出包含 WHERE、ORDER BY、LIMIT 的查询,并说清执行顺序。
  • 能正确使用 BETWEEN、AND/OR、IN、LIKE、IS NULL。
  • 能解释 = NULL 为什么查不到数据,并知道 NOT IN 的 NULL 陷阱。
  • 能查出订单 1002 的两条评价、订单 1007 的取消后支付、订单 1002 的 0.10 金额差。

下一章

聚合与分组:从"查行"升级到"算指标",用 COUNT、SUM、GROUP BY 算出月度销售额、品类占比和客单价。

本章任务0 / 9
本页目录