use*_*506 14 mysql database-design subtypes
我在重新创建Users和Items之间具有一对多(1:M)关系的数据库时遇到了一些麻烦。
这很简单,是的;但是,每个Item 都属于某个类别(例如,汽车、船或飞机),并且每个类别都有特定数量的属性,例如:
Car
结构体:
+----+--------------+--------------+
| PK | Attribute #1 | Attribute #2 |
+----+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)
Boat
结构体:
+----+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 |
+----+--------------+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)
Plane
结构体:
+----+--------------+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 | Attribute #4 |
+----+--------------+--------------+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)
由于属性(列)数量的这种多样性,我最初认为为每个Category创建一个单独的表是个好主意,因此我会避免多个NULL,从而更好地利用索引。
虽然一开始看起来不错,但我找不到通过数据库创建Items和Categories之间关系的方法,因为至少以我作为数据库管理员的经验来看,在创建外键时,我明确地通知了数据库表名和列。
最后,我想要一个可靠的结构来存储所有数据,同时拥有所有方法来列出用户可能通过一个查询拥有的所有项目的所有属性。
我可以使用服务器端语言对动态查询进行硬编码,但我觉得这是错误的并且不是很理想。
附加信息
这些是我对 MDCCL 评论的回应:
1.在您的业务环境中有多少个感兴趣的项目类别,三个(即汽车、船和飞机)或更多?
其实很简单:一共只有五个Categories。
2.同一个物品是否总是属于同一个用户(也就是说,一旦给定的物品被“分配”给某个用户,它就不能改变)?
不,他们可以改变。在问题的虚构场景中,就像用户 A 为用户 B 出售 Item #1 一样,因此必须反映所有权。
3.是否有一些或所有类别共享的属性?
未共享,但从记忆中,我可以看出所有Categories中至少存在三个属性。
4. User和Item关系的基数是否有可能是多对多(M:N)而不是一对多(1:M)?例如,在以下业务规则的情况下:
A User owns zero-one-or-many Items
和An Item is owned by one-to-many Users
不,因为Items会描述一个物理对象。用户将拥有他们的虚拟副本,每个副本都由唯一的GUID v4标识
5.关于您对问题评论之一的以下回应:
“在这个问题的虚构场景中,就像用户 A 为用户 B 出售 Item #1 一样,因此必须反映所有权。”
可以这么说,您似乎正计划跟踪项目所有权的演变。通过这种方式,您希望存储此类现象的哪些属性?只有指示特定属性的修改用户谁是所有者特定的项目?
不,不是真的。该所有权可能会改变,但我并不需要跟踪先前的所有者。
MDC*_*CCL 20
根据您对所考虑的业务环境的描述,存在一个超类型-子类型结构,其中包含Item —超类型 — 及其每个类别,即Car、Boat和Plane(以及另外两个未公开的)—亚型——。
我将在下面详细说明我将用来管理上述场景的方法。
为了开始描绘相关的概念模式,目前确定的一些最重要的业务规则(将分析仅限于三个公开的类别,以保持尽可能简短)可以表述如下:
图 1显示了我创建的 IDEF1X 1图,用于将之前的公式与其他相关的业务规则分组在一起:
超类型
一方面,Item,超类型,呈现了所有Categories共有的属性†或属性,即,
亚型
另一方面,属于每个特定Category的属性‡,即,
显示在相应的子类型框中。
身份标识
然后,Item.ItemId PRIMARY KEY (PK) 已经将3迁移到不同角色名称的子类型,即,
互斥关联
如图所示,在 (a) 每个超类型出现和 (b) 其互补子类型实例之间存在一对一(1:1) 基数比的关联或关系。
的独占亚型符号描绘的事实,亚型是相互排斥的,即,混凝土物品发生可以仅由单个亚型实例加以补充:任一个汽车,或一个平面,或者一个船(从未由零或更小,也没有由两个或更多)。
† , ‡我使用了经典的占位符名称来授权某些实体类型属性,因为问题中未提供它们的实际面额。
因此,为了讨论说明性逻辑设计,我根据上面显示和描述的 IDEF1X 图导出了以下 SQL-DDL 语句:
-- You should determine which are the most fitting
-- data types and sizes for all your table columns
-- depending on your business context characteristics.
-- Also, you should make accurate tests to define the
-- most convenient INDEX strategies based on the exact
-- data manipulation tendencies of your business context.
-- As one would expect, you are free to utilize
-- your preferred (or required) naming conventions.
CREATE TABLE UserProfile (
UserId INT NOT NULL,
FirstName CHAR(30) NOT NULL,
LastName CHAR(30) NOT NULL,
BirthDate DATE NOT NULL,
GenderCode CHAR(3) NOT NULL,
Username CHAR(20) NOT NULL,
CreatedDateTime DATETIME NOT NULL,
--
CONSTRAINT UserProfile_PK PRIMARY KEY (UserId),
CONSTRAINT UserProfile_AK1 UNIQUE ( -- Composite ALTERNATE KEY.
FirstName,
LastName,
GenderCode,
BirthDate
),
CONSTRAINT UserProfile_AK2 UNIQUE (Username) -- ALTERNATE KEY.
);
CREATE TABLE Category (
CategoryCode CHAR(1) NOT NULL, -- Meant to contain meaningful, short and stable values, e.g.; 'C' for 'Car'; 'B' for 'Boat'; 'P' for 'Plane'.
Name CHAR(30) NOT NULL,
--
CONSTRAINT Category_PK PRIMARY KEY (CategoryCode),
CONSTRAINT Category_AK UNIQUE (Name) -- ALTERNATE KEY.
);
CREATE TABLE Item ( -- Stands for the supertype.
ItemId INT NOT NULL,
OwnerId INT NOT NULL,
CategoryCode CHAR(1) NOT NULL, -- Denotes the subtype discriminator.
Foo CHAR(30) NOT NULL,
Bar CHAR(40) NOT NULL,
Baz CHAR(55) NOT NULL,
CreatedDateTime DATETIME NOT NULL,
--
CONSTRAINT Item_PK PRIMARY KEY (ItemId),
CONSTRAINT Item_to_Category_FK FOREIGN KEY (CategoryCode)
REFERENCES Category (CategoryCode),
CONSTRAINT Item_to_User_FK FOREIGN KEY (OwnerId)
REFERENCES UserProfile (UserId)
);
CREATE TABLE Car ( -- Represents one of the subtypes.
CarId INT NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
Qux DATE NOT NULL,
Corge DECIMAL(5,2) NOT NULL,
--
CONSTRAINT Car_PK PRIMARY KEY (CarId),
CONSTRAINT Car_to_Item_FK FOREIGN KEY (CarId)
REFERENCES Item (ItemId),
CONSTRAINT ValidQux_CK CHECK (Qux >= '1990-01-01')
);
CREATE TABLE Boat ( -- Stands for one of the subtypes.
BoatId INT NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
Grault SMALLINT NOT NULL,
Garply DATETIME NOT NULL,
Plugh CHAR(63) NOT NULL,
--
CONSTRAINT Boat_PK PRIMARY KEY (BoatId),
CONSTRAINT Boat_to_Item_FK FOREIGN KEY (BoatId)
REFERENCES Item (ItemId),
CONSTRAINT ValidGrault_CK CHECK (Grault <= 10000)
);
CREATE TABLE Plane ( -- Denotes one of the subtypes.
PlaneId INT NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
Xyzzy BIGINT NOT NULL,
Thud TEXT NOT NULL,
Wibble CHAR(20) NOT NULL,
Flob BIT(1) NOT NULL,
--
CONSTRAINT Plane_PK PRIMARY KEY (PlaneId),
CONSTRAINT Plane_to_Item_PK FOREIGN KEY (PlaneId)
REFERENCES Item (ItemId),
CONSTRAINT ValidXyzzy_CK CHECK (Xyzzy <= 3258594758)
);
Run Code Online (Sandbox Code Playgroud)
这已经在运行在 MySQL 8.0 上的这个 db<>fiddle 中进行了测试。
如图所示,超实体类型和每个子实体类型由相应的基表表示。
列CarId
,BoatId
和 被PlaneId
约束为适当表的 PK,通过 FK 约束§帮助表示概念级的一对一关联ItemId
,该约束指向列,列被约束为Item
表的 PK 。这意味着,在实际的“对”中,超类型和子类型行都由相同的 PK 值标识;因此,现在提
§为了防止关于(特别是 FOREIGN)KEY 约束定义的问题和错误——你在评论中提到的情况——,考虑到手头不同表之间发生的存在依赖性非常重要,如说明性 DDL 结构中表的声明顺序,我也在这个 db<>fiddle 中提供。
? 例如,将具有AUTO_INCREMENT属性的附加列附加到建立在 MySQL 上的数据库的表中。
完整性和一致性注意事项
必须指出的是,在您的业务环境中,您必须 (1) 确保每个“超类型”行始终由其对应的“子类型”对应行补充,并且反过来,(2) 保证所述“子类型”行与“超类型”行的“鉴别器”列中包含的值兼容。
以声明方式强制执行这种情况会非常优雅,但不幸的是,据我所知,没有一个主要的 SQL 平台提供适当的机制来这样做。因此,在ACID TRANSACTIONS 中诉诸程序代码是非常方便的,以便在您的数据库中始终满足这些条件。其他选择是使用触发器,但可以这么说,它们往往会使事情变得不整洁。
具有与上述类似的逻辑设计,创建一个或多个视图将非常实用,即包含属于两个或多个相关基表的列的派生表。通过这种方式,您可以,例如,直接从这些视图中进行 SELECT,而不必在每次必须检索“组合”信息时编写所有 JOIN。
样本数据
在这方面,让我们说基表是用下面显示的示例数据“填充”的:
--
INSERT INTO UserProfile
(UserId, FirstName, LastName, BirthDate, GenderCode, Username, CreatedDateTime)
VALUES
(1, 'Edgar', 'Codd', '1923-08-19', 'M', 'ted.codd', CURDATE()),
(2, 'Michelangelo', 'Buonarroti', '1475-03-06', 'M', 'michelangelo', CURDATE()),
(3, 'Diego', 'Velázquez', '1599-06-06', 'M', 'd.velazquez', CURDATE());
INSERT INTO Category
(CategoryCode, Name)
VALUES
('C', 'Car'), ('B', 'Boat'), ('P', 'Plane');
-- 1. ‘Full’ Car INSERTion
-- 1.1
INSERT INTO Item
(ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
(1, 1, 'C', 'Motorway', 'Tire', 'Chauffeur', CURDATE());
-- 1.2
INSERT INTO Car
(CarId, Qux, Corge)
VALUES
(1, '1999-06-11', 999.99);
-- 2. ‘Full’ Boat INSERTion
-- 2.1
INSERT INTO Item
(ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
(2, 2, 'B', 'Ocean', 'Anchor', 'Sailor', CURDATE());
-- 2.2
INSERT INTO Boat
(BoatId, Grault, Garply, Plugh)
VALUES
(2, 10000, '2016-03-09 07:32:04.000', 'So far so good.');
-- 3 ‘Full’ Plane INSERTion
-- 3.1
INSERT INTO Item
(ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
(3, 3, 'P', 'Sky', 'Wing', 'Aviator', CURDATE());
-- 3.2
INSERT INTO Plane
(PlaneId, Xyzzy, Thud, Wibble, Flob)
VALUES
(3, 3258594758, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sollicitudin pharetra sem id elementum. Sed tempor hendrerit orci. Ut scelerisque pretium diam, eu sodales ante sagittis ut. Phasellus id nunc commodo, sagittis urna vitae, auctor ex. Duis elit tellus, pharetra sed ipsum sit amet, bibendum dapibus mauris. Morbi condimentum laoreet justo, quis auctor leo rutrum eu. Sed id nibh non leo sodales pulvinar. Nam ornare ipsum nunc, eget molestie nulla ultrices vel. Curabitur fermentum nisl quis lorem aliquam pretium aliquam at mauris. In vestibulum, tellus et pharetra sollicitudin, mi lacus consectetur dolor, id volutpat nulla eros a mauris. ', 'Here we go!', TRUE);
--
Run Code Online (Sandbox Code Playgroud)
然后,一个有利的观点是从Item
,Car
和收集列UserProfile
:
--
CREATE VIEW CarAndOwner AS
SELECT C.CarId,
I.Foo,
I.Bar,
I.Baz,
C.Qux,
C.Corge,
U.FirstName AS OwnerFirstName,
U.LastName AS OwnerLastName
FROM Item I
JOIN Car C
ON C.CarId = I.ItemId
JOIN UserProfile U
ON U.UserId = I.OwnerId;
--
Run Code Online (Sandbox Code Playgroud)
自然地,可以遵循类似的方法,以便您也可以直接从单个表中选择“完整”Boat
和Plane
信息(在这些情况下是派生的)中。
之后——如果你不介意结果集中存在 NULL 标记——使用以下 VIEW 定义,你可以,例如,从表Item
, Car
, Boat
,Plane
和 中“收集”列UserProfile
:
--
CREATE VIEW FullItemAndOwner AS
SELECT I.ItemId,
I.Foo, -- Common to all Categories.
I.Bar, -- Common to all Categories.
I.Baz, -- Common to all Categories.
IC.Name AS Category,
C.Qux, -- Applies to Cars only.
C.Corge, -- Applies to Cars only.
--
B.Grault, -- Applies to Boats only.
B.Garply, -- Applies to Boats only.
B.Plugh, -- Applies to Boats only.
--
P.Xyzzy, -- Applies to Planes only.
P.Thud, -- Applies to Planes only.
P.Wibble, -- Applies to Planes only.
P.Flob, -- Applies to Planes only.
U.FirstName AS OwnerFirstName,
U.LastName AS OwnerLastName
FROM Item I
JOIN Category IC
ON I.CategoryCode = IC.CategoryCode
LEFT JOIN Car C
ON C.CarId = I.ItemId
LEFT JOIN Boat B
ON B.BoatId = I.ItemId
LEFT JOIN Plane P
ON P.PlaneId = I.ItemId
JOIN UserProfile U
ON U.UserId = I.OwnerId;
--
Run Code Online (Sandbox Code Playgroud)
此处显示的视图代码仅用于说明。当然,进行一些测试练习和修改可能有助于加速手头查询的(物理)执行。此外,您可能需要根据业务需求为所述视图删除或添加列。
示例数据和所有视图定义都包含在此 db<>fiddle 中,以便可以“在操作中”观察它们。
数据操作:应用程序代码和列别名
应用程序代码的使用(如果这就是“服务器端特定代码”的意思)和列别名是您在下一条评论中提出的其他重要点:
- 我确实设法解决了服务器端特定代码的 [a JOIN] 问题,但我真的不想这样做 - 并且 - 向所有列添加别名可能会“造成压力”。
- 解释的很好,非常感谢。但是,正如我所怀疑的那样,由于与某些列的相似性,我在列出所有数据时必须操纵结果集,因为我不想使用多个别名来保持语句更清晰。
虽然使用应用程序代码是处理数据集的表示或图形特征(即计算机化信息系统的外部表示)的非常合适的资源,但最重要的是避免执行数据逐行检索以防止执行速度问题。目标应该是通过 SQL 平台的(精确的)设置引擎提供的强大的数据操作工具来“获取”相关的数据集,以便您可以优化系统的行为。
此外,使用别名来重命名某个范围内的一个或多个列可能看起来很紧张,但就个人而言,我认为这种资源是一种非常强大的工具,有助于 (i) 上下文化和 (ii) 消除含义和意图的歧义归因于列; 因此,对于感兴趣的数据的操作,这是应该彻底考虑的一个方面。
你不妨找帮助这一系列的帖子和这组帖子其中包含我对另外两个案例的看法,其中包括具有互斥子类型的超类型-子类型关联。
我还为涉及超类型-子类型集群的业务环境提出了一个解决方案,其中子类型在这个(较新的)答案中并不相互排斥。
1所 对于信息建模集成定义( IDEF1X)是被确立为一个非常可取的数据建模技术标准由美国在1993年12月美国国家标准与技术研究院(NIST)。它是有坚实基础的(a)上的一些理论著作的撰写由独家发起的的关系模型,即EF科德博士; 关于 (b)实体关系视图,由PP Chen 博士开发;以及 (c) 逻辑数据库设计技术,由 Robert G. Brown 创建。
2在 IDEF1X 中,角色名称是分配给 FK 属性(或属性)的独特标签,以表达其在其各自实体类型范围内的含义。
3 IDEF1X 标准将键迁移定义为“将父实体或通用实体的主键作为外键放置在其子实体或类别实体中的建模过程”。