原创

二进制字符串与文本字符串:摘要字段怎么存

SHA-256 是 32 字节,常见日志却显示 64 个字符。数据库里把它定义成 CHAR(64) 并没有存错,只是存了十六进制文本。要不要换成二进制列,关键取决于摘要的输入契约和查询方式,不能只凭“二进制更省”就改表。

MySQL 8.4 的 SHA2(value, 256) 返回十六进制字符串;UNHEX 把每两个十六进制字符还原成一个字节。展示形式与计算结果应该分层处理:API 可以传 64 位文本,数据库保存 32 字节,查询结果再按接口约定编码。

摘要字段与身份字段各司其职

假设一个文件清单用独立整数主键,摘要负责相同内容检测。先采用变长二进制列并约束真实长度,避免短输入被悄悄补齐。

CREATE TABLE artifact_digest (
    id BIGINT NOT NULL,
    file_name VARCHAR(200) NOT NULL,
    byte_length BIGINT UNSIGNED NOT NULL,
    sha256 VARBINARY(32) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uq_artifact_sha256 (sha256),
    CONSTRAINT ck_sha256_exact_length
        CHECK (OCTET_LENGTH(sha256) = 32)
) ENGINE=InnoDB;

SET @digest_hex = SHA2('sample-content-v1', 256);

INSERT INTO artifact_digest (id, file_name, byte_length, sha256)
VALUES (101, 'sample.txt', 17, UNHEX(@digest_hex));

SELECT id, file_name, OCTET_LENGTH(sha256) AS stored_bytes,
       LOWER(HEX(sha256)) AS sha256_hex
FROM artifact_digest;

sample-content-v1 是 17 个 ASCII 字节,示例的长度与内容一致。真实文件摘要应由文件实际字节计算;路径、文件名和 JSON 序列化后的元数据不能混作文件内容。对文本做摘要时,还要固定 UTF-8、换行符和是否包含 BOM,数据库无法知道两个调用方有没有先改写输入。

这里让摘要唯一,是文件去重策略的一部分。若业务允许相同内容被不同用户独立登记,唯一键就应包含相应业务维度,或者把内容对象与用户文件记录分开。不能因为算法相同,就让一个全局唯一索引吞掉本来应该存在的两条授权关系。

独立主键也有实际用途:摘要算法可能升级,文件属性可能修正,其他表的引用不必随内容指纹一起改。摘要适合回答“内容是否相同”,不必同时承担所有对象身份。

BINARY(32) 看起来严格,实际会补零

定长二进制列会把短值在右侧补 0x00。如果输入已经由可靠程序保证为 32 字节,BINARY(32) 是合理选择;如果期望数据库直接拒绝短值,它不是长度验证器。

CREATE TABLE binary_padding_sample (
    id INT NOT NULL,
    fixed_value BINARY(4) NOT NULL,
    variable_value VARBINARY(4) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

INSERT INTO binary_padding_sample
    (id, fixed_value, variable_value)
VALUES (1, UNHEX('AB'), UNHEX('AB'));

SELECT HEX(fixed_value), OCTET_LENGTH(fixed_value),
       HEX(variable_value), OCTET_LENGTH(variable_value)
FROM binary_padding_sample;

预期前者是 AB000000、长度 4,后者是 AB、长度 1。对补齐后的定长列添加“长度等于 4”的检查没有意义,因为存储值已经变成 4 字节。前面的摘要表采用 VARBINARY(32) 配合长度检查,才会拒绝短值。

-- 单独执行;预期 CHECK 约束失败。
INSERT INTO artifact_digest (id, file_name, byte_length, sha256)
VALUES (102, 'broken.txt', 0, UNHEX('AB'));

过长输入也应在应用入口拒绝,并使用严格 SQL 模式。仅靠目标列宽度,在允许截断的会话条件下可能把错误输入改成一个合法长度的值。约束验证存储值,入口验证原始值,两者都需要明确。

文本方案的代价并不等于四倍

一个 SHA-256 摘要的二进制载荷是 32 字节,ASCII 十六进制载荷是 64 字节。若用 CHAR(64) CHARACTER SET ascii 保存,不能再拿 utf8mb4 的最大单字符宽度去宣称它必然占 256 字节。真实页空间还受列类型、行格式和索引记录等条件影响。

二进制方案缩短了摘要索引中的键,但这不等于查询自动快一倍。缓存命中、数据量、访问路径、网络和应用解码都参与成本。没有相应工作负载的测量,就只谈确定的宽度与比较语义。

文本方案的优势是手工查询和日志定位直接,且与很多外部工具的输入相符。若决定保留文本,应固定字符集、长度和大小写规范,避免同一摘要出现大小写两份表示。使用不区分大小写的规则可以让它们比较相同,但仍不能替代验证输入是否真的是 64 位十六进制。

二进制值按字节比较,没有文本排序规则的大小写和尾随空格问题。客户端直接显示出来可能是十六进制,也可能是转义内容;API 不应把驱动返回的字节对象直接交给 JSON 序列化器猜测。

在参数这一侧转换

查询时把外部文本解码成二进制,再比较原列:

SET @requested_hex = SHA2('sample-content-v1', 256);

EXPLAIN
SELECT id, file_name
FROM artifact_digest
WHERE sha256 = UNHEX(@requested_hex);

不推荐把常规查询写成 HEX(sha256) = ?。那会对索引列施加函数,普通的摘要索引不能因此自然变成对十六进制表达式的索引。这个模型已具备二进制索引,没必要每次查找先把整列编码回文本。

应用端可以直接绑定 32 字节参数,连 UNHEX 也不需要。使用十六进制参数时,先验证恰好 64 个 ASCII 十六进制字符;UNHEX 对非法字符可能返回 NULL,把“参数无效”变成“查无此文件”,会误导调用方。零结果与输入错误应有不同的响应。

再插入相同摘要的另一份表示:

-- 单独执行;预期唯一键失败,大小写编码还原后仍是同一组字节。
INSERT INTO artifact_digest (id, file_name, byte_length, sha256)
VALUES (103, 'copy.txt', 17, UNHEX(UPPER(@digest_hex)));

算法输出是否碰撞、业务允许怎样的去重策略,与十六进制大小写是不同问题。对内容一致性要求高的业务,可在候选相同摘要后继续比较大小或实际内容;这属于业务完整性策略,不能靠文本列改二进制列自动解决。

迁移时先拒绝坏编码,再做转换

下面给出一个可独立建立的旧表模型。它故意不带十六进制检查,用于发现历史坏数据。

CREATE TABLE legacy_artifact_digest (
    id BIGINT NOT NULL,
    sha256_hex VARCHAR(100) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

INSERT INTO legacy_artifact_digest (id, sha256_hex) VALUES
    (1, SHA2('one', 256)),
    (2, UPPER(SHA2('two', 256))),
    (3, 'not-a-hash');

SELECT id, sha256_hex
FROM legacy_artifact_digest
WHERE OCTET_LENGTH(sha256_hex) <> 64
   OR NOT REGEXP_LIKE(sha256_hex, '^[0-9A-Fa-f]{64}$', 'c');

先隔离第三行,不要用 COALESCE(UNHEX(...), 0x00) 给坏摘要补一个默认值。之后再对合法行按解码结果分组,检查旧系统是否允许大小写两份文本分别占位。迁移时这种表示差异会合并成相同二进制键,必须在创建唯一索引前处理。

切换应用后,日志继续输出规范的小写十六进制,存储和参数使用原始字节。数据库里少存一半摘要载荷,排查时仍然能拿日志值定位记录,这两件事并不冲突。

正文到此结束
评论插件初始化中...
Loading...
本文目录