如何使用 MySQL 存储和改变 bitmap/bitset?
How do you store and mutate a bitmap/bitset using MySQL?
我想创建一个 table 列来存储一个 500 字节的位图 (500 bytes * 8 bits per byte = 4000 bits
) 并执行操作以改变位图中某些索引处的位(设置为 1 或 0)。
不过,documentation page on bitmaps is mostly empty leaving me with the raw bit functions作为唯一指南。如何在 MySQL 中创建、计数、读取和改变位图作为列类型?
使用 bin
和 lpad
你可以打印一个 64 位数字作为二进制字符串。
LPAD(BIN(34), 64, '0')
0000000000000000000000000000000000000000000000000000000000100010
但是,如何打印出可能有 4000 位长的 binary/blob/varbinary 字符串?
(注:不是指位图索引)
首先,升级到 MySQL 8.0。这不适用于 MySQL.
的早期版本
您应该使用 BINARY、VARBINARY 或 BLOB,具体取决于您要存储的位域的长度。
mysql> create table mytable ( bits binary(500) );
ERROR 1074 (42000): Column length too big for column 'bits' (max = 255); use BLOB or TEXT instead
mysql> create table mytable ( bits blob(500) );
Query OK, 0 rows affected (0.02 sec)
使用 UNHEX() 从十六进制字符串形成位域。直接使用二进制字节太难了。
mysql> insert into mytable set bits = unhex(repeat('00', 500));
Query OK, 1 row affected (0.00 sec)
您可以对位域使用 |
、&
、^
和 ~
等位运算符。但字符串必须相同长度!
mysql> update mytable set bits = bits | b'01000';
ERROR 3513 (HY000): Binary operands of bitwise operators must be of equal length
这很不方便,但您必须使用 CONCAT() 形成长度正确的字符串:
mysql> update mytable set bits = bits | unhex(concat(repeat('00', 498), 'ffff'));
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
正在读取位串(我已将其缩短以用于显示):
mysql> select hex(bits) from mytable\G
*************************** 1. row ***************************
hex(bits): 000...000FFFF
BIT_COUNT() 函数有效:
mysql> select bit_count(bits) from mytable;
+-----------------+
| bit_count(bits) |
+-----------------+
| 16 |
+-----------------+
mysql> select bit_count(~bits) from mytable;
+------------------+
| bit_count(~bits) |
+------------------+
| 3984 |
+------------------+
到这个时候,您应该坐下来思考 SQL 是否是完成此任务的最佳工具,即使具有 MySQL 8.0 的功能。您会发现在大多数其他编程语言中进行按位运算更加灵活和强大。
使用 BLOB 作为数据类型并编写一个函数将:
- 提取需要更新的字节
- 更改字节中的位
- 将改变后的字节插入blob中原来的位置
这是一种实现方式:
delimiter //
create function set_bit(b blob, pos int, val int) returns blob reads sql data
comment 'changes the bit at position <pos> (0: right most bit) to <val> in the blob <b>'
begin
declare len int; -- byte length of the blob
declare byte_pos int; -- position of the affected byte (1: left most byte)
declare bit_pos int; -- position within the affected byte (0: right most bit)
declare byte_val int; -- value of the affected byte
set len = length(b);
set byte_pos = len - (pos div 8);
set bit_pos = pos mod 8;
set byte_val = ord(substring(b, byte_pos, 1)); -- read the byte
set byte_val = byte_val & (~(1 << bit_pos)); -- set the bit to 0
set byte_val = byte_val | (val << bit_pos); -- set the bit to <val>
return insert(b, byte_pos, 1, char(byte_val)); -- replace the byte and return
end //
delimiter ;
一个简单的测试:
create table test(id int, b blob);
insert into test(id, b) select 1, 0x000000;
insert into test(id, b) select 2, 0xffffff;
我们有两个 blob 位掩码(每个 3 个字节)- 一个全是 0,一个全是 1。在两者中,我们将位置 10
的位(从右起第 11 位)设置为 1
并将位置 11
的位(从右起第 12 位)设置为 0
.
update test set b = set_bit(b, 10, 1);
update test set b = set_bit(b, 11, 0);
select id, hex(b), to_base2(b) from test;
结果:
| id | hex(b) | to_base2(b) |
| --- | ------ | -------------------------- |
| 1 | 000400 | 00000000 00000100 00000000 |
| 2 | FFF7FF | 11111111 11110111 11111111 |
注意:to_base2()
是一个自定义函数,returns 一个带有 BLOB 位表示的字符串,仅用于演示目的。
这适用于 MySQL 5.x 以及 8.0.
可以在单个表达式中内联实现它(不需要函数)- 但这相当不可读:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b = insert(
t.b,
length(t.b) - (i.pos div 8),
1,
char(ord(
substring(t.b, length(t.b) - (i.pos div 8), 1))
& ~(1 << (i.pos mod 8))
| (i.val << (i.pos mod 8)
))
);
在MySQL 8.0 中它更简单一些,因为我们不需要提取字节并且可以对blob 进行位操作。但是我们需要确保操作数的长度相同:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b = t.b
& (~(concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos))
| (concat(repeat(0x00,length(t.b)-1),char(i.val)) << i.pos)
另一种方式:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b =
case when i.val = 1
then t.b | concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos
else t.b & ~(concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos)
end
我想创建一个 table 列来存储一个 500 字节的位图 (500 bytes * 8 bits per byte = 4000 bits
) 并执行操作以改变位图中某些索引处的位(设置为 1 或 0)。
不过,documentation page on bitmaps is mostly empty leaving me with the raw bit functions作为唯一指南。如何在 MySQL 中创建、计数、读取和改变位图作为列类型?
使用 bin
和 lpad
你可以打印一个 64 位数字作为二进制字符串。
LPAD(BIN(34), 64, '0')
0000000000000000000000000000000000000000000000000000000000100010
但是,如何打印出可能有 4000 位长的 binary/blob/varbinary 字符串?
(注:不是指位图索引)
首先,升级到 MySQL 8.0。这不适用于 MySQL.
的早期版本您应该使用 BINARY、VARBINARY 或 BLOB,具体取决于您要存储的位域的长度。
mysql> create table mytable ( bits binary(500) );
ERROR 1074 (42000): Column length too big for column 'bits' (max = 255); use BLOB or TEXT instead
mysql> create table mytable ( bits blob(500) );
Query OK, 0 rows affected (0.02 sec)
使用 UNHEX() 从十六进制字符串形成位域。直接使用二进制字节太难了。
mysql> insert into mytable set bits = unhex(repeat('00', 500));
Query OK, 1 row affected (0.00 sec)
您可以对位域使用 |
、&
、^
和 ~
等位运算符。但字符串必须相同长度!
mysql> update mytable set bits = bits | b'01000';
ERROR 3513 (HY000): Binary operands of bitwise operators must be of equal length
这很不方便,但您必须使用 CONCAT() 形成长度正确的字符串:
mysql> update mytable set bits = bits | unhex(concat(repeat('00', 498), 'ffff'));
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
正在读取位串(我已将其缩短以用于显示):
mysql> select hex(bits) from mytable\G
*************************** 1. row ***************************
hex(bits): 000...000FFFF
BIT_COUNT() 函数有效:
mysql> select bit_count(bits) from mytable;
+-----------------+
| bit_count(bits) |
+-----------------+
| 16 |
+-----------------+
mysql> select bit_count(~bits) from mytable;
+------------------+
| bit_count(~bits) |
+------------------+
| 3984 |
+------------------+
到这个时候,您应该坐下来思考 SQL 是否是完成此任务的最佳工具,即使具有 MySQL 8.0 的功能。您会发现在大多数其他编程语言中进行按位运算更加灵活和强大。
使用 BLOB 作为数据类型并编写一个函数将:
- 提取需要更新的字节
- 更改字节中的位
- 将改变后的字节插入blob中原来的位置
这是一种实现方式:
delimiter //
create function set_bit(b blob, pos int, val int) returns blob reads sql data
comment 'changes the bit at position <pos> (0: right most bit) to <val> in the blob <b>'
begin
declare len int; -- byte length of the blob
declare byte_pos int; -- position of the affected byte (1: left most byte)
declare bit_pos int; -- position within the affected byte (0: right most bit)
declare byte_val int; -- value of the affected byte
set len = length(b);
set byte_pos = len - (pos div 8);
set bit_pos = pos mod 8;
set byte_val = ord(substring(b, byte_pos, 1)); -- read the byte
set byte_val = byte_val & (~(1 << bit_pos)); -- set the bit to 0
set byte_val = byte_val | (val << bit_pos); -- set the bit to <val>
return insert(b, byte_pos, 1, char(byte_val)); -- replace the byte and return
end //
delimiter ;
一个简单的测试:
create table test(id int, b blob);
insert into test(id, b) select 1, 0x000000;
insert into test(id, b) select 2, 0xffffff;
我们有两个 blob 位掩码(每个 3 个字节)- 一个全是 0,一个全是 1。在两者中,我们将位置 10
的位(从右起第 11 位)设置为 1
并将位置 11
的位(从右起第 12 位)设置为 0
.
update test set b = set_bit(b, 10, 1);
update test set b = set_bit(b, 11, 0);
select id, hex(b), to_base2(b) from test;
结果:
| id | hex(b) | to_base2(b) |
| --- | ------ | -------------------------- |
| 1 | 000400 | 00000000 00000100 00000000 |
| 2 | FFF7FF | 11111111 11110111 11111111 |
注意:to_base2()
是一个自定义函数,returns 一个带有 BLOB 位表示的字符串,仅用于演示目的。
这适用于 MySQL 5.x 以及 8.0.
可以在单个表达式中内联实现它(不需要函数)- 但这相当不可读:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b = insert(
t.b,
length(t.b) - (i.pos div 8),
1,
char(ord(
substring(t.b, length(t.b) - (i.pos div 8), 1))
& ~(1 << (i.pos mod 8))
| (i.val << (i.pos mod 8)
))
);
在MySQL 8.0 中它更简单一些,因为我们不需要提取字节并且可以对blob 进行位操作。但是我们需要确保操作数的长度相同:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b = t.b
& (~(concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos))
| (concat(repeat(0x00,length(t.b)-1),char(i.val)) << i.pos)
另一种方式:
update test t
cross join (select 10 as pos, 1 as val) i -- input
set t.b =
case when i.val = 1
then t.b | concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos
else t.b & ~(concat(repeat(0x00,length(t.b)-1),char(1)) << i.pos)
end