如何旋转 MySQL 实体-属性-值模式
Posted
技术标签:
【中文标题】如何旋转 MySQL 实体-属性-值模式【英文标题】:How to pivot a MySQL entity-attribute-value schema 【发布时间】:2010-10-13 13:59:58 【问题描述】:我需要设计存储文件的所有元数据(即文件名、作者、标题、创建日期)和自定义元数据(已由用户添加到文件中,例如 CustUseBy、CustSendBy)的表。无法预先设置自定义元数据字段的数量。实际上,确定在文件中添加了哪些自定义标签以及添加了多少自定义标签的唯一方法是检查表中存在的内容。
为了存储它,我创建了一个基表(包含文件的所有常见元数据)、一个 Attributes
表(保存可能在文件上设置的其他可选属性)和一个 FileAttributes
表(分配一个文件属性的值)。
CREAT TABLE FileBase (
id VARCHAR(32) PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(255),
author VARCHAR(255),
created DATETIME NOT NULL,
) Engine=InnoDB;
CREATE TABLE Attributes (
id VARCHAR(32) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL
) Engine=InnoDB;
CREATE TABLE FileAttributes (
sNo INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
fileId VARCHAR(32) NOT NULL,
attributeId VARCHAR(32) NOT NULL,
attributeValue VARCHAR(255) NOT NULL,
FOREIGN KEY fileId REFERENCES FileBase (id),
FOREIGN KEY attributeId REFERENCES Attributes (id)
) Engine=InnoDB;
样本数据:
INSERT INTO FileBase
(id, title, author, name, created)
VALUES
('F001', 'Dox', 'vinay', 'story.dox', '2009/01/02 15:04:05'),
('F002', 'Excel', 'Ajay', 'data.xls', '2009/02/03 01:02:03');
INSERT INTO Attributes
(id, name, type)
VALUES
('A001', 'CustomeAttt1', 'Varchar(40)'),
('A002', 'CustomUseDate', 'Datetime');
INSERT INTO FileAttributes
(fileId, attributeId, attributeValue)
VALUES
('F001', 'A001', 'Akash'),
('F001', 'A002', '2009/03/02');
现在的问题是我想以这样的方式显示数据:
FileId, Title, Author, CustomAttri1, CustomAttr2, ...
F001 Dox vinay Akash 2009/03/02 ...
F002 Excel Ajay
什么查询会产生这个结果?
【问题讨论】:
你打算使用什么编程语言? mysql ,我打算通过 MYSQL Query 得到想要的结果,然后将这个结果加入到其他表中,并将想要的结果传递给 FrontEnd。 MySQL 不是一种编程语言... 【参考方案1】:题中提到了MySQL,其实这个DBMS有专门针对这类问题的功能:GROUP_CONCAT(expr)
。看看MySQL reference manual on group-by-functions。该功能是在 MySQL 4.1 版中添加的。您将在查询中使用GROUP BY FileID
。
我不太确定您希望结果如何。如果您希望为每个项目列出每个属性(即使未设置),这将更加困难。但是,这是我的建议:
SELECT bt.FileID, Title, Author,
GROUP_CONCAT(
CONCAT_WS(':', at.AttributeName, at.AttributeType, avt.AttributeValue)
ORDER BY at.AttributeName SEPARATOR ', ')
FROM BaseTable bt JOIN AttributeValueTable avt ON avt.FileID=bt.FileID
JOIN AttributeTable at ON avt.AttributeId=at.AttributeId
GROUP BY bt.FileID;
这会以相同的顺序为您提供所有属性,这可能很有用。输出将如下所示:
'F001', 'Dox', 'vinay', 'CustomAttr1:varchar(40):Akash, CustomUseDate:Datetime:2009/03/02'
这样你只需要一个数据库查询,并且输出很容易解析。如果你想在数据库中将属性存储为真实的日期时间等,你需要使用动态 SQL,但我会保持清醒并将值存储在 varchars 中。
【讨论】:
【参考方案2】:这种查询的一般形式是
SELECT file.*,
attr1.value AS 'Attribute 1 Name',
attr2.value AS 'Attribute 2 Name',
...
FROM
file
LEFT JOIN attr AS attr1
ON(file.FileId=attr1.FileId and attr1.AttributeId=1)
LEFT JOIN attr AS attr2
ON(file.FileId=attr2.FileId and attr2.AttributeId=2)
...
因此,您需要根据需要的属性动态构建查询。在 php-ish 伪代码中
$cols="file";
$joins="";
$rows=$db->GetAll("select * from Attributes");
foreach($rows as $idx=>$row)
$alias="attr$idx";
$cols.=", $alias.value as '".mysql_escape_string($row['AttributeName'])."'";
$joins.="LEFT JOIN attr as $alias on ".
"(file.FileId=$alias.FileId and ".
"$alias.AttributeId=$row['AttributeId']) ";
$pivotsql="select $cols from file $joins";
【讨论】:
为了实现结果,我可以创建一个过程并在其中写入一个光标以在记录中移动。如果可能,请举例说明我会感谢您的帮助。 是的,如果您执行并遍历 $pivotsql 查询,每一行都将是一个文件,并且每个属性都有一个列,如果该文件不存在该属性,则该列将为 NULL . 这似乎正是我过去几周所追求的。非常感谢 我想知道......在同一个表上使用多个 LEFT JOIN 将连接的行收集为上述解决方案中的列是否存在任何性能问题? 这将取决于表定义及其大小。对生成的 SQL 运行 EXPLAIN 以衡量性能影响。【参考方案3】:如果您正在寻找比 group-concat 结果更有用(和可连接)的东西,请尝试以下解决方案。我创建了一些与您的示例非常相似的表来说明这一点。
这适用于:
您想要一个纯 SQL 解决方案(无代码、无循环) 您有一组可预测的属性(例如,非动态) 您可以在需要添加新属性类型时更新查询 您希望得到可以作为子选择加入、联合或嵌套的结果表 A(文件)
FileID, Title, Author, CreatedOn
表 B(属性)
AttrID, AttrName, AttrType [not sure how you use type...]
表 C(文件_属性)
FileID, AttrID, AttrValue
传统查询会拉出许多冗余行:
SELECT * FROM
Files F
LEFT JOIN Files_Attributes FA USING (FileID)
LEFT JOIN Attributes A USING (AttributeID);
AttrID FileID Title Author CreatedOn AttrValue AttrName AttrType
50 1 TestFile Joe 2011-01-01 true ReadOnly bool
60 1 TestFile Joe 2011-01-01 xls FileFormat 文本
70 1 TestFile Joe 2011-01-01 false Private bool
80 1 测试文件 Joe 2011-01-01 2011-10-03 LastModified date
60 2 LongNovel Mary 2011-02-01 json FileFormat 文本
80 2 LongNovel Mary 2011-02-01 2011-10-04 LastModified date
70 2 LongNovel Mary 2011-02-01 true 私有 bool
50 2 LongNovel Mary 2011-02-01 true ReadOnly bool
50 3 ShortStory Susan 2011-03-01 false ReadOnly bool
60 3 ShortStory Susan 2011-03-01 ascii 文件格式文本
70 3 ShortStory Susan 2011-03-01 false Private bool
80 3 ShortStory Susan 2011-03-01 2011-10-01 LastModified date
50 4 利润损失法案 2011-04-01 false ReadOnly bool
70 4 利润损失法案 2011-04-01 true Private bool
80 4 损益法案 2011-04-01 2011-10-02 LastModified date
60 4 利润损失法案 2011-04-01 text FileFormat text
50 5 MonthlyBudget George 2011-05-01 false ReadOnly bool
60 5 MonthlyBudget George 2011-05-01 二进制文件格式文本
70 5 MonthlyBudget George 2011-05-01 false Private bool
80 5 MonthlyBudget George 2011-05-01 2011-10-20 LastModified date
这种合并查询(使用 MAX 的方法)可以合并行:
SELECT
F.*,
MAX( IF(A.AttrName = 'ReadOnly', FA.AttrValue, NULL) ) as 'ReadOnly',
MAX( IF(A.AttrName = 'FileFormat', FA.AttrValue, NULL) ) as 'FileFormat',
MAX( IF(A.AttrName = 'Private', FA.AttrValue, NULL) ) as 'Private',
MAX( IF(A.AttrName = 'LastModified', FA.AttrValue, NULL) ) as 'LastModified'
FROM
Files F
LEFT JOIN Files_Attributes FA USING (FileID)
LEFT JOIN Attributes A USING (AttributeID)
GROUP BY
F.FileID;
FileID Title Author CreatedOn ReadOnly FileFormat Private LastModified
1 测试文件乔 2011-01-01 真 xls 假 2011-10-03
2 LongNovel Mary 2011-02-01 true json true 2011-10-04
3 ShortStory Susan 2011-03-01 假 ascii 假 2011-10-01
4 利润损失法案 2011-04-01 虚假文本 真实 2011-10-02
5 MonthlyBudget George 2011-05-01 错误二进制错误 2011-10-20
【讨论】:
【参考方案4】:这是 SQL 中标准的“行到列”问题。
它最容易在 SQL 之外完成。
在您的应用程序中,执行以下操作:
定义一个简单的类来包含文件、系统属性和用户属性的集合。对于这种客户属性集合,列表是一个不错的选择。我们称这个类为 FileDescription。
在文件和文件的所有客户属性之间执行简单连接。
编写一个循环来从查询结果中组装 FileDescriptions。
获取第一行,创建 FileDescription 并设置第一个客户属性。
虽然需要获取更多行:
获取一行 如果该行的文件名与我们正在构建的 FileDescription 不匹配:完成构建 FileDescription;将此附加到文件描述的结果集合中;使用给定名称和第一个客户属性创建一个全新的空 FileDescription。 如果此行的文件名与我们正在构建的 FileDescription 匹配:将另一个客户属性附加到当前 FileDescription【讨论】:
嗨,谢谢.. 但我在 PL 中并不那么笨,我在 T-SQL 方面有经验,你能在 Breif 中解释一下如何通过示例来实现这一点。多谢。 ——【参考方案5】:我一直在尝试不同的答案,而 Methai 的答案对我来说是最方便的。我当前的项目,虽然它使用 Doctrine 和 MySQL,但有很多松散的表。
以下是我对 Methai 解决方案的体验结果:
创建实体表
DROP TABLE IF EXISTS entity;
CREATE TABLE entity (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
author VARCHAR(255),
createdOn DATETIME NOT NULL
) Engine = InnoDB;
创建属性表
DROP TABLE IF EXISTS attribute;
CREATE TABLE attribute (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL
) Engine = InnoDB;
创建属性值表
DROP TABLE IF EXISTS attributevalue;
CREATE TABLE attributevalue (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
value VARCHAR(255) NOT NULL,
attribute_id INT UNSIGNED NOT NULL,
FOREIGN KEY(attribute_id) REFERENCES attribute(id)
) Engine = InnoDB;
创建实体属性值连接表
DROP TABLE IF EXISTS entity_attributevalue;
CREATE TABLE entity_attributevalue (
entity_id INT UNSIGNED NOT NULL,
attributevalue_id INT UNSIGNED NOT NULL,
FOREIGN KEY(entity_id) REFERENCES entity(id),
FOREIGN KEY(attributevalue_id) REFERENCES attributevalue(id)
) Engine = InnoDB;
填充实体表
INSERT INTO entity
(title, author, createdOn)
VALUES
('TestFile', 'Joe', '2011-01-01'),
('LongNovel', 'Mary', '2011-02-01'),
('ShortStory', 'Susan', '2011-03-01'),
('ProfitLoss', 'Bill', '2011-04-01'),
('MonthlyBudget', 'George', '2011-05-01'),
('Paper', 'Jane', '2012-04-01'),
('Essay', 'John', '2012-03-01'),
('Article', 'Dan', '2012-12-01');
填充属性表
INSERT INTO attribute
(name, type)
VALUES
('ReadOnly', 'bool'),
('FileFormat', 'text'),
('Private', 'bool'),
('LastModified', 'date');
填充属性值表
INSERT INTO attributevalue
(value, attribute_id)
VALUES
('true', '1'),
('xls', '2'),
('false', '3'),
('2011-10-03', '4'),
('true', '1'),
('json', '2'),
('true', '3'),
('2011-10-04', '4'),
('false', '1'),
('ascii', '2'),
('false', '3'),
('2011-10-01', '4'),
('false', '1'),
('text', '2'),
('true', '3'),
('2011-10-02', '4'),
('false', '1'),
('binary', '2'),
('false', '3'),
('2011-10-20', '4'),
('doc', '2'),
('false', '3'),
('2011-10-20', '4'),
('rtf', '2'),
('2011-10-20', '4');
填充 entity_attributevalue 表
INSERT INTO entity_attributevalue
(entity_id, attributevalue_id)
VALUES
('1', '1'),
('1', '2'),
('1', '3'),
('1', '4'),
('2', '5'),
('2', '6'),
('2', '7'),
('2', '8'),
('3', '9'),
('3', '10'),
('3', '11'),
('3', '12'),
('4', '13'),
('4', '14'),
('4', '15'),
('4', '16'),
('5', '17'),
('5', '18'),
('5', '19'),
('5', '20'),
('6', '21'),
('6', '22'),
('6', '23'),
('7', '24'),
('7', '25');
显示所有记录
SELECT *
FROM `entity` e
LEFT JOIN `entity_attributevalue` ea ON ea.entity_id = e.id
LEFT JOIN `attributevalue` av ON ea.attributevalue_id = av.id
LEFT JOIN `attribute` a ON av.attribute_id = a.id;
id 标题作者创建在 entity_id 属性值_id id 值属性_id id 名称类型
1 TestFile Joe 2011-01-01 00:00:00 1 1 1 true 1 1 ReadOnly bool
1 TestFile Joe 2011-01-01 00:00:00 1 2 2 xls 2 2 文件格式文本
1 测试文件乔 2011-01-01 00:00:00 1 3 3 假 3 3 私有布尔
1 TestFile Joe 2011-01-01 00:00:00 1 4 4 2011-10-03 4 4 LastModified 日期
2 LongNovel Mary 2011-02-01 00:00:00 2 5 5 真 1 1 只读布尔
2 LongNovel Mary 2011-02-01 00:00:00 2 6 6 json 2 2 文件格式文本
2 LongNovel Mary 2011-02-01 00:00:00 2 7 7 真 3 3 私有布尔
2 LongNovel Mary 2011-02-01 00:00:00 2 8 8 2011-10-04 4 4 LastModified 日期
3 ShortStory 苏珊 2011-03-01 00:00:00 3 9 9 假 1 1 只读布尔
3 ShortStory Susan 2011-03-01 00:00:00 3 10 10 ascii 2 2 文件格式文本
3 ShortStory Susan 2011-03-01 00:00:00 3 11 11 假 3 3 私人布尔
3 ShortStory Susan 2011-03-01 00:00:00 3 12 12 2011-10-01 4 4 LastModified 日期
4 利润损失法案 2011-04-01 00:00:00 4 13 13 false 1 1 ReadOnly bool
4 利润损失法案 2011-04-01 00:00:00 4 14 14 文本 2 2 文件格式文本
4 利润损失法案 2011-04-01 00:00:00 4 15 15 true 3 3 Private bool
4 损益法案 2011-04-01 00:00:00 4 16 16 2011-10-02 4 4 LastModified date
5 MonthlyBudget George 2011-05-01 00:00:00 5 17 17 false 1 1 ReadOnly bool
5 MonthlyBudget George 2011-05-01 00:00:00 5 18 18 二进制 2 2 文件格式文本
5 MonthlyBudget George 2011-05-01 00:00:00 5 19 19 false 3 3 Private bool
5 MonthlyBudget George 2011-05-01 00:00:00 5 20 20 2011-10-20 4 4 LastModified 日期
6 纸简 2012-04-01 00:00:00 6 21 21 二进制 2 2 文件格式文本
6 纸简 2012-04-01 00:00:00 6 22 22 假 3 3 私有布尔
6 论文 Jane 2012-04-01 00:00:00 6 23 23 2011-10-20 4 4 LastModified 日期
7 随笔 John 2012-03-01 00:00:00 7 24 24 二进制 2 2 文件格式文本
7 随笔约翰 2012-03-01 00:00:00 7 25 25 2011-10-20 4 4 LastModified 日期
8 文章 Dan 2012-12-01 00:00:00 NULL NULL NULL NULL NULL NULL NULL NULL
数据透视表
SELECT e.*,
MAX( IF(a.name = 'ReadOnly', av.value, NULL) ) as 'ReadOnly',
MAX( IF(a.name = 'FileFormat', av.value, NULL) ) as 'FileFormat',
MAX( IF(a.name = 'Private', av.value, NULL) ) as 'Private',
MAX( IF(a.name = 'LastModified', av.value, NULL) ) as 'LastModified'
FROM `entity` e
LEFT JOIN `entity_attributevalue` ea ON ea.entity_id = e.id
LEFT JOIN `attributevalue` av ON ea.attributevalue_id = av.id
LEFT JOIN `attribute` a ON av.attribute_id = a.id
GROUP BY e.id;
id 标题 作者 createdOn ReadOnly FileFormat Private LastModified
1 TestFile 乔 2011-01-01 00:00:00 真 xls 假 2011-10-03
2 LongNovel Mary 2011-02-01 00:00:00 true json true 2011-10-04
3 ShortStory 苏珊 2011-03-01 00:00:00 假 ascii 假 2011-10-01
4 利润损失法案 2011-04-01 00:00:00 false text true 2011-10-02
5 MonthlyBudget George 2011-05-01 00:00:00 false binary false 2011-10-20
6 纸简 2012-04-01 00:00:00 NULL 二进制 false 2011-10-20
7 随笔约翰 2012-03-01 00:00:00 NULL 二进制 NULL 2011-10-20
8 文章 Dan 2012-12-01 00:00:00 NULL NULL NULL NULL
【讨论】:
【参考方案6】:但是,有一些解决方案可以将行用作列,也就是转置数据。 它涉及在纯 SQL 中执行此操作的查询技巧,或者您将不得不依赖某些仅在某些数据库中可用的功能,使用数据透视表(或交叉表)。
As exemple you can see how to do this here in Oracle (11g).
编程版本将更易于维护和制作,而且可以与任何数据库一起使用。
【讨论】:
请解释我是否将属性值存储在带有分隔符的表的列中,那么我们将属性名称存储在哪里。如果我们将属性名称和值都存储在带有分隔符的 2 列中(在一个文件有多个属性的情况)那么我如何转置这些值 因为您在运行前不知道自定义属性的数量,所以我会采用编程方式。使用您的程序动态构建查询,或者使用连接提取所有数据并使用 lopp 处理它。构建复杂的查询将使可维护性和调整变得复杂。【参考方案7】:部分答案,因为我不了解 MySQL(好吧)。在 MSSQL 中,我会查看数据透视表或在存储过程中创建临时表。这可能是一个艰难的时期......
【讨论】:
嗨,谢谢.. 但我在 PL 中并不那么笨,我在 T-SQL 方面有经验,你能在 Breif 中解释一下如何通过示例来实现这一点。非常感谢。以上是关于如何旋转 MySQL 实体-属性-值模式的主要内容,如果未能解决你的问题,请参考以下文章