comparing bit array to bit mask in tsql - sql

I've been working with tsql for quite a while now but I've never seen binary anding or oring in WHERE clause. Now I'm developing new application that would benefit from applying a bitmask.
Lets say I have 16 product grades. Each grade is representrd by a bit position in a bit[] column. So grade A109 would be 0000000000000001, grade B704 would be 0000001000000000, grade V64 is 0100000000000000 and so on. Any grade can only have single 1 in its array column. Now let's say I can turn each of those 3 grades into one another in manufacturing process. So my bit mask for this search would be 0100001000000001. How would I write a WHERE clause to list items of all those 3 grades?

I did some more research and the best solution is to compare masks with bitwise AND operator like this
WHERE mask1 & mask2 <> 0
This is easy, simple and cohesive.

First time for me too. Interesting.
declare #A109 int = 1;
declare #B704 int = 512;
declare #V64 int = 16384;
declare #Xx int = 32;
declare #mask int = 16897; --#A109+#B704+#V64
create table #MyData (
name char(2),
value int);
insert #MyData
values ('a', #A109), ('b1', #B704), ('b2',#B704), ('c', #Xx);
select
name,
value
from #MyData
where (value & #mask) in (#A109, #B704, #V64);
drop table #MyData;
It seems you can't do bitwise operations on binary data! "In a bitwise operation, only one expression can be of either binary or varbinary data type".

In your UI allow the user to select as many grades as (s)he likes. Behind the scenes each grade maps to an int - the Grading table from MarkD's solution. Sum these ints in the application. Pass the total to the SP.
SQL:
create procedure dbo.GetMaskedList #mask int = 0;
...
WHERE BinGrading = #mask;
UI:
int mask = 0;
foreach(item in selectList)
{
mask += item.BinScore;
}
exec GetMaskedList #mask = mask;

In this case, the WHERE clause would be quite simple as you would deal with the decimal (base 10) counterparts of the binary numbers... Where it gets cute, is on the JOIN. To clarify, the approach does not use a bit[] column or a BINARY type - just INT.
Hope you find this helpful.
DECLARE #G1 INT = 1, -- 0000001
#G2 INT = 2, -- 0000010
#G3 INT = 4, -- 0000100
#G4 INT = 8, -- 0001000
#G5 INT = 16, -- 0010000
#G6 INT = 32, -- 0100000
#G7 INT = 64 -- 1000000
;WITH Grading (Grade, BinScore) AS
(
SELECT 'G1', 1 UNION ALL
SELECT 'G2', 2 UNION ALL
SELECT 'G3', 4 UNION ALL
SELECT 'G4', 8 UNION ALL
SELECT 'G5', 16 UNION ALL
SELECT 'G6', 32 UNION ALL
SELECT 'G7', 64
)
,Product (ProductName, BinGrading) AS
(
SELECT 'Foobar', 73 UNION ALL
SELECT 'Franglesnap', 3 UNION ALL
SELECT 'Mebble', 32
)
SELECT *
FROM Product
WHERE BinGrading = (#G1 + #G4 + #G7)
-- Alternatively...
--SELECT *
--FROM Product P
--JOIN Grading G ON P.Bingrading & G.BinScore > 0

Related

Get SQL records matching multiple lines conditions

I have such a table (simplified for exhibit) with SQLServer 2012:
ParentId
Val
11111
1
11111
2
22222
1
22222
2
22222
3
33333
1
Fiddle here : http://sqlfiddle.com/#!18/67a210/1
I'm using a SP with a parameter #filterIds, which contains a string like 1, 2, 3 (note the delimiter is comma and space).
I need a request to get all the lines with a same ParentId which has a Val of all values of #filtersIds.
If #filtersId is 1, 2, 3, result must be 22222 because it's the only one which has lines with Val = 1, Val = 2 and Val = 3.
If #filtersId is 1, 2, results must be 11111 and 22222 because they both have lines with Val = 1 and Val = 2.
If #filtersId is 1, 4, there's no result at all because there's no ParentId with Val = 1 and Val = 4.
I tried with some JOIN but it seems over-complicated for a such a simple request. Is there some quick-and-easy solution I haven't think about ?
You can build a filter table and find all the parentID values that don't have a filter criteria that isn't met.
Note that you can bypass the first step (where I build the filter list cteFilter from values in the data) using the function STRING_SPLIT if your SQL is new enough, but I showed the version you'll need for older SQL.
Also note that using integer values matched against a string filter presents an unusual problem - you can have a value match a part of the intended filter value, i.e. if your filter is '100, 250' you need to take steps to insure a value of 25 does not match. Adding delimiters around both the filter string and the strings generated from the values will allow you to test for only a complete match.
Thanks for providing sample data in Fiddle, it made it easier to answer your question.
DECLARE #Filter nvarchar(50) = '1, 2, 3';
--DECLARE #Filter nvarchar(50) = '1, 2';
create table #Sample(parentId int not null, Val int not null)
insert into #Sample(parentId, Val)
values (11111, 1), (11111, 2), (22222, 1), (22222, 2), (22222, 3), (33333, 1)
;with cteVals as ( --Don't apply functions more than necessary
SELECT DISTINCT Val FROM #Sample
), cteFilter as (--NOTE: For QL Server 2016 (13.x) and later, use string_split to generate this list!
SELECT Val --Otherwise, build a list by finding all Val values in your filter string
FROM cteVals --Next line is complicated to insure matching start and end values
--even if values are of different lengths, we don't want 25 to match filter '100, 250'!
WHERE CHARINDEX(CONCAT(', ', FORMAT(Val, '#'),', '), CONCAT(', ', #Filter, ', ')) > 0
)SELECT DISTINCT parentId --Find all of the ParentIDs
FROM #Sample as S
WHERE NOT EXISTS ( --That don't have any filter requirements ...
SELECT * FROM cteFilter as F
WHERE NOT EXISTS ( --such that the filter
SELECT * FROM #Sample as S2 --isn't in the sample data
WHERE S2.Val = F.Val --matching the filter value
AND S2.parentId = S.parentId --and also matching the parentID in question
)
)
DROP TABLE #Sample
EDIT: Credit to Joe Celko, I used his article https://www.red-gate.com/simple-talk/databases/sql-server/t-sql-programming-sql-server/divided-we-stand-the-sql-of-relational-division/ in troubleshooting my solution, which originally did not work
This is a case of Relational Division With Remainder. There are a number of solutions.
An implementation that is usually more efficient than the other answer gave (a doubly-nested NOT EXISTS) is to join the input list, group by ParentId and ensure that you have as many matches as there are rows in the input list.
You should pass your data in as a Table Valued Parameter. As a hack you can convert to a table variable using STRING_SPLIT, but I'd advise you not to if possible.
Let us assume you have a Table Parameter #input of the form:
CREATE TYPE dbo.IntList TABLE (Value int PRIMARY KEY /* must be unique */);
Then you can do as follows:
DECLARE #count int = (SELECT COUNT(*) FROM #input);
SELECT
t.ParentID
FROM MyTable t
JOIN #input i ON i.Value = t.val
GROUP BY
t.ParentID
HAVING COUNT(*) = #count;
db<>fiddle
You may also want to take a look at other Relational Division techniques.

Moving calculations using SQL

I need to calculate a new column using moving calculations.
For example, I have a table:
A
B
10
15
11
14
12
13
I need to calculate new column where the 1st value is calculated like 5000/10*15, the 2nd value is (5000 / 10 * 15) / 11 * 14, the 3rd one is ((5000 / 10 * 15) / 11 * 14) / 12 * 13 and so on. Where 5000 is a random value and in the future I will use it like a parameter in a stored procedure.
I know, that in Excel for example we can reffer to the previous calculated cell. How can it be calculated using SQL?
Thank you!
create table #test (A int,B int)
insert into #test values(10,15),(11,14),(12,13)
declare #seed int=5000;
;with temp as (
select A,B,row_number() over(order by a) rn from #test
),
cte as
(
select #seed/A*B calculated,rn from temp where rn=1
union all
select c. calculated/t.A*t.B,t.rn from temp t
join cte c on t.rn=c.rn+1
)
select * from cte
There is a warning in the docs that reads:
If there are multiple assignment clauses in a single SELECT statement,
SQL Server does not guarantee the order of evaluation of the
expressions. Note that effects are only visible if there are
references among the assignments.
It means there is no guarantee that it will evaluate the expression left-to-right. For this code:
declare #a int, #b int;
select #a = 2, #b = #a * 3;
select #a, #b;
The result could be 2, 6 (#a = ... evaluated first) or 2, NULL (#b = ... evaluated first).

TSQL - Split GUID/UNIQUEIDENTIFIER

Case: We have smart guids in a table and need to extract 2nd and 4th parts out of it. I was thinking about writing a function that can take in #partnumber and return the extracted value for it.
e.g.
DECLARE #Guid UNIQUEIDENTIFIER = 'A7DDAA60-C33A-4D7A-A2D8-ABF20127C9AE'
1st part = A7DDAA60, 2nd part = C33A, 3rd part = 4D7A, 4th part =
A2D8, and 5th part = ABF20127C9AE
Based on the #partnumber, it would return one of those values.
I'm trying to figure out how to split it most efficiently (STRING_SPLIT doesn't guarantee order).
I am not sure exactly what you mean by "smart" guids, but why not just cast it to a char and pull out the parts by position?
create table t(myguid uniqueidentifier);
declare #p tinyint = 5;
select case #p
when 1 then left(c.v, 8)
when 2 then substring(c.v, 10, 4)
when 3 then substring(c.v, 15, 4)
when 4 then substring(c.v, 20, 4)
when 5 then right(c.v, 12)
end
from t
cross apply (select cast(t.myguid as char(36))) c(v)
You can use, OPENJSON
DECLARE #Guid UNIQUEIDENTIFIER = 'A7DDAA60-C33A-4D7A-A2D8-ABF20127C9AE',
#s varchar(100)
Select #s = replace(#guid,'-','","')
Select * from
(
Select [key] + 1 as Poistion, Value as Part
FROM OPENJSON('["' + #s + '"]')
) Q
Where Poistion in (2,4)
Here is the fiddle.

How to select records testing bits in BINARY column?

I have 2 tables: one of them contains column binary(128), where every bit is some flag. Also I have other table contains list of bit positions which need to check in query.
This is example how I do the query for one bit.
How to do it universally, i.e. to select records testing bits in positions from the second table?
Should it be a function? what?
DECLARE #nByteNum integer
DECLARE #nBitNumInByte integer
DECLARE #nMask integer
DECLARE #nBigBitNum integer
declare #t table(id int not null identity, id1 int, banner binary(128))
declare #bitpositions table(id int not null identity, position int)
insert into #bitpositions(position) values(8)
insert into #bitpositions(position) values(24)
insert into #bitpositions(position) values(30)
insert into #t(id1, banner)
select 1, 0x0
union all
select 1, 0x000100FF
union all
select 1, 0x010200FF
union all
select 10, 0x010208
union all
select 10, 0x000100
union all
select 10, 0x040000
select * from #t
-- This is for one bit
SET #nBigBitNum= 24
SET #nByteNum= #nBigBitNum/8
SET #nBitNumInByte= #nBigBitNum % 8 -- 0,1...6,7
SET #nMask = POWER(2, #nBitNumInByte ) -- 128,64,... 2,1
SET #nByteNum= #nByteNum +1
select * from #t where SUBSTRING(banner, #nByteNum,1)&#nMask=#nMask
Bitwise operations in SQL Server do not work on BINARY data in the way that you are expecting. They work on integer datatypes only as noted on the MSDN page for & (Bitwise AND).
The general syntax for checking bitmasked values in a query is: WHERE {Column} & {BitMaskedValue} = {BitMaskedValue}.
Look at the following examples using your desired scenario of trying to find records that have bit positions 8 and 16 turned on.
-- 24 = 16 + 8
SELECT 24 & 16 -- 16
SELECT 24 & 8 -- 8
SELECT 24 & 17 -- 16
SELECT 24 & 32 -- 0
SELECT 24 & 26 -- 24

Find missing numerical values in range [duplicate]

This question already has answers here:
How can we find gaps in sequential numbering in MySQL?
(16 answers)
Closed 8 years ago.
I've read several articles that one of the mistakes a common programmer does is not using SQL's potential and since then I started searching for replacing parts of my code with SQLish solutions rather than fetching data and processing with a programming language, although I'm a real rookie with SQL.
Say I have a table randomly populated with values from 0 to 10 and I want to know which values are missing in this range.
For example, the table consists these values: 0, 1, 3, 4, 5, 7, 8, 9.
The query should return: 2, 6, 10.
[F5] solution (assuming sql server):
-- table with id=0..10
drop table #temp
GO
create table #temp (
id int not null identity(0,1),
x int
)
GO
insert into #temp (x) values(0)
GO 11
-- your number:
drop table #numbers
GO
select
*
into #numbers
from (
select 0 as n union all select 1 union all select 3 union all select 4 union all select 5 union all select 7 union all select 8 union all select 9
) x
GO
-- result:
select
*
from #temp t
left join #numbers n
on t.id=n.n
where 1=1
and n.n is null
This solution uses SQL-Server-Syntax (but AFAIK only GO is specific to the SQL Server Management Studio)
I would join against a table valued function that gets you all numbers in a certain range (example fiddle):
CREATE FUNCTION dbo.GetNumbersInRange(#Min INT, #Max INT)
RETURNS #trackingItems TABLE (Number INT)
AS BEGIN
DECLARE #counter INT = #Min
WHILE (#counter <= #Max)
BEGIN
INSERT INTO #trackingItems (Number) SELECT #counter
SELECT #counter = #counter + 1
END
RETURN
END
GO
As an example I have set up a table that contains some numbers (with gaps)
CREATE TABLE MyNumbers (Number INT)
INSERT INTO MyNumbers (Number)
SELECT 1
UNION
SELECT 2
UNION
SELECT 4
UNION
SELECT 5
UNION
SELECT 7
UNION
SELECT 8
To find the missing numbers you can use a LEFT JOIN like this
SELECT
AllNumbers.Number
FROM GetNumbersInRange(1, 10) AS AllNumbers
LEFT JOIN MyNumbers ON AllNumbers.Number = MyNumbers.Number
WHERE MyNumbers.Number IS NULL