选择列表
选择列表是 SELECT 与 FROM 之间的部分,用来定义查询结果有哪些列、每列如何计算以及叫什么名字:
SELECT
order_id,
unit_price * quantity AS line_amount,
CASE WHEN quantity > 0 THEN '有效' ELSE '无效' END AS item_status
FROM order_items;
这里的三个选择项会形成结果表的三列。选择列表的本质是把表表达式产生的每一行投影成需要的输出结构。
基本结构
选择列表由一个或多个逗号分隔的表达式组成:
SELECT expression_1, expression_2, expression_3
FROM table_expression;
每个表达式产生一列,可以是:
- 输入表中的列;
- 字面量或绑定参数;
- 算术、字符串、日期等表达式;
- 函数调用;
CASE、CAST、COALESCE等表达式;- 聚合函数或窗口函数;
- 返回单个值的标量子查询。
SELECT
o.order_id,
CURRENT_TIMESTAMP AS queried_at,
ROUND(o.total_amount * 0.95, 2) AS discounted_amount,
COUNT(*) OVER (PARTITION BY o.customer_id) AS customer_order_count
FROM orders AS o;
不同表达式的可用性受查询层次影响。例如使用普通聚合函数后,未聚合列通常必须满足 GROUP BY 规则。
选择已有列
只查询需要的列,是最稳定的写法:
SELECT customer_id, name, email
FROM customers;
查询涉及多个数据源时,应使用表名或别名限定列:
SELECT c.name, o.order_id, o.total_amount
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id;
限定名可以避免 id、name、created_at 等同名列产生歧义,也让读者不必反查表结构。
输出列按选择列表中的书写顺序排列。这个顺序属于查询结果结构的一部分,但结果行的先后顺序仍必须由 ORDER BY 决定。
计算输出列
选择列表不只是复制已有列,还可以计算新值:
SELECT
product_id,
unit_price,
quantity,
unit_price * quantity AS amount
FROM order_items;
数据库会为每个表达式推断结果类型。编写计算列时应检查:
- 输入类型是否兼容;
- 整数除法是否会丢失小数;
- 数值精度是否足够;
- 输入为
NULL时结果如何; - 函数和日期运算是否依赖特定方言。
需要控制类型时,可以显式转换:
SELECT CAST(quantity AS DECIMAL(12, 2)) / NULLIF(total_quantity, 0) AS ratio
FROM inventory_summary;
输出列别名
使用 AS 为结果列提供明确名称:
SELECT
customer_id AS id,
first_name || ' ' || last_name AS full_name
FROM customers;
别名有三个主要用途:
- 为计算表达式提供稳定、可读的列名;
- 避免连接后出现多个同名输出列;
- 让应用程序、报表或外层查询能够按名称引用结果。
AS 对列别名通常可以省略,但明确写出更不容易与关键字或表达式混淆:
SELECT total_amount amount
FROM orders;
如果别名包含空格、特殊字符或与关键字冲突,通常需要使用数据库支持的标识符引用符号。不同数据库可能使用双引号、反引号或方括号。为了可移植性,优先选择简单的字母、数字和下划线名称。
别名的可见范围
输出别名不是同一选择列表中的过程式变量。下面的写法在多数数据库中无效:
SELECT
unit_price * quantity AS amount,
amount * tax_rate AS tax_amount
FROM order_items;
可以重复表达式,或增加一个查询层:
SELECT amount, amount * tax_rate AS tax_amount
FROM (
SELECT unit_price * quantity AS amount, tax_rate
FROM order_items
) AS item_amounts;
WHERE 在逻辑上早于 SELECT,因此通常也不能使用同层输出别名。ORDER BY 在逻辑上较晚,通常允许使用输出别名:
SELECT unit_price * quantity AS amount
FROM order_items
ORDER BY amount DESC;
若输出别名与输入列同名,不同子句甚至不同数据库的名称解析规则可能不同。避免重名比记忆这些差异更可靠。
* 的展开规则
SELECT * 会展开当前表表达式可见的所有列:
SELECT *
FROM customers;
也可以只展开某个数据源:
SELECT c.*, o.order_id, o.total_amount
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id;
* 适合交互式探索和临时排查,但稳定的应用查询通常应显式列出字段,原因包括:
- 表新增列会悄悄改变结果结构;
- 可能读取大文本、二进制或敏感列;
- 连接后容易出现重复列名;
- 增加数据库、网络和应用反序列化开销;
- 代码审查时看不出接口真正依赖哪些字段。
某些数据库支持 * EXCEPT (...)、* EXCLUDE (...) 或 * REPLACE (...) 等扩展,但它们不是通用 SQL,应确认产品和版本。
没有 FROM 的选择列表
如果表达式不依赖表中的行,很多数据库允许省略 FROM:
SELECT 1 + 2 AS result, CURRENT_TIMESTAMP AS queried_at;
它适合测试表达式、查看函数结果或返回单行常量。部分数据库历史上要求从专用的单行表读取,例如 Oracle 常见 FROM DUAL;应以目标版本为准。
ALL 与 DISTINCT
默认情况下,SELECT 保留选择列表产生的所有行,即 ALL:
SELECT ALL country
FROM customers;
ALL 通常省略。要删除完全重复的结果行,可以使用 DISTINCT:
SELECT DISTINCT country
FROM customers;
当选择多列时,DISTINCT 比较的是整行组合:
SELECT DISTINCT country, city
FROM customers;
只有 country 和 city 都相同的行才算重复。它不会“只按第一列去重并随意保留其他列”。
在去重语义中,两行对应位置的 NULL 通常会被视为相同,因此多个完全相同的空值组合只保留一行。这与普通的 NULL = NULL 比较并不是同一个规则。
不要用 DISTINCT 掩盖连接问题
连接结果比预期多时,先检查关系基数和连接条件:
SELECT DISTINCT c.customer_id, c.name
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id;
如果真正的问题是遗漏连接条件,DISTINCT 只会隐藏错误并增加排序或哈希成本。若需求只是判断客户是否有订单,EXISTS 往往更直接:
SELECT c.customer_id, c.name
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
);
PostgreSQL 的 DISTINCT ON
PostgreSQL 提供非标准的 DISTINCT ON,可按指定表达式组保留第一行:
SELECT DISTINCT ON (customer_id)
customer_id,
order_id,
created_at
FROM orders
ORDER BY customer_id, created_at DESC, order_id DESC;
这表示每位客户保留最新的一张订单。决定“第一行”的 ORDER BY 不可省略,并应包含稳定的并列排序键。其他数据库通常使用 ROW_NUMBER() 窗口函数实现同类需求。
聚合查询中的选择列表
选择列表包含聚合函数时,要区分分组键与聚合值:
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_amount
FROM orders
GROUP BY customer_id;
customer_id 标识每个分组,另外两列对组内行计算。随意再加入 order_id 通常不合法,因为一组中可能存在多个订单号:
-- 通常错误:order_id 既未聚合,也未由分组规则确定
SELECT customer_id, order_id, COUNT(*)
FROM orders
GROUP BY customer_id;
部分数据库或配置会宽松接受这种查询,但返回哪一个 order_id 可能不确定,不应依赖。
窗口函数不会合并行
聚合函数配合 GROUP BY 会把多行压缩成一行;窗口函数则为每个输入行计算附加值:
SELECT
order_id,
customer_id,
total_amount,
SUM(total_amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;
结果仍然每张订单一行,只是多出客户累计金额。窗口函数通常在 WHERE 之后计算,因此不能直接在同层 WHERE 中按窗口结果筛选;可使用派生表或 CTE 增加一层。
输出结构是查询契约
应用程序读取查询结果时,选择列表就是接口的一部分。应保持以下约定:
- 输出列名称明确且不重复;
- 列顺序稳定,但应用代码优先按名称读取;
- 数值精度、日期时区和空值语义清楚;
- 不返回无关或无权限暴露的列;
- 变更已有列名和类型时评估下游兼容性。
即使数据库允许两个输出列同名,序列化为对象或映射时也可能覆盖其中一个值。
常见错误
在生产接口中长期使用 SELECT *
表结构变化后,查询结果也会变化。显式列名既能固定契约,也便于权限审查和性能分析。
误解 DISTINCT 的范围
DISTINCT 作用于完整选择列表,不是只作用于紧随其后的第一列。需要“每组取一行”时,应明确使用窗口函数、聚合后连接或目标数据库提供的专用语法。
依赖自动生成的列名
复杂表达式的默认名称可能难以预测,也可能因驱动而异。凡是需要被外层查询或应用读取的计算列,都应设置别名。
忽略 NULL 和类型
price * quantity 任一输入为 NULL 时通常也为 NULL;整数运算和小数运算的结果类型也可能不同。不要只看示例数据的显示结果,应核对真实类型和业务含义。
检查清单
编写选择列表时,可以依次确认:
- 是否只返回真正需要的列;
- 多表查询中的列来源是否明确;
- 每个计算列是否有清楚且唯一的别名;
- 表达式的类型、精度和
NULL行为是否符合预期; - 聚合列与分组键是否合法;
- 是否误用
DISTINCT修补连接结果; - 输出结构变化是否会影响应用程序或报表。
选择列表决定“结果长什么样”。让每一列的来源、计算规则、名称和类型都明确,查询结果才适合作为稳定的数据接口。