查询概述
SQL 查询用于从一个或多个表中读取数据。最常用的语句是 SELECT:先确定要读取的列,再确定数据来源、筛选条件和结果顺序。本文以通用 SQL 为主,方言差异会单独说明。
一条查询的基本结构
SELECT column_1, column_2
FROM table_name
WHERE condition
ORDER BY column_1 ASC
LIMIT 20;
可以把它理解为五个问题:
SELECT:结果要展示哪些列或表达式?FROM:从哪张表或哪个查询结果读取?WHERE:哪些行符合条件?ORDER BY:结果按什么顺序返回?LIMIT:最多返回多少行?
下面的示例假设有 customers(客户)和 orders(订单)两张表。
选择列与计算列
SELECT id, name, email
FROM customers;
SELECT * 会读取所有列,适合临时探索,不建议用于稳定的应用接口:表增加列后,返回结构和网络传输量都会改变。
可以在查询中计算新列,并使用别名改善可读性:
SELECT
product_name,
price,
quantity,
price * quantity AS amount
FROM order_items;
表达式可以使用算术运算、字符串函数、日期函数和条件表达式。函数名称和日期运算写法通常因数据库而异,使用前应核对目标数据库文档。
筛选行:WHERE
SELECT id, name
FROM customers
WHERE status = 'active'
AND created_at >= '2025-01-01';
常用条件包括:
-- 范围与集合
WHERE price BETWEEN 10 AND 100
WHERE status IN ('paid', 'shipped')
-- 模糊匹配(% 表示任意长度,_ 表示一个字符)
WHERE name LIKE '张%'
-- 空值判断,不能写 = NULL
WHERE deleted_at IS NULL
AND 的优先级高于 OR。混合使用时请加括号,避免误读:
WHERE (status = 'active' AND level = 'vip') OR is_staff = TRUE
NULL 表示未知或缺失。与 NULL 的比较结果不是 TRUE,所以应使用 IS NULL 或 IS NOT NULL。涉及 NULL 的布尔运算遵循三值逻辑,不能简单按“非真即假”理解。
去重:DISTINCT
SELECT DISTINCT country
FROM customers;
DISTINCT 会对所选列的组合去重,而不是只对某一列去重。它可能需要排序或哈希,数据量大时应关注执行计划;如果只是为了消除连接错误产生的重复行,应先检查连接条件。
排序与分页
SELECT id, name, created_at
FROM customers
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 40;
没有 ORDER BY 时,数据库不承诺返回顺序。分页排序应提供稳定的第二排序键(通常是唯一键),否则同一记录可能在不同页之间跳动。
LIMIT ... OFFSET ... 在深页时可能需要跳过大量行。大数据量或持续变化的数据更适合基于游标的分页:
SELECT id, name, created_at
FROM customers
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
FETCH FIRST 20 ROWS ONLY;
LIMIT、OFFSET 和 FETCH FIRST 的可用性及参数写法取决于数据库;分页参数应由应用层参数化传入。
聚合与分组
聚合函数把多行计算成一个值:
SELECT
COUNT(*) AS order_count,
SUM(total_amount) AS total_amount,
AVG(total_amount) AS average_amount,
MIN(total_amount) AS minimum_amount,
MAX(total_amount) AS maximum_amount
FROM orders
WHERE status = 'paid';
需要按客户分别统计时使用 GROUP BY:
SELECT customer_id, COUNT(*) AS order_count, SUM(total_amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
WHERE 在分组前筛选行;HAVING 在分组后筛选组:
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 3;
在标准 SQL 中,SELECT 中非聚合列通常必须出现在 GROUP BY 中。COUNT(*) 统计行数,COUNT(column) 不统计该列为 NULL 的行。
连接多张表
使用连接把相关表横向组合。最常见的是内连接和左连接:
-- 只保留有订单的客户
SELECT c.id, c.name, o.id AS order_id, o.total_amount
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id;
-- 保留所有客户,没有订单时订单列为 NULL
SELECT c.id, c.name, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id;
连接条件写在 ON 中。对左连接结果进行筛选时,把右表条件写进 WHERE 可能意外变成内连接:
-- 仍保留没有已支付订单的客户
SELECT c.id, c.name, o.id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id AND o.status = 'paid';
连接前应确认关联键的唯一性。一个客户匹配多条订单时,客户列会重复,这是关系展开的正常结果,不要盲目添加 DISTINCT 掩盖数据问题。
子查询与 CTE
子查询是嵌套在另一条 SQL 中的查询。EXISTS 适合判断是否存在关联行:
SELECT c.id, c.name
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.id AND o.status = 'paid'
);
公共表表达式(CTE)使用 WITH 为中间结果命名,可让复杂查询更易阅读:
WITH customer_totals AS (
SELECT customer_id, SUM(total_amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
)
SELECT c.name, t.total_amount
FROM customers AS c
JOIN customer_totals AS t ON t.customer_id = c.id
WHERE t.total_amount > 1000;
CTE 是组织查询的语法工具,不一定会物化为临时表;是否展开或物化由数据库优化器和方言决定。
集合运算
集合运算把多个 SELECT 的结果纵向合并:
SELECT email FROM customers
UNION
SELECT email FROM newsletter_subscribers;
UNION 会去重,UNION ALL 保留重复行且通常更快。参与运算的查询必须返回相同数量、兼容类型的列;最终排序应放在整个集合运算之后。
逻辑处理顺序
SQL 的书写顺序不等于数据库的逻辑处理顺序。常用的逻辑顺序是:
FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET
因此,WHERE 通常不能直接引用同层 SELECT 中定义的别名;可以使用重复表达式、子查询或 CTE。优化器可能采用不同的物理执行计划,但结果必须符合 SQL 语义。
安全、正确性与性能基础
- 使用驱动提供的参数绑定,不要把用户输入直接拼接到 SQL 字符串中,以防 SQL 注入。
- 明确列名和排序规则,避免依赖表顺序、插入顺序或隐式类型转换。
- 为高频筛选、连接和排序列设计合适索引,但不要为每一列盲目建索引;索引会占空间并增加写入成本。
- 用
EXPLAIN或数据库提供的执行计划工具检查大查询,关注全表扫描、连接方式、估算行数和排序开销。 - 只返回业务需要的列和行,尽早在
WHERE中过滤;生产代码避免无条件SELECT *。 - 处理金额、时间和时区时遵循数据库与业务约定,不要依赖客户端默认格式。
常见误区检查
- 忘记
ORDER BY,却假设结果有固定顺序。 - 用
= NULL判断空值,而不是IS NULL。 - 把
HAVING当作普通行过滤,导致先分组再过滤的额外开销。 - 左连接后在
WHERE过滤右表列,意外丢失左表中未匹配的行。 - 用
NOT IN处理可能包含NULL的子查询,结果可能因三值逻辑而不是预期;可评估改用NOT EXISTS。 - 通过应用层拼接字符串实现动态条件,造成注入风险或类型错误。
掌握 SELECT 的各个阶段后,可以把复杂需求拆成“数据来源 → 行筛选 → 分组统计 → 列投影 → 排序分页”,再根据目标数据库补充具体方言。