【问题标题】:Design question: Filterable attributes, SQL设计问题:可过滤属性、SQL
【发布时间】:2010-02-08 05:12:45
【问题描述】:

我的数据库中有两个表,OperationEquipment。一个操作需要零个或多个属性。但是,属性的​​属性有一些逻辑:

  • 操作Foo 需要设备AB
  • Bar 操作不需要任何设备
  • 操作Baz 需要设备BCD
  • 操作Quux 需要设备(AB)和(CD

用 SQL 表示这一点的最佳方式是什么?

我相信人们以前也这样做过,但我不知道从哪里开始。

(FWIW,我的应用程序是用 Python 和 Django 构建的。)

更新 1: 将有大约 1000 个 Operation 行和大约 30 个 Equipment 行。信息以 CSV 格式提供,类似于上面的描述:Quux, (A & B) | (C & D)

更新 2: 连词和析词的层次不应该太深。 Quux 的例子可能是最复杂的,虽然似乎有 A | (D & E & F) 的情况。

【问题讨论】:

  • 设备是 A/B/C/D 类的设备,还是特定的设备?即,Equipment 表是否正好有四条记录,还是有四种不同类型的大量记录?
  • 设备表会有很多行(大约 30 行)。
  • 谢谢。当我注意到 SO avatar 大小为 32x32px 时,我确切地知道要使用哪个图标 :)

标签: sql mysql orm entity-relationship


【解决方案1】:

想一想如何在 OO 设计中对操作进行建模:操作将是公共超类 Operation 的子类。每个子类都具有该操作所需的相应设备的强制对象成员。

用 SQL 建模的方法是Class Table Inheritance。创建一个通用的超级表:

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

然后为每个操作类型定义一个子表,其中包含每个所需设备类型的列。例如,OperationFooequipAequipB 各有一个列。由于它们都是必需的,因此这些列是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,那么equipBequipC 中的至少一个必须是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 中执行此操作的最佳方法是使用声明性约束来建模您的业务规则。很显然,这要求你每次创建新的操作类型时都要创建一个新的子表。当操作和业务规则从不(或几乎从不)改变时,这是最好的。但这可能不符合您的项目要求。大多数人说,“但我需要一个不需要改变架构的解决方案。”

大多数开发人员可能不进行类表继承。更常见的是,他们只是像其他人提到的那样使用一对多的表结构,并仅在应用程序代码中实现业务规则。也就是说,您的应用程序包含仅插入适合每种操作类型的设备的代码。

依赖应用程序逻辑的问题在于它可能包含错误并可能插入不满足业务规则的数据。类表继承的优势在于,通过精心设计的约束,RDBMS 始终如一地强制执行数据完整性。您可以确保数据库确实无法存储不正确的数据。

但这也可能会受到限制,例如,如果您的业务规则发生变化并且您需要调整数据。在这种情况下,常见的解决方案是编写一个脚本来转储所有数据,更改您的架构,然后以现在允许的形式 (Extract, Transform, and Load = ETL) 重新加载数据。

所以你必须决定:你想在应用程序层还是在数据库架构层编写代码?使用这两种策略都有合理的理由,但无论哪种方式都会很复杂。


关于您的评论:您似乎在谈论将表达式存储为数据字段中的字符串。我建议反对这样做。数据库用于存储数据,而不是代码。您可以在约束或触发器中执行一些有限的逻辑,但代码属于您的应用程序。

如果您有太多操作要在单独的表中建模,请在应用程序代码中建模。将表达式存储在数据列中并期望 SQL 使用它们来评估查询就像围绕大量使用 eval() 设计应用程序一样。

【讨论】:

  • P.S.:MySQL 不支持检查约束,因此您必须使用触发器或外键来实现它们。
  • 是的。你是对的——我问如何在 SQL 中做到这一点。但是,我更新了帖子,解释说会有一千种Operations。我不禁想到将您的建议缩减为 OperationConjunctionOperationDisjunction 表可能会起作用。
【解决方案2】:

我认为您应该在OperationEquipment 之间建立一对多或多对多的关系,具体取决于每件设备或每种设备类型是否有一个Equipment 条目.

我建议不要将业务逻辑放入您的数据库架构中,因为业务逻辑可能会发生变化,您宁愿不必更改架构作为响应。

【讨论】:

  • 谢谢,但我没有将业务逻辑放入数据库——我试图表示可以选择实体的条件。
  • 对,什么定义了这些条件?
【解决方案3】:

看起来您需要能够将某些设备组合在一起作为合取或析取并将这些组组合在一起......

OperationEquipmentGroup
   id int
   operation_id int 
   is_conjuction bit 

OperationEquipment
   id int
   operation_equipment_group_id int
   equipment_id

如果这很重要,您可以添加排序列,也可以在组表中添加另一列来指定组的组合方式(仅在排序时才有意义)。但是,通过您的示例,看起来组只是结合在一起的。

【讨论】:

    【解决方案4】:

    由于操作可以有一个或多个设备,您应该使用链接表。您的架构将是这样的:

    操作

    • 身份证
    • 其他栏目

    设备

    • 身份证
    • 其他栏目

    Operation_Equipment_Link

    • 操作 ID
    • 设备ID

    第三个表中的两个字段可以设置为复合主键,因此您不需要第三个字段,并且可以更轻松地将重复项排除在表之外。

    【讨论】:

      【解决方案5】:

      除了 Nicholai 的建议,我还解决了一个类似的问题,如下所示:

      表操作有一个附加字段“OperationType”

      Table Equipment 有一个附加字段“EquipmentType”

      我有一个附加表“DefaultOperationEquipmentType”,指定每个 OperationType 需要包含哪个 EquipmentType,例如

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

      我的应用程序不需要像(A 或 B)这样的复杂条件,因为在我的业务逻辑中,两种替代设备都属于同一类型的设备,例如在 PC 环境中,我可以拥有鼠标 (A) 或轨迹球 (B) 设备,但它们都属于 EquipmentType "PointingDevice_Type"

      希望有帮助

      【讨论】:

      • 啊,但我确实需要复杂的逻辑。想象一下在您需要Keyboard AND (Mouse OR Joystick OR WacomTablet) 的环境中
      • 这完全取决于您如何设置设备类型(或您想要的类别); AND是通过将多个设备类型分配给单个操作类型来实现的,OR是通过将多个设备分配给单个设备类型来实现的。当键越过 (equipment_type, equipment) 时,一个设备可以是更多设备类型的成员 - 例如 CADPointers 包含 (Tablet, Digipen, Mouse) 类型,OfficePointers 包含 (Mouse, Trackball) 和 Keyboards 包含 (GermanKey, USKey) 。 ..
      • ... 现在您可以定义带有类型(CadPointers、键盘)的 OperationType CADPCType 和带有类型(OfficePointers、键盘)的 OfficePCType,最后通过所有连接定义 CAD-PC 和 Office-PC 操作您将能够参考正确的设备。
      【解决方案6】:

      注意我没有在野外测试过这个。话虽如此,我能看到的最好的映射方法是使用非规范化表进行分组。

      *(除了比尔的方式,这种方式很难设置,但如果正确完成,则非常熟练)

      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 
      

      【讨论】:

        【解决方案7】:

        尽管我讨厌在 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 的情况下真正做到更少(通过将多个设备组合到一个列中)。

        【讨论】:

          【解决方案8】:

          据我了解,您希望以一种允许您稍后将业务逻辑应用于它的方式存储与操作相关的设备,在这种情况下,您将需要 3 个表:

          操作:

          • 身份证
          • 姓名

          设备:

          • 身份证
          • 姓名

          操作_设备:

          • equipment_id
          • operation_id
          • 符号

          其中符号是 A、B、C 等...

          如果你有(A & B) | (C & D)这样的条件,你可以很容易地知道哪个设备是哪个。

          【讨论】:

            猜你喜欢
            • 2013-04-27
            • 2011-08-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-08-08
            • 1970-01-01
            • 2014-03-17
            • 1970-01-01
            相关资源
            最近更新 更多