直接拿加密字段去 GROUP BY ,你会发现一件事——没戏。真的没戏。这不是数据库欺负你,是加密算法在设计上就没打算让你这么干。

你想,同一个邮箱地址,用 AES 配上随机 IV 加密之后再存进去,字节完全不一样。数据库的 GROUP BY 是纯字节比较,它不会触发解密逻辑——它只认那一串 0101。于是每组都只有一条记录,COUNT 永远返回 1。这在 PostgreSQL、MySQL、SQL Server 上是通病,没有哪家能例外。
为什么加密字段不能直接 GROUP BY
根本原因就在这儿:加密算法的设计目标是要保证相同明文的密文看起来完全不同,这跟 GROUP BY 的字节比较逻辑完全相悖。AES 的随机 IV、填充方式差异、Base64 编码不一致,随便一个因素就能让密文长得完全不同。你看到的“每条记录自成一组”“COUNT 始终为 1”,就是这个原因。数据库不负责解密,它只认二进制是否相等——既然不等,那就每一行都是孤家寡人。
实时解密再分组:能用但极不推荐
当然,你也可以在查询时现场把数据解密出来再分组。MySQL 里的 AES_DECRYPT() 就能干这事儿,但得同时处理三件事:
AES_DECRYPT()返回的是VARBINARY,你得用CONVERT(... USING utf8mb4)转成字符串,否则字符集会错乱,出来的东西根本没法看。- 解密失败会返回
NULL,而NULL在分组里自成一组,必须加WHERE AES_DECRYPT(...) IS NOT NULL来过滤掉,否则数据会莫名其妙地多出一行。 - 最关键的是,每次查询都得全表解密加类型转换,索引完全用不上。10 万行以上的数据,查询速度会肉眼可见地降下来。
说实话,这种写法只适合小数据量的临时排查。贴个例子方便你理解:
SELECT CONVERT(AES_DECRYPT(encrypted_email, 'key') USING utf8mb4) AS email_plain, COUNT(*)
FROM users
WHERE AES_DECRYPT(encrypted_email, 'key') IS NOT NULL
GROUP BY CONVERT(AES_DECRYPT(encrypted_email, 'key') USING utf8mb4);
真正可行的方案:写入时归一化或结构层预解密
把解密的时机从查询时挪到写入时或表结构层,才能在安全性和性能之间找到平衡。几个靠谱的思路:
- 入库时额外存一个确定性哈希字段,比如
HMAC-SHA256(email, 'salt'),然后直接GROUP BY email_hash。这个方案又快、又支持索引、还防碰撞。 - MySQL 8.0+ 可以用
STORED生成列:ALTER TABLE users ADD COLUMN email_plain VARCHAR(255) GENERATED ALWAYS AS (CONVERT(AES_DECRYPT(encrypted_email, 'key') USING utf8mb4)) STORED;,之后直接给这个列建索引,查询就会快很多。 - PostgreSQL 则推荐物化视图:
CREATE MATERIALIZED VIEW users_email_plain AS SELECT id, convert_from(decrypt(encrypted_email, 'key'::bytea, 'aes'), 'UTF8') AS email FROM users;,定期刷新后直接分组,性能和灵活性都兼顾。
注意,生成列和物化视图都需要把密钥硬编码在定义里。密钥轮换时,这两类对象都得重建——这一点常被忽略,上线前务必走一遍流程,验证它的可维护性。
别碰 WHERE 或 GROUP BY 里的解密函数
写 GROUP BY AES_DECRYPT(encrypted_email, 'key') 或者 WHERE AES_DECRYPT(...) = 'xxx' 是最典型的误用方式。MySQL 会直接报错(Invalid use of group function),PostgreSQL 也拒绝执行——volatile 函数无法被索引。这类写法既不快也不可靠,且无法被任何优化器加速。
说到底,加密字段分组的本质矛盾是:业务需要语义一致性(email 相同就算一组),而加密保障的是字节不可预测性(密文永远不同)。绕不开的取舍是——要么放弃可逆加密,改用哈希归一化;要么接受写入时多存一列脱敏标识。想靠一条 SQL 现场解密搞定,只会让查询越来越慢、越来越难维护。
