Вопрос разработки: фильтруемые атрибуты, SQL

У меня есть две таблицы в моей базе данных, Operation и Equipment. Для операции требуется ноль или более атрибутов. Однако есть некоторая логика в том, как атрибутировать атрибуты:

  • Для операции Foo требуется оборудование A и B
  • Операция Bar не требует оборудования
  • Для операции Baz требуется оборудование B и либо C, либо D
  • Для операции Quux требуется оборудование (A или B) и (C или D)

Как лучше всего представить это в SQL?

Я уверен, что люди делали это раньше, но я не знаю, с чего начать.

(FWIW, мое приложение построено на Python и Django.)

Обновление 1: будет около тысячи Operation строк и около тридцати Equipment строк. Информация поступает в формате CSV, аналогичном описанию выше: Quux, (A & B) | (C & D)

Обновление 2: уровень союзов и дизъюнкций не должен быть слишком глубоким. Пример Quux, вероятно, самый сложный, хотя, похоже, есть A | (D & E & F) случай.


person a paid nerd    schedule 08.02.2010    source источник
comment
Относится ли оборудование к классам оборудования A / B / C / D или к конкретным элементам оборудования? т.е. будет ли таблица оборудования иметь ровно четыре записи или большое количество записей четырех разных типов?   -  person Vince Bowdren    schedule 09.02.2010
comment
В таблице оборудования будет много строк (около 30).   -  person a paid nerd    schedule 09.02.2010
comment
Спасибо. Как только я заметил, что размер аватара SO был 32x32px, я точно знал, какой значок использовать :)   -  person a paid nerd    schedule 17.02.2010


Ответы (8)


Подумайте о том, как вы бы смоделировали операции в объектно-ориентированном дизайне: операции будут подклассами общего суперкласса Operation. Каждый подкласс должен иметь обязательные элементы объекта для соответствующего оборудования, необходимого для этой операции.

Способ моделирования этого с помощью SQL - наследование таблицы классов. Создайте общую супертаблицу:

CREATE TABLE Operation (
  operation_id   SERIAL PRIMARY KEY,
  operation_type CHAR(1) NOT NULL,
  UNIQUE KEY (operation_id, operation_type),
  FOREIGN KEY (operation_type) REFERENCES OperationTypes(operation_type)
);

Затем для каждого типа операции определите подтаблицу со столбцом для каждого требуемого типа оборудования. Например, OperationFoo имеет столбец для каждого из equipA и equipB. Поскольку они оба необходимы, столбцы равны NOT NULL. Ограничьте их правильными типами, создав супертаблицу Class Table Inheritance для оборудования.

CREATE TABLE OperationFoo (
  operation_id   INT PRIMARY KEY,
  operation_type CHAR(1) NOT NULL CHECK (operation_type = 'F'),
  equipA         INT NOT NULL,
  equipB         INT NOT NULL,
  FOREIGN KEY (operation_id, operation_type) 
      REFERENCES Operations(operation_d, operation_type),
  FOREIGN KEY (equipA) REFERENCES EquipmentA(equip_id),
  FOREIGN KEY (equipB) REFERENCES EquipmentB(equip_id)
);

Таблица OperationBar не требует оборудования, поэтому в нем нет столбцов экипировки:

CREATE TABLE OperationBar (
  operation_id   INT PRIMARY KEY,
  operation_type CHAR(1) NOT NULL CHECK (operation_type = 'B'),
  FOREIGN KEY (operation_id, operation_type) 
      REFERENCES Operations(operation_d, operation_type)
);

В таблице OperationBaz есть одно необходимое оборудование equipA, и тогда хотя бы одно из equipB и equipC должно быть NOT NULL. Используйте для этого ограничение CHECK:

CREATE TABLE OperationBaz (
  operation_id   INT PRIMARY KEY,
  operation_type CHAR(1) NOT NULL CHECK (operation_type = 'Z'),
  equipA         INT NOT NULL,
  equipB         INT,
  equipC         INT,
  FOREIGN KEY (operation_id, operation_type) 
      REFERENCES Operations(operation_d, operation_type)
  FOREIGN KEY (equipA) REFERENCES EquipmentA(equip_id),
  FOREIGN KEY (equipB) REFERENCES EquipmentB(equip_id),
  FOREIGN KEY (equipC) REFERENCES EquipmentC(equip_id),
  CHECK (COALESCE(equipB, equipC) IS NOT NULL)
);

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

CREATE TABLE OperationQuux (
  operation_id   INT PRIMARY KEY,
  operation_type CHAR(1) NOT NULL CHECK (operation_type = 'Q'),
  equipA         INT,
  equipB         INT,
  equipC         INT,
  equipD         INT,
  FOREIGN KEY (operation_id, operation_type) 
      REFERENCES Operations(operation_d, operation_type),
  FOREIGN KEY (equipA) REFERENCES EquipmentA(equip_id),
  FOREIGN KEY (equipB) REFERENCES EquipmentB(equip_id),
  FOREIGN KEY (equipC) REFERENCES EquipmentC(equip_id),
  FOREIGN KEY (equipD) REFERENCES EquipmentD(equip_id),
  CHECK (COALESCE(equipA, equipB) IS NOT NULL AND COALESCE(equipC, equipD) IS NOT NULL)
);

Это может показаться большим трудом. Но вы спросили, как это сделать в SQL. Лучший способ сделать это в SQL - использовать декларативные ограничения для моделирования бизнес-правил. Очевидно, это требует, чтобы вы создавали новую подтаблицу каждый раз, когда вы создаете новый тип операции. Это лучше всего, когда операции и бизнес-правила никогда (или почти никогда) не меняются. Но это может не соответствовать требованиям вашего проекта. Большинство людей говорят: «Но мне нужно решение, которое не требует изменения схемы».

Большинство разработчиков, вероятно, не используют наследование таблиц классов. Чаще всего они просто используют структуру таблицы «один ко многим», как упоминали другие люди, и реализуют бизнес-правила исключительно в коде приложения. То есть ваше приложение содержит код для вставки только оборудования, подходящего для каждого типа операции.

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

Но это также может быть ограничивающим фактором, например, если ваши бизнес-правила меняются, и вам нужно скорректировать данные. Распространенное решение в этом случае - написать сценарий для выгрузки всех данных, изменить схему и затем перезагрузить данные в разрешенной форме (Извлечь, преобразовать и загрузить = ETL).

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


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

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

person Bill Karwin    schedule 12.02.2010
comment
P.S .: MySQL не поддерживает проверочные ограничения, поэтому вам придется реализовать их с помощью триггеров или внешних ключей. - person Bill Karwin; 15.02.2010
comment
Ага. Вы правы - я спросил, как это сделать в SQL. Однако я обновил сообщение, объяснив, что будет тысяча видов Operation. Я не могу избавиться от мысли, что сокращение вашего предложения до таблиц OperationConjunction и OperationDisjunction может сработать. - person a paid nerd; 17.02.2010

Я думаю, вы должны иметь отношение «один ко многим» или «многие ко многим» между Operation и Equipment, в зависимости от того, есть ли одна запись Equipment для каждой единицы оборудования или для каждого типа оборудования.

Я бы не советовал добавлять бизнес-логику в схему базы данных, поскольку бизнес-логика может меняться, и вам не нужно менять схему в ответ.

person danben    schedule 08.02.2010
comment
Спасибо, но я не помещаю бизнес-логику в базу данных - я пытаюсь представить условия, в которых может быть выбран объект. - person a paid nerd; 11.02.2010
comment
Верно, а что определяет эти условия? - person danben; 11.02.2010

Похоже, вам нужно уметь сгруппировать определенное оборудование вместе как соединение или дизъюнкцию и объединить эти группы вместе ...

OperationEquipmentGroup
   id int
   operation_id int 
   is_conjuction bit 

OperationEquipment
   id int
   operation_equipment_group_id int
   equipment_id

Вы можете добавить столбцы упорядочивания, если это важно, и, возможно, еще один столбец в таблицу групп, чтобы указать, как группы объединяются (имеет смысл только в том случае, если они упорядочены). Но, судя по вашим примерам, похоже, что группы только соединяются вместе.

person dotjoe    schedule 10.02.2010

Поскольку у Операции может быть одно или несколько единиц оборудования, вам следует использовать таблицу связи. Ваша схема будет такой:

Операция

  • ID
  • другой столбец

Оборудование

  • ID
  • другой столбец

Operation_Equipment_Link

  • OperationID
  • EquipmentID

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

person Nicholai    schedule 08.02.2010

В дополнение к предложению Николая я решил аналогичную проблему:

В таблице Operation есть дополнительное поле OperationType.

В таблице «Оборудование» есть дополнительное поле «Тип оборудования».

У меня есть дополнительная таблица DefaultOperationEquipmentType, в которой указывается, какой EquipmentType необходимо включить в каждый OperationType, например

OperationType  EquipmentType
==============.=============.
Foo_Type       A_Type
Foo_Type       B_Type
Baz_Type       B_Type
Baz_Type       C_Type

Мое приложение не требует сложных условий, таких как (A или B), потому что в моей бизнес-логике оба альтернативных оборудования относятся к одному типу оборудования, например. в среде ПК я мог бы иметь оборудование Mouse (A) или Trackball (B), но они оба принадлежат EquipmentType "PointingDevice_Type"

надеюсь, это поможет

person MikeD    schedule 10.02.2010
comment
Ах, но мне действительно нужна сложная логика. Представьте себе, что в вашей среде вам нужно Keyboard AND (Mouse OR Joystick OR WacomTablet) - person a paid nerd; 17.02.2010
comment
все дело в том, как вы настраиваете типы оборудования (или классы, если хотите); И реализуется путем присвоения нескольких типов оборудования одному типу операции, ИЛИ реализуется несколькими устройствами, назначенными одному типу оборудования. По мере прохождения ключа (тип_оборудования, оборудование) одно оборудование может входить в состав нескольких типов оборудования - например, типа CADPointers содержат (планшет, Digipen, мышь), OfficePointers содержат (мышь, трекбол) и клавиатуры содержат (GermanKey, USKey). .. - person MikeD; 18.02.2010
comment
... теперь вы можете определить OperationType CADPCType с типами (CadPointers, Keyboards) и OfficePCType с типами (OfficePointers, Keyboards) и, наконец, для операции CAD-PC и Office-PC через все соединения вы сможете ссылаться на правильные оборудование. - person MikeD; 18.02.2010

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

* (кроме способа Билла, который сложно настроить, но мастерски, если все сделано правильно)

Operations:
--------------------
Op_ID int not null pk
Op_Name varchar 500

Equipment: 
--------------------
Eq_ID int not null pk
Eq_Name varchar 500
Total_Available int

Group:
--------------------
Group_ID int not null pk
-- Here you have a choice. You can either:
-- Not recommended   
Equip varchar(500) --Stores a list of EQ_ID's {1, 3, 15}
-- Recommended
Eq_ID_1 bit
Eq_1_Total_Required
Eq_ID_2 bit
EqX: A | (D & E & F)Total_Required
Eq_ID_3 bit
Eq
Operations:
--------------------
Op_ID    Op_Name
1        X

Equipment: 
--------------------
Eq_ID    Eq_Name    Total_Available
1        A          5
-- ... snip ...
22       D          15
23       E          0
24       F          2

Group:
--------------------
Group_ID    Eq_ID_1    Eq_1_Total_Required -- ... etc. ...
1           TRUE       3
-- ... snip ...
2           FALSE      0

Operations_to_Group_Mapping:
--------------------
Group_ID    Op_ID
1           1
2           1 
Total_Required -- ... etc. Operations_to_Group_Mapping: -------------------- Group_ID int not null frk Op_ID int not null frk

Таким образом, в случае X: A | (D & E & F)

Operations:
--------------------
Op_ID    Op_Name
1        X

Equipment: 
--------------------
Eq_ID    Eq_Name    Total_Available
1        A          5
-- ... snip ...
22       D          15
23       E          0
24       F          2

Group:
--------------------
Group_ID    Eq_ID_1    Eq_1_Total_Required -- ... etc. ...
1           TRUE       3
-- ... snip ...
2           FALSE      0

Operations_to_Group_Mapping:
--------------------
Group_ID    Op_ID
1           1
2           1 
person Sean Vieira    schedule 16.02.2010

Как бы мне ни было неприятно помещать рекурсивные (древовидные) структуры в SQL, похоже, что это действительно то, что вы ищете. Я бы использовал что-то подобное:

Operation
----------------
OperationID            PK
RootEquipmentGroupID   FK -> EquipmentGroup.EquipmentGroupID
...

Equipment
----------------
EquipmentID            PK
...

EquipmentGroup
----------------
EquipmentGroupID       PK
LogicalOperator

EquipmentGroupEquipment
----------------
EquipmentGroupID |     (also FK -> EquipmentGroup.EquipmentGroupID)
EntityType       |     PK (all 3 columns)
EntityID         |     (not FK, but references either Equipment.EquipmentID
                        or EquipmentGroup.EquipmentGroupID)

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

Каждая группа оборудования может быть либо and группой, либо or группой (как указано в столбце LogicalOperator). Члены каждой группы определены в таблице EquipmentGroupEquipment, где EntityID ссылается либо на Equipment.EquipmentID, либо на другой EquipmentGroup.EquipmentGroupID, цель определяется значением в EntityType. Это позволит вам составить группу, состоящую из оборудования или других групп.

Это позволит вам изобразить что-то столь же простое, как «требуется оборудование A», которое будет выглядеть следующим образом:

EquipmentGroupID   LogicalOperator
--------------------------------------------
1                  'AND'

EquipmentGroupID   EntityType   EntityID
--------------------------------------------
1                  1            'A'

... вплоть до вашего "A | (D & E & F)", который будет выглядеть так:

EquipmentGroupID   LogicalOperator
--------------------------------------------
1                  'OR'
2                  'AND'

EquipmentGroupID   EntityType   EntityID
--------------------------------------------
1                  1            'A'
1                  2            2 -- group ID 2
2                  1            'D'
2                  1            'E'
2                  1            'F'

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

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

person Adam Robinson    schedule 17.02.2010

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

Операции:

  • ID
  • название

Оборудование:

  • ID
  • название

Операционное_оборудование:

  • equipment_id
  • operation_id
  • символ

Где символ - A, B, C и т. Д.

Если у вас есть условие типа (A & B) | (C & D), вы можете легко узнать, какое оборудование какое.

person Leo Jweda    schedule 15.02.2010