跳到主要内容

选择列表

选择列表是 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;

数据库会为每个表达式推断结果类型。编写计算列时应检查:

  1. 输入类型是否兼容;
  2. 整数除法是否会丢失小数;
  3. 数值精度是否足够;
  4. 输入为 NULL 时结果如何;
  5. 函数和日期运算是否依赖特定方言。

需要控制类型时,可以显式转换:

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;整数运算和小数运算的结果类型也可能不同。不要只看示例数据的显示结果,应核对真实类型和业务含义。

检查清单​

编写选择列表时,可以依次确认:

  1. 是否只返回真正需要的列;
  2. 多表查询中的列来源是否明确;
  3. 每个计算列是否有清楚且唯一的别名;
  4. 表达式的类型、精度和 NULL 行为是否符合预期;
  5. 聚合列与分组键是否合法;
  6. 是否误用 DISTINCT 修补连接结果;
  7. 输出结构变化是否会影响应用程序或报表。

选择列表决定“结果长什么样”。让每一列的来源、计算规则、名称和类型都明确,查询结果才适合作为稳定的数据接口。