本文探讨如何在MSSQL、Sybase,及Oracle三种数据库中进行位运算。由于系统设计原因,MSSQL和Sybase可以很好的支持位运算,但是Oracle的愿生类型并不支持位运算。本文将介绍MSSQL和Sybase位运算的不同之处,然后介绍如何在Oracle中模拟位运算。
通常,对于应用程序来说,位运算仅对整数有意义。在MSSQL和SYBASE中,有三种数据类型用来表示整数。其定义和值范围见下表描述。
类型 |
字节数 |
值范围 |
tinyint |
1 |
0至28-1 |
smallint |
2 |
-215至215-1 |
int |
4 |
-231至231-1 |
可以看出,这些数据类型正好使用了字节的倍数来容纳其值,如同C语言一样。这样的数据类型是最适合于位运算的。
在MSSQL中还有一个bigint类型,它使用8个字节来容纳更大范围的整数。但是在Sybase中没有对应的类型。
要介绍位运算,必须首先知道如何用二进制来表达整数。在MSSQL中,二进制常量均以 0x(一个零和小写字母x)开始,后面跟着位模式的十六进制表示。在Sybase中,以0x开始的数表示的仅为二进制,即只能出现0或者1。
因此,对于一些数字来说,虽然MSSQL和Sybase采用同样的表示方式,但是含义有所不同。原因在于MSSQL的十六进制数的形式对应于SYBASE的二进制数,而不是SYBASE的十六进制数。 是不是感到这话有点饶口?还是让例子来澄清一切吧。 打开MSSQL的查询分析器或者SYBASE的SQL Advantage,运行语句,结果如下表所示。
数据库 |
表达式 |
结果 |
MSSQL |
Select 0x1 + 0 |
1 |
SYBASE |
Select 0x1 + 0 |
16777216 |
MSSQL |
Select 0x001 + 0 |
1 |
SYBASE |
Select 0x001 + 0 |
65536 |
MSSQL |
Select 0x00001 + 0 |
1 |
SYBASE |
Select 0x00001 + 0 |
256 |
MSSQL |
Select 0x0000001 + 0 |
1 |
SYBASE |
Select 0x0000001 + 0 |
1 |
为何值的差异如此之大?原因在于上面表格中SYBASE的值是二进制数,不是十六进制数。SYBASE的二进制数以0x开头,最长允许8位。如果不足8位,则在右边添加0;如果超过8位,则截取左边8位。其值的计算可以用下面的公式来表示:
v = v12 * 2^24 + v34 * 2^16 + v56 * 2^8 + v78 * 2^0
其中:v12表示第1、2位组成的数的十进制值。v34、v56、v78的含义依次类推。 还必须说明,v12的填充方式是左填零,即1和01的值是相同的。SYBASE二进制数这种表示方式十分拗口。根据这个规则,来分析下面的SYBASE数字:
Sybase数字 |
右填充0后的结果 |
V12 |
V34 |
V56 |
V78 |
结果 |
0x1 |
01000000 |
1 |
0 |
0 |
0 |
1 * 2^24 |
0x10 |
10000000 |
2 |
0 |
0 |
0 |
2 * 2^24 |
0x000011 |
00001100 |
0 |
0 |
3 |
0 |
3 * 2^8 |
本人认为,上面最难理解的是0x1为何没有填充成0x10000000!本人建议在书写Sybase的二进制数字时最好写成偶数位,以便理解。
那么SYBASE的十六进制到底怎么表示呢?很简单,把MSSQL的十六进制数变成字符串即可,并且用hextoint函数来取得它的值。
例如,MSSQL的语句
if @action_in & 0x0ff
可以变成SYBASE的语句
if @action_in & hextoint(‘0x0ff’)
注意,十六进制数采用左边填充零,’0x0ff’ 等于’0x000000ff’。 将十进制转为十六进制的函数是inttohex。
SYBASE之所以采用字符串的形式来表示十六进制时为了做到平台无关性。MSSQL完全不用考虑这个问题,因为它从来就只支持Windows。
位运算符通常对整数有效。这里必须明确指出,二进制值本身不是整数,所以不能直接写以下语句:
select 0x001 & 0x011
MSSQL会提示出错。
但是如果写成
select 1 & 0x011
就可以进行计算。原因在于在这个语句中0x011会被自动转换成1的数据类型tinyint。
在MSSQL中,要查看一个整数的二进制形式,可以使用CAST函数或者CONVERT函数。
例如:
select convert(varbinary, 435455)
或者
select cast(435455 as varbinary)
显示结果为:
0x0006A4FF
在Sybase中,可以使用inttohex函数来显示整数的二进制形式。
有四种基本的位操作运算:按位与&,按位或|,取反~,异或^。
本文不介绍这些操作的详细用法,因为在操作手册上已经说明清楚。
通常不建议在数据库中大量使用位操作。数据库是进行数据存取的合适地点,但是不是进行数据运算的合适地点。
在基本操作的基础上,可以发展出其他一些有用的操作。本人在实际应用中使用过很多操作,以下是其中一些例子:
· 取某一位的值
· 设置某一位的值
· 计算非零位个数
· 交换两个位的值
下面占用一些篇幅来实现一下上面的几个例子。
由于二进制是由0和1组成,取某一位的值等价于判断该位是0或1。因此,我们可以使用“按位与”的方法来取值。
· 如果想取第n位值,可使用:
case @v_value & power(2,n-1) when 0 then 0 else 1 end
例如,我们要取一个变量的第四位的值,可以另其与24-1即二进制值1000即十六进制值0x08进行与运算,如果结果是0,则该位值是0,否则为1。
在MSSQL和Sybase中,可用以下语句实现:
select (case @v_value & power(2,4-1) when 0 then 0 else 1 end)
在Sybase中,还可以更加明确的直接写出二进制:
select (case @v_value & 0x00001000 when 0 then 0 else 1 end)
在设置某一位的值时,由于结果取决于给定值(0或1),不取决于原始值,因此给定值必须能够屏蔽原始值的影响。给定值不同,采用的方法也不同。
· 如果想把第n位值设置成1,可以使用“或”方法:
@v_value | power(2, n-1)
例如,执行以下语句将把数值2(二进制0010)的第4位变成1,因此结果变成10(二进制1010)。
select 2 | power(2, 4-1)
· 如果想把第n位值设置成0,可以使用“与”方法:
@v_value & ~power(2, n-1)
例如,执行以下语句将把数值10(二进制1010)的第4位变成0,因此结果变成2(二进制0010)。
select 10 & ~power(2, 4-1)
计算非零个数用一句SQL是做不到了,可以用下面的一段T-SQL代码来实现:
declare @v_result int,
@v_value int
select @v_value = 11
select @v_result = 0
while @v_value != 0
begin
select @v_result = @v_result +
(case @v_value & 1 when 0 then 0 else 1 end)
select @v_value = @v_value / 2
end
select @v_result
上面代码中假设@v_value是要计算的值,变量@v_result存放结果并在最后显示。
例如上面的11(二进制1011),其结果是3。
交换两个位的值可以用一段代码来实现:
declare @v_value int,
@m int,
@n int
select @v_value = 11, @m=2, @n=3
if @v_value & power(2,@n-1) <> @v_value & power(2,@m-1)
begin
if @v_value & power(2,@n-1) = 0
begin
select @v_value = @v_value | power(2, @n-1)
select @v_value = @v_value & ~power(2, @m-1)
end
else
begin
select @v_value = @v_value | power(2, @m-1)
select @v_value = @v_value & ~power(2, @n-1)
end
end
select @v_value
上述代码假设变量@v_value是将被变换的值,@m和@n指明了位置。只有在两个位置的值不同时才进行交换。交换实际上是将两个不同位置的值分别取反。
例如,13(二进制1011)第2位和第3位交换后的结果是15(二进制1101)。
在将包含位操作的T-SQL应用程序迁移到PL/SQL时,开发者发现Oracle并没有提供类似的位操作运算。这造成了一定的麻烦。
由于Oracle在表示整数时,采用了与MSSQL和Sybase不同的方式,并不采用2的次方的字节数来容纳,这实际上从本质上限制了位操作。
应用程序是否应该在数据库代码中使用位操作本身是一个设计问题,一般来说应该尽量避免。但是对于一些迁移自其他数据库的应用程序,要摒弃位操作会导致大量修改,甚至造成结构上的调整。因此,必须在Oracle中也实现位操作。
通常有三种方式可以用来实现位操作,根据我的使用经验,其优先级别分别为:
虽然Oracle不直接支持位操作,单是为了方便起见,Oracle从8i开始还是提供了一个函数bitand来模拟“与”操作。
但是Oracle并没有提供其他位操作函数。因此,我们无法认为Oracle直接提供了位操作。
Oracle有理由不直接提供位操作支持。由于Oracle提供了极为方便的Java存储过程,我们可以很方便的用来实现所有位操作。
下面我们准备实现四种基本的位操作。关于Java存储过程的内容,本文不给予太多描述,只引用必要的部分用于阐述问题。
首先,我们需要一个Java类,里面实现了位运算的四种基本功能。
public class BitOper {
public static int bitAnd(int a, int b) {
return a & b;
}
public static int bitOr(int a, int b) {
return a | b;
}
public static int bitXor(int a, int b) {
return a ^ b;
}
public static int bitNot(int a) {
return ~a;
}
}
为了便于描述问题,我将这个类设计的相当简单。
将其编译后得到BitOper.class。
接下来,使用Oracle提供的一个工具loadjava将上述类加载到数据库中。该工具在安装Oracle时已经自动安装。
loadjava –user user_name/password@sid BitOper.class
该命令执行完后,如果没有报错则表示加载成功。
最后一步工作是创建PL/SQL包装函数。这里有两种选择,一种是创建独立的函数,一种是创建用包封装的函数。为了减少篇幅,我选择使用第一种。
create or replace function bit_and
(
int1 number,
int2 number
) return number
DETERMINISTIC
AS LANGUAGE JAVA
NAME 'BitOper.bitAnd(int, int) return int';
/
create or replace function bit_or
(
int1 number,
int2 number
) return number
DETERMINISTIC
AS LANGUAGE JAVA
NAME 'BitOper.bitOr(int, int) return int';
/
create or replace function bit_xor
(
int1 number,
int2 number
) return number
DETERMINISTIC
AS LANGUAGE JAVA
NAME 'BitOper.bitXor(int, int) return int';
/
create or replace function bit_not
(
int1 number
) return number
DETERMINISTIC
AS LANGUAGE JAVA
NAME 'BitOper.bitNot(int) return int';
/
将上述代码在SQLPLUS中执行后,当前用户就具备了四种基本位操作的功能。
不妨使用几个例子来测试一下:
select bit_and(9,8) "9and8",
bit_or(3,4) "3or4",
bit_xor(3,5) "3xor5",
bit_not(3) "not3"
from dual;
其结果为:
9and8 3or4 3xor5 not3
---------- ---------- ---------- ----------
8 7 6 -4
看起来一切都很简单,你是否这么认为呢?