跳到主要内容

值存储

查询中的临时表达式可以保留自己的类型,但写入表列时,数据库必须把值转换成目标列声明的类型。这一步称为赋值转换或存储转换。它决定数据能否落库,也决定长度、精度、舍入、溢出和约束何时生效。

本文以通用 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;

一行非法通常会让整个语句失败。安全导入流程应先识别坏行,再导入好行:

  1. 保留原始文本和来源标识。
  2. 使用格式校验或安全转换得到解析列。
  3. 把失败行写入错误表,记录原因。
  4. 只把已验证的行写入正式表。

正则表达式只能检查外形,不能完整替代数据库解析规则;例如指数、负数、小数位和时区都需要单独测试。

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 可能返回字符串,以避免 JavaScript Number 的精度损失;
  • BIGINT 可能映射到语言的大整数或字符串;
  • 时间戳可能转换为带时区或无时区对象;
  • NULL 可能映射为 null、None 或其他空值。

应用接口应记录数据库类型到语言类型的映射。不要只看 JSON 展示结果判断数据库实际保存的是数字还是文本。

迁移与回填时的安全策略​

修改列类型或批量回填时,建议分阶段执行:

  1. 统计现有值的格式、范围和空值比例。
  2. 增加临时规范化列或暂存表。
  3. 分批转换,记录失败行并验证计数。
  4. 通过约束和索引确认新值满足业务规则。
  5. 切换读写路径后,再删除旧列。

直接执行 ALTER COLUMN ... TYPE 可能锁表、触发全表重写或在第一条坏数据处失败。具体锁和转换策略属于数据库方言,必须先阅读目标版本文档并在副本上演练。

常见误区​

认为字符串一定可以写入任何列​

有些数据库会接受 '10' 写入整数列,有些场景会拒绝;即使接受,也不代表 '10元'、空字符串和空格拥有同样行为。

依赖静默截断​

被截断的用户名、金额或日期会造成不可逆的数据损失。需要截断时显式执行并记录。

用 COALESCE 捕获转换错误​

COALESCE(CAST('bad' AS INTEGER), 0) 仍可能先因 CAST 失败;请使用安全转换或预校验。

忽略约束执行时机​

类型转换成功不代表约束成功。负数、重复键、缺失外键和不满足 CHECK 的值仍会被拒绝。

小结​

  • 写入表列时,数据库会执行赋值转换,然后检查长度、精度和约束。
  • 显式转换、参数化写入和分阶段导入能让错误更早暴露。
  • 字符串长度、数值舍入、日期时区、NULL 和域约束都需要单独设计。
  • 外层转换无法挽回已经发生的溢出或精度损失。
  • 批量导入和类型迁移应保留原始值、隔离坏行并在目标数据库上演练。