值存储
查询中的临时表达式可以保留自己的类型,但写入表列时,数据库必须把值转换成目标列声明的类型。这一步称为赋值转换或存储转换。它决定数据能否落库,也决定长度、精度、舍入、溢出和约束何时生效。
本文以通用 SQL 为主。不同数据库对“可赋值转换”的范围、字符串截断、数值舍入以及错误信息有差异;示例应在目标数据库及版本上验证。
写入时发生了什么
CREATE TABLE products (
product_id BIGINT,
price DECIMAL(12, 2),
stock INTEGER,
created_on DATE
);
INSERT INTO products (product_id, price, stock, created_on)
VALUES ('1001', '19.90', '8', '2025-01-01');
上例中的字符串字面量可能被数据库转换为 BIGINT、DECIMAL、INTEGER 和 DATE。转换成功后,数据库还会执行 NOT NULL、CHECK、唯一性、外键和域约束等检查。可以把流程理解为:
输入表达式 → 转换为列类型 → 检查长度/精度 → 检查约束 → 写入存储
转换失败时,通常整条 INSERT 或 UPDATE 失败;不要假设数据库会自动跳过坏行。
显式转换比依赖赋值转换更稳定
当来源类型与目标列一致时,不需要额外转换;类型不一致时,推荐显式写出意图:
INSERT INTO products (product_id, price, stock, created_on)
VALUES (
CAST(:product_id AS BIGINT),
CAST(:price AS DECIMAL(12, 2)),
CAST(:stock AS INTEGER),
CAST(:created_on AS DATE)
);
显式转换有三个好处:
- 错误更靠近输入边界,容易定位;
- 代码审查时能看出允许的格式和精度;
- 换数据库或驱动时,不依赖一组隐含的赋值规则。
应用程序更推荐使用参数绑定,并让驱动传递正确的参数类型。手工拼接 '19.90'、日期和转义字符既容易出错,也会造成 SQL 注入风险。
字符串长度和类型修饰符
字符列可能带长度限制:
CREATE TABLE users (
username VARCHAR(20) NOT NULL
);
写入超过限制的值时,数据库可能报错、截断空格,或按方言规则处理。不要把“当前没有报错”理解成跨数据库保证。需要截断时,应在业务层明确执行并记录,而不是依赖数据库静默改变用户输入。
CHAR(n) 通常会补齐或忽略尾随空格,VARCHAR(n) 通常保存实际长度;Unicode 字符是按字符数还是字节数计量,也可能受数据库和字符集影响。涉及姓名、地址和多语言文本时,应确认长度语义。
数值精度、舍入和溢出
CREATE TABLE invoices (
total DECIMAL(10, 2) NOT NULL
);
写入 123.456 时,数据库可能舍入为 123.46,也可能拒绝;写入超过整数位范围时通常会报溢出。精度定义的是列能保存的范围,不是展示格式:
-- 先在业务规则中明确舍入,再写入列
INSERT INTO invoices(total)
VALUES (CAST(ROUND(:raw_total, 2) AS DECIMAL(10, 2)));
中间计算也需要足够宽的类型。若先把很大的乘积放入窄类型,再转换成宽类型,溢出已经发生,外层 CAST 无法挽回:
-- 先提升输入,再进行乘法
SELECT CAST(unit_price AS DECIMAL(18, 4)) * CAST(quantity AS DECIMAL(18, 4))
FROM order_items;
日期、时间和时区
日期文本写入 DATE 列时,格式和会话设置可能参与解析:
INSERT INTO events (starts_on)
VALUES (CAST(:starts_on_iso AS DATE));
优先使用 ISO 风格参数或真正的日期参数,避免依赖 01/02/2025 这类地区格式。把带时区时间写入不带时区列,可能丢失时区信息;把时间戳转成 DATE 则会丢掉时分秒。存储前应明确业务需要的是“瞬时点”还是“当地日历时间”。
NULL、默认值和空字符串
NULL 是缺失/未知,不等同于空字符串、0 或默认值:
INSERT INTO products (price, stock)
VALUES (NULL, NULL);
如果列允许 NULL,上例会保存空值;如果需要默认值,通常应省略列或使用 DEFAULT:
INSERT INTO products (product_id, stock)
VALUES (:product_id, DEFAULT);
把空字符串转换为 NULL 是业务决策,不同数据库对空字符串和 NULL 的关系也可能不同。可以在写入前明确处理:
INSERT INTO users (nickname)
VALUES (NULLIF(TRIM(:nickname), ''));
COALESCE 只处理已经是 NULL 的值,不能捕获非法数字或日期转换异常。
域、约束与转换顺序
支持域类型的数据库允许在基础类型上附加约束:
-- PostgreSQL 示例
CREATE DOMAIN positive_amount AS DECIMAL(12, 2)
CHECK (VALUE >= 0);
写入域列时,值先转换为基础类型,再检查域约束。'12.345' 可能先按小数规则舍入,然后再检查是否非负;'-1' 即使格式正确,也会因域约束失败。表级 CHECK、外键和唯一性约束同样不是类型转换的替代品:类型正确的数据仍可能不满足业务条件。
INSERT ... SELECT 的批量转换
从暂存表导入时,源列类型常常与目标表不同:
INSERT INTO orders (order_id, total_amount, ordered_on)
SELECT CAST(order_id_text AS BIGINT),
CAST(total_text AS DECIMAL(14, 2)),
CAST(order_date_text AS DATE)
FROM staging_orders;
一行非法通常会让整个语句失败。安全导入流程应先识别坏行,再导入好行:
- 保留原始文本和来源标识。
- 使用格式校验或安全转换得到解析列。
- 把失败行写入错误表,记录原因。
- 只把已验证的行写入正式表。
正则表达式只能检查外形,不能完整替代数据库解析规则;例如指数、负数、小数位和时区都需要单独测试。
COPY、CSV 和外部输入
批量加载工具通常先把字段读成文本,再按目标列类型解析。以下问题很常见:
- CSV 中的空字段代表
NULL还是空字符串; - 千分位、货币符号和本地小数点是否允许;
- 日期格式和时区是否与数据库会话一致;
- 引号、换行和编码是否正确。
不要把 CSV 的“显示格式”直接当作数据库类型。导入前应固定编码、分隔符、空值标记和日期/数字格式,并用小批量样本验证边界值。
更新时的同样规则
UPDATE 的赋值转换与 INSERT 相同:
UPDATE products
SET price = CAST(:new_price AS DECIMAL(12, 2)),
stock = CAST(:new_stock AS INTEGER)
WHERE product_id = :product_id;
不要为了更新一列而把整行先转换成文本再写回。这样会增加精度丢失和并发覆盖的风险。使用参数化的窄更新,并让 WHERE 条件使用列的真实类型。
生成列、触发器和存储类型
生成列或触发器可能在显式写入后再次计算和转换:
CREATE TABLE order_items (
unit_price DECIMAL(12, 2),
quantity INTEGER,
amount DECIMAL(14, 2)
GENERATED ALWAYS AS (unit_price * quantity) STORED
);
这里输入列先完成赋值转换,再计算生成列,生成结果还必须符合 DECIMAL(14, 2)。触发器也可能把文本规范化、把时区转换或填充默认值。排查“数据库写入后值变了”时,应同时检查列类型、生成表达式、触发器和默认值。
从数据库读回的类型
存储类型决定驱动返回值的类型,但客户端可能再次转换:
DECIMAL可能返回字符串,以避免 JavaScriptNumber的精度损失;BIGINT可能映射到语言的大整数或字符串;- 时间戳可能转换为带时区或无时区对象;
NULL可能映射为null、None或其他空值。
应用接口应记录数据库类型到语言类型的映射。不要只看 JSON 展示结果判断数据库实际保存的是数字还是文本。
迁移与回填时的安全策略
修改列类型或批量回填时,建议分阶段执行:
- 统计现有值的格式、范围和空值比例。
- 增加临时规范化列或暂存表。
- 分批转换,记录失败行并验证计数。
- 通过约束和索引确认新值满足业务规则。
- 切换读写路径后,再删除旧列。
直接执行 ALTER COLUMN ... TYPE 可能锁表、触发全表重写或在第一条坏数据处失败。具体锁和转换策略属于数据库方言,必须先阅读目标版本文档并在副本上演练。
常见误区
认为字符串一定可以写入任何列
有些数据库会接受 '10' 写入整数列,有些场景会拒绝;即使接受,也不代表 '10元'、空字符串和空格拥有同样行为。
依赖静默截断
被截断的用户名、金额或日期会造成不可逆的数据损失。需要截断时显式执行并记录。
用 COALESCE 捕获转换错误
COALESCE(CAST('bad' AS INTEGER), 0) 仍可能先因 CAST 失败;请使用安全转换或预校验。
忽略约束执行时机
类型转换成功不代表约束成功。负数、重复键、缺失外键和不满足 CHECK 的值仍会被拒绝。
小结
- 写入表列时,数据库会执行赋值转换,然后检查长度、精度和约束。
- 显式转换、参数化写入和分阶段导入能让错误更早暴露。
- 字符串长度、数值舍入、日期时区、
NULL和域约束都需要单独设计。 - 外层转换无法挽回已经发生的溢出或精度损失。
- 批量导入和类型迁移应保留原始值、隔离坏行并在目标数据库上演练。