Aggregate bitfield values with binary OR - sql-server

I have a table with int values being used as bitfields (where each bit is a flag).
Now I would like to aggregate them with a binary operation (in my case OR) so that:
SELECT 1 AS bitfield
INTO #TABLE
UNION ALL SELECT 1 + 2 + 8 + 32
UNION ALL SELECT 2 + 128
UNION ALL SELECT 2 + 32
SELECT AND_AGGR(bitfield) -- Invalid because AND_AGGR doesn't exist
FROM #TABLE
DROP #TABLE
would result in the value 171
What would be a good way to do this that hopefully doesn't require a lot of | and MAX (but if you must, you must)?
I am using MS SQL Server 2008 myself, but solutions on other servers are also of interest.

On MySQL and PostgreSQL you can use BIT_OR.
I don't think SQL Server has this aggregate function.
You could do it with lots of MAX and & as you said:
MAX(x & 1) + MAX(x & 2) + ... + MAX(x & 128)

If you're expecting the result 171, surely you mean binary OR not AND?
In any case, this solution aggregates the values into a variable:
SELECT 1 AS bitfield
INTO #TABLE
UNION ALL SELECT 1 + 2 + 8 + 32
UNION ALL SELECT 2 + 128
UNION ALL SELECT 2 + 32
DECLARE #i int = 0
SELECT #i = #i | bitfield
FROM #TABLE
SELECT #i
DROP TABLE #table
This might not meet your requirements if you want to group the aggregation by another field.
It is also unlikely to perform well on a large table.

In MS SQL Server
DECLARE #agg VARCHAR(MAX) = '0001,0011,0101,0101,0101'
SELECT CONVERT(binary(4), VALUE, 2) , VALUE FROM STRING_SPLIT( #agg , ',')
DECLARE #sum AS BIGINT = 0
DECLARE #mul AS BIGINT = 0xffffffff
SELECT #sum |= v
, #mul &= v
FROM STRING_SPLIT( #agg , ',')
CROSS APPLY (VALUES (CONVERT(binary(4), VALUE, 2))) _(v)
PRINT FORMAT(#sum,'X8')
PRINT FORMAT(#mul,'X8')
Prints
VALUE
---------- ------------
0x00010000 0001
0x00110000 0011
0x01010000 0101
0x01010000 0101
0x01010000 0101
01110000
00010000
In more complex word you need:
CREATE OR ALTER FUNCTION dbo.BOR( #agg VARCHAR(MAX))
RETURNS BIGINT
AS
BEGIN
DECLARE #sum AS BIGINT = 0
SELECT #sum |= CONVERT(BIGINT, VALUE)
FROM STRING_SPLIT( #agg , ',')
RETURN #sum
END
GO
CREATE OR ALTER FUNCTION dbo.BAND( #agg VARCHAR(MAX))
RETURNS BIGINT
AS
BEGIN
DECLARE #mul AS BIGINT = 0xffffffffffffffff
SELECT #mul &= CONVERT(BIGINT, VALUE)
FROM STRING_SPLIT( #agg , ',')
RETURN #mul
END
GO
when using bitmap of payment periods
;WITH delayedPayment AS
(SELECT * FROM ( VALUES
( 123, 67, '2020-2-1')
,( 123, 67, '2020-4-1')
,( 123, 67, '2020-5-1')
,( 123, 67, '2020-6-1')
,( 123, 68, '2020-6-1') -- another agreement
,( 123, 67, '2020-12-1')
,( 456, 69, '2020-4-1')
,( 456, 69, '2020-8-1')
,( 456, 69, '2020-10-1')
,( 456, 69, '2020-11-1')) _(cuno, loan, missedDuedate)
)
, delayPattern AS
(SELECT cuno
, sum_months
, bor_months
, IIF( FORMAT( CAST(bor_months AS BIGINT), 'X16') LIKE '%111%', 'dalyad 3+ month in row', NULL) delayState
FROM (SELECT cuno
, SUM(POWER( 16.0, CONVERT( BIGINT, DATEDIFF( month, missedDuedate, '2020-12-1')))) sum_months
, dbo.BOR( STRING_AGG( CONVERT( BIGINT, POWER( 16.0, DATEDIFF( month, missedDuedate, '2020-12-1'))),',')) bor_months
FROM delayedPayment
GROUP BY cuno
) s
)
SELECT cuno
, FORMAT( CAST(sum_months AS BIGINT), 'X16') sum_months
, FORMAT( CAST(bor_months AS BIGINT), 'X16') bor_months
, delayState
FROM delayPattern
cuno sum_months bor_months delayState
123 00000*10112*000001 00000*10111*000001 dalyad 3+ month in row
456 0000000100010110 0000000100010110 NULL
But sometimes just one need to think and you can do it with SUM
, delayPattern AS -- optimal
(SELECT cuno
, bor_months
, IIF( FORMAT( CAST(bor_months AS BIGINT), 'X16') LIKE '%111%', 'dalyad 3+ month in row', NULL) delayState
FROM (SELECT cuno
, SUM(POWER( 16.0, missedmonth)) bor_months
FROM ( SELECT DISTINCT cuno
, missedmonth
FROM delayedPayment
CROSS APPLY (VALUES ( DATEDIFF( month, missedDuedate, '2020-12-1'))) _(missedmonth)
GROUP BY cuno, missedmonth
) ss
GROUP BY cuno
) s
)
SELECT cuno
, FORMAT( CAST(bor_months AS BIGINT), 'X16') bor_months
, delayState
FROM delayPattern
Will print
cuno bor_months delayState
123 0000010111000001 dalyad 3+ month in row
456 0000000100010110 NULL
NOTE: I am using HEX format and POWER(16.0, X) , just to be lazy, POWER(2.0, X) will be correct, but then you need bin->string formatter. Something like this:
CREATE OR ALTER FUNCTION dbo.toBinaryString(#p INT)
RETURNS VARCHAR(24)
AS
BEGIN
RETURN REVERSE(REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE( FORMAT(#p,'X8'),
'0', '....'), '1', '...x'),'2', '..x.'),'3', '..xx'),
'4', '.x..'), '5', '.x.x'),'6', '.xx.'),'7', '.xxx'),
'8', 'x...'), '9', 'x..x'),'A', 'x.x.'),'B', 'x.xx'),
'C', 'xx..'), 'D', 'xx.x'),'E', 'xxx.'),'F', 'xxxx'),
'.','0'),'x','1'))
END

Related

How to pad a number with zero?

A table was built, however, there is one column for the District School. This field should be in a 0000 format, however when the table was built, it dropped the leading zeros, so, if a District School number was 0001, it came in as 1, same as if the DS number was 0052, it came in as 52, but I need to alter that column so that all values are reflected as 0000. How do I go about doing that?
SELECT RIGHT('0000'+ISNULL(field,''),4)
CREATE FUNCTION [dbo].[strCeros]
(
#numero INT,
#cifras TINYINT
)
RETURNS NVARCHAR
(
25
)
AS
BEGIN
DECLARE #str NVARCHAR(25)
IF #cifras > 25
SET #cifras = 25
SET #str = RIGHT(
REPLICATE('0', #cifras) + CAST(#numero AS NVARCHAR(25)),
#cifras
)
RETURN #str
END
GO
SELECT dbo.strCeros(99, 4) padZeros
select id , REPLICATE('0', 4 - len( cast(id as nvarchar) ) ) + cast(id as nvarchar)
from (
select 1 id
union
select 5 id
union
select 8 id
union
select 19 id
union
select 33 id
union
select 52 id
) dts

Create varchar column with mixed values from other columns in SQL

I have this scenario:
CREATE TABLE tbl(templateId INT, id INT, name NVARCHAR(50), value NVARCHAR(50), row INT);
INSERT INTO tbl(templateId, id, name, value, row)
VALUES
(1, 12, 'question1', '5 To 10', 1),
(2, 12, 'question2', 'washing machine', 1),
(3, 12, 'question3', 'yes', 1),
(4, 12, 'question2', 'tv', 2),
(5, 12, 'question1', '11 To 15', 2),
(6, 12, 'question1', '16 To 20', 2),
(7, 12, 'question4', 'employed' 2);
The data must be grouped by id and row
and what I would need would be another column with data like this:
-If we have different questions on the same row (grouped by id = 12 and row = 1):
(question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
-If we have different questions on the same row and one of them has many answers it should look like this (id = 12 and row = 2):
(question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))
I managed to create the first case, but I’m having problems with the second. For the second I created something like
(question2: (tv) [AND] question1: (11 To 15) OR question1:(16 To 20) OR question4:(employed))
but it's not good, the answers for question1 have to be separated by comma and the name shouldn't be displayed everytime. Moreover, it puts [AND] only between the first two names, it should be between question1 [AND] question4 as well, I just don't know how to replace that OR...
I’ve created a function like this :
declare #result varchar(1000), #name1 varchar(250), #name2 varchar(250),
#duplicates int;
set #result = '';
set #duplicates = 0;
set #name1 = '';
set #name2 = '';
SELECT #result = #result + ' [AND] ' + t.name + ': (' + t.value + ')',
#duplicates = (len(#result) - len(replace(#result,t.name,''))) /
LEN(t.name)
FROM tbl t
WHERE t.id = #table_id and t.row = #row
if(len(#result)>0)
if (#duplicates > 1)
begin
SET #result =replace(substring(#result, 7, LEN(#result) - 4), '
[AND] ', ' OR ');
SET #name1 = LEFT(#result,CHARINDEX(': ',#result)-1);
SET #name2 = SUBSTRING(SUBSTRING(#result,CHARINDEX('OR ', #result)
+ 2,LEN(#result)), 0,CHARINDEX(':', #result) + 0)
if (#name1 <> #name2)
begin
SET #result=STUFF(#result, CHARINDEX('OR', #result), LEN('OR'),
'[AND]')
end
end
else
begin
SET #result=substring(#result, 7, LEN(#result) - 4);
end
return #result;
I hope I managed to make clear what I want to accomplish. Every suggestion will be highly appreciated. Thanks !
Give this a shot
Example
;with cte as (
Select top 1 with ties
[id]
,[row]
,[name]
,TempValue = Stuff((Select ', ' + value From tbl Where [Row]=A.[Row] and [Name]=A.[Name] For XML Path ('')),1,2,'')
,RN = Row_Number() over (Partition By [id],[row] Order by templateId)
From tbl A
Order by Row_Number() over (Partition By ID,[name],row order by templateid)
)
Select [Row]
,NewValue = '('+Stuff((Select ' [AND] ' +concat(Name,': (',TempValue,')') From cte Where [Row]=A.[Row] Order by RN For XML Path ('')),1,7,'')+')'
From cte A
Group By [Row]
Returns
Row NewValue
1 (question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
2 (question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))

TSQL Pivot Table [duplicate]

I have small table which contains students marks. Table data is shown in below image.
It is look like below in excel
I want to calculate the total using dynamic SQL. I don't want to update it. However, I just want to select all the data with calculated total using dynamic SQL.
Please refer below code:
DECLARE #SQL NVARCHAR(MAX)=''
DECLARE #SNumberList NVARCHAR(MAX)=''
DECLARE #CalculatedLineNumbers NVARCHAR(MAX)=''
SELECT #CalculatedLineNumbers = #CalculatedLineNumbers+ ', '+
CASE WHEN SNo = 7 THEN '[1] + [4] [7]'
WHEN SNo = 8 THEN '[2] + [5] [8]'
WHEN SNo = 9 THEN '[3] + [6] [7]'
ELSE QUOTENAME(SNo)
END
FROM Student
SELECT #SNumberList = #SNumberList+ ', '+QUOTENAME(SNo)
FROM Student
SELECT #SNumberList=STUFF(#SNumberList, 1,1, ''),
#CalculatedLineNumbers=STUFF(#CalculatedLineNumbers,1,1,'')
SET #SQL= '
SELECT Year,'+#CalculatedLineNumbers+'
FROM
(
SELECT *
from Student s) AS J
PIVOT
(
MAX([Marks]) FOR Marks IN ('+#SNumberList+')
) AS P'
EXEC SP_EXECUTESQL #SQL
Taking the excel screenshot to be the expected output, you could accomplish this with just specifying the Year of interest.
Sample Data:
create table #sample_data
(
SNo int
, [LineNo] int
, ColumnNo int
, LineName varchar(15)
, ColumnName varchar(25)
, Marks int
, [Year] int
)
insert into #sample_data
values (1, 1, 1, 'Math', 'Jay', 97, 2018)
, (2, 1, 2, 'Math', 'Sam', 95, 2018)
, (3, 1, 3, 'Math', 'Jack', 90, 2018)
, (4, 2, 1, 'Science', 'Jay', 87, 2018)
, (5, 2, 2, 'Science', 'Sam', 88, 2018)
, (6, 2, 3, 'Science', 'Jack', 86, 2018)
, (7, 3, 1, 'Total', 'Jay', null, 2018)
, (8, 3, 2, 'Total', 'Sam', null, 2018)
, (9, 3, 3, 'Total', 'Jack', null, 2018)
Answer:
The script below, determines the relevant ColumnName values based on setting the Year, and forces the columns to show up in the expected order based on the ColumnNo values. After pivoting the appropriate records, the query makes use of the group by grouping sets to generate the Total record.
declare #ColumnNameList nvarchar(max)
, #ColumnNameListSums nvarchar(max)
, #DynamicQuery nvarchar(max)
, #Year int = 2018 --set by OP in question
--get the full list of ColumnNames in a delimeter ("|") seperated string
set #ColumnNameList =
(
select stuff((
select '| ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
--its possible to use the previous variable as well, but easier to create another one
set #ColumnNameListSums =
(
select stuff((
select ', sum(a.' + a.ColumnName + ') as ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
set #DynamicQuery =
'
select isnull(b.LineName, ''Total'') as LineName
, b.' + ltrim(replace(#ColumnNameList, '| ', ', b.')) + '
from (
select a.LineName
, ' + #ColumnNameListSums + '
from (
select t.LineName
, t.ColumnName
, t.Marks
, t.[Year]
from #sample_data as t
where t.LineName <> (''Total'') --don''t need it, will generate totals later
and t.[Year] = ' + cast(#Year as char(4)) + '
) as a
pivot (max(a.Marks) for a.ColumnName in ([' + ltrim(replace(#ColumnNameList, '| ', '], [')) + '])) as a
group by grouping sets
(
(
a.LineName
)
,
(
--purposefully left empty
)
)
) as b
'
print #DynamicQuery --in order to see query being executed
exec(#DynamicQuery);
Output:
Given the sample data, the following output is generated.
+----------+-----+-----+------+
| LineName | Jay | Sam | Jack |
+----------+-----+-----+------+
| Math | 97 | 95 | 90 |
| Science | 87 | 88 | 86 |
| Total | 184 | 183 | 176 |
+----------+-----+-----+------+
SQL Server does not do "double headers", so you can't get the 2018 in the output of a query. You could manually add the top header of "2018" in row 1 in excel.

How to use dynamic SQL to add value of 2 columns

I have small table which contains students marks. Table data is shown in below image.
It is look like below in excel
I want to calculate the total using dynamic SQL. I don't want to update it. However, I just want to select all the data with calculated total using dynamic SQL.
Please refer below code:
DECLARE #SQL NVARCHAR(MAX)=''
DECLARE #SNumberList NVARCHAR(MAX)=''
DECLARE #CalculatedLineNumbers NVARCHAR(MAX)=''
SELECT #CalculatedLineNumbers = #CalculatedLineNumbers+ ', '+
CASE WHEN SNo = 7 THEN '[1] + [4] [7]'
WHEN SNo = 8 THEN '[2] + [5] [8]'
WHEN SNo = 9 THEN '[3] + [6] [7]'
ELSE QUOTENAME(SNo)
END
FROM Student
SELECT #SNumberList = #SNumberList+ ', '+QUOTENAME(SNo)
FROM Student
SELECT #SNumberList=STUFF(#SNumberList, 1,1, ''),
#CalculatedLineNumbers=STUFF(#CalculatedLineNumbers,1,1,'')
SET #SQL= '
SELECT Year,'+#CalculatedLineNumbers+'
FROM
(
SELECT *
from Student s) AS J
PIVOT
(
MAX([Marks]) FOR Marks IN ('+#SNumberList+')
) AS P'
EXEC SP_EXECUTESQL #SQL
Taking the excel screenshot to be the expected output, you could accomplish this with just specifying the Year of interest.
Sample Data:
create table #sample_data
(
SNo int
, [LineNo] int
, ColumnNo int
, LineName varchar(15)
, ColumnName varchar(25)
, Marks int
, [Year] int
)
insert into #sample_data
values (1, 1, 1, 'Math', 'Jay', 97, 2018)
, (2, 1, 2, 'Math', 'Sam', 95, 2018)
, (3, 1, 3, 'Math', 'Jack', 90, 2018)
, (4, 2, 1, 'Science', 'Jay', 87, 2018)
, (5, 2, 2, 'Science', 'Sam', 88, 2018)
, (6, 2, 3, 'Science', 'Jack', 86, 2018)
, (7, 3, 1, 'Total', 'Jay', null, 2018)
, (8, 3, 2, 'Total', 'Sam', null, 2018)
, (9, 3, 3, 'Total', 'Jack', null, 2018)
Answer:
The script below, determines the relevant ColumnName values based on setting the Year, and forces the columns to show up in the expected order based on the ColumnNo values. After pivoting the appropriate records, the query makes use of the group by grouping sets to generate the Total record.
declare #ColumnNameList nvarchar(max)
, #ColumnNameListSums nvarchar(max)
, #DynamicQuery nvarchar(max)
, #Year int = 2018 --set by OP in question
--get the full list of ColumnNames in a delimeter ("|") seperated string
set #ColumnNameList =
(
select stuff((
select '| ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
--its possible to use the previous variable as well, but easier to create another one
set #ColumnNameListSums =
(
select stuff((
select ', sum(a.' + a.ColumnName + ') as ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
set #DynamicQuery =
'
select isnull(b.LineName, ''Total'') as LineName
, b.' + ltrim(replace(#ColumnNameList, '| ', ', b.')) + '
from (
select a.LineName
, ' + #ColumnNameListSums + '
from (
select t.LineName
, t.ColumnName
, t.Marks
, t.[Year]
from #sample_data as t
where t.LineName <> (''Total'') --don''t need it, will generate totals later
and t.[Year] = ' + cast(#Year as char(4)) + '
) as a
pivot (max(a.Marks) for a.ColumnName in ([' + ltrim(replace(#ColumnNameList, '| ', '], [')) + '])) as a
group by grouping sets
(
(
a.LineName
)
,
(
--purposefully left empty
)
)
) as b
'
print #DynamicQuery --in order to see query being executed
exec(#DynamicQuery);
Output:
Given the sample data, the following output is generated.
+----------+-----+-----+------+
| LineName | Jay | Sam | Jack |
+----------+-----+-----+------+
| Math | 97 | 95 | 90 |
| Science | 87 | 88 | 86 |
| Total | 184 | 183 | 176 |
+----------+-----+-----+------+
SQL Server does not do "double headers", so you can't get the 2018 in the output of a query. You could manually add the top header of "2018" in row 1 in excel.

subquery with table value function not working

DECLARE #temp AS TABLE (id INT, NAME VARCHAR(20) )
DECLARE #str VARCHAR(20) = '1,2'
INSERT INTO #temp (id, NAME)
VALUES (1, ''), (2, ''), (2, '')
SELECT *
FROM #temp a
WHERE id IN ((SELECT String FROM dbo.FN_SplitStrings(#str,',')))
I'm getting the following error while running this
Conversion failed when converting the varchar value '1,2' to data type
int.
Code:
CREATE function [dbo].[FN_SplitStrings]
(
#StringToSplit varchar(8000),
#Separator varchar(128)
)
RETURN TABLE
AS
RETURN
with indices as
(
select
0 S, 1 E
union all
select
E, charindex(#Separator, #StringToSplit, E) + len(#Separator)
from
indices
where E > S
)
select
substring(#StringToSplit,S, case when E > len(#Separator)
then e-s-len(#Separator) else len(#StringToSplit) - s + 1 end) String ,
S StartIndex
from
indices
where
S > 0
Try this. This splitting can be used without a function
DECLARE #temp AS TABLE
(
id INT,
NAME VARCHAR(20)
)
DECLARE #str VARCHAR(20)='1,2'
INSERT INTO #temp
( id, NAME )
VALUES ( 1, '' ),
( 2, ''),
( 2, '')
SELECT * FROM #temp a
WHERE id IN
(
SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'KeyWords'
FROM
(
-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
SELECT CAST ('<M>' + REPLACE(#str, ',', '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
)
Click here to view the result
EDIT:
You had some problems in function for splitting.
Function
ALTER FUNCTION dbo.FN_SplitStrings(#StringToSplit varchar(8000),#Separator char(1))
RETURNS table
AS
RETURN (
WITH splitter_cte AS (
SELECT CHARINDEX(#Separator, #StringToSplit) as pos, 0 as lastPos
UNION ALL
SELECT CHARINDEX(#Separator, #StringToSplit, pos + 1), pos
FROM splitter_cte
WHERE pos > 0
)
SELECT SUBSTRING(#StringToSplit, lastPos + 1,
case when pos = 0 then 80000
else pos - lastPos -1 end) as String
FROM splitter_cte
)
Query
DECLARE #temp AS TABLE (id INT, NAME VARCHAR(20) )
DECLARE #str VARCHAR(20) = '1,2'
INSERT INTO #temp (id, NAME)
VALUES (1, ''), (2, ''), (2, '')
SELECT *
FROM #temp a
WHERE id IN ((SELECT String FROM dbo.FN_SplitStrings(#str,',')))
Click here to view result
The reason is a mix in data types and the fact that your function is an inline table valued functions which means that it is embedded into the query before query optimization takes place.
If you remove where S > 0 from your function and execute it with 1,2, the result of the function is:
String
------
1,2
1
2
Notice the first row where the value is 1,2.
When the optimizer does its job with your query the comparison against the column id is done before the where clause of the function is evaluated. In that comparison you have an implicit cast to int and 1,2 can not be casted to an int.
To fix this you can make sure that the String column of your split function is always an int (and perhaps changing the name of the column in the process).
select
cast(substring(#StringToSplit,S, case when E > len(#Separator)
then e-s-len(#Separator)
else len(#StringToSplit) - s + 1
end) as int) String ,

Resources