加密字段不能直接GROUP BY,因密文随机性导致每行唯一;脱敏字段须先生成再分组,避免动态计算;推荐用STORED生成列预解密或写入时归一化哈希/掩码字段以保障性能与准确性。

加密字段不能直接 GROUP BY,脱敏字段也不能靠“边查边脱敏”来分组——这两类操作若在查询时动态计算,必然不可索引、性能崩盘、结果错乱。
为什么 GROUP BY encrypted_email 总是每行一组?
数据库只做字节比对,不触发解密。AES 加密带随机 IV,相同邮箱每次加密结果都不同;哪怕用固定 IV,Base64 编码差异或填充字节也会让密文不等价。你看到的 COUNT(*) = 1 每行,就是这个原因。MySQL、PostgreSQL、SQL Server 全部如此,没有例外。
常见错误现象包括:
-
GROUP BY AES_DECRYPT(encrypted_email, 'key')在 MySQL 报Invalid use of group function,在 PostgreSQL 报column "xxx" must appear in the GROUP BY clause - 写成
SELECT AES_DECRYPT(...), COUNT(*) GROUP BY AES_DECRYPT(...)—— 表达式重复写两遍,且无法走索引 - 漏掉
WHERE AES_DECRYPT(...) IS NOT NULL,导致 NULL 自成一组,统计多出一堆脏数据
MySQL 8.0+ 推荐用 STORED 生成列预解密
把解密逻辑固化进表结构,后续当普通字段用,可建索引、可 GROUP BY、无运行时开销。
必须注意三点:
- 用
STORED(不是VIRTUAL),否则无法建索引:ALTER TABLE users ADD COLUMN email_plain VARCHAR(255) GENERATED ALWAYS AS (CONVERT(AES_DECRYPT(encrypted_email, 'my_key') USING utf8mb4)) STORED; - 显式
CONVERT(... USING utf8mb4),避免隐式转换导致索引失效 - 立刻建索引:
CREATE INDEX idx_email_plain ON users(email_plain);
之后分组就和普通字段一样:SELECT email_plain, COUNT(*) FROM users GROUP BY email_plain;
⚠️ 密钥轮换时需 DROP COLUMN + 重建,所有行会重新计算生成列值——上线前务必验证流程。
脱敏字段分组必须先生成掩码再分组,不能 GROUP BY 原始字段
想统计“138****1234”出现次数,GROUP BY 的对象必须是脱敏后的字符串,而不是原始手机号。
正确做法是:
- 在
SELECT和GROUP BY中使用**完全一致的表达式**:SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS masked_phone, COUNT(*) FROM users WHERE phone IS NOT NULL GROUP BY CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)); - 用
COALESCE(phone, '')统一空值语义,否则 NULL 会让整个表达式返回 NULL,导致分组断裂 - 避免在
WHERE或HAVING中对脱敏后字段做精确匹配(如WHERE masked_phone = '138****1234'),这等于变相还原明文
如果脱敏逻辑复杂(比如正则替换、多层嵌套),建议封装为数据库函数或用 CTE 提前算好,别堆在 GROUP BY 里重复写。
真正适合分组的不是可逆加密,而是确定性哈希或写入时归一化
业务上要高频统计某类用户,最稳的方案是在写入时就存一个可索引的归一化字段,比如:
-
email_domain_hash BINARY(32):存HMAC-SHA256(SUBSTRING_INDEX(email, '@', -1), 'salt') -
phone_prefix CHAR(3):存手机号前三位,直接可索引、可GROUP BY - 用 MyBatis-Plus 等 ORM 在应用层完成脱敏/哈希,数据库只存结果
这类字段不依赖密钥、不触发解密、不拖慢查询,密钥轮换只需重跑一次 ETL,不影响线上服务。而所有试图在 GROUP BY 里塞 AES_DECRYPT、ENCRYPTBYKEY、SUBSTRING(ENCRYPT...) 的写法,本质都是拿性能换便利,数据量一上来就暴露问题。
最容易被忽略的点是:生成列、物化视图、哈希字段,全都要求密钥或 salt 硬编码在定义里——一旦密钥变更,这些结构必须重建,且历史数据不会自动更新。这不是配置问题,是架构约束。

















