Когда лучше хранить флаги в виде битовой маски, а не использовать ассоциативную таблицу?

#c# #sql-server #database-design #bitmask

#c# #sql-server #база данных-дизайн #битовая маска

Вопрос:

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

В каких случаях вариант 2 был бы лучше?

Вариант 1

Используйте ассоциативную таблицу.

Пользователь
----
Идентификатор пользователя (PK)
Имя
Отдел
Разрешение
----
PermissionId (PK)
Имя
Разрешение пользователя
----
Идентификатор пользователя (FK)
PermissionId (FK)

Вариант 2

Сохраняйте битовую маску для каждого пользователя.

Пользователь
----
Идентификатор пользователя (PK)
Имя
Отдел
Разрешения
 [Flags]
enum Permissions {
    Read = 1,
    Create = 2,
    Download = 4,
    Print = 8,
    Approve = 16
}
  

Ответ №1:

Великолепный вопрос!

Во-первых, давайте сделаем некоторые предположения о «лучшем».

Я предполагаю, что вы не очень заботитесь о дисковом пространстве — битовая маска эффективна с точки зрения пространства, но я не уверен, что это имеет большое значение, если вы используете SQL server.

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

 select * from user where permsission amp; CREATE = TRUE
  

(сегодня у меня нет доступа к SQL Server, в дороге). Этот запрос не сможет использовать индекс из-за математической операции — поэтому, если у вас огромное количество пользователей, это было бы довольно болезненно.

Я предполагаю, что вы заботитесь о ремонтопригодности. С точки зрения удобства сопровождения битовая маска не так выразительна, как базовая проблемная область, поскольку хранит явные разрешения. Вам почти наверняка придется синхронизировать значения флагов битовой маски для нескольких компонентов, включая базу данных. Не невозможно, но неприятно сзади.

Итак, если нет другого способа оценить «лучше», я бы сказал, что маршрут с битовой маской не так хорош, как хранение разрешений в нормализованной структуре базы данных. Я не согласен, что это было бы «медленнее, потому что вам нужно выполнить объединение» — если у вас нет полностью неработоспособной базы данных, вы не сможете измерить это (тогда как выполнение запросов без использования активного индекса может стать заметно медленнее даже с несколькими тысячами записей).

Комментарии:

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

2. Разве SQL Server не упаковывает смежные битовые поля в байты, в основном сохраняя их как битовую маску.

Ответ №2:

Лично я бы использовал ассоциативную таблицу.

Поле битовой маски очень сложно запросить и объединить.

Вы всегда можете сопоставить это с вашим перечислением C # flags и, если производительность улучшится, провести рефакторинг базы данных.

Удобство чтения вместо преждевременной оптимизации 😉

Комментарии:

1. Управление и сопровождение. Насколько сложнее будет поддерживать и управлять данными, хранящимися в базе данных, когда важная информация скрыта в столбце битовой маски? И любой прирост производительности почти наверняка не будет достаточно большим, чтобы реально что-то изменить.

Ответ №3:

Нет окончательного ответа, поэтому делайте то, что подходит вам. Но вот в чем моя загвоздка:

Используйте вариант 1, если

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

Используйте вариант 2, если

  • Разрешения будут ограничены несколькими
  • Вы ожидаете миллионы пользователей

Комментарии:

1. Миллионы строк — это тривиальное число в современных (и даже прилично устаревших) СУБД

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

Ответ №4:

Сохраняйте разрешения нормализованными (т. Е. не в битовой маске). Хотя это, очевидно, не является требованием для вашего сценария (особенно если разрешения не будут часто меняться), это сделает выполнение запросов намного проще и очевиднее.

Ответ №5:

Я не советую использовать битовую маску по следующим причинам:

  • Индекс не может быть использован эффективно
  • Запрос сложнее
  • Серьезно ухудшается читаемость / обслуживание
  • Среднестатистический разработчик не знает, что такое битовая маска
  • Гибкость снижена (верхний предел до nr битов в числе)

В зависимости от ваших шаблонов запросов, запланированного набора функций и распределения данных я бы выбрал ваш вариант 1 или даже что-то простое, как:

 user_permissions(
   user_id
  ,read     
  ,create   
  ,download 
  ,print    
  ,approve  
  ,primary key(user_id)
);
  

Добавление столбца — это модификация схемы, но я предполагаю, что для добавления привилегии «Purge» потребуется некоторый код для ее выполнения, поэтому привилегии, возможно, не должны быть такими динамичными, как вы думаете.

Если у вас какое-то неправильное распределение данных, например, у 90% базы пользователей нет единого разрешения, следующая модель также работает нормально (но разваливается при выполнении больших сканирований (одно 5-стороннее объединение против одного полного сканирования таблицы).

 user_permission_read(
   user_id
  ,primary key(user_id)
  ,foreign key(user_id) references user(user_id)
)

user_permission_write(
   user_id
  ,primary key(user_id)
  ,foreign key(user_id) references user(user_id)
)

user_permission_etcetera(
   user_id
  ,primary key(user_id)
  ,foreign key(user_id) references user(user_id)
)
  

Ответ №6:

Единственный случай, когда я могу вспомнить, когда я бы использовал поле битовой маски для хранения разрешений, — это когда вы действительно ограничены в объеме имеющейся у вас физической памяти….например, на старом мобильном устройстве. По правде говоря, объем памяти, который вы экономите, того не стоит. Даже у миллионов пользователей место на жестком диске стоит дешево, и вы можете расширить разрешения и т.д. намного проще использовать подход без битовой маски (это касается отчетности о том, у кого какие разрешения и т.д.)

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

Ответ №7:

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

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

Ответ №8:

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

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

Ответ №9:

Ваши запросы будут выполняться быстрее с использованием перечисления flags (битовая маска), потому что вам не нужно будет включать join в связанную таблицу, чтобы понять значение.

Комментарии:

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

2. @Адам Робинсон, (1) Нет, на самом деле это совсем не подразумевает этого. Это подразумевает, что запрос будет выполняться быстрее , что правильно. (2) Вы сравниваете наиболее высокооптимизированный запрос в ассоциативной таблице с наиболее плохо оптимизированным запросом в целочисленном поле. Это действительно не очень практично.

3. Хотя, безусловно, возможно, что код, который вы пишете для интерпретации битовой маски, будет более эффективным, чем соединение с USER_PERMISSION таблицей, представляется маловероятным, что разница в производительности будет значимой — вряд ли это будет операция с узким местом — и в коде происходит существенная потеря ясности.

4. В вашей первоначальной версии было сказано «быстро», а не «быстрее», как сейчас, отсюда и мой первый комментарий. Да, я сравниваю «наиболее высокооптимизированный запрос» для ассоциативной версии, но это также версия, которая, скорее всего, будет на месте. Я сравниваю это с «самым плохо оптимизированным» запросом к полю битовой маски, потому что, опять же, это то, что, скорее всего, будет на месте. Нет способа создать побитовый индекс для поля, и если вы планируете проверять разрешения как часть запроса, побитовая операция неизбежна. У вас есть лучший вариант для этого?