有人在群里发了一条 SQL:SELECT 1;,然后问:没有 FROM,没有表,这算哪门子查询?我当时的回答是:别小看这一行,SELECT 1就是一条最简单的“常量记录”查询,它返回一行一列,在造测试数据、补统计空行、写临时映射表这些场景里,比很多复杂的查询都好用得多。
这次我把“数据库 select 构造一条常量记录和多条常量记录”这件事彻底讲透。适合刚学 SQL 的学生、写报表经常要对齐数据的数据分析师,以及天天和 MySQL、PostgreSQL、SQL Server、Oracle、达梦这类数据库打交道的开发。你会看到什么叫常量记录、为什么有些数据库必须写FROM DUAL、怎么拼出多行常量表,以及我实际踩过的类型和空值相关的坑。
1. 单条常量记录:不带表名的 SELECT 到底是怎么回事
1.1 常量记录的本质:从 SELECT 1 说起
SELECT 1;这个语句看起来奇怪,是因为很多人对 SQL 的第一印象是“查询表的数据”,潜意识里觉得必须有 FROM 和表名。但 SQL 标准本身是允许没有 FROM 的,SELECT 后面可以直接跟字面量。它和普通查询一样,返回一个结果集,只不过这个结果集的行和列全部来自你手写的常量,而不是磁盘上的某张表。
如果用关系运算的视角来看,一条常量记录就是一张只有一行、若干列的关系表。比如:
SELECT 1 AS id, '张三' AS user_name, 25 AS age;返回结果就是一行三列。这一行数据不会污染任何物理表,查询结束就没了,适合临时造数用。数据库优化器通常会把它识别成“常量表”,执行计划里会用非常轻量的方式处理,几乎不消耗 IO。
我见过不少面试题喜欢问SELECT 1到底有什么意义。它常见用途包括:测试数据库连接是否正常、在 EXISTS 子查询里代替SELECT 1作为占位、或者配合FROM DUAL执行一段和表无关的日期计算。但“构造一条常量记录”这个功能本身,才是它最值得被开发记住的点。
1.2 一条常量记录能带什么内容
常量不一定是数字和字符串,只要是能在查询里写出来的字面量或表达式,都能作为一列。比如日期、布尔值、空值、计算式、函数返回值:
SELECT 100 * 0.15 AS tax, CURRENT_DATE AS today, '2025-06-01' AS biz_date, NULL AS remark;这句话看起来简单,实际在项目里我经常拿它验证数据库当前时间、测试日期函数写法,或者临时算一下分页总数。需要注意,不同数据库的“当前时间函数”命名不一样:MySQL 和 PostgreSQL 用CURRENT_DATE,SQL Server 用GETDATE(),Oracle 用SYSDATE。真正写常量记录时,不要想当然认为所有数据库语法一致。
另外一个容易忽视的点是:常量记录里的“常量”其实可以是常量表达式,比如100 * 0.15。优化器会在一开始就把能算的都算好,不会在每一行重复计算。对于只有一行的情况无所谓,但如果这个常量表后续被 JOIN 到几百万行的业务表,预计算能力能省下不少性能开销。
1.3 不同数据库怎么执行“无表查询”
有些数据库语法宽松,SELECT 1;直接能跑;有些数据库则强制要求必须有 FROM,于是产生了一个叫“DUAL”的特殊单行表。我整理了一份对照,方便你写跨库脚本时直接查:
| 数据库 | 单条常量记录写法 | 说明 |
|---|---|---|
| MySQL | SELECT 1 AS id; | 支持省略 FROM,也可写SELECT 1 FROM DUAL; |
| PostgreSQL | SELECT 1 AS id; | 支持省略 FROM |
| SQLite | SELECT 1 AS id; | 支持省略 FROM |
| SQL Server | SELECT 1 AS id; | 支持省略 FROM |
| Oracle | SELECT 1 AS id FROM DUAL; | 必须带 DUAL |
| 达梦 | SELECT 1 AS id FROM DUAL; | 看兼容模式,Oracle 模式下必须带 DUAL; MySQL 兼容模式可省略 |
不要觉得 DUAL 是什么神秘的东西,它就是一张固定有一行一列的内部表。Oracle 保留它是为了兼容早期语法限制。你不需要关心它里面存的什么,只记住“在 Oracle 里不带表名查常量,就加 FROM DUAL”就行。达梦这类数据库也有 DUAL,具体能不能省略,要看创建库时选的兼容模式。
2. 多条常量记录:从 UNION ALL 到 VALUES 行构造器
2.1 最通用的写法:UNION ALL 拼接多条行
真正到了“多条常量记录”这个需求,最通用、最容易理解的写法是UNION ALL。一条记录就是一个 SELECT,多个 SELECT 用 UNION ALL 串起来:
SELECT 1 AS id, '待审核' AS status_name UNION ALL SELECT 2, '审核通过' UNION ALL SELECT 3, '已驳回';UNION ALL 会把每个查询的结果全部保留,形成多行结果。注意一点:列名由第一条 SELECT 决定,后面几条 SELECT 不需要再写 AS,直接给值就行。
为什么不推荐用UNION?因为UNION默认会去重,如果常量记录里出现重复行,老老实实的两条数据会被合并成一条,行数对不上,而且还会多一次排序去重,白白增加开销。构造常量记录不属于“数据清洗”,绝大多数情况下你要的就是原样保留,所以请优先用UNION ALL。
我之前见过一个报表 SQL,有人为了把三行映射数据拼出来,用了UNION,结果恰好第二行和第三行内容相同,页面上的下拉选项就少了一项。排查了半天才发现不是程序的问题,是 UNION 悄悄吃了重复行。这种问题不致命,但确实浪费时间。
2.2 用 VALUES 行构造器拼多条记录
如果你嫌 UNION ALL 一大串太啰嗦,PostgreSQL 和 SQL Server 还有更紧凑的写法,叫“VALUES 行构造器”。常见用法是把它放在 FROM 子句里当成派生表:
SELECT * FROM ( VALUES (1, '待审核'), (2, '审核通过'), (3, '已驳回') ) AS t(id, status_name);这里最关键的一步是给派生表起别名,后面括号里是列名。没有AS t(id, status_name),SQL Server 和 PostgreSQL 会直接报错,因为派生表必须有名字,而列名又是后续引用所必需的。
这种写法比 UNION ALL 清晰得多。数据很多时,一眼就能看到每一行是什么,适合维护静态枚举。不过它有个限制:在 Oracle 里这样写会报错,Oracle 合力方式还是用 DUAL + UNION ALL。MySQL 8 也支持 VALUES 相关的语法,但为了照顾团队里用老版本的人,我一般还是用 UNION ALL 更省事。
2.3 常用数据库多条写法对照
写一套要在多个数据库上跑的 SQL 时,建议直接抄下面这个表里的推荐写法:
| 数据库 | 推荐的多条常量记录写法 |
|---|---|
| MySQL / SQLite | SELECT 1, 'a' UNION ALL SELECT 2, 'b'; |
| PostgreSQL | SELECT * FROM (VALUES (1,'a'),(2,'b')) AS t(id, name); |
| SQL Server | SELECT * FROM (VALUES (1,'a'),(2,'b')) AS t(id, name); |
| Oracle | SELECT 1, 'a' FROM DUAL UNION ALL SELECT 2, 'b' FROM DUAL; |
不要为了“统一”强行用一种写法。比如 Oracle 的FROM DUAL在 MySQL 里也能跑,但 MySQL 的省略式在 Oracle 里跑不了;VALUES 行构造器在 PostgreSQL 和 SQL Server 里很好用,拿到 Oracle 就废了。最稳的跨库方案,是所有数据库都支持的UNION ALL,代价只是代码长一点。
3. 常量记录表不是玩具:四个高频使用场景
3.1 给统计报表补空行
做报表的人最烦一个问题:某天没有订单、某商品没有销量、某渠道没有用户,结果按天或按状态分组统计出来的结果缺了一行。前端画图时数据点对不上,不下拉一列数据,图表就断掉。
这种场景就可以用常量记录构造一张“完整维度表”,再 LEFT JOIN 业务数据:
WITH dates AS ( SELECT '2025-06-01' AS d UNION ALL SELECT '2025-06-02' UNION ALL SELECT '2025-06-03' ) SELECT dates.d, COALESCE(SUM(o.amount), 0) AS total_amount FROM dates LEFT JOIN orders o ON o.order_date = dates.d GROUP BY dates.d ORDER BY dates.d;这里dates这个 CTE 里的三条字符串常量就是完整的日期集合。即使某天没有任何订单,LEFT JOIN 后也会保留这一行,再用COALESCE把 NULL 改成 0,图表就连续了。注意,COALESCE各数据库都支持,如果你见到IFNULL或NVL,那是某些库特有的替代写法。
3.2 临时码表:避免满屏 CASE WHEN
业务表里存状态码 1、2、3,前端要显示“待审核、审核通过、已驳回”。最朴素的做法是写一堆 CASE WHEN,但状态一多,SQL 就变得又臭又长。这时候可以用常量记录构造临时码表,再 JOIN 一下:
WITH status_map AS ( SELECT 1 AS status, '待审核' AS status_name UNION ALL SELECT 2, '审核通过' UNION ALL SELECT 3, '已驳回' ) SELECT b.biz_no, b.status, m.status_name FROM biz_table b LEFT JOIN status_map m ON b.status = m.status;好处是:状态码的值和中文名的对应关系单独集中在 CTE 里,后续想加状态,只改常量部分,不动主查询。如果这种码表本身在项目里很常用,更规范的做法是建一张真正的字典表,但有时候你是临时查一次数、导一次报表,为这个动数据库结构不值当,常量记录 CTE 就是性价比最高的方案。
3.3 造测试数据和课程设计初始化数据
写课程设计、做本地演示、给开发环境造几条初始数据,大多数人第一反应是打开客户端手动 INSERT。但如果数据是几十行,手动录入又慢又容易错。用“常量记录 + INSERT INTO SELECT”可以一次性灌进去:
INSERT INTO student (id, name, score) SELECT 1, '张三', 90 UNION ALL SELECT 2, '李四', 85 UNION ALL SELECT 3, '王五', 92;这个写法在 MySQL、PostgreSQL、SQL Server 上都通用。比一条条 INSERT 效率高,也比整段 VALUES 插入更灵活,因为 SELECT 后面可以接计算表达式、调用函数。如果你的库里已经建好了事务,建议在外面包一层事务,造数失败能整体回滚。
3.4 大批量造数:从常量表到序列展开
再造几百条测试数据时,手写常量就不划算了。你可以把常量记录和递归查询或数字表结合,快速展开成批量数据。以 PostgreSQL 为例:
WITH RECURSIVE seq AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM seq WHERE n < 100 ) SELECT n, 'user_' || n AS user_name FROM seq;SQL Server 也能用类似思路,但字符串连接要用CONCAT(n, 'user_')而不是||。这类写法本质上是“先构造一条起始常量,再基于常量做递归展开”,常量记录是种子,递归是引擎。
4. 构造常量记录时踩过的坑:类型、空值、去重与排序
4.1 列数不一致是最常见报错
UNION ALL要求每个 SELECT 的列数必须完全一致。比如:
SELECT 1, 'a' UNION ALL SELECT 2;这条 SQL 基本没有数据库会放过。MySQL 报错信息是“The used SELECT statements have a different number of columns”,SQL Server 是“所有查询中的列数必须相同”。报错虽然明显,但真实项目里常量行多、列也多时,少写一列是经常的事。
我的习惯是:先写第一行完整列名,后面每一行对照第一行数一遍,不要凭感觉。如果常量记录超过十列,我会改成 VALUES 行构造器,因为它的表格形态更直观,缺列一眼就能发现。
4.2 类型推断和隐式转换的坑
列数一致不代表类型匹配。常量记录里最容易踩的是类型推断不一致:
SELECT 1 AS n UNION ALL SELECT 'abc';第一行是数字,第二行是字符串。PostgreSQL 会直接报“integer 和 text 无法匹配”,SQL Server 则可能尝试把'abc'转成数字,转换失败再报错。与其让数据库猜,不如在第一行就把类型定死,比如都用字符串:
SELECT '1' AS n UNION ALL SELECT 'abc';还有一种情况是数字精度问题。第一行写1.0,第二行写1.5,有些数据库会统一成 DECIMAL,没问题;但第一行写1,第二行写1.5,某些数据库会把所有行转成高精度类型,结果看起来没问题,实际类型变了。做报表时如果后续要四舍五入,可能和预期不一致。稳妥的做法是:所有常量行里的同一列,保持完全一致的写法风格。
4.3 NULL 的坑:类型不明的空值
NULL是常量记录里最容易让人挠头的东西。直接写SELECT NULL,很多数据库不知道它该是什么类型,一旦和其他类型 UNION ALL,就出问题。
SELECT NULL AS v UNION ALL SELECT 1;在 PostgreSQL 里,这种写法经常报错或者把 1 转成字符串,因为第一行的 NULL 被推断成未知类型。解决方案是显式声明:
SELECT CAST(NULL AS INT) AS v UNION ALL SELECT 1;SQL Server 里SELECT NULL通常默认推断成 INT,但不建议依赖这个默认行为。写常量记录如果必须带空值,最好每一条都显式 CAST 一次,宁可啰嗦,不要留下隐患。排序时也要注意:NULL 在不同数据库的排列顺序不一样,有的在最前,有的在最后,如果对常量记录排序并且里面有空值,先确认数据库的默认行为,否则结果集顺序可能会让你困惑。
4.4 UNION 与 ORDER BY、LIMIT 的组合问题
常量记录拼接后,如果你想排序或取前几条,ORDER BY 和 LIMIT 必须放在整个语句的最后面,而不是某一支的后面:
SELECT 1 AS n UNION ALL SELECT 3 UNION ALL SELECT 2 ORDER BY n DESC LIMIT 1;这没问题。但如果写成分支自带 ORDER BY:
SELECT 1 AS n ORDER BY n UNION ALL SELECT 2;很多数据库会直接报语法错误。SQL Server 不支持在单条 SELECT 里随便加 ORDER BY,除非和 TOP 组合。即使某些数据库允许,含义也和你想的完全不同。另外,用UNION会有去重和排序动作,如果业务要求行数精确,切记用UNION ALL。
4.5 字符串转义和动态拼接
常量记录里写字符串,单引号转义是个隐蔽的问题。SQL 字符串里的单引号通常写成两个单引号:
SELECT 'it''s a test' AS note;如果常量记录是在程序代码里动态拼接出来的,单引号转义更麻烦。我之前见过 Java 代码里拼 SQL,直接把'拼进去,结果用户输入包含单引号时,SQL 直接断句报错。这个问题的本质不是常量记录本身的问题,而是动态拼 SQL 的安全问题。建议所有动态 SQL 都走参数化,或者至少用框架自带的转义函数,不要手工拼字符串。
5. 造数规模化:从几十行到几十万行的思路演进
5.1 手工拼装的上限和代价
手写常量记录适合几行到几十行的场景,超过一百行就开始吃力。我曾经为了一个测试需求,手工拼了二百行 UNION ALL,代码文件又长又难读,后来加数据时把自己绕晕了。这个体验告诉我:常量记录不是万能造数工具,它有明确的使用边界。
一个简单的判断标准:如果常量数据超过 50 行,或者后续会频繁增改,就应该考虑用 CTE + 递归,或者干脆建一张物理表。手写 UNIION ALL 不只是代码体积问题,更重要的是维护成本。你很难在一长串SELECT 1里快速定位某一行。
5.2 三套批量展开方案
批量生成几十万行测试数据,常见思路有三种。
第一,递归 CTE。前面已经演示过,适合生成连续的序号、日期、时间序列。
WITH RECURSIVE seq AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM seq WHERE n < 1000 ) SELECT n, CONCAT('user_', n) AS user_name FROM seq;第二,数字辅助表。在库里建一张只放数字的表,从 1 到几万甚至几百万,然后通过 CROSS JOIN 乘法展开:
SELECT a.n + b.n * 1000 AS n FROM numbers a CROSS JOIN numbers b;如果 numbers 表里有 0 到 999,这样一次就能出 100 万行。数字辅助表在很多场景都值得建一张,比递归 CTE 快得多,复杂度也低。
第三,存储过程/程序循环插入。适合需要造复杂业务数据的情况,比如每个用户连带生成订单、日志。用程序循环逐条插入时,记得批量提交,不要一条一条提交,否则性能差到让你怀疑数据库有问题。
5.3 工程化建议:用 CTE 封装常量集
最后分享一个实用技巧:如果一套 SQL 里多处要用到同一份常量集合,比如很多报表都缺不了“月份映射”或“状态码映射”,不要在每个地方重复写 UNION ALL,而是用 CTE 或视图封装一次。
WITH month_map AS ( SELECT 1 AS m, '一月' AS month_name UNION ALL SELECT 2, '二月' UNION ALL SELECT 3, '三月' ) SELECT ... FROM month_map ...CTE 在一条 SQL 里只能算一次,多个引用共用,逻辑上相当于临时视图。更通用的做法是建物理视图或者固化字典表,但如果只是单条分析 SQL 内部复用,CTE 是最合适的。
最后说点个人经验
我刚开始写 SQL 的时候,也以为SELECT 1这种不带表的查询只是无聊的小技巧。后来做报表、做数据清洗、搞课程设计造数,才意识到常量记录的价值在于“用手上的字面量快速构造出一张关系表”。它让 SQL 在不依赖物理表的前提下,也可以完成 JOIN、UNION、补行、映射这些关系操作。
如果非要给一个实践顺序:几行以内,首选 UNION ALL;需要在 PostgreSQL 和 SQL Server 之间迁移,试 VALUES 行构造器;几十行以上,停手,换递归 CTE 或物理表。写之前先确认当前数据库支持哪套语法,再把 NULL 和类型提前定好,这个技巧能帮你少踩很多坑。