PostgreSQL技术问答05 - 位运算
本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:
文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。
本文的主要内容是关于在PostgreSQL中实现位运算相关的问题。
什么是位运算
在信息技术和计算机程序中,位运算是指对二进制数进行运算的一种操作。在计算机中,所有的数据都是用二进制数表示的,因此位运算可以直接操作数据的二进制表示,从而实现一些特别的业务功能和需求。常见的位运算包括:
- 按位与 &
对两个二进制数的每一位进行与运算,需要两个位都为1,结果才为1(有点像相乘)。通常使用 & 符号表示。
例如 5 & 4, 即0b101 & 0b100 = 0b100 = 4。
如果从集合的角度来理解,把一个二进制数看成一个集合,每一位都看成集合的一个成员或者属性,那么按位与的意思就是找到两个集合中都有的成员或者属性,就是求交集。
- 按位或 | (或)
对两个二进制数的每一位进行或运算,只需要其中一个为1,结果就为1(相加) 。通常用 | 符号表示。
例如: 5 | 4,即 0b101 | 0b100 = 0b101 = 5。
同样的,如果以集合的角度来理解,按位或操作就是求两个集合的并集。
- 按位异或 ^
对两个二进制数的每一位进行异或运算,规则为,如果两个位不同,异或结果就为1。
例如: 5 | 4,即 0b101 ^ 0b100 = 0b001 = 1。
异或的集合意义,就是求两个集合的并集,减去它们交集的部分。
上面三种位运算,如果表示成为集合的形式,可以参考下面的图形:
位运算的基础理论大致就是这样,主要的问题是,我们应当如何将它们很好的运用到开发和业务应用程序当中呢?我们可以进一步结合集合的处理方法,来对很多业务场景状态进行抽象。
例如,我们经常会在应用中,遇到需要对业务对象进行分类处理的需求。这种分类,可以通过业务对象是否具有某种业务标签来表示。在传统的数据设计中,我们可能需要设计很多“是否”形式的字段或者属性,然后使用很多IF语句来进行检查和处理。这时,我们可以借助位运算的方式,快速、一致、可扩展的来表示这些属性和基于属性标识进行数据的除了。这时,我们可以用一个整体的“属性集合”的数据字段(就是一个整数)来标识这些属性标签状态,预先约定好每个位表示一个属性标签。通过设置和查询这些属性位,就可以组合完成很多业务的操作和处理。我们将会从后面的例子可以看到,使用位运算进行这些处理,可能对开发者入门的要求相对比较高,但一旦掌握和熟悉,是非常直观、方便和高效的。
上面三种操作是对于普通的业务实现而言,都是比较常见的,所以PostgreSQL对于上面几种位运算方式,都有很好的支持。
除此之外,在通用的编程语言中,还有一些位运算操作,它们一般都是和比较底层的二进制数据处理相关的,这里也简单列举一下,读者知晓即可。
- 按位取反 ~
即将一个数值的每个二进制位都分别进行取反。例如: ~5 = -6。这里看起来不太合理,5的二进制取反后是"010",十进制表示为4,不是-6。但实际上,如果使用32位整数系统,5的完整表现形式并不是简单的101,而是完整的: 0b0...0101,取反后是0b1...1010。这时,这数值的第一位是1,表示这个二进制数是一个负数,而二进制负数转换为十进制,保留负号,再对其按位取反后加1,即 - 110(101+1),就是-6。其实,对于正的十进制整数n,其取反的简单计算形式是 -(n+1)。
按位取反的数学意义是求一个整数的补码。补码是一种用来表示负数的编码方式,在计算机中广泛使用。补码可以用一种统一的方式来表示正数和负数,并且可以直接用补码进行加法和减法运算。
- 向左移位 <<
一般使用符号 << 表示。向左移动n位,就是在进制值后加入n个0,好像就是这个数值向移动了n位。在数学意义上,其实就是乘以2(二进制)n次。
如 3 << 2 = 12,作为算数计算是 3x2x2= 12,使用二进制计算,3的二进制形式为"11",左移两位是"1100",转换为十进制也是12。
- 向右移位 >>
一般使用符号>>表示。 和左移位类似,就是截掉二进制表示中最右边的n位,好像就是向右移动了n位。
如 13 >> 2 = 3, 使用二进制计算,13是1101,右移(截取)2位为11,就是3。这里再使用十进制做数学运算就不太合适了,如果要强行计算,移动一位,就是整数被2除然后向下取整。
为什么要在数据库层面实现位运算操作
位运算作为一种基础的数据处理模式,在所有的主流编程平台和语言都是支持的。但在以SQL作为主要语言的关系数据库系统内,由于缺乏传统的二进制计算和流程控制等机制,一般并没有内置支持,相关的数据处理需要在外部应用程序中进行。
这样带来的问题就是增加了复杂性。特别是在基于业务属性标签的处理操作上,如果能够在数据库内部就可以完成,将极大的简化开发和操作的环节。所以,作为一种比较通用的业务抽象,在数据库层面,能够很好的支持位运算的相关处理,对于一个成熟的数据库系统而言是非常有必要的。
笔者在早期的应用开发过程中,在Oracle数据库系统(版本好像是10G)中,曾经有用到过它的位运算机制。当时它好像是通过了内置函数的方法,提供了如BitAnd、BitOr、BitXOr等等,当时记得好像为了实现一个“逻辑去除”的方法,还编写了自定义方法来进行实现。然后,在PostgreSQL中,笔者欣喜的发现,PG竟然是使用了和一般程序一样的操作符来提高了Bitwise的功能,就进一步理解了PG的先进性和易用性。
在PG中实现的基于位运算的业务逻辑处理机制,和其他特别是传统的基于多个状态字段的解决方案相比(后面会讨论),笔者感觉,主要有两个方法的好处:
- 可扩展性
比如要对字段状态(业务状态)进行扩展,不需要定义额外的字段,只需要在外部程序中,定义相关状态的处理逻辑就可以了。这个扩展方式不需要修改数据库结构,简单方便。
- 性能
一般而言,位运算操作的是以整数形式呈现而实际上是底层的二进制数据,在计算机系统底层的处理模式上是更加友好的。作为对比,使用字符串或者数组,都会引入额外的操作步骤,所以相对而言,位运算的处理效率更高。同时这个数据类型占用的磁盘和内存空间更小,再加上PG提供的计算索引,这些特性,都有利于获得更好的性能。
PostgreSQL如何支持位运算
PostgreSQL中,提供了相关的运算符,可以方便的进行位运算操作。如前面一些程序的示例,可以在PostgreSQL中,使用SQL进行表达(就是这么简单!!):
defaultdb=> select 5 & 4, 5 | 4, 5 # 4, ~5, B'10001' << 3, B'10001' >> 2;
?column? | ?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------+----------+----------
4 | 5 | 1 | -6 | 01000 | 00100
//
稍微注意一下,PG中xor需要使用"#"符号,而不是一般的"^"符号,因为那是求多次方的意思。这些都是10进制整数计算的形式。另外,PostgreSQL的位运算支持特定比较完善,和很好的支持了取反和移位等操作。
结合具体的应用需求,笔者感觉,对于位运算的应用,可以分为以下几个方面。
-
字段值计算:简单的位运算,作为赋值和查询的基础,或者就是数值计算
-
字段值赋值:就是使用位运算计算的结果,给记录字段赋值
-
位运算查询,就是使用位运算结果作为数据查询的条件,来满足一些业务上的需求
下面我们以一个简单的实例,来探讨一下其具体应用的方式。基本场景是,有一个数据表结构如下:
-
id(int) 记录标识,主键
-
ivalue(int) 状态标识, 可选状态包括A(1,二进制001),B(2,二进制010),C(4,二进制100),可以组合
则可以使用下列的SQL语句进行相关操作:
// 插入记录1, 状态为A
insert into udata(id,ivalue) values(1, 1);
// 插入记录2,状态为B和C
insert into udata(id,ivalue) values(1, 2+4);
// 更新记录1, 增加状态C
update udata set ivalue = ivalue | 4 where id=1;
// 更新记录1, 去除状态A
update udata set ivalue = ivalue - (ivalue & 1) where id=1;
// 或者作为附加条件
update udata set ivalue = ivalue - 1 where id=1 and (ivalue & 1) > 0 ;
// 更新记录2, 增加状态C,同时去除状态A
update udata set ivalue = (ivalue | 4) - (ivalue & 1) where id=1
// 查询带有状态A记录
select * from udata where (ivalue & 1) = 1;
// 查询带有状态A和C的记录
select * from udata where (ivalue & (1+4)) = (1+4);
// 查询带有状态A或者C的记录
select * from udata where (ivalue & (1+4)) > 0;
// 查询不带有状态A的记录
select * from udata where (ivalue & 1) = 0;
补充说明一下,在业务上,状态标识只是一个抽象概念,可以应用到很多场景。比如可以是工作流程的审批状态,对象数据的分类属性,业务标签等等,都可以用这个概念进行表达。关键是可以将一个状态映射到二进制数据的一个位上进行操作。
在理解了以上基本操作后,这里总结几个要点:
- 可以结合使用位运算符和算数运算符,表达完整计算逻辑
- 为数据增加一个状态,可以直接使用"|"
- 为数据减少一个状态,要注意,不能直接减,实际上是当前状态减去&状态的值
- 在多个状态中,判断状态的存在性,可以使用 & > 0, 不存在性当然就是 & = 0
- 在多个状态中,判断状态的确实性,就是只有这个状态,可以使用 & =
- 可以使用状态组合的判断方式,同时检查多个状态
- 简单状态下,可以用算数方法(+-)增加或者减少状态
还有其他技术方案吗
对于状态、标签管理类的应用需求,使用整数字段结合位运算进行的处理,看起来确实不是那么直观。但实际上,如果业务设计和数据字典和映射定义的清晰明确的话,前面已经提到,这基本上是效率最高的一种方式。主要问题就是开发者的学习、应用和数据设计的门槛较高,如果使用整数,状态的容量有限(32位),只支持枚举类型的状态,不能随意设计标签,需要设计和实现映射机制等等。
那有没有其他的技术方案呢?当然是有的,笔者这里尝试总结一下:
- 状态信息表
我们也可以设计和使用另外一个专门的状态信息表,并且和主对象表进行关联,来实现状态和标签的管理。这也是传统关系数据库的处理方式。这种方式应该适合于状态非常多的场景,这种情况其实在现实世界并不多见。现实世界中,大多数状态或者标签都是非常有限的。
- 状态字段
这也是另外一种传统方法,为每个状态和属性都设计一个字段。这个方法的好处是直观,实现方便,并且字段状态之间的关联完全解耦。但对于不是每个对象都有的状态或者属性,可能会浪费比较多的空间。还有就是对数据库运行和管理不太友好,需要维护很多字段,数据结构调整也不方便。
- 属性标识字符串
受到位运算操作的启发,笔者觉得可以设计一种字符串,然后用字符串中的字符来代表状态或者标签属性。使用这种方式,需要设计和实现标签字符的映射机制,和字符串的操作机制。好处是容易扩展。另外字符串处理的性能,肯定比数值类型要低。
- Postgres Array 字段
PostgreSQL有比较强大的原生数组数据类型,和配套的操作机制。利用这个数组可以实现多个状态和标签的管理。关于PG数组,笔者会另行著文讨论。
- Postgres bit string
PostgreSQL还有一种bit String的数据类型,就是形式上它是一个字符串,但实际上是一个二进制数据。当然,作为一个独立的数据类型,PG也提供了相关配套的函数,来进行这一类数据处理的操作。所以,作为实质上的二进制数据,bit String也可以用于处理状态属性这种应用场景。关于这个数据类型,笔者有机会也会另行著文讨论。
小结
本文探讨了位运算的基本概念,然后结合示例,讨论了在PostgreSQL中,是如何进行位运算,和在实际的应用场景中,是如何使用它来进行业务处理的。
转载自:https://juejin.cn/post/7369978126146224139