Вопрос проектирования: фильтруемые атрибуты, SQL - PullRequest
4 голосов
/ 08 февраля 2010

У меня есть две таблицы в моей базе данных, 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).

Ответы [ 8 ]

11 голосов
/ 13 февраля 2010

Подумайте о том, как вы смоделируете операции в ОО-дизайне: операции были бы подклассом общего суперкласса 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.Укрепите их до правильных типов, создав супер-таблицу наследования таблиц классов для оборудования.

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)
);

Table 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().

2 голосов
/ 08 февраля 2010

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

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

1 голос
/ 11 февраля 2010

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

OperationEquipmentGroup
   id int
   operation_id int 
   is_conjuction bit 

OperationEquipment
   id int
   operation_equipment_group_id int
   equipment_id

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

0 голосов
/ 17 февраля 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 (объединяя несколько устройств в один столбец).

0 голосов
/ 16 февраля 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
Eq_2_Total_Required
Eq_ID_3 bit
Eq_3_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 
0 голосов
/ 15 февраля 2010

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

Операции:

  • ID
  • имя

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

  • ID
  • имя

Operations_Equipment:

  • equipment_id
  • operation_id
  • символ

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

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

0 голосов
/ 11 февраля 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"

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

0 голосов
/ 08 февраля 2010

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

Операция

  • ID
  • othercolumn

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

  • ID
  • othercolumn

Operation_Equipment_Link

  • OperationID
  • EquipmentID

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

...