SQL中不查表也能造数据?一文讲透SELECT构造常量记录的技巧与坑

发布时间:2026/9/18 21:44:49
SQL中不查表也能造数据?一文讲透SELECT构造常量记录的技巧与坑 我在数据库这个行当里泡了十多年日常被问到最多的往往不是复杂的性能调优反而是那些看起来“特别基础”的小问题。就拿今天要聊的这件事来说怎么用一条SELECT不碰任何表凭空造出一条数据来甚至一次性造出好几条这种操作在圈里常被称为“构造常量记录”或者“伪表查询”。听起来不值一提但真到了写的时候坑一点不比复杂报表少而且它还特别考验你对数据库语法的理解。这篇文章就把这个主题拆开聊透从单条常量的最简写法到多条常量的几种构造方式再到不同数据库平台的差异最后讲几个进阶玩法和排查经验希望能帮你把这个顺手的小技巧真正用起来。1. 为什么需要SELECT构造常量记录三个高频场景1.1 从一次“造数”经历说起上个月帮同事排查一个报表问题当时的SQL逻辑是按月份统计销售额没有订单的月份也要补0不能直接不显示。同事的第一反应是建一张临时表把12个月份放进去。我说你先别急着建表拿一条SELECT直接把这个“月份表”造出来试试。他愣了一下然后在编辑器里敲出了类似这样的东西SELECT 2024-01 AS month, 0 AS cnt UNION ALL SELECT 2024-02, 0 UNION ALL SELECT 2024-03, 0;跑完之后他说了句让我印象很深的话“原来数据真的可以不从表里来。”这个例子特别典型。在很多人的认知里SELECT后面必须跟FROM必须有真实存在的表。但从SQL标准开始SELECT就可以没有FROM个别数据库加个虚拟表兜底可以直接输出常量、表达式或者函数计算结果。这就是构造常量记录的本质不依赖任何持久化存储直接在查询里“造”出结构化的数据行。1.2 具体哪些场景最值得用我总结下来下面这几类场景用常量记录是最省事的快速验证函数和表达式。比如你想确认某个数据库里日期加减的写法一条SELECT NOW() INTERVAL 1 DAY就能试出来不用去翻文档更不用找张带日期字段的表。**构造测试输入。**写存储过程、报表、接口前需要一组结构化数据来验证逻辑。造临时表麻烦用常量记录几秒搞定。**报表补零和基准维度。**统计每个月、每个状态的数据时如果某个月没有记录SQL结果里就少了这一行。用常量记录构造“基准数据”再LEFT JOIN业务表就能优雅地补出0值。**连接探活和工具调试。**数据库客户端连上之后随手敲一行SELECT 1能通就说明连接没问题。这在排查连接池、网络、权限问题时尤其常用。**临时参数和配置。**某些查询需要临时拼一组配置项又不想动数据库里的配置表用常量记录作为子查询即可。1.3 为什么不用临时表或INSERT你可能会问临时表不行吗INSERT不行吗当然行但要分场景。临时表适合数据量大、需要反复使用、可能需要加索引的情况它的缺点是重要写DDL涉及事务和日志用完要清理。而SELECT常量记录完全在内存中计算语句结束即消失不产生任何落盘数据。对于一次性的验证、补零和测试场景SELECT常量的心智负担和操作成本都低得多。所以我的建议很直接**一两行、十几行的固定数据优先用SELECT常量记录上百行、要反复JOIN且要索引的才考虑临时表或正式表。**很多人习惯了一种方式就容易忘了另一种但两个都是工具箱里应有备用的家伙。2. 单条常量记录的写法与细节2.1 最简写法SELECT 1先从最简单的开始。在MySQL、PostgreSQL、SQL Server、SQLite里直接执行SELECT 1;结果就是一列一行数值1。改成SELECT hello; SELECT NOW(); SELECT DATABASE();都是同理。这条SQL表面上是“查询”实际上就是在计算一个表达式输出一个单行单列的结果。但Oracle是个例外。Oracle的SELECT语法强制要求带FROM子句所以你在Oracle里要写成SELECT 1 FROM DUAL;这里的DUAL是Oracle专门为这类“不查表”的场景准备的一张虚拟表官方名字叫“DUAL”意思是“双重的”“对偶的”它就固定提供一行一列内容无所谓。SELECT SYSDATE FROM DUAL这类写法在Oracle里可以说是刻进DNA的习惯。国产数据库里有不少是兼容Oracle的比如达梦数据库同样保留了FROM DUAL的写法。2.2 带字段名的单条常量记录实际干活时光输出一个值往往不够你需要的是“带列名的结构化记录”。比如SELECT 张三 AS user_name, 25 AS user_age, 研发部 AS dept_name;这段SQL在MySQL里直接执行就能返回一行数据三列分别为user_name、user_age、dept_name。这里的字段别名很重要因为后续如果把这个查询作为子查询外部引用列名时就要靠它。从这条SQL里可以延伸出几个容易忽略的点字符串常量必须用单引号。在Oracle里尤其忌讳双引号因为双引号在Oracle里表示“标识符”列名、表名你写SELECT abc FROM DUALOracle会理解成你要查一个叫abc的列然后报错ORA-00904。日期常量建议用标准日期字面量。SELECT DATE 2024-06-01 AS d可以在多个数据库里用。如果直接写SELECT 2024-06-01在MySQL里会被当成数值减法算出20072024减去6再减1这种低级错误我见过不止一次。数值常量默认类型在各平台不尽相同。比如PostgreSQL里SELECT 1得到的是integerSELECT 1.5是numeric。后面如果要和其他列做运算要注意隐式转换。2.3 用常量记录当表达式“计算器”单条常量记录还有一个非常实用的用途当计算器用。SELECT 1 1 AS result; SELECT (100 - 30) * 2 AS total; SELECT CONCAT(Hello, , World) AS greeting; SELECT DATE_ADD(2024-06-01, INTERVAL 30 DAY) AS new_date; SELECT DATEDIFF(2024-06-01, 2024-01-01) AS diff_days;我经常在连上数据库客户端后顺手验证一个日期函数、字符串函数的语法和结果比打开系统计算器、翻文档快得多。这本质上是把SELECT语句当成了一个“关系数据库里的JS console”。有个热词叫select replace(getdate(), -, _)这其实是一段典型的SQL Server风格写法。GETDATE()在SQL Server里取当前时间REPLACE替换字符串中的横杠为下划线。如果换到MySQL你得写成SELECT REPLACE(NOW(), -, _)换到Oracle则是SELECT REPLACE(SYSDATE, -, _) FROM DUAL。三个数据库函数名都不一样用常量记录一跑就能立刻发现差异这也是它的价值所在。2.4 单条常量记录加条件判断常量记录还可以带WHERE条件。比如SELECT 满足条件 AS result WHERE 1 1; -- 返回一行 SELECT 满足条件 AS result WHERE 1 2; -- 返回空结果这在排查逻辑、验证条件分支时很有用。但要注意在Oracle里不能直接这么写必须写成SELECT 满足条件 AS result FROM DUAL WHERE 1 1;因为WHERE子句是依附于FROM后的表来执行的Oracle没有FROM就谈不上“哪一行满足条件”。2.5 单条常量记录的经验笔记别名别碰保留字。比如SELECT x AS user在某些数据库里可能报错换成user_name或加反引号、双引号、方括号看平台才安全。中文乱码时先查会话字符集。SELECT character_set_clientMySQL或SELECT USERENV(language) FROM DUALOracle这类方式能快速定位。常量记录不是“免检产品”。你自己造的数据也要保证准确尤其是日期、金额这类敏感字段后面在排查章节我会讲一个真实翻车案例。3. 多条常量记录的三种构造方案3.1 UNION ALL拼接最通用最推荐要一次性造出多行数据最朴素也最可靠的办法就是用UNION ALL把多个单条SELECT串起来SELECT 2024-01 AS month, 0 AS cnt UNION ALL SELECT 2024-02, 0 UNION ALL SELECT 2024-03, 0;结果就是三行两列的一张“内存表”。为什么这里必须用UNION ALL而不是UNION两个原因。第一UNION会做去重操作如果常量的行本身就不重复去重就是白干还增加排序和比较的开销第二UNION的语义是合并去重UNION ALL的语义是直接拼接我们构造数据时通常希望保留所有行所以UNION ALL从语义到性能都更合适。用UNION ALL构造多行常量时还有几个要点各分支的列数必须一致。第一行选了3列后续每行也得是3列否则直接报错。结果集的列名由第一个SELECT的别名决定。后续分支的别名写不写都不影响结果列名我建议写上保证可读性。各分支列的类型要兼容。第一个分支第一列是字符串第二个分支第一列突然给个数字部分数据库会做隐式转换部分会直接报类型不一致错误。稳妥的做法是用CAST显式转换。3.2 值构造器VALUES代码更简洁在不少数据库里还可以用值构造器row value constructor也常被称为表值构造器来写多行常量。写法大致是MySQL 8.0.19以上版本VALUES ROW(1, 华东), ROW(2, 华北), ROW(3, 华南);PostgreSQL和SQL Server的写法很接近SELECT * FROM (VALUES (1, 华东), (2, 华北), (3, 华南) ) AS t(id, name);SQLite也支持类似的语句SELECT * FROM (VALUES (1, 华东), (2, 华北), (3, 华南) );这种写法比一串UNION ALL干净不少尤其适合行数固定的配置类数据。但它的兼容性问题比较明显MySQL老版本不支持独立的VALUES语句Oracle传统版本也不支持把VALUES放在FROM后面当派生表用。如果你写代码要跨数据库发布或者你不确定对方数据库版本最稳妥的方案仍然是UNION ALL。我自己在实际项目里有一个习惯代码评审时看到UNION ALL串十行以上我就会建议改成CTE或临时表因为可读性已经不行了。常量记录的位置应该在“少量、临时、一次性”而不是承载几百行配置数据。3.3 递归CTE生成序列动态多行记录除了手工枚举固定行还有一种“动态多条常量记录”的玩法用递归CTE生成序列。比如要生成1到10的数字序列WITH RECURSIVE seq (n) AS ( SELECT 1 UNION ALL SELECT n 1 FROM seq WHERE n 10 ) SELECT n FROM seq;这在以PostgreSQL、MySQL 8.0为代表的平台上写法一致。SQL Server和Oracle的递归CTE不需要RECURSIVE关键字直接写WITH seq (n) AS (...)。实际工作中我常用递归CTE生成连续日期序列用于补全报表时间轴WITH RECURSIVE dates (d) AS ( SELECT DATE(2024-01-01) UNION ALL SELECT d INTERVAL 1 DAY FROM dates WHERE d DATE(2024-01-31) ) SELECT d FROM dates;关于递归CTE有几个坑必须提醒终止条件必须写对。忘了写WHERE就是无限递归轻则查询卡死重则打爆数据库。MySQL里可以设置cte_max_recursion_depth作为兜底但千万别指望它。递归的起始行是常量但递归出来的每一行本质上是通过上一层计算得到的所以它已经不是严格意义上的“常量记录”而是“规则生成的序列”。它和手工枚举的定位不同适用于数据量大、有规律的场景。3.4 三种方案怎么选方案适用行数跨平台兼容性可读性典型场景UNION ALL1到几十行最好几乎所有数据库都支持行数多时偏啰嗦月份表、状态映射、少量测试数据值构造器VALUES1到几十行视数据库而定新版本支持好简洁同一平台下的固定配置递归CTE几十到上万行主流数据库均支持适合有规律的数据连续日期、递增序列、层级数据4. 主流数据库下常量记录的写法差异速查4.1 一张表看懂各平台差异我平时主要接触MySQL、Oracle、PostgreSQL、SQL Server也用过SQLite和达梦这里整理成一张表供参考数据库SELECT 1要不要FROM值构造器写法递归CTE关键字MySQL 5.7不需要不支持独立VALUES只能INSERT里用不支持WITHMySQL 8.0不需要VALUES ROW(1,a), ROW(2,b)WITH RECURSIVEOracle需要FROM DUAL传统版本常用UNION ALLWITH cte AS (...) 递归不用RECURSIVEPostgreSQL不需要SELECT * FROM (VALUES ...) AS t(...)WITH RECURSIVESQL Server不需要SELECT * FROM (VALUES ...) AS t(...)WITH cte AS (...)不用RECURSIVESQLite不需要支持VALUES派生表WITH RECURSIVE达梦视兼容模式通常需要FROM DUAL建议UNION ALL兼容Oracle风格这张表不需要背但值得收藏。你只要记住一个大原则写之前先确认平台别把一种数据库的SQL习惯原样搬到另一种上。4.2 为什么MySQL的SELECT可以没有FROM不少新人会疑惑SELECT不是查询表吗怎么可以不写FROM这其实涉及SQL语法设计上的一个历史特点。MySQL的语法从一开始就允许SELECT省略FROM子句此时相当于在一张“隐式的单行虚拟表”上做计算所以SELECT 1、SELECT NOW()都能直接跑。SQL Server同样允许PostgreSQL、SQLite也允许。唯独Oracle因为语法树和历史包袱必须引入DUAL这个显式的虚拟表来兜底。4.3 顺带说一句热词里的SELECT TOP最近看到有人搜select top 1000 * from [dbo].[dc_ods_rkyxjc_lotdatacollection]这是SQL Server系工具自动生成的“选择前1000行”脚本。TOP是SQL Server的方言关键字MySQL对应LIMITOracle对应FETCH FIRST 1000 ROWS ONLY或ROWNUM。这类差异和常量记录本没有直接关系但恰恰说明一个问题数据库方言无处不在你在这个平台写顺手的SQL换到另一个平台可能连跑都跑不起来。了解字段和SQL构造差异是吃这碗饭的基本功。4.4 函数名差异也是常见陷阱回到常量记录做表达式计算的场景很多函数在不同数据库里名字完全不同功能MySQLOracleSQL ServerPostgreSQL当前时间NOW()SYSDATEGETDATE()NOW()字符串拼接CONCAT()日期加天数DATE_ADD()date 1DATEADD()date INTERVAL 1 day这些差异用常量记录一条条试是最快的验证方式。你不需要把文档背下来但你要知道“怎么快速验证”。5. 进阶玩法常量记录与真实业务查询的组合5.1 报表补零用常量表当基准维度回到开头的报表需求。假设有一张sales_order表字段有order_date和amount要按月份统计2024年第一季度的销售额没有订单的月份也要显示0。用常量记录构造月份基准表SELECT dm.month, COALESCE(SUM(s.amount), 0) AS total_amount FROM ( SELECT 2024-01 AS month UNION ALL SELECT 2024-02 UNION ALL SELECT 2024-03 ) dm LEFT JOIN sales_order s ON DATE_FORMAT(s.order_date, %Y-%m) dm.month GROUP BY dm.month ORDER BY dm.month;关键点在于常量表放在LEFT JOIN的左侧这样即使右侧没有匹配的订单记录左侧月份仍然会保留下来配合COALESCE把NULL转成0。如果没有这张常量表直接对sales_order分组2024-02这个没订单的月份就“凭空消失”了。业务字段多时也可以先用月份常量表生成时间序列再在后续查询里多次JOIN这种“先造基准再关联事实”的思路是做报表的同学必须熟练掌握的。5.2 用常量记录当“临时字典表”SQL里经常要给状态码做翻译。比如订单表的status字段1表示待支付2表示已支付3表示已发货。你当然可以用CASE WHENSELECT status, CASE status WHEN 1 THEN 待支付 WHEN 2 THEN 已支付 WHEN 3 THEN 已发货 END AS status_name FROM orders;但如果多个查询都要用每处写一遍CASE很啰嗦。这时可以构造一个临时的状态字典SELECT o.id, o.status, d.status_name FROM orders o LEFT JOIN ( SELECT 1 AS status_cd, 待支付 AS status_name UNION ALL SELECT 2, 已支付 UNION ALL SELECT 3, 已发货 ) d ON o.status d.status_cd;这样字典和主查询分离代码清晰多了。临时性、小规模的映射直接用常量记录不值得为它建一张正式字典表。5.3 INSERT INTO SELECT从常量记录写到业务表有时候不只要“查”常量记录还要把它插入正式表。常见写法是INSERT INTO region (id, name) VALUES (1, 华东), (2, 华北), (3, 华南);也可以用常量SELECT的方式INSERT INTO region (id, name) SELECT 1, 华东 UNION ALL SELECT 2, 华北 UNION ALL SELECT 3, 华南;INSERT INTO SELECT的优势是可以通过WHERE条件控制插入逻辑比如防止重复插入INSERT INTO region (id, name) SELECT 1, 华东 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM region WHERE id 1);这在初始化数据、写批量脚本时特别实用。如果要插入上万行测试数据就不再适合手工枚举常量了用递归CTE批量生成是更高效的方式INSERT INTO seq_table (n) WITH RECURSIVE seq (n) AS ( SELECT 1 UNION ALL SELECT n 1 FROM seq WHERE n 10000 ) SELECT n FROM seq;5.4 把常量记录包装成参数集在存储过程或报表开发里我偶尔会把“参数”包装成常量子查询让整条SQL看起来更结构化。比如SELECT * FROM ( SELECT debug AS log_level, 1 AS enabled ) param WHERE param.enabled 1;或者配合变量做动态判断。虽然这类需求不算高频但它体现了常量记录的通用性常量记录不只是“临时测试数据”它可以作为一个独立的逻辑单元嵌入复杂SQL中让SQL的可读性和可维护性都更好。6. 常量记录常见报错与排查记录6.1 高频报错对照表报错信息出现场景解决方案ORA-00923: FROM keyword not found where expectedOracle/达梦中写SELECT 1漏了FROM加FROM DUALERROR 1064 (42000): You have an error in your SQL syntax在MySQL老版本里用了VALUES ROW(...)改用UNION ALL或确认版本≥8.0.19The used SELECT statements have a different number of columnsUNION ALL各分支列数不一致统一列数空缺补NULLORA-01790: expression must have same datatype as corresponding expressionOracle中UNION ALL列类型不兼容用CAST显式统一类型ORA-00904: ABC: invalid identifierOracle中把字符串写成了双引号改用单引号乱码中文常量输出为问号检查客户端字符集、NLS_LANG或连接参数递归CTE超出深度限制递归终止条件没写对或写太深检查WHERE终止条件或调大深度限制6.2 几个容易忽略的细节**UNION ALL的结果列名只看第一个分支。**后续分支起什么别名都不影响结果集列名。这一点经常被忽略尤其是多个分支列名不一样时最终结果列名会让人困惑。**字符串常量宽度按第一个分支推定。**比如第一个分支是SELECT a后面拼接SELECT 很长的一段字符串在某些数据库里可能出现截断或比较异常。稳妥起见关键列用CAST(xxx AS VARCHAR(50))显式声明类型。Oracle里的双引号是雷区。SELECT hello FROM DUAL不是查询字符串hello而是查询列hello直接报ORA-00904。这是从MySQL转Oracle的同学最容易踩的坑。**值构造器的行数别搞太大。**值构造器适合少量固定数据行数几十上百时可以但如果要几千行SQL语句本身的解析和内存开销就不划算了这时候用递归CTE或者临时表更合理。6.3 一次真实的翻车案例聊一个我亲眼见过的案例。有同事在MySQL里构造一批测试日期写成SELECT 2024-02-29 AS d UNION ALL SELECT 2024-02-30 UNION ALL SELECT 2024-03-01;单看每条SELECT都能跑拼接起来也不报错。但下游程序解析时遇到2024-02-30这个不存在的日期就崩了。排查了半天发现是常量记录本身造错了数据。这个案例提醒我常量记录虽然是“造”出来的数据但它同样需要业务正确性校验。日期、金额、状态值这类字段造数时就要想清楚业务规则不能因为不是查真实表就随意填。6.4 我的排查顺序真遇到常量记录相关的SQL运行异常我一般按这个顺序排查先在脑子里过一遍数据库类型和版本。MySQL 5.7和MySQL 8.0、Oracle和达梦可能踩的坑完全不一样。单独执行其中一个分支看是否报错。如果单条能跑通问题多半出在UNION ALL的列数或类型上。检查第一个分支的列名和类型尤其注意隐式转换。检查字符集相关的设置。把结果逐步缩小先去掉WHERE再去掉最后一个UNION ALL分支二分定位。这套顺序帮我快速定位过不少问题也分享给你参考。最后分享一点使用心得这个技巧看起来小但对提升SQL开发效率是真的有效。我个人在团队里带新人时经常把常量记录当作“SQL沙盒”来用不准备任何测试表直接让新同学用SELECT构造一批数据然后练习聚合、子查询、CTE和窗口函数。这个训练方式不需要维护测试库也不影响任何业务数据练习成本几乎为零。你也可以试试在常用的数据库客户端里用今天讲的几种方式造几行常量记录玩熟了之后你会发现很多原来要建临时表的场景其实一条SELECT就够了。