类型转换概述
SQL 中的每个值都有数据类型,例如整数、字符、日期和布尔值。类型转换就是把一个值按目标类型重新解释或格式化,例如把文本 '42' 转成整数 42,或者把时间戳转换成日期。
类型转换主要解决两个问题:让数据类型匹配,以及明确计算和比较的语义。转换不是“把任何内容都变成想要的样子”:目标类型的格式、范围和精度都有约束,转换失败、截断或舍入都可能改变结果。
本文以通用 SQL 为主。不同数据库对可转换的类型、函数名称、失败行为和隐式转换优先级都有差异,示例用于理解概念;投入生产前应核对目标数据库及版本的文档。
显式转换和隐式转换
显式转换
显式转换由开发者在 SQL 中写出,意图清晰,结果更容易审查和测试。标准 SQL 最常见的写法是 CAST:
SELECT
CAST('42' AS INTEGER) AS quantity,
CAST(19.9 AS DECIMAL(10, 2)) AS price,
CAST(created_at AS DATE) AS created_date
FROM orders;
CAST(expression AS type) 的左侧是待转换表达式,右侧是目标类型。目标类型可以是 INTEGER、DECIMAL(10, 2)、VARCHAR(20)、DATE 等,但具体类型名称和参数规则取决于数据库。
部分数据库还提供方言函数,例如 SQL Server 的 CONVERT(type, expression)、MySQL 的 CONVERT(expression, type) 或 CAST(... AS CHAR)。它们可能支持格式样式参数,但可移植性较弱。需要跨数据库时,优先使用标准 CAST,并把方言写法集中在适配层。
隐式转换
隐式转换由数据库根据上下文自动完成。例如,某些数据库会把字符 '10' 自动转换成数字后再与整数比较:
-- 是否能执行、如何转换,取决于数据库
SELECT *
FROM products
WHERE product_code = 10;
隐式转换写起来方便,但不应成为业务逻辑的基础,原因包括:
- 同一条 SQL 在不同数据库中可能成功、失败或得到不同结果;
- 字符串格式不合法时,查询可能在运行时才报错;
- 自动选择的类型可能造成溢出、舍入或整数除法;
- 对列做转换可能无法使用索引,导致查询变慢。
当比较、计算或写入的数据类型不一致时,通常应显式转换,并确保转换方向符合业务含义。
转换的三个步骤
可以按下面的思路分析任何一次类型转换:
- 确认来源类型:值是文本、数字、日期还是
NULL?是否可能包含空字符串、前导零或非法字符? - 确定目标类型和规则:需要多大范围、多少精度、什么日期格式?超出范围时应报错还是取默认值?
- 验证边界和后续用途:转换结果用于展示、比较、排序、计算还是连接?是否会影响索引、时区或精度?
例如,订单金额从文本导入数据库时,不应只写 CAST(amount_text AS DECIMAL(10, 2)),还要决定空文本、货币符号、超长小数和负数如何处理。
常见类型转换
字符串与数字
SELECT
CAST('2024' AS INTEGER) AS year_number,
CAST(7 AS VARCHAR(10)) AS year_text;
数字转字符串通常用于展示或拼接;字符串转数字则用于排序和数学运算。数字文本的格式必须符合目标数据库的规则:'42' 通常可以转换,而 '42元'、'1,000' 或 '' 是否可以转换,不能假设跨数据库一致。
不要用字符串保存本应计算的数量,然后按字符串排序:
-- 字符串排序可能是 1、10、2
SELECT code FROM products ORDER BY code;
-- 先转换为数字,才按数值顺序排序
SELECT code
FROM products
ORDER BY CAST(code AS INTEGER);
更好的长期方案是从数据源开始使用正确的数值列;查询时反复转换通常说明表结构或导入流程需要改进。
精确数值与近似数值
涉及金额、税率和可对账结果时,优先转换为 DECIMAL/NUMERIC,并指定足够的精度和小数位:
SELECT CAST(unit_price AS DECIMAL(12, 2)) * quantity AS amount
FROM order_items;
转换为 FLOAT、REAL 或 DOUBLE PRECISION 会得到近似值,适合测量和科学计算,不适合作为财务精确值。转换到较小的 DECIMAL(p, s) 可能舍入或报错,具体行为要用目标数据库测试。
日期和时间
SELECT
CAST(created_at AS DATE) AS created_date,
CAST('2025-01-01' AS DATE) AS start_date
FROM events;
把时间戳转成 DATE 通常会丢掉时分秒;带时区的时间还可能先按会话时区换算,再截取日期。不要依赖 '01/02/2025' 这类受地区影响的字符串格式,优先使用 ISO 风格的日期文本或参数绑定,并明确时区。
日期转字符串通常不是通用 CAST 能完整表达的格式化需求。数据库一般提供 TO_CHAR、DATE_FORMAT、FORMAT 等方言函数,使用时应确认语言、时区和格式标记。
布尔值
不同数据库对布尔类型和文本表示的支持不一致。TRUE、FALSE、1、0、'Y'、'N' 不应直接互换:
-- 目标数据库支持 BOOLEAN 时
SELECT CAST(is_enabled_text AS BOOLEAN)
FROM feature_flags;
如果来源数据使用多种标记,应先用明确的 CASE 映射并处理未知值,而不是依赖隐式转换:
SELECT CASE UPPER(TRIM(flag_text))
WHEN 'Y' THEN TRUE
WHEN 'N' THEN FALSE
ELSE NULL
END AS is_enabled
FROM staging_flags;
上例中的布尔字面量和函数写法仍可能有方言差异;NULL 表示无法判断,不等同于 FALSE。
NULL 与类型转换
NULL 表示未知或缺失。大多数数据库中,CAST(NULL AS INTEGER) 的结果仍是 NULL,不会自动变成 0:
SELECT CAST(NULL AS INTEGER) AS quantity;
如果需要默认值,应先明确业务语义,再使用 COALESCE:
SELECT COALESCE(CAST(quantity_text AS INTEGER), 0) AS quantity
FROM import_rows;
这里的 0 表示“确实为零”。若原值是“未知”,直接替换成零会丢失信息。还要注意:先转换再 COALESCE 时,非法文本仍可能让查询失败;COALESCE 只能处理 NULL,不能捕获转换异常。
转换失败、截断和舍入
转换失败一般发生在三类情况:
- 格式不合法:例如把
'abc'转为整数; - 范围不够:例如把超大整数转为
SMALLINT; - 精度不够:例如把多位小数放入
DECIMAL(5, 2)。
数据库可能直接报错,也可能截断或舍入。不要把“当前没有报错”当作数据安全的证明,尤其要测试空值、负数、最大值、最小值、非法字符和多余小数位。
部分数据库提供安全转换函数,例如 SQL Server 的 TRY_CAST、BigQuery 的 SAFE_CAST。它们通常在失败时返回 NULL,但并非通用 SQL:
-- 仅在支持 TRY_CAST 的数据库中使用
SELECT TRY_CAST(raw_value AS INTEGER) AS parsed_value
FROM staging_data;
跨数据库的通用做法是先用格式校验筛选,再执行 CAST,同时把失败行写入隔离表或错误报告。不要只用正则表达式判断数字:符号、小数、指数和数据库允许的空白规则都需要纳入测试。
CASE 与条件转换
当来源值有多种格式时,先分类再转换通常更安全:
SELECT CASE
WHEN raw_status IN ('1', 'true', 'TRUE') THEN 1
WHEN raw_status IN ('0', 'false', 'FALSE') THEN 0
ELSE NULL
END AS status_code
FROM staging_data;
CASE 的各分支最终需要兼容的结果类型。某些数据库可能在执行前就推断或转换所有分支,因此不要假设“不会走到的分支”一定不会触发非法转换;必要时使用数据库提供的安全转换函数,并在目标环境验证。
对比较、连接和排序的影响
比较
比较两种类型时,转换方向会影响结果。例如数字 2 与文本 '02' 在数值语义下相等,在字符串语义下不一定相等。比较前应统一类型:
SELECT *
FROM accounts
WHERE CAST(account_id AS VARCHAR(20)) = :account_id_text;
更推荐让参数使用列的真实类型,避免转换列本身:
SELECT *
FROM accounts
WHERE account_id = :account_id;
连接
连接键应在表设计阶段保持相同类型。对连接列做转换不仅容易产生匹配错误,还可能让两张表的索引失效:
-- 不推荐:每行都转换,且可能无法使用 account_id 索引
... JOIN accounts AS a
ON CAST(events.account_id_text AS INTEGER) = a.account_id
如果历史数据暂时无法修改,应在数据清洗阶段转换并存储规范化列,或建立合适的表达式索引(前提是目标数据库支持)。
排序和索引
对字符列转换后排序可以得到数值顺序,但通常需要额外计算。对 WHERE、JOIN 或 ORDER BY 中的索引列套函数,也可能使普通索引无法直接使用。应查看执行计划,必要时使用正确类型的列、生成列或专用索引。
参数化查询优于手工拼接
应用程序传入的值应使用参数绑定,并让驱动或数据库知道参数类型:
SELECT id, total_amount
FROM orders
WHERE customer_id = :customer_id
AND created_at >= :start_date;
参数化不仅能防止 SQL 注入,也能避免手工拼接引号、日期格式和小数点造成的转换问题。表名、列名等不能作为普通值参数绑定时,应使用服务端维护的白名单映射。
实践建议
- 优先从源头修正类型:表列、参数和应用对象尽量保持一致,减少查询时转换。
- 重要逻辑显式转换:金额、日期、连接键和跨类型比较不要依赖隐式规则。
- 明确失败策略:区分报错、返回
NULL、默认值和隔离脏数据,不要静默吞掉错误。 - 预留计算空间:
DECIMAL的精度应覆盖中间结果,而不只是最终展示位数。 - 先转换参数,少转换列:这样更有利于使用索引,也更容易发现非法输入。
- 测试方言和边界:在目标数据库上验证格式、时区、舍入、溢出、
NULL和执行计划。
小结
CAST是通用 SQL 中最常见的显式转换语法;CONVERT、TRY_CAST等通常属于数据库方言。- 隐式转换可能隐藏格式错误、精度损失和性能问题;跨类型逻辑应尽量显式化。
- 字符串转数字、时间戳转日期、精确数值与浮点数之间的转换都可能丢失信息。
NULL、非法格式、范围溢出、舍入和时区必须分别设计和测试。- 表列和参数优先使用正确类型,避免在索引列、连接键上临时转换。