web-dev-qa-db-ja.com

t-sqlでBitMaskをデコードして集約する

アクセス許可を格納するビットマスクフィールドを含むテーブルがあります。各ビットは、特定のアクセス許可が付与されているかどうかを示します。これは簡単な例です:

DECLARE @T TABLE (id smallint identity, BitMask tinyint);
INSERT INTO @T (BitMask) VALUES
  (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);


SELECT
  t.id, t.BitMask, bm.BitNum, bm.Permission
FROM @T t
OUTER APPLY (
  SELECT * FROM (VALUES
    (t.id, 0, 'Can X'),
    (t.id, 1, 'Can Y'),
    (t.id, 2, 'Can Z')
  ) bm(id, BitNum, Permission)
  WHERE t.BitMask & POWER(2, bm.BitNum) <> 0
) bm

これは次の情報を返します:

id     BitMask BitNum      Permission
------ ------- ----------- ----------
1      0       NULL        NULL
2      1       0           Can X
3      2       1           Can Y
4      3       0           Can X
4      3       1           Can Y
5      4       2           Can Z
6      5       0           Can X
6      5       2           Can Z
7      6       1           Can Y
7      6       2           Can Z
8      7       0           Can X
8      7       1           Can Y
8      7       2           Can Z
9      8       NULL        NULL
10     9       0           Can X

(15 row(s) affected)

ここまでは順調ですね。問題は、IDで集計を実行しようとすると、1つのフィールドにすべてのアクセス許可があるためです。次のAPPLY句を追加して標準のXML list-string-aggを実行しようとしましたが、エラーInvalid object name 'bm'.

OUTER APPLY (
  SELECT 
   ParamList = STUFF(
     (
       SELECT  '; ' + a.Permission
       FROM bm a WHERE a.id = b.id
       ORDER BY a.BitNum
       FOR XML PATH(''), TYPE).value('.', 'varchar(max)'
     ), 1, 2, ''
   )
  FROM bm b
  GROUP BY b.id
) q

何か案は?

4
JoeNahmias

最初に、元のコードにいくつかのマイナーな調整を行う必要があります。

  1. _0_は「権限なし」を意味するため、_0_の権限値を持つことは意味がありません。
  2. 「BitNum」は、POWER関数で使用する直接の値ではありません。 _1_の「ビット」値が必要な場合、これは2を_0_の累乗で計算します。したがって、POWER関数で使用するには、「BitNum」から_1_を減算する必要があります。

これら2つの変更を念頭に置いて、元のクエリに対する以下の変更により、正しい初期結果セットが得られます。

_DECLARE @T TABLE (id SMALLINT IDENTITY(1, 1), BitMask TINYINT);
INSERT INTO @T (BitMask) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);

SELECT  t.id, t.BitMask, bm.BitNum, bm.Permission
FROM    @T t
OUTER APPLY (
  SELECT * FROM (VALUES
    (1, 'Can Y'),
    (2, 'Can Z')
  ) bm(BitNum, Permission)
  WHERE t.BitMask & POWER(2, bm.BitNum - 1) <> 0
) bm
_

そして、そのクエリはさらに次のように単純な_LEFT JOIN_に縮小/簡略化できます。

_SELECT  t.id, t.BitMask, bm.BitNum, bm.Permission
FROM    @T t
LEFT JOIN (VALUES
    (1, 'Can Y'),
    (2, 'Can Z')
          ) bm(BitNum, Permission)
  ON t.BitMask & POWER(2, bm.BitNum - 1) <> 0
_

結果(12行):

_id  BitMask BitNum  Permission
1   0       NULL    NULL
2   1       1       Can Y
3   2       2       Can Z
4   3       1       Can Y
4   3       2       Can Z
5   4       NULL    NULL
6   5       1       Can Y
7   6       2       Can Z
8   7       1       Can Y
8   7       2       Can Z
9   8       NULL    NULL
10  9       1       Can Y
_

次に、適切な基本クエリがあるので、元のクエリには各権限が個別の行としてあり、「ビットマスク」ごとに1つの行にグループ化したいので、APPLYを単純に追加することはできません。したがって、リクエストを次のように(または類似したものに)再構成する必要があります。

_SELECT   t.id, t.BitMask, PermissionList = 
(
  SELECT PermissionList = STUFF(
     (
       SELECT  '; ' + bm.Permission
       FROM   (VALUES
                    (1, 'Can Y'),
                    (2, 'Can Z')
              ) bm(BitNum, Permission)
        WHERE  t.BitMask & POWER(2, bm.BitNum - 1) <> 0
        ORDER BY bm.BitNum
        FOR XML PATH(''), TYPE).value('.', 'varchar(max)'), 1, 2, ''
   )
)
FROM @T t
GROUP BY t.id, t.BitMask;
_

結果(10行):

_id  BitMask     PermissionList
1   0           NULL
2   1           Can Y
3   2           Can Z
4   3           Can Y; Can Z
5   4           NULL
6   5           Can Y
7   6           Can Z
8   7           Can Y; Can Z
9   8           NULL
10  9           Can Y
_

SQLCLRを使用して、このタイプのString.Join()操作を実行できるユーザー定義集計(UDA)を作成することもできます。そして、このような集約関数は SQL# ライブラリー(私は作成者ですが、この関数は無料バージョンで使用できます)にすでに存在していますが、コンマを使用するようにハードコーディングされています。 (スペースなし)を区切り文字として使用し、一致するものがなければNULLの代わりに空の文字列を返します。しかし、それははるかに読みやすいクエリになります:

_SELECT  t.id, t.BitMask, SQL#.Agg_Join(bm.Permission) AS [PermissionList]
FROM    @T t
LEFT JOIN (VALUES
    (1, 'Can Y'),
    (2, 'Can Z')
          ) bm(BitNum, Permission)
  ON t.BitMask & POWER(2, bm.BitNum - 1) <> 0
GROUP BY t.id, t.BitMask;
_

これは、SQLCLR UDAを使用する方が必ずしも良い選択であると言っているのではなく、それがaの選択であること、および特定の要件、より良いかもしれません。

または、SQL Server 2017以降、これを処理できる組み込み集計関数 STRING_AGG があります。


ビットマスクに対してビット値をテストするわずかに異なる方法は、_<> 0_に対してではなく、ビット値のAND演算をビット値自体と比較することです。

_DECLARE @T TABLE (id SMALLINT IDENTITY(1, 1), BitMask TINYINT);
INSERT INTO @T (BitMask) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);

 -- based on "improved" query
SELECT   t.id, t.BitMask, [PermissionList] = 
(
  SELECT [PermissionList] = STUFF(
     (
       SELECT  '; ' + bm.Permission
       FROM   (VALUES
                    (0, 'Default'),
                    (1, 'Can Y'),
                    (2, 'Can Z')
              ) bm(BitNum, Permission)
        WHERE  t.BitMask & POWER(2, bm.BitNum - 1) = POWER(2, bm.BitNum - 1)
        ORDER BY bm.BitNum
        FOR XML PATH(''), TYPE).value('.', 'varchar(max)'), 1, 2, ''
   )
)
FROM @T t
GROUP BY t.id, t.BitMask;
_

これにより、値として_0_を使用することに少し近づきますが、その場合、その値がすべてのレコードで暗黙的に使用されるという問題があります。上記の結果(ON条件が変更され、かつ_0_レコードが追加されたことに注意してください):

_id  BitMask     PermissionList
1   0           Default
2   1           Default; Can Y
3   2           Default; Can Z
4   3           Default; Can Y; Can Z
5   4           Default
6   5           Default; Can Y
7   6           Default; Can Z
8   7           Default; Can Y; Can Z
9   8           Default
10  9           Default; Can Y
_
4
Solomon Rutzky