Какой лучший способ эмулировать перечисление флагов (c#) в SQL? - PullRequest
1 голос
/ 17 апреля 2020

Было совершенно очевидно использовать для этого побитовые операторы, так как это, в основном, то, что перечисление flags использует внутри. Я нашел способ выполнить sh это:

Редактировать: предыдущий запрос был неправильным, и я думаю, что вопрос не совсем ясен. Сначала я предоставлю некоторый фон;

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

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

В следующем примере я сделаю запрос для всех объектов, помеченных как 'State_2'

-- Set up the table and fill it up with some example data
create table #ObjectsWithMultipleStates (Flag int, ObjectValue nvarchar(255))
insert into #ObjectsWithMultipleStates 
values 
    (1, 'Object_1'),(2, 'Object_2'),(3, 'Object_3'),(4, 'Object_4'),(5, 'Object_5'),
    (6, 'Object_6'),(7, 'Object_7'),(8, 'Object_8'),(9, 'Object_9'),(10, 'Object_10'),
    (11, 'Object_11'),(12, 'Object_12'),(13, 'Object_13'),(14, 'Object_14'),(15, 'Object_15'),
    (16, 'Object_16'),(17, 'Object_17'),(18, 'Object_18'),(19, 'Object_19'),(20, 'Object_20')

-- Example flag enum, which these values relate to
create table #States (Id int, [Name] nvarchar(255))
insert into #States values (1, 'State_1'),(2, 'State_2'),(4, 'State_3'),(8, 'State_4'),(16, 'State_5')

-- For this example, we'll get the enum's int value by its name
declare @FlagValue int = (select Id from #States where [Name] = 'State_2')

-- Returns 2, 3, 6, 7, 10, 11, 14, 15, 18, 19 (which seems about right)
select * from #ObjectsWithMultipleStates
where Flag|@FlagValue = Flag

Как отметил TomTom, это не позволяет эффективно использовать индексы, что делает этот запрос довольно медленный.

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

select * from #ObjectsWithMultipleStates where Flag in (
-- This returns all possible flag combinations (would be wrapped in a UDF in reality)
select Val 
from(
    SELECT ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n[Val]
    FROM (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) ones(n),
         (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tens(n),
         (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) hundreds(n),
         (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) thousands(n)
    WHERE ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n BETWEEN 1 AND POWER(2, (select count(*) from #States)) -1
) possibleValues
where Val|@FlagValue = Val)

Но это имеет довольно много накладных расходов.

Есть ли более эффективный способ справиться с этим?

РЕДАКТИРОВАТЬ # 2: Ответ Venkataraman R заставил меня понять, что сохранение фактического значения флага - глупая идея, и мы никогда не будем возможность запросить этот эффективный запрос.

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

-- Note that I've removed the flags column, and added an Id from this example
create table #ObjectsWithMultipleStates (Id int, ObjectValue nvarchar(255))
insert into #ObjectsWithMultipleStates 
values 
    (1, 'Object_1'),(2, 'Object_2'),(3, 'Object_3'),(4, 'Object_4'),(5, 'Object_5'),
    (6, 'Object_6'),(7, 'Object_7'),(8, 'Object_8'),(9, 'Object_9'),(10, 'Object_10'),
    (11, 'Object_11'),(12, 'Object_12'),(13, 'Object_13'),(14, 'Object_14'),(15, 'Object_15'),
    (16, 'Object_16'),(17, 'Object_17'),(18, 'Object_18'),(19, 'Object_19'),(20, 'Object_20')

-- Example flag enum, which these values relate to
create table #States (Id int, [Name] nvarchar(255))
insert into #States values (1, 'State_1'),(2, 'State_2'),(4, 'State_3'),(8, 'State_4'),(16, 'State_5')

-- Example relationship table
create table #ObjectState(ObjectId int, StateId int)
insert into #ObjectState values 
    (1, 1), (2, 2), (3, 1), (3, 2) -- Etc.

declare @FlagValue int = (select Id from #States where [Name] = 'State_2')

-- Finally, we can perform a decent query
select * from #ObjectsWithMultipleStates where Id in (
    select ObjectId from #ObjectState where StateId = @FlagValue
)

Я думаю, что это самое эффективное, мы сможем получить его.

Ответы [ 3 ]

2 голосов
/ 17 апреля 2020

Для этого было совершенно очевидно использовать побитовые операторы,

На самом деле это не так. Это противоречит всему, что есть SQL, включая эффективное использование индексов - то, о чем вы обычно не заботитесь о представлении в памяти. Что очевидно, так это использование одного логического поля для каждого поля флага.

Я не администратор базы данных, поэтому мне было интересно

База данных ADMIN имеет дело с управлением базой данных. Вы говорите, что вы не разработчик, который занимается базами данных. Обвинение в том, что я не являюсь администратором, - это все равно, что сказать: «Я не механик c, поэтому я не знаю лучшего пути для моей машины из А в В». Администраторы MANAGE, программисты разрабатывают.

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

В более современных версиях SQL Server вы МОЖЕТЕ установить упакованное поле и функцию для извлечения одного значения и индексирования поля это определяется как использование этой функции.

1 голос
/ 17 апреля 2020

Enum - это в основном атрибут домена. Это что-то похожее на тип данных, где вы указываете диапазон значений. Например, TinyInt может иметь значения от 1 до 255.

В случае Enum вы указываете диапазон значений для Enum. Например. EmployeeTypeEnum может иметь значения: FullTimeEmployee, ContractEmployee

Ниже приведен подход к обработке типов перечисления в SQL:

В SQL, необходимо создайте отдельную таблицу для хранения перечисления

EmployeeType

+----------------+------------------+
| EmployeeTypeId | EmployeeTypeName |
+----------------+------------------+
|              1 | FullTimeEmployee |
|              2 | ContractEmployee |
+----------------+------------------+

Необходимо определить PRIMARY KEY для идентификатора.

ALTER TABLE EmployeeType ADD CONSTRAINT PK_EmployeeType EmployeeType(EmployeeTypeId)

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

Employee

+------------+--------------+----------------+
| EmployeeId | EmployeeName | EmployeeTypeId |
+------------+--------------+----------------+
|          1 | Venkat       |              1 |
+------------+--------------+----------------+

Вам необходимо определите FOREIGN KEY для идентификатора домена.

ALTER TABLE Employee ADD CONSTRAINT FK_Employee_EmployeeType FOREIGN KEY (EmployeeTypeId) REFERENCES EmployeeType(EmployeeTypeId)

UPDATE В C# вы получите целочисленное представление enum

int EmployeeType = (int) EmployeeEnum.Type;

В SQL, вы передайте это целое число в тип enum, чтобы получить соответствующее значение.

SELECT EmployeeId, EmployeeName 
FROM Employee
Where EmployeeTypeId = @EmployeeType
1 голос
/ 17 апреля 2020

Битовые флаги действительно имеют значение, только если объект может иметь несколько состояний одновременно.

В этом случае вы либо:

  1. Сохраняете сумму битов в одиночное поле в БД.

Преимущество заключается в сохранении только 1 поля, но недостатком является то, что каждый клиент должен декодировать значение.

Сохранение каждого состояния в отдельной таблице, связанной с объектом.

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

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

Тогда вы можете либо:

  1. Сохранить значение или строка в одно поле в БД.

Это просто, но не поддерживает принципы нормализации БД. Это нормально, если вы просто сохраняете состояние для одного типа объекта (таблицы).

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

Это соответствует методам нормализации БД и обеспечивает лучшую расширяемость в БД в будущее, если / когда вы добавите больше перечислений. И должен использоваться, если перечисление является доступом из нескольких типов объектов (таблиц).

...