アクセス許可を格納するビットマスクフィールドを含むテーブルがあります。各ビットは、特定のアクセス許可が付与されているかどうかを示します。これは簡単な例です:
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
何か案は?
最初に、元のコードにいくつかのマイナーな調整を行う必要があります。
0
_は「権限なし」を意味するため、_0
_の権限値を持つことは意味がありません。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
_