资讯动态

SQL常量记录构造实战:从SELECT 1到多行拼接

发布时间:2026/9/18 18:51:00 来源:尧图企业网站定制
有人在群里发了一条 SQLSELECT 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_DATESQL Server 用GETDATE()Oracle 用SYSDATE。真正写常量记录时不要想当然认为所有数据库语法一致。另外一个容易忽视的点是常量记录里的“常量”其实可以是常量表达式比如100 * 0.15。优化器会在一开始就把能算的都算好不会在每一行重复计算。对于只有一行的情况无所谓但如果这个常量表后续被 JOIN 到几百万行的业务表预计算能力能省下不少性能开销。1.3 不同数据库怎么执行“无表查询”有些数据库语法宽松SELECT 1;直接能跑有些数据库则强制要求必须有 FROM于是产生了一个叫“DUAL”的特殊单行表。我整理了一份对照方便你写跨库脚本时直接查数据库单条常量记录写法说明MySQLSELECT 1 AS id;支持省略 FROM也可写SELECT 1 FROM DUAL;PostgreSQLSELECT 1 AS id;支持省略 FROMSQLiteSELECT 1 AS id;支持省略 FROMSQL ServerSELECT 1 AS id;支持省略 FROMOracleSELECT 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 / SQLiteSELECT 1, a UNION ALL SELECT 2, b;PostgreSQLSELECT * FROM (VALUES (1,a),(2,b)) AS t(id, name);SQL ServerSELECT * FROM (VALUES (1,a),(2,b)) AS t(id, name);OracleSELECT 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 BYSELECT 1 AS n ORDER BY n UNION ALL SELECT 2;很多数据库会直接报语法错误。SQL Server 不支持在单条 SELECT 里随便加 ORDER BY除非和 TOP 组合。即使某些数据库允许含义也和你想的完全不同。另外用UNION会有去重和排序动作如果业务要求行数精确切记用UNION ALL。4.5 字符串转义和动态拼接常量记录里写字符串单引号转义是个隐蔽的问题。SQL 字符串里的单引号通常写成两个单引号SELECT its 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 和类型提前定好这个技巧能帮你少踩很多坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价