37

How can one use in SQL Server the processing of the Flags such as on enums in C#?


For example, how would one return a list of users that are part of a list or conditions like so:

ConditionAlpha = 2
ConditionBeta  = 4
ConditionGamma = 8

...

Then there will be users with some of these conditions against them like so:

User1: 6 (conditions Alpha and Beta)
User2: 4 (condition Beta)
User3: 14 (conditions Alpha, Beta and Gamma)

...

We want to be able to do a query where we say get all users with the first condition Alpha and in this scenario it would return users 1 and 3 even though they have other conditions as well.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
user351711
  • 3,171
  • 5
  • 39
  • 74
  • 6
    It would be more "SQL-like" to store this information in a many-to-many table. So you'd store rows `(1,1),(1,2),(2,2),(3,1),(3,2),(3,3)` in a separate table. It would make for more natural querying, and offers indexing opportunities. – Damien_The_Unbeliever Nov 30 '12 at 13:04

5 Answers5

37

The bitwise operator for checking whether a flag is set in SQL is &. The WHERE clause needs to evaluate to a BOOLEAN expression, like this:

create table #temp (id int, username varchar(20), flags int)

insert into #temp values
(1, 'User1', 6 /* (2 | 4) */),
(2, 'User2', 4),
(3, 'User3', 14 /* (2 | 4 | 8) */)

declare @ConditionOne int = 2

select *
from   #temp
where  flags & @ConditionOne <> 0

declare @ConditionTwo int = 4

select *
from   #temp
where  flags & @ConditionTwo <> 0

declare @ConditionThree int = 8

select *
from   #temp
where  flags & @ConditionThree <> 0

drop table #temp

These queries return the following resultsets:

id          username             flags
----------- -------------------- -----------
1           User1                6
3           User3                14

id          username             flags
----------- -------------------- -----------
1           User1                6
2           User2                4
3           User3                14

id          username             flags
----------- -------------------- -----------
3           User3                14
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
James L.
  • 9,384
  • 5
  • 38
  • 77
28

While the bitwise operator suggested by James will work, it will not be very performant in a relational database, especially when you try to scale to millions of records. The reason is that functions in the where clause are not sargable (they prevent an index seek).

What I would do would be create a table which contains all possible combinations of flags and conditions, which will enable an index seek on the condition.

Populate FlagConditions. I used a single (tinyint). Should you need more Flags, you should be able to expand on this approach:

CREATE TABLE FlagConditions (
      Flag TINYINT
    , Condition TINYINT
    , CONSTRAINT Flag_Condition PRIMARY KEY CLUSTERED (Condition,Flag)
);

CREATE TABLE #Flags (
      Flag TINYINT IDENTITY(0,1) PRIMARY KEY CLUSTERED
    , DummyColumn BIT NULL);
GO

INSERT #Flags
        ( DummyColumn )
SELECT NULL;
GO 256

CREATE TABLE #Conditions(Condition TINYINT PRIMARY KEY CLUSTERED);

INSERT #Conditions ( Condition )
    VALUES  (1),(2),(4),(8),(16),(32),(64),(128);

INSERT FlagConditions ( Flag, Condition )        
    SELECT
    Flag, Flag & Condition
    FROM #Flags f
    CROSS JOIN #Conditions c
    WHERE Flag & Condition <> 0;

DROP TABLE #Flags;
DROP TABLE #Conditions;

Now you can use the FlagConditions table any time you need to efficiently seek on an enum bitwise condition:

DECLARE @UserFlags TABLE (Username varchar(10), Flag tinyint);

INSERT @UserFlags(Username, Flag)
    VALUES ('User1',6),('User2',4),('User3',14);

DECLARE @Condition TINYINT = 2;

SELECT u.*
FROM @UserFlags u
INNER JOIN FlagConditions fc ON u.Flag = fc.Flag
WHERE fc.Condition = @Condition;

This returns:

Username   Flag
---------- ----
User1      6
User3      14

Your DBA will thank you for going this set oriented route.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
brian
  • 3,635
  • 15
  • 17
  • I don't understand how this answer has 20 upvotes when the SQL it contains literally doesn't even parse (the "go 256"). Even after removing the 256, it doesn't do what the answer says it should. – Ian Kemp Aug 06 '20 at 11:32
  • 1
    @IanKemp This will work in SSMS or sqmcmd. GO is not a T-SQL statement but a batch separator. It is recognized by sql tools like SSMS. It has the syntax `GO [count]` where count is the number of times to run the preceding batch. So in this case it runs the insert 256 times. docs – Justin Sep 10 '20 at 18:50
  • elegant but what about if you need to filter on two or more conditions? – CervEd Dec 20 '20 at 16:45
  • you do an additional join for each condition? – CervEd Dec 20 '20 at 16:54
  • It's a very elegant solution. I took the liberty of updating the answer to a pure TSQL method of generating 256 rows since `GO n` isn't supported in all clients – CervEd Nov 14 '21 at 17:14
  • I still think this is a great answer that I've found useful many times. For some reason my edit was not accepted. I find myself coming back here often but I hate SSMS with a vengeance and think the solution deserves a more compatible solution which I posted below – CervEd Feb 16 '22 at 13:40
7

I had almost the same problem and could come up with such solution:

SELECT  t.value
    , ISNULL(t.C1 + ', ', '') + ISNULL(t.C2, '') + ISNULL(', ' + t.C3, '') AS [type]
FROM
(
    SELECT value,
        CASE WHEN (type & 2) <> 0  THEN 'Type1' END AS C1,
         CASE WHEN (type & 4) <> 0  THEN 'Type2' END AS C2,
         CASE WHEN (type & 8) <> 0  THEN 'Type3' END AS C3
    FROM db.Agent
) t

and the result was as follows:

value       type
----------  ------------------------------------
14          Type1, Type2, Type3
12          Type2, Type3
14          Type1, Type2, Type3
saeedehp
  • 79
  • 1
  • 3
1

C# Enum: CopEntry = 1 << 17

SQL Server: case when (Features & power(2, 17)) = 0 then 0 else 1 end as COPEntry

Graham Laight
  • 4,700
  • 3
  • 29
  • 28
1

Same as except it's pure T-SQL so it works not only in SSMS or elsewhere

DROP TABLE IF EXISTS flag_conditions
CREATE TABLE flag_conditions
(
    flag      TINYINT,
    condition TINYINT,
    CONSTRAINT Flag_Condition PRIMARY KEY CLUSTERED (condition, flag)
);

CREATE TABLE #Flags
(
    Flag        TINYINT IDENTITY (0,1) PRIMARY KEY CLUSTERED,
    DummyColumn BIT NULL
);
GO

WITH x AS (SELECT n FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v(n))
INSERT #Flags
    (DummyColumn)
SELECT NULL
FROM (
         SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
         FROM x ones,
              x tens,
              x hundreds,
              x thousands
     ) t
WHERE n <= 256;

SELECT *
FROM #Flags;

CREATE TABLE #Conditions
(
    Condition TINYINT PRIMARY KEY CLUSTERED
);

INSERT #Conditions (Condition)
VALUES (1),
       (2),
       (4),
       (8),
       (16),
       (32),
       (64),
       (128);

INSERT flag_conditions (flag, condition)
SELECT Flag,
       Flag & Condition
FROM #Flags f
         CROSS JOIN #Conditions c
WHERE Flag & Condition <> 0;

DROP TABLE #Flags;
DROP TABLE #Conditions;

SELECT *
FROM flag_conditions;
CervEd
  • 3,306
  • 28
  • 25