为什么要使用位运算(bitwise)
笔者最早接触到位运算的实际应用,应该是看到了Linux系统中,文件权限的标识和控制方式。Linux系统的每个文件,都使用一个数字来表示和控制其当前的访问权限,如"777","750"等。当时不明觉厉,但后来经过研究学习和理解其设计思路和原理后,才知道,它其实是三组数字(分别代表属主用户、组和所有用户的权限),每个数字又是一个操作权限的组合(7=1+2+4,可以表示读、写和执行三种权限,建议读者有兴趣可以去深入了解),就理解了可以使用单个数字来表示多种状态的思想方法,这是一种典型的计算机、软件程序和数字编码的思维方式。
后来,在很多应用系统中,经常会遇到,需要表示多个对象状态和标签化属性的情况。可能进一步细化包括是否类型和枚举类型甚至组合枚举类型,如是否已报名,是否优秀学生,学历大学|学历高中,权限组合等。原来通常的处理方式是为数据库设计不同的字段,这样虽然简单清楚,但显然在实现、扩展和维护上就比较复杂(所以我们经常看到数据库表有很多字段的原因),其实在很多情况下,也会影响数据库运行的效率。
针对上述需求和问题,我们认为,可以利用软件的位运算机制,来简化上述应用场景的实现。如可以用单一的数据库字段,来表示和处理多种类型甚至复合化的信息和状态。这样可能会带来以下潜在的优势:
- 单个数据库字段和位运算处理效率比较高
- 编程和扩展比较简单,只需要维护一个状态字典
- 甚至在增加新的信息、状态和业务时,在一定范围内不用增加新的数据库字段
- 特别对于大量是否类型的属性,可以简化共享"否"状态,只需要表示"是"状态
- 状态位其实可以进行组合处理
当然问题也有,就是可理解性和应用的门槛比较高,对编程人员技术水平要求较高,需要技术人员更具有软件和程序的思维方式,理解数据处理的基本原理和逻辑。同时,一个字段多个语义,模块化程度不好,在不熟悉情况的时候,操作和维护容易出现错误和干扰。所以需要谨慎平衡使用。
应用场景
下面的一些具体的应用场景中,我们可以考虑应用位运算来处理数据:
- 表达多个是和否的状态复合概念(如是否学生、是否已注册等)
- 复合表达某些属性或者属性的集合
- 标签化的属性管理
- 权限控制
其实,我们可以将这些情况,都抽象为对象的某种标签和属性,那么位运算就可以简化为对这些标签和属性的处理。这样,通用的处理方式包括:
- 增加标签
- 去除标签
- 使用标签进行查询
- 标签的复合查询
这些都可以使用标准计算机程序中的位运算相关概念来实现,本文我们主要讨论在SQL语句中的实现。本文基于Postgres数据库进行讨论,其实其他数据库系统也有相关的概念,最低程度也可以通过自定义函数来实现,但那就有点脱离使用SQL的初衷了。总体而言Postgres中的实现是比较完整和简洁的。
PG位运算概述
postgres提供了运算符,来进行标准的位和逻辑运算:
- 或,即位运算+,用于增加标签,运算符为 |
- 非,即位运算减,用于去除标签,这个没有简单的操作,可以通过组合-和&完成操作。
- 与,运算符为 &,可以用于条件检查
我们下面以一个报名缴费场景为例来进行讨论。有一个学生表students,使用一个status(int4)字段来表示这个学生在报名过程中的各种状态,为此我们约定使用如下状态标识:
- 1: 尚未开始报名
- 2: 学生已报名,如果学生报名成功,则增加此状态,同时应该去除尚未开始报名的状态
- 4: 学生已缴费,如果学生报名完成,并且缴费成功,我们增加此状态
- 8: 信息已确认,如果学生报名和缴费完成,我们增加此状态
其中,状态1为初始状态,这里只是方便讨论去除标签的功能,实际系统中其实可以简化。比如如果学生报名,修改状态需要增加2并且除去1;然后在学生缴费后,可以增加状态4;并且如果学生本身的报名状态就是2的话,不会影响其他状态。此处 1,2,4,8等,看起来挺怪,但其实用二进制表示,就可以看出来它们实际上是在不同的进位上,叠加或者去除操作不会相互影响,就达到了使用一个整数表示多种状态的目的。
下面我们来看看,在PGS中,实际上是如何使用SQL语句,来实现这些语义目的操作的。
增加标签
为某个字段增加一个标签或属性,可以使用 | 运算符。如下面的语句可以用于表示更新学生状态已经报名
update students set status = status | 2;
可以一次增加多个标签,如同时更新学生报名和缴费的状态(6=4+2):
update students set status = status | 6;
使用位运算,不用考虑原来的状态,无论是否已经有此状态,都可以附加状态;这里显然就不能用简单的算术加法运算,因为不能再次操作,必须要先判断一下状态,否则就会破坏语义,得到不希望得到的结果。
去除标签
去除标签没有直接的运算符(直接使用xor计算会有问题),我们可以使用组合计算,如下面的语句:
update students set status = status - (status & 1);
可以在增加状态的同时,消除一些状态,如真正的报名成功操作应该如下:
update students set status = (status | 2) - (status & 1);
作为一种通用的标签操作模式,我们应该统一使用这种模式,这样可以不同判断当前的状态和操作。
update students set status = (status | $1) - (status & 2);
通过控制这两个参数(参数值为0,可以不影响原来的值),可以进行任意类型的操作。
查询信息
使用位运算查询,可以简单的进行查询,也可以非常灵活的组合各种查询条件。
查询所有未报名的学生(具有未报名状态标签,简单的标签查询其实是进行的与运算):
select * from students where (status & 1) = 1;
查询已报名并且已缴费的学生:
select * from students where (status & 6) = 6;
查询已报名但未缴费的学生:
select * from students where (status & 6) = 2;
通过查询和修改的语句组合,我们甚至可以在SQL中实现更复杂的功能。
JS实现简述
本文的主要内容是讨论SQL中的位运算实现,实际上在所有主流编程语言中,它也是一种基本操作。我们以JS为例,考虑以下操作和计算,其实和前面的SQL操作都可以对应起来:
7 & 1 = 1
6 & 1 = 0
6 & 5 = 4
7 | 1 = 7
6 | 1 = 7
7 - (7 & 3) = 4