Nowadays, you are unlikely to need any bit-level manipulation of data for general work in SQL Server, since most of the places where it was once necessary are no longer so. You can see some of the problems we used to face in deprecated parts of the system such as sys.syspermissions and some system stored procedures (e.g. sys.sp_MSdependencies) that used bitwise flags as parameters. Since the introduction of the BIT Datatype to SQL Server in 2005, the last reason for doing routine bit manipulation, economy of storage, disappeared. Nowadays, the SQL Server Database Engine can index bit columns , and optimize the storage of them if you use enough of them in a table. If there are eight or fewer bit columns in a table, the columns are stored as one byte. If there are from nine up to sixteen bit columns, the columns are stored as two bytes, and so on. You can now do easily-SARGable queries on bit-fields. The practice of using bitmaps in tables for indicating sequence has been rendered unnecessary by the introduction of window functions.
However, once in the while, it is useful: Bitwise manipulation can solve some types of problem that are awkward to do any other way such as assessing check or parity bits in constraints, doing simple encryption or data-compression (or both at the same time!). It is also interesting when you are exploring the way that data is stored on disk or investigating the intimate secrets of the transaction log. Again, certain applications will throw up special cases that still require a bitmap; particularly where information is heavily sequential in nature, but this is rare. I once used a bitmap in SQL Server for an effective algorithm for allocating seats, or blocks of adjacent seats in a football stadium according to a number of different rules. Occasionally, you’ll find tables from ‘legacy’ databases that use bitmaps coded in integer or binary columns and which will need shredding. Yes, it still pays to keep up your knowledge of bit-manipulation.
Datatypes and their representation or notation.
In any traditional storage system, there is a clear distinction between the storage form of a datatype, its representation form and its transmission form. A datatype is usually stored in the compact form of its binary value, ranging in size from byte, through various widths of integer and strings. Encryption and compression merely translates these values. Datatype notations are string versions of the datatypes, in a format designed to be readable, and most computer languages have ‘format’ commands to do this conversion. Although there is usually a clear distinction between the two, there are hybrid types such as BCD, Binary-Coded-Decimal. The ‘transmission’ representation of a datatype is an unambiguous international standard for transferring information between different systems, to be read by machines rather than humans. Here, in this article that aims to show bitwise manipulation, we will need to tackle the topic of converting between the storage form of data, and its representation form as binary notation, a string comprising a series of ‘0’s and ‘1’s. The hex notation of the value of a datatype usually has a prefix ‘0x’.There is no general agreement on the binary equivalent, so I leave it out in this article, though I have a liking for the ‘0b’ prefix
An example of Bitwise manipulation
Just to kick off, here is an example of a ‘legacy’ SQL Server function that returns a bitmap: columns_Updated()
. It is only usable or relevant in a trigger, and tells you what columns were updated (or inserted) in a table. It is used most often perform whatever trigger-based action you need only when particular relevant columns are updated.
If you have a column that is rarely changed, but, if it does, it requires data manipulation elsewhere in the database, then it makes sense to do this. You know the rows that have changed from the INSERTED and DELETED temporary, memory-resident table. If there is a change in the value of data in a particular column and row, you have to do something; maybe just an action to maintain referential integrity, or a complex check on the integrity of the new data that can’t be done in a constraint.
The data in columns_Updated()
is simply a bitmap of the columns. If a particular column in the map was altered, then the bit was set to 1. If you know, by patient experiment, which bit corresponds with each column, and you don’t make rash changes to the metadata, then you’re probably home and dry at this point until someone unwittingly inserts or deletes a column, and thereby changes the order! Just to make things more interesting for you, Microsoft chose to make the bitmap a LittleEndian one. MSDN’s description of this is a classic
‘COLUMNS_UPDATED returns one or more bytes that are ordered from left to right, with the least significant bit in each byte being the rightmost. The rightmost bit of the leftmost byte represents the first column in the table; the next bit to the left represents the second column, and so on. COLUMNS_UPDATED returns multiple bytes if the table on which the trigger is created contains more than eight columns, with the least significant byte being the leftmost.‘
So that’s clear then. Mercifully there are some examples.
Here is an example of using it to find out what columns have changed. We are using AdventureWorks, and I’ve chosen a table with more than sixteen values. You may need to temporarily disable the existing trigger to try this out.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
IF EXISTS ( SELECT * FROM sys.triggers WHERE name LIKE 'SalesOrderHeaderAudit' ) --trigger names must be unique in the DB DROP TRIGGER sales.SalesOrderHeaderAudit GO CREATE TRIGGER Sales.SalesOrderHeaderAudit ON Sales.SalesOrderHeader /** summary: > This is a simple sample trigger that sends a list of the columns affected by the UPDATE of a table (Sales.SalesOrderHeader) via the message stream. It uses a temporary table because of a bug in the way BIGINTs use bitwise operators, and to provide a safe way of doing the string concatenation trick Author: Phil Factor Revision: 1.1 date: 10 November 2014 **/ AFTER UPDATE AS DECLARE @TheInt bigint, @ListOfColumns VARCHAR(8000), @Digits CHAR(16), @end INT, @ii INT, @LittleEndianHex VARCHAR(64); DECLARE @temp TABLE (Name VARCHAR(2000),Bits bigint,Column_ID INT PRIMARY KEY); SELECT @LittleEndianHex=CONVERT(VARCHAR(100),columns_Updated(),2);--get the bitmap of cols /*turn this wierd thing into a proper bitmask so it is easy to use (so column 1 is bit 1, column 2 is bit 2, column 3 is bit 3 ...and so on... */ SELECT @end=LEN (@LittleEndianHex);, because we work backwards from the end SELECT @Theint =0, @ii=@end, @digits='123456789ABCDEF'--0 (and any rubbish) is returned as 0! WHILE @ii>0 BEGIN SELECT @TheInt=CHARINDEX(SUBSTRING(@LittleEndianHex,@ii-1,1),@digits)+(@TheInt*16); SELECT @TheInt=CHARINDEX(SUBSTRING(@LittleEndianHex,@ii,1),@digits)+(@TheInt*16); SELECT @ii-=2; END --OK. We know what we are dealing with. A conventional bitmap. Now we simply join with -- the system table to get the names of the columns affected INSERT INTO @temp (Bits, name, column_ID) SELECT CONVERT(bigint,POWER(2,Column_ID-1) & @TheInt ) AS bits,Name, column_ID FROM sys.columns WHERE OBJECT_NAME(OBJECT_ID) ='SalesOrderHeader'; -- and produce a comma-delimited list. SELECT @ListOfColumns=COALESCE(@ListOfColumns+',','')+Name FROM @temp WHERE Bits>0; PRINT @ListOfColumns |
With a bit of extra code, this would give you a log of all the changes, with the before and after values, but you can do this without the bitmap. The columns_Updated
() function is better for using to implement column-based trigger actions, or for specifying actions to be executed only when one or more specified columns change.
Converting between Hex, decimal and binary notations of integers and varbinaries.
Before you do any serious experimenting with bitwise manipulations in SQL Server, there are two useful functions you’ll need for converting to and from binary notation. This is because the CAST and CONVERT functions will convert HEXT to INT and INT to HEX, but will do neither Octal nor Binary notations. We’d like to see individual bits just to see what is happening with bitwise logic. One utility converts an integer or varbinary into a string binary-notation representation, and there is another utility to convert back the other way, from a string binary value to an BigInt.
Although the functions pass BigInt, beware that the MSDN documentation lists only ints, smallints and tinyints as being suitable for use with the bitwise operators. Although I found one or two very strange quirks, Bigint Seems to work as you’d expect.
There is a complication with converting between HEX or Binary to decimal, or the other way. HEX or binary has no intrinsic way of representing negative numbers. This is done by ‘Two’s Complement’. Although this makes arithmetic easy, it complicates the conversion, and can cause difficulties when using maths functions to manipulate bits. However, this is only used with SMALLINTs, INTs and BIGINTs. Both ‘1111111111111111’ and ‘11111111111111111111111111111111’, for example, mean -1, as does 64 ‘1’s; because they represent SMALLINTs, INTs and BIGINTs accordingly. It will all become clear by experiment.
Firstly, we’ll create our functions that convert to or from the binary notation (e.g. 9 to 1001, or 1100 to 12). We start with ToBinaryString. We are able to make use of the conversion routine from a varbinary to hex to make this easier and more versatile. If you are stuck with SQL 2005 or early versions of 2008, you’ll need to adopt the XML trick that I’ve included, commented out.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
IF OBJECT_ID (N'ToBinaryString') IS NOT NULL DROP FUNCTION ToBinaryString GO CREATE FUNCTION ToBinaryString /** summary: > Converts any type of integer that can be implicitly converted to a bigint into a binary string (e.g. 3 = 11) Author: Phil Factor Revision: 1.0 date: 20 May 2013 example: code: | Select dbo.ToBinaryString (CAST(0xFF AS tinyint))--11111111 Select dbo.ToBinaryString (8045)--1111101101101 Select dbo.ToBinaryString (0xFFFF) Select dbo.ToBinaryString (0xAFDC1111) returns: > The number as a binary string **/ ( @InputInteger VARBINARY(8) ) RETURNS VARCHAR(70) AS BEGIN DECLARE @representation VARCHAR(70); SELECT @Representation= REPLACE( REPLACE( CONVERT(VARCHAR(64), @InputInteger, 2), --convert to HEX '0','0000'), '1','0001'); --you need to do 0 and 1 first, and separately. --before 2008, one had to use this to get the hex conversion... --select cast('' as xml).value('xs:hexBinary(sql:variable("@InputInteger") )', 'varchar(64)'); SELECT @Representation= REPLACE(@Representation,HexDigit,BinaryDigits) FROM ( VALUES('2','0010'), ('3','0011'),('4','0100'),('5','0101'),('6','0110'), ('7','0111'),('8','1000'), ('9','1001'),('A','1010'),('B','1011'), ('C','1100'),('D','1101'),('E','1110'),('F','1111'))f(HexDigit,BinaryDigits); /* this simply replaces the ASCII HEX notation with its ASCII binary equivalent. I haven't performance-tested it to see if it is quicker or slower than the maths technique, but it is more like SQL! */ RETURN @Representation; END GO---and we now finish off by executing some quick assertion tests to check IF ( SELECT dbo.ToBinaryString (3))<> '00000000000000000000000000000011' RAISERROR('''ToBinaryString '' stopped working (1)',16,1); IF ( SELECT dbo.ToBinaryString (287))<> '00000000000000000000000100011111' RAISERROR('''ToBinaryString'' stopped working (2)',16,1); IF ( SELECT dbo.ToBinaryString (56))<> '00000000000000000000000000111000' RAISERROR('''ToBinaryString'' stopped working (3)',16,1); IF ( SELECT dbo.ToBinaryString (-170))<> '11111111111111111111111101010110' RAISERROR('''ToBinaryString'' stopped working (4)',16,1); IF ( SELECT dbo.ToBinaryString (CONVERT(smallint,-1)))<> '1111111111111111' RAISERROR('''ToBinaryString'' stopped working (5)',16,1); IF ( SELECT dbo.ToBinaryString (CONVERT(INT,-1)))<> '11111111111111111111111111111111' RAISERROR('''ToBinaryString'' stopped working (6)',16,1); IF ( SELECT dbo.ToBinaryString (CONVERT(Bigint,-1)))<> ( SELECT REPLICATE('1',64)) RAISERROR('''ToBinaryString'' stopped working (7)',16,1); IF ( SELECT dbo.ToBinaryString (0xAFDC))<> '1010111111011100' RAISERROR('''ToBinaryString '' stopped working (1)',16,1); GO |
In order to convert the other way, faced, for example with a string of ‘0’s and ‘1’s, we need to know what we want to convert to, and determine the shortest length of integer datatype that can accommodate the results. and we have the complication of having to follow the twos complement convention where appropriate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
IF OBJECT_ID (N'BinaryStringToInt') IS NOT NULL DROP FUNCTION BinaryStringToInt GO --and create BinaryStringToInt CREATE FUNCTION BinaryStringToInt /** summary: > Converts a string that is a binary number into a bigint (e.g. 3 = '11') Author: Phil Factor Revision: 1.0 date: 24 May 2013 example: code: | Select dbo.BinaryStringToInt('10101010') Select dbo.BinaryStringToInt('1110'+'1111') returns: > the binary string as an integer **/ ( @InputBinaryString VARCHAR(2000) ) RETURNS bigint AS BEGIN DECLARE @ii INT, @len INT,@Output Bigint, @length INT, @Negative bit,@CurrentBit INT; SELECT @InputBinaryString=REPLACE(@InputBinaryString,' ',''); --remove any extra spaces SELECT @Len=LEN(@InputBinaryString), @Output=0,@ii=1 --determine what sort of value it is SELECT @length=CASE WHEN @len=1 THEN 2000 --no two's complement WHEN @len<=8 THEN 2000 --no two's complement WHEN @len<=16 THEN 16 -- -2^15 (-32,768) to 2^15-1 (32,767) WHEN @len<=32 THEN 32 -- -2^31 (-2,147,483,648) to 2^31-1 (2,147,483,647) WHEN @len<=64 THEN 64 -- -2^63 (-9,223,372,036,854,775,808) to 2^63-1 ELSE 2000 END; --no two's complement --checkk to see if we have to do twos complement maths on it. IF (@Len=@Length AND @length IN (16,32,64)) IF SUBSTRING(@InputBinaryString,@ii,1)='1' SELECT @Negative=1; --flag that we need to 'twos complement' IF (@InputBinaryString NOT LIKE '%[^01]%')--check for errors BEGIN --doing repeated multiplication to do the conversion WHILE(@ii <= @len AND @@Error=0) BEGIN SELECT @currentBit=CAST(SUBSTRING(@InputBinaryString,@ii,1) AS INT); IF @negative<>0 SELECT @CurrentBit= CASE @CurrentBit WHEN 0 THEN 1 ELSE 0 END; SELECT @Output = (@output*2)+@CurrentBit; SELECT @ii = @ii + 1; END IF @negative<>0 SELECT @Output = -(@Output+1); --twos complement END ELSE SELECT @Output=NULL; --unknown or error RETURN @output; END GO --and do our unit 'assertion' test to check that nothing obvious is broken IF ( SELECT dbo.BinaryStringToInt ('10101010')) <> 170 RAISERROR('''BinaryStringToInt '' stopped working (1)',16,1); IF ( SELECT dbo.BinaryStringToInt ('1111111111111111')) <> -1 RAISERROR('''BinaryStringToInt '' stopped working (2)',16,1); IF ( SELECT dbo.BinaryStringToInt ('11111111111111111111111111111111')) <> -1 RAISERROR('''BinaryStringToInt '' stopped working (3)',16,1); IF ( SELECT dbo.BinaryStringToInt ('100011111'))<> 287 RAISERROR('''BinaryStringToInt'' stopped working (4)',16,1); IF ( SELECT dbo.BinaryStringToInt ('111111111'))<> 511 RAISERROR('''BinaryStringToInt'' stopped working (5)',16,1); IF ( SELECT dbo.BinaryStringToInt ('NotAnumber'))IS NOT NULL RAISERROR('''BinaryStringToInt'' stopped working (6)',16,1); IF ( SELECT dbo.BinaryStringToInt (NULL))IS NOT NULL RAISERROR('''BinaryStringToInt'' stopped working (7)',16,1); IF ( SELECT dbo.BinaryStringToInt ('10000000000000000000000000000000')) <> -2147483648 RAISERROR('''BinaryStringToInt'' stopped working (8)',16,1); IF ( SELECT dbo.BinaryStringToInt ('10001000100010001000100010001000')) <> -2004318072 RAISERROR('''BinaryStringToInt'' stopped working (9)',16,1); GO |
The third utility that we need for doing bitwise manipulation is only of value in going from binary notation to HEX.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
IF OBJECT_ID (N'BinaryStringToHex') IS NOT NULL DROP FUNCTION BinaryStringToHex; GO CREATE FUNCTION BinaryStringToHex /** summary: > Converts a string that is a binary number into a Hex string Revision: 1.0 date: 20 June 2013 example: code: | Select dbo.BinaryStringToHex('10101010') Select dbo.BinaryStringToHex('10001000100010001000100010001000') returns: > the binary string as a hex string **/ ( @InputBinaryString VARCHAR(2000) ) RETURNS VARCHAR(252) AS BEGIN DECLARE @ii INT, @len INT,@Output Bigint, @byte tinyint,@Hex VARCHAR(100), @length INT, @Negative bit, @CurrentBit INT; SELECT @InputBinaryString=REPLACE(@InputBinaryString,' ',''); SELECT @Len=LEN(@InputBinaryString), @Output=0, @ii=1; SELECT @length=CASE WHEN @len<=4 THEN 4 -- NIBBLE WHEN @len<=8 THEN 8 -- TINYINT WHEN @len<=16 THEN 16 -- smallint WHEN @len<=32 THEN 32 -- int WHEN @len<=64 THEN 64 -- Bigint ELSE 2000 END --crikey IF (@Length <> 2000) SELECT @InputBinaryString=REPLICATE('0',@Length-@len)+@InputBinaryString ELSE IF @Len % 4 >0 --round up to a 4-bit boundary SELECT @InputBinaryString=REPLICATE('0',4-(@len % 4))+@InputBinaryString; SELECT @byte=0, @ii=1; IF (@InputBinaryString NOT LIKE '%[^01]%')--check for errors WHILE @ii<=LEN(@InputBinaryString) BEGIN --do each representation of a bit value in turn. SELECT @currentBit=CAST(SUBSTRING(@InputBinaryString,@ii,1) AS INT); SELECT @byte=(@byte*2)+@CurrentBit; --Select @ii,@byte,@currentbit,@hex,@CurrentBit IF @ii % 4=0 SELECT @hex=COALESCE(@hex,'0x')+SUBSTRING('0123456789ABCDEF',@byte+1,1),@Byte=0; SELECT @ii=@ii+1; END ELSE SELECT @Hex=NULL; RETURN @hex; END GO --here we do our assertion tests as a first-line check of brokenness IF ( SELECT dbo.BinaryStringTohex ('1')) <> '0x1' RAISERROR('''BinaryStringToHex '' stopped working (1)',16,1); IF ( SELECT dbo.BinaryStringTohex ('11111')) <> '0x1F' RAISERROR('''BinaryStringToHex '' stopped working (2)',16,1); IF ( SELECT dbo.BinaryStringTohex ('1010101111001101')) <> '0xABCD' RAISERROR('''BinaryStringToHex '' stopped working (3)',16,1); IF ( SELECT dbo.BinaryStringTohex ('1')) <> '0x1' RAISERROR('''BinaryStringToHex '' stopped working (4)',16,1); IF ( SELECT dbo.BinaryStringTohex ('11111')) <> '0x1F' RAISERROR('''BinaryStringToHex '' stopped working (5)',16,1); IF ( SELECT dbo.BinaryStringTohex ('BADSTRING')) IS NOT NULL RAISERROR('''BinaryStringToHex '' stopped working (6)',16,1); |
Now we’ve got that out of the way, we can demonstrate some of the operations that are possible.
First off, the top bit of an integer (bit 32 in an INT) is different in that it represents the sign bit. Our function can’t easily deal with this because it doesn’t know what sort of integer (Int, smallint or tinyint) you’ve passed to it, so it determines the smallest integer that can accommodate the value. It then checks to see if the ‘sign bit’ is set. This can produce results that are counter-intuitive.
1 2 3 4 |
SELECT dbo.BinaryStringToInt ('11111111111111111111111111111111') -- -1 SELECT dbo.BinaryStringToInt ('1111111111111111111111111111111') -- 2147483647 |
We can convert from decimal to hex without a special utility
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
SELECT CONVERT(VARBINARY(8), 2147483647)--convert the result to HEX -- 0x7FFFFFFF --Hex input is implicitly converted SELECT dbo.ToBinaryString (0xFFFFFFFF) --11111111111111111111111111111111 --if you are stuck on slightly old versions of SQL Server, you can still use this function IF OBJECT_ID (N'DecToHex') IS NOT NULL DROP FUNCTION DecToHex; GO CREATE FUNCTION DecToHex (@InputInteger VARBINARY(8)) RETURNS VARCHAR(64) AS BEGIN RETURN CAST('' AS XML).value('xs:hexBinary(sql:variable("@InputInteger") )', 'varchar(64)'); END GO SELECT dbo.dectohex (12345) -- 00003039 |
We can easily see what can be stored in each of the different integer formats by doing a conversion from a varbinary. This is because it truncates the excess.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--and we can use up to 64 bit on a BIGINT SELECT LEN(dbo.ToBinaryString (0xFFFFFFFFFFFFFFFF));--64 bits --so lets just check on what is available for each type SELECT CONVERT(VARBINARY(8), CONVERT(BIGINT, 0xFFFFFFFFFFFFFFFFFFFF)); --0xFFFFFFFFFFFFFFFF-64 bit SELECT CONVERT(VARBINARY(8), CONVERT(INT, 0xFFFFFFFFFFFFFFFFFFFF)); --0xFFFFFFFF--32 bit SELECT CONVERT(VARBINARY(8), CONVERT(SMALLINT, 0xFFFFFFFFFFFFFFFFFFFF)); --0xFFFF --16-bit SELECT CONVERT(VARBINARY(8), CONVERT(TINYINT, 0xFFFFFFFFFFFFFFFFFFFF)); --0xFF --a 8-bit byte SELECT CONVERT(VARBINARY(8), CONVERT(bit, 0xFFFFFFFFFFFFFFFFFFFF)); --0x01 --a 1-bit bit SELECT CONVERT(VARBINARY(8), 2147452927); --int to hex conversion SELECT dbo.toBinaryString(2147452927); -- int to binary notation |
Having seen the size of the different integer datatypes, let’s now look at the bitwise operations. we’ll start by demonstrating what happens when we use the various bitwise operators, using the utilities I’ve introduced to you
There are three that operate on two integer or binary expressions. These are & (Bitwise AND)| (Bitwise OR) and ^ (Bitwise Exclusive OR).
~ (Bitwise NOT) Performs a bitwise logical NOT operation on a single integer value.
We can now peer at what is going on at the bit level. If you experiment with different values, it is a great way of getting a feel for what is going on.
We’ll run through examples using an INT integer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
--results of a bitwise AND. (try flipping bits to convince yourself) SELECT dbo.toBinaryString (CONVERT (INT, -- first we do a bitwise AND ( dbo.BinaryStringToInt ('01111111111110111101111101111111')--AND this ... & dbo.BinaryStringToInt ('01111111011111111000011111100111')--... with this ... ) -- 01111111011110111000011101100111 --to get this. ) ); -- only where there were bits set in the same position in both numbers were the bits set --results of using a bitwise OR. SELECT dbo.toBinaryString (CONVERT (INT, -- then we do a bitwise OR ( dbo.BinaryStringToInt ('01111111111110111101111101111111')--OR this ... | dbo.BinaryStringToInt ('01111111011111111000011111100111')--... with this ... ) -- 01111111111111111101111111111111 --to get this. ) ); -- 01111111111111111101111111111111 --to get this. -- where there were bits set in the same position in either numbers were the bits set --results of using a bitwise XOR (exclusine OR) SELECT dbo.toBinaryString (CONVERT (INT, -- and the last we do a bitwise XOR ( dbo.BinaryStringToInt ('01111111111110111101111101111111')--XOR this ... ^ dbo.BinaryStringToInt ('01111111011111111000011111100111')--... with this ... ) -- 00000000100001000101100010011000 --to get this. ) ); --only where one but not both bits set in the that position in the numbers were the bits set --useful for toggling individual bits --and finally theresults of using a bitwise NOT. SELECT dbo.toBinaryString (Convert (int, -- and the last we do a bitwise XOR ~ dbo.BinaryStringToInt ('01111111111110111101111101111111')--NOT this ... ) -- 10000000000001000010000010000000 --to get this. ); --only where one but not both bits set in the that position in the numbers were the bits set |
These are by no means the only useful ways of manipulating bits. Beware, though, that you can easily get in a muddle with the effect of two’s complement that is used to represent negative integers.
You can shift left and right easily. You’ll get an ‘overflow’ error if you try to shift into the left-most bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
SELECT dbo.ToBinaryString ( --shift right CONVERT(tinyint, dbo.BinaryStringToInt ('00100001')/2) --shift this right ); -- 00010000 to get this SELECT dbo.ToBinaryString ( --shift left CONVERT(tinyint, dbo.BinaryStringToInt ('00100001')*2) --shift this left ); -- 01000010 to get this SELECT dbo.ToBinaryString ( --shift left CONVERT(tinyint, dbo.BinaryStringToInt ('00100001')*4) --shift this left ); -- 01000010 to get this --What if you want to set a particular bit? Lets say you want to set the seventh bit? SELECT dbo.ToBinaryString (CONVERT (tinyint,POWER(2,7-1))) --Set bit 7 -- 01000000 to get this --so you can set any bits this way. DECLARE @BitToSet INT; SELECT @BitToSet =20; SELECT dbo.ToBinaryString (CONVERT (INT,POWER(2,@BitToSet-1))); --00000000000010000000000000000000 if @BitToSet is set to 20 --What if you want to set several bits at once? Say bits 3, 4 and 10 SELECT dbo.ToBinaryString (CONVERT (smallint,POWER(2,3-1) |POWER(2,4-1)|POWER(2,10-1))); -- 0000001000001100 GO |
So, with the means to convert between decimal, hex and binary notations, we can more easily explore data. With the bitwise operators we can do stuff. What stuff? Well, the following is very much illustrative, as it isn’t of serious use but it shows the power of bitmaps. In our illustration, the entire algorithm becomes useless if the bicycle company were to introduce many more colours. I’ve seen it used in the past more effectively for weekdays or weeks of the year, which aren’t going to change much. More damaging is the fact that joins are likely to be inefficient with tables of any size.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
IF EXISTS ( SELECT * FROM tempdb.information_schema.tables WHERE TABLE_NAME LIKE '#colours%') DROP TABLE #colours GO CREATE TABLE #colours(color_id INT IDENTITY,color VARCHAR(6), code INT not null default 0) INSERT INTO #colours(color,code) SELECT x,0 FROM ( VALUES ('Aqua'),('Azure'),('Beige'),('Black'),('Blue'),('Brown'),('Cyan'),('Gold'), ('Gray'),('Green'),('Ivory'),('Khaki'),('Lime'),('Maroon'),('Navy'),('Olive'), ('Orange'),('Orchid'),('Pink'),('Plum'),('Purple'),('Red'),('Salmon'),('Sienna'), ('Silver'),('Snow'),('Tan'),('Teal'),('Violet'),('White'),('Yellow'))f(x); UPDATE #colours SET code=POWER(2,color_id-1);--OK it isn't real data! ALTER TABLE #colours ADD PRIMARY KEY (code) IF EXISTS ( SELECT * FROM tempdb.information_schema.tables WHERE TABLE_NAME LIKE '#bicycles%') DROP TABLE #bicycles; CREATE TABLE #bicycles(BicycleName VARCHAR(20) PRIMARY KEY, colourmap INT); INSERT INTO #bicycles(BicycleName,colourmap) SELECT x,CAST( RAND(CHECKSUM(NEWID()))*2147483647 AS INT) FROM ( VALUES ('Hercules'),('Tartar'),('Sussex'),('Canute'),('Saxon'),('Sondes'),('Tiger'), ('Echo'),('Chaplin'),('Minerva'),('Nelson'),('Nile'),('Tweed'),('Undine'), ('Clyde'),('Gem'),('Eagle'),('Falcon'),('Lion'),('Volcano'),('Vesuvius'), ('Meteor'),('Swale'),('Magnus'),('Aeolus'),('Os'),('Brigand'),('Ruby'), ('Aas'),('Adrian'),('Huz'),('Rose'),('Dawn'),('Bluebell'),('Reindeer'), ('Enigma'),('Europa'),('Terrier'),('Belgravia'),('Lyons'),('Richmond'), ('Jumbo'),('Gladstone'),('Special'),('Pioneer'),('Precursor'),('Cowes'), ('Osborne'),('Newport'))f(x); --find out what colours are available for each bicycle SELECT BicycleName,color FROM #bicycles INNER JOIN #colours ON colourmap & code <>0 ORDER BY bicyclename; --find out what colours are not available for each bicycle SELECT BicycleName,color FROM #bicycles INNER JOIN #colours ON colourmap & code =0 ORDER BY bicyclename; --How many colours are available for each bicycle SELECT BicycleName, COUNT(*) AS ColourCount FROM #bicycles INNER JOIN #colours ON colourmap & code <>0 GROUP BY bicyclename ORDER BY COUNT(*) DESC go-------------------------------------------------- --now for the next trick we need fewer bicycles IF EXISTS ( SELECT * FROM tempdb.information_schema.tables WHERE TABLE_NAME LIKE '#bicycles%') DROP TABLE #bicycles; GO CREATE TABLE #bicycles(BicycleName VARCHAR(20) PRIMARY KEY, colourmap INT) INSERT INTO #bicycles(BicycleName,colourmap) SELECT x,CAST( RAND(CHECKSUM(NEWID()))*2147483647 AS INT) FROM ( VALUES ('Hercules'),('Tartar'),('Sussex') )f(x); --what colours aren't chosen by any bicycle? DECLARE @bitmap INT=0 --sadly there is no OR aggregation is SQL SELECT @Bitmap=@bitmap | colourmap FROM #bicycles; SELECT color FROM #colours WHERE @Bitmap & code = 0; |
For some bitwise operations, it makes sense to have a helper table, though for our bicycle example, it has proved unnecessary. Here is a simple way of creating the table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
IF EXISTS ( SELECT * FROM information_schema.tables WHERE TABLE_NAME LIKE 'Bithelper') DROP TABLE Bithelper GO CREATE TABLE BitHelper ( TheBit INT IDENTITY, TheMask AS POWER(CONVERT(bigint,2),CONVERT(bigint,TheBit-1)) PERSISTED ) DECLARE @ii INT SELECT @ii=63 WHILE @ii>0 BEGIN INSERT INTO BitHelper DEFAULT VALUES; SELECT @ii=@ii-1; END --and we can then check that it is OK SELECT dbo.ToBinaryString(TheMask) FROM BitHelper ORDER BY TheBit |
Conclusions
The evolution of SQL Server has gradually reduced the requirement for the database developer to be familiar with data at BIT level or to understand bitwise operations. It hasn’t entirely removed the requirement, though. Like a lot of the capabilities of SQL Server, they are there when you need them and the requirement can come up unexpectedly. I still find myself occasionally needing to work with data at the machine level, and I keep my aging HP 16C programmers’ calculator close at hand.
Just occasionally you’ll stumble over a requirement!
Load comments