Sin
In a
Nutshell
MongoDB 位运算查询的一个小坑

MongoDB 位运算查询的一个小坑

April 23, 2018 /专业打杂一百年

MongoDB 3.2 引入了位查询,这个功能在权限和多选项这样的场景下非常适用,我们也在积极尝试用这个比较小众的特性去解决实际问题。 不过最近遇到了一个小坑,这里记录一下。

TL;NR: 位运算查询中的 position 匹配规则是这样的:先按字节正序数(从左往右);每个字节(8比特)中再从最低有效位开始数(从右往左)。

什么是位运算查询

关于 MongoDB bitwise query 的中文资料好像没有多少,就先科普一下吧。

我们知道计算机中数据是用二进制保存的,比如十进制数字 42 就可以表示为二进制数 101010. 有时候我们可能需要按照二进制的某一位是 0 还是 1 进行查询。 MongoDB 中就有这样一些位运算操作符:

  • $bitsAllSet 匹配的数字或二进制值,其中这组位的位置都有一个值为1。
  • $bitsAnySet 匹配的数字或二进制值,其中这组位的任意一个位置有一个值为1。
  • $bitsAllClear 匹配的数字或二进制值,其中这组位的位置都有一个值为0。
  • $bitsAnyClear 匹配的数字或二进制值,其中这组位的任意一个位置有一个值为0。

参数可以是一个 32 位无符号整数、二进制数据、或者数组。 为数字和二进制数据时,参数作为一个掩码,对其中为 1 的那些位进行比较; 为数组时,参数作为一个位置的的数组,对这些位置的位进行比较。

应用场景

一个最广为人知的案例就是 Linux 文件权限的表示了。 基本上,只要是一堆布尔状态的组合,存储成二进制格式都是一个很好的选择。

好的好的我知道了,那么你到底遇到了什么坑?

问题发生在使用位置数组进行查询的时候。 MongoDB 使用 BSON 格式存储数据,在 BSON 中数据被序列化成小端序格式, 位序号从 0 开始,从最低有效位开始数。例如十进制数字 254 的比特位表示如下:

比特值: 1 1 1 1 1 1 1 0
位置号: 7 6 5 4 3 2 1 0

实践一下。我们插入一条记录:

db.test.insert({ "_id" : 1, "a" : BinData(0, "AQ=="), "binaryValueofA" : "0000 0001" });

然后查询:

db.test.find({ a: { $bitsAnySet: [0] } }); // 查询第 0 位为 1 的记录
// { "_id" : 1.0, "binaryValueofA" : "0000 0001" }

完美,继续下一条:

db.test.insert({ "_id" : 2, "a" : BinData(0, "AAE="), "binaryValueofA" : "0000 0000 0000 0001" });

然后查询:

db.test.find({ a: { $bitsAnySet: [0] } });
// { "_id" : 1.0, "binaryValueofA" : "0000 0001" }

纳尼,怎么还是原来那条? 换个查询条件试试:

db.test.find({ a: { $bitsAnySet: [8] } });
// { "_id" : 2.0, "binaryValueofA" : "0000 0000 0000 0001" }

是不是表现非常诡异?明明最末位是 1,结果位置传 8 才能查出来。

刚开始面对 bug 时一头雾水,根本找不到规律。 于是 Google、StackOverflow 各种翻资料,结果也是一无所获。

迫不得已,我一介 C++ 菜鸡跑去翻了下 MongoDB 源码,最后发现了这样一段注释:

// Map to byte position and bit position within that byte. Note that byte positions
// start at position 0 in the char array, and bit positions start at the least
// significant bit.

我看到官方文档的示例,就武断地以为对于二进制数据是整块数据从右往左进行匹配。 结果匹配顺序其实是这样的:先按字节正序数(从左往右);每个字节(8比特)中再从最低有效位开始数(从右往左)。

对于一个两字节的二进制数据,比特位表示如下:

比特值:0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0
位置号:7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8

算是个小坑,但是由于网上没有找到任何先例(也许是因为大家都比较聪明没有像我一样误解?),花费了不少时间。 也许 MongoDB 的文档上可以针对性说明一下。