Побитовое и в Sql Server

У меня очень типичная ситуация. У нас есть таблица под названием Users, которая имеет столбец под названием Branches (varchar 1000).

организация может иметь 1000 филиалов. Поэтому, если пользователь имеет доступ к ветвям 1, 5 и 10, строка ветвей будет выглядеть так:

1000100001000000000......

(т. е. 1 для позиции, к которой пользователь имеет доступ к филиалу на основе номера филиала). Пожалуйста, не советуйте лучшие варианты хранения данных, это приходит ко мне из наследия приложение, развернутое на разных континентах.

теперь, учитывая этот фон (и учитывая, что может быть > 10000 пользователей), я хочу найти всех пользователей, которые имеют доступ к любому из данного набора ветвей, например, найти всех пользователей, которые имеют доступ к любой ветви 10, 65, 90 или 125.

одним из простых решений является преобразование желаемого набора ветвей (т. е. 10, 65, 90, 125) в строку ветви (00000010100 и т. д.), затем используйте скалярный UDF для итерации по обеим строкам ветви и верните true при первом совпадении, где 2 строки ветви имеют 1, и false, если в общей позиции нет 1.

кроме этого, у меня также есть возможность поиска в приложении на C#. Некоторые из этих пользователей являются привилегированными (около 1000 или более), и их данные кэшируются в приложении, поскольку к ним очень часто обращаются. Но для других пользователей, которые не являются привилегированными, данные только в БД.

У меня есть 2 вопроса здесь: 1) для поиска в БД, есть ли лучший способ кроме подхода UDF, о котором я упоминал. 2) для привилегированных пользователей, что было бы лучше с точки зрения производительности, поиск в приложении (который далее может быть основан на цикле for на строках ветвей, таких как в UDF, или как оператор Linq Intersect на 2 массивах ветвей, т. е. Linq Intersect на [1,5,9,50,80,200] и [6,90,256,300] и т. д.) Будет ли поиск в БД производить более быстрые результаты или поиск на основе приложений?

рассмотрим, что могут быть другие параметры для поиска в обоих случаях, например, фамилия начнем с ...

мой текущий подход заключается в фильтрации строк в БД для обеих ситуаций сначала по другим параметрам (например, фамилия начинается с). Затем используйте скалярный UDF для фильтрации этого результирующего набора на основе ветвей, а затем верните результаты.

6 ответов


сделайте это в SQL, это будет только в 100 раз быстрее, чем делать это в C# или другом интерфейсе.

используйте встроенную таблицу чисел, чтобы разбить длинную строку на позиции (серия номеров доходит до 2047).

образец таблицы

create table users (userid int)
insert users select 1 union all select 2

create table permission (userid int, bigstr varchar(1000))
insert permission
select 1, REPLICATE('0', 56) + '1' -- 57th
        + REPLICATE('0', 32) + '1' -- 90th
        + REPLICATE('0', 64) + '1' -- 155th
        + REPLICATE('0', 845)
insert permission
select 2, REPLICATE('0', 66) + '1' -- 67th
        + REPLICATE('0', 98) + '1' -- 166th
        + REPLICATE('0', 657) + '1' -- 824th
        + REPLICATE('0', 176)

пример, показывающий все соответствующие разрешения против списка

select *
from users u
inner join permission p on p.userid=u.userid
inner join master..spt_values v on v.type='p'
  and SUBSTRING(p.bigstr,v.number,1) = '1'
  and v.number between 1 and LEN(p.bigstr)  -- or 1000 if it is always 1000
where v.number in (57,90,824)

найти пользователей, которые имеют доступ хотя бы к одной ветке в списке:

select distinct u.userid
from users u
inner join permission p on p.userid=u.userid
inner join master..spt_values v on v.type='p'
  and SUBSTRING(p.bigstr,v.number,1) = '1'
  and v.number between 1 and LEN(p.bigstr)  -- or 1000 if it is always 1000
where v.number in (57,90,824)

etc..


возможно, вы захотите построить строками вида __1_1____1% на запрос чтобы найти всех пользователей, которые имеют доступ к разделам 3, 5 и 10.

чтобы построить эти строки, самый простой способ-начать со строки _ символы длиной до самого большого номера ветви в вашем наборе (или больше), а затем замените individual _ символы 1 символы, а затем добавьте % в конце.

что касается того, быстрее ли это, чем делать цикл в база данных или цикл в вашем приложении, я думаю, что ваш лучший подход - просто проверить его.


Побитовое Членство В Группе:

из комментариев, я предполагаю, что мы не можем использовать таблицу связей для группы. Вот побитовое решение, которое не использует строки. Это не может быть приемлемым ответом, потому что количество битов довольно сильно ограничивает количество групп. Однако, используя целые числа с явными сравнениями значений, база данных может эффективно использовать свои индексы. Поэтому я добавил его для случая, когда количество групп / роли / все, что достаточно ограничено, чтобы соответствовать.
PS: извините двоично-десятичные беспорядки, я просто подключил вещи на лету. Не стесняйтесь комментировать и исправлять, если у меня есть какие-либо ошибки.

Каждой группе присваивается немного:

G1: 0001
G2: 0010
G3: 0100
G4: 1000

Членство пользователей в группах рассчитывается с помощью bitwise &. Вот несколько примеров с двоичным и десятичным эквивалентами:

U1: G1:             0001 (01)
U2: G2:             0010 (02)
U3: G3:             0100 (04)
U4: G4:             1000 (08)
U5: G1 & G2:        0011 (03)
U6: G2 & G3:        0110 (06)
U7: G1 & G3:        0101 (05)
U8: G2 & G4:        1010 (10)
U9: G1 & G2 & G4:   1011 (11)

Теперь вычислите, используя итерацию из 1-N (N-количество групп) и получить список всех возможных целочисленных значений, которые может внести любая конкретная группа. Например, G1 будет присутствовать в любом нечетном числе:

G1' : 0001 (01), 0011 (03), 0101 (05), 0111 (07), 1001 (09), 1011 (11), 1101 (13), 1111 (15)
G2' : 0010 (02), 0011 (03), 0110 (06), 0111 (07), 1010 (10), 1011 (11), 1110 (14), 1111 (15)
G3' : 0100 (04), 0101 (05), 0110 (06), 0111 (07), 1100 (12), 1101 (13), 1110 (14), 1111 (15)
G4' : 1000 (08), 1001 (09), 1010 (10), 1011 (11), 1100 (12), 1101 (13), 1110 (14), 1111 (15)

вы можете сделать это с помощью цикла от 1-1000 с побитовым и десятичным значением группы 1,2,4,8 и т. д.
Сохраните значения в памяти или вставьте их в таблицу, хранящую возможные memerships ваших групп, например possible_memberships.

Get me users in G1:
   Q: select * from users where group_memberships in (1, 3, 5, 7, 9, 11, 13, 15);
   A: U1, U5, U7, U9

Get me users in G2:
   Q: select * from users where group_memberships in (2, 3, 6, 7, 10, 11, 14, 15);
   A: U2, U5, U6, U8, U9

Если у вас есть таблица групп со столбцом "possible_memberships", вы можете поместить туда значения, сохранив вас от необходимости отправлять все значения по проводам и разрешать кэшировать подселект в базе данных. p>

Get me users in G3:
   Q: select * from users where group_memberships in (select possible_memberships from groups where name = 'G3');
   A: U3, U7, U6

используйте подобный запрос. В SQL-сервер, _ использовать в как выражение соответствует любому одиночному символу. Чтобы получить тех пользователей, которые находятся в филиалах 1,5, и 10, вы могли бы к нему так:

SELECT columns FROM Users WHERE BRANCHES LIKE '1___1____1%'

Это не особенно эффективно (это не очень sargable), но он должен работать, и это, вероятно, не хуже, чем ваш вариант udf.


обновление

это не является возможным решением с 1000-битным номером. Я оставлю его, если кто-то с меньшим количеством вариантов столкнется с этим сообщением.


можете ли вы внести какие-либо изменения в схему БД вообще? Если вы можете добавить вычисляемый столбец, содержащий целочисленное представление двоичного числа, которое у вас есть в varchar, вы можете использовать побитовую логику, чтобы выбрать то, о чем вы говорите, полностью в БД и довольно быстро.

вот пример о чем я говорю:

with temp as
(
   select 1 as BranchNumber -- 1
   union 
   select 2 -- 01
   union
   select 5 -- 101
   union
   select 7 -- 111
   union
   select 15 as number -- 111
)

--Select users that belong to branch 2    
    SELECT * from temp
    where (BranchNumber & 2) = 2

--returns 2,7,15



--Select users that belong to at least branches 1,2 and 3    
    SELECT * from temp
    where (BranchNumber & 7) = 7

--returns 7,15

чтобы преобразовать двоичный файл в число, вам, вероятно, придется создать UDF, который вы можете вызвать для заполнения нового столбца. Я немного покопался и нашел это, что кажется хорошей отправной точкой. Я еще не тестировал его, поэтому будьте осторожны:

SET NOCOUNT ON
CREATE TABLE #nums (pos bigint)
DECLARE @cntr int
SET @cntr = 0
WHILE @cntr < 63 BEGIN
   INSERT INTO #nums VALUES (@cntr)
   SET @cntr = @cntr + 1
END

DECLARE @binstring varchar(63)
SET @binstring = '10000010000000001000011000000000'

SELECT
   IntegerVal =
      sum(power(convert(bigint,2),pos)
      * substring(reverse(@binstring),pos+1,1)) -- yeah, implicit conversion
   FROM #nums

DROP TABLE #nums

побитовый запрос, вероятно, превзойдет строковые функции, хотя 1000-битное целое число не существует. Однако как она может быть получена из строки вы можете разбить его на определенное количество целочисленных наборов и запросов по ним. Вам просто нужно будет понять значимые биты для каждого столбца и соответствующим образом настроить ввод определенной константой или просто ударить ее в набор битов, представляющих строки, и преобразовать в int.