先说结论:BIT_COUNT 这个函数,在 MySQL 里用好了是神器,用错了就是坑。核心就是,它统计的是整数在无符号二进制形式下,“1”这个位出现的次数。不是字符串,不是十六进制文本,也不是拿个负数去跑补码位模式。
比如 BIT_COUNT(5) 返回 2,因为 5 的二进制是 101,看得出两个 1;BIT_COUNT(0) 自然是 0;但 BIT_COUNT(-1) 呢?MySQL 会先把它转成一个巨大的无符号数(64 位下是 18446744073709551615),然后数出来 64 个 1。嗯,这往往不是你要的结果。

函数到底在“数”什么?
所以,用之前得先确认一件事:你的字段,确实存的都是非负整数吗?最好是 TINYINT UNSIGNED、INT UNSIGNED 这种。否则结果不可靠,这是最容易出问题的地方。
几个常见的“我以为能行”的误操作
很多人以为 BIT_COUNT 可以直接扔进去一个字符串或者十六进制值,实际上一跑就发现不对劲:
BIT_COUNT('1010')直接报错:Operand should be a number,字符串不是数字。BIT_COUNT(UNHEX('A'))返回 2——因为UNHEX('A')先转成字节0x0A,也就是十进制 10,10 的二进制是1010,有两个 1。但你要是想统计的是字母 "A" 这个字符的 ASCII 二进制里有多少个 1,那就完全跑偏了。- 有些表里权限字段是
TINYINT(有符号),还存了个负数。这时BIT_COUNT的结果会突然跳变,调试起来会非常头大。
正确的做法是什么样的?
- 确保输入是无符号整数:建表时直接定义为
INT UNSIGNED,或者查询时用CAST(col AS UNSIGNED)强制转一下。 - 如果原始数据是十六进制字符串,比如
'FF',那就先用CONV('FF', 16, 10)转成数字,再传给BIT_COUNT。 - 字段语义必须是位图:也就是说,存的值本身就是拿来做位运算的,而且只写入非负值,否则就别折腾了。
真实查询和性能注意事项
假设有一张用户权限表,perms 列是 INT UNSIGNED,每个 bit 代表一个功能开关。想找出开启了超过 3 个功能的用户,很自然就会写:
SELECT user_id, perms, BIT_COUNT(perms) AS active_bits FROM users WHERE BIT_COUNT(perms) > 3;
这个查询能跑,但有几个地方需要特别留个心眼:
BIT_COUNT是计算型函数,不走索引。所以WHERE BIT_COUNT(perms) > 3必然是全表扫描。- 如果业务经常按“开启的位数范围”来筛选,建议直接加一列
bit_count_cache TINYINT UNSIGNED,用触发器或者应用层维护这个值。 - MySQL 8.0+ 支持函数索引,但你得用生成列的方式来建,比如虚拟列 + 索引,不是所有情况下都通用。
跨数据库的“陷阱”
这一点尤其需要警惕:BIT_COUNT 是 MySQL 特有的函数。PostgreSQL、SQL Server、SQLite 都不支持同名函数。如果你在做的项目未来可能会从 MySQL 迁移到其他数据库,或者要同时兼容多个数据库,那就别指望 BIT_COUNT 能通用。
- PostgreSQL:没有内置的 BIT_COUNT,得靠
LENGTH(REPLACE(TRANSLATE(x::text, '0', ''), '1', ''))这类非常绕的写法,或者自定义函数。 - SQL Server:通常要手动拆位,比如
(x & 1) + ((x >> 1) & 1) + ...,写起来很啰嗦。 - SQLite:更没有,只能应用层处理或用扩展。
这意味着,位统计的逻辑最好下沉到应用层去处理,或者用可移植的表达式替代——虽然写起来更麻烦,但至少不会数据库一换就跑不起来了。
最后,还有一个容易让人踩坑的细节:BIT_COUNT 对 NULL 返回的是 NULL,不是 0。如果字段允许为空,别忘了加 IFNULL(BIT_COUNT(col), 0) 显式处理,否则筛选条件可能悄无声息地忽略掉一批数据。
