近日,PostgreSQL 社区出现一则引发广泛讨论的技术问题报告:某用户在执行 SELECT * 并同时指定显式列名的查询时,发现结果集返回了重复的列名,尽管原始表中并不存在任何重复列。该问题迅速在 DBA 和开发者群体中发酵,不少人在复现后表示“令人困惑”,甚至怀疑是数据库内核的严重缺陷。经 PostgreSQL 核心团队确认,这并非 bug,而是一种容易被误解的 SQL 语义行为。

现象复现:重复列名凭空出现

据用户描述,其表结构设计如下:

CREATE TABLE test (
    id INTEGER,
    name TEXT,
    created_at TIMESTAMP
);

执行以下查询时出现问题:

SELECT *, id, name FROM test;

结果集中出现了两次 idname 列,尽管表本身并无重复列。许多开发者第一时间认为 PostgreSQL 的 SELECT * 展开机制存在异常,但经过深入排查,发现这其实是 SQL 标准所定义的行为,而非数据库实现错误。

技术分析:SELECT * 为何会“复制”列

PostgreSQL 解析 SELECT * 时,会将其展开为表或视图的所有列。当你在同一查询中又显式列出了某些列,这些列被独立地追加到结果集中。由于 * 已经包含了所有列,显式列名自然会导致重复。

关键点在于:SELECT * 是在查询解析阶段将表的所有列名逐个替换,而不是一个“整体占位符”。因此,SELECT *, id 等价于 SELECT id, name, created_at, id, name。尽管原始表结构不存在重复列,但结果集可以包含来自同一表的多份相同列数据。

影响范围:从“视觉困惑”到实际业务风险

这一现象虽然符合 SQL 规范,却在实际生产中带来潜在风险:

  1. 驱动层解析错误:某些数据库中间件或 ORM(如 JDBC、pdo_pgsql)会按列名索引数据。重复列名可能导致 ResultSet.getColumnIndex("id") 返回第一个匹配项,但开发者可能预期的是第二个,从而取错值。
  2. 导出与数据迁移:使用 COPY 命令或第三方导出工具时,重复列名会破坏 CSV 表头结构,导致导入失败。
  3. 视图与子查询:若将此类查询封装为视图,后续引用该视图时无法通过列名唯一识别字段,可能引发难以调试的隐式错误。

事实上,在 PostgreSQL 官方邮件列表以及 Stack Overflow 上,类似问题每年都会被反复提及。核心开发者 Tom Lane 曾解释:“这种写法通常是开发者疏忽所致,而非有意为之。尽管标准允许,但我们建议在需要明确列名时完全放弃 SELECT *。”

专家建议:规范写法规避隐患

针对此问题,资深 PostgreSQL 顾问、社区贡献者 Karen Jex 在接受本刊采访时建议:

除非是对表结构进行快速探查,否则永远不要在生产代码中将 SELECT * 和显式列名混合使用。如果需要部分列,请直接列出所需列名;如果需要所有列,请使用 SELECT * 但不添加额外列。若确实需要追加计算列,可考虑使用 SELECT *, ... AS extra 的方式,但追加列不能与表中已有列重名。

此外,许多数据库 linter 工具(如 sqlls、pgMustard)已加入针对此类情况的告警规则。PostgreSQL 15 引入了 information_schema.columns 的优化,但尚未在语义层面禁止重复列名输出。

总结:标准允许,不等于最佳实践

本次事件再次提醒广大技术从业者,SQL 标准为灵活性留出了充足空间,但这种灵活性可能带来意料之外的副作用。“没有重复列的表”与“没有重复列名的结果集”是两个截然不同的概念。在数据库系统逻辑日趋复杂的今天,清晰、无歧义的查询写法才是保障系统稳定性的基石。正如 PostgreSQL 官方维基所强调的那样:“SELECT * 是懒惰的捷径,而显示列名是责任的开始。”

目前,该问题已在社区形成统一认知:这不是缺陷,而是一次生动的 SQL 语义教育课。对于已受影响的业务系统,建议立即审查所有包含 SELECT *, column 模式的查询,并进行规范化重构,以防隐患在未来某个不可预知的时刻爆发。