SQL问题:一对多关系和EAV模型

时间:2012-06-21 20:40:37

标签: mysql sql

晚上好, 我是网络编程的新手,我需要你的帮助来解决SQL查询固有的问题。 我正在使用的数据库引擎是MySQL,我通过PHP访问它,在这里我将解释我的数据库的简化版本,只是为了解决想法。 我们假设使用包含三个表的数据库:teamsteams_informationattributes。更确切地说:

1)teams是一个包含意大利足球队基本信息的表格(足球,而不是美式足球:D),它由三个区域组成:'id'(int, primary key),'name '(varchar,团队名称),nicknameVarchar,团队昵称);

2)attributes是一个表格,其中包含有关足球队的可能信息列表,例如city(球队主场比赛的城市),captain(队长的队长)全名),f_number(粉丝数)等。此表由三个字段组成:id (int, primary key)attribute_namevarchar,属性的标识符),attribute_desc(文本,属性含义的解释) 。该表的每条记录代表足球队的一个可能属性;

3)teams_information是一个表格,其中提供了有关team表中列出的团队的一些信息。此表包含三个字段:id(int,主键),team_idint,标识团队的外键),attribute_idint ,一个外键,用于标识attributes表中列出的一个属性),attribute_valuevarchar,属性值)。每条记录代表一个团队的单个属性。一般来说,不同的团队将拥有不同数量的信息,因此对于某些团队而言,可以获得大量属性,而对于其他团队,只有少量属性可用。

请注意,teamsteams_information之间的关系是一对多,attributesteams_information之间存在相同的关系

嗯,鉴于这个模型,我的目的是实现一个网格(可能使用ExtJS 4.1)向用户显示意大利足球队的名单,该网格的每个记录将代表一个足球队并将包含所有可能的属性:一些字段可能为空(因为,对于考虑的团队,对应的属性是未知的),而其他字段将包含存储在teams_information表中的值(对于所考虑的团队)。 根据上面的网格字段是:id,team_name和一些字段来表示'attributes'表中列出的所有不同属性。

我的问题是:我可以通过使用SINGLE SQL查询(可能是一个正确的SELECT查询来实现这样的网格,从数据库表中获取所需的所有数据)吗? 任何人都可以建议我如何写一个类似的查询(如果它存在)?

先谢谢你的帮助。

问候。

恩里科。

4 个答案:

答案 0 :(得分:3)

对你的问题的简短回答是否定的,MySQL中没有简单的构造来实现你正在寻找的结果集。

但是可以仔细(辛苦地)制作这样的查询。这是一个例子,我相信你将能够破译它。基本上,我在选择列表中使用相关子查询,对于我想要返回的每个属性。

SELECT t.id
     , t.name
     , t.nickname

     , ( SELECT v1.attribute_value 
           FROM team_information v1 
           JOIN attributes a1
             ON a1.id = v1.attribute_id AND a1.attribute_name = 'city'
          WHERE v1.team_id = t.id ORDER BY 1 LIMIT 1
       ) AS city

     , ( SELECT v2.attribute_value
           FROM team_information v2 JOIN attributes a2
             ON a2.id = v2.attribute_id AND a2.attribute_name = 'captain'
          WHERE v2.team_id = t.id ORDER BY 1 LIMIT 1
       ) AS captain

     , ( SELECT v3.attribute_value
           FROM team_information v3 JOIN attributes a3
             ON a3.id = v3.attribute_id AND a3.attribute_name = 'f_number'
          WHERE v3.team_id = t.id ORDER BY 1 LIMIT 1
       ) AS f_number

  FROM teams t
 ORDER BY t.id

对于“多值”属性,您必须分别拉出属性的每个实例。 (使用LIMIT指定是否检索第一个,第二个等)

     , ( SELECT v4.attribute_value
           FROM team_information v4 JOIN attributes a4
             ON a4.id = v4.attribute_id AND a4.attribute_name = 'nickname'
          WHERE v4.team_id = t.id ORDER BY 1 LIMIT 0,1
       ) AS nickname_1st

     , ( SELECT v5.attribute_value
           FROM team_information v5 JOIN attributes a5
             ON a5.id = v5.attribute_id AND a5.attribute_name = 'nickname'
          WHERE v5.team_id = t.id ORDER BY 1 LIMIT 1,1
       ) AS nickname_2nd

     , ( SELECT v6.attribute_value
           FROM team_information v6 JOIN attributes a6
             ON a6.id = v6.attribute_id AND a6.attribute_name = 'nickname'
          WHERE v6.team_id = t.id ORDER BY 1 LIMIT 2,1
       ) AS nickname_3rd

我在这里使用昵称作为例子,因为美国足球俱乐部经常有多个昵称,例如:芝加哥火足球俱乐部有绰号:'The Fire','LaMáquinaRoja','Men in Red','CF97'等人。

不是对你的问题的回答,但是......

之前我曾多次提到过,我不喜欢使用EAV数据库实施多少次? IMO应该是一个非常简单的查询,变成一个过于复杂的可能轻微调暗查询的野兽。

创建一个表中每个“属性”是一个单独的列是不是更简单?然后查询返回合理的结果集看起来会更合理......

SELECT id, name, nickname, city, captain, f_number, ... FROM team

但真正令我不寒而栗的是,有些开发人员决定将LDQ作为视图“隐藏”在数据库中,以启用“更简单”的查询。

如果您选择这条路线,请请您拒绝将此查询作为视图存储在数据库中的任何冲动。

答案 1 :(得分:1)

我将采取略有不同的路线。斯宾塞的答案太棒了,它很好地解决了这个问题,但仍存在很大的潜在问题。

您尝试在网站上显示的数据在数据库中过度标准化。我不会详细说明,因为斯宾塞的答案再次强调了这个问题。

相反,我想推荐一种能够对数据进行非规范化的解决方案。

将所有Team数据转换为包含许多列的单个表。 (如果问题中没有涉及播放器数据,那将是第二个表格,但我现在将对此进行掩饰。)

当然,你会有一堆整个列,而很多行的很多列可能都是NULL。它没有标准化,并且它不漂亮,但这是你获得的巨大优势。

您的查询变为:

SELECT * FROM Teams

就是这样。这显示在网站上,你就完成了。你可能不得不自己去实现这个架构,但是完全值得投入时间。

答案 2 :(得分:0)

我认为您所说的是您希望属性表中的行在结果记录集中显示为列。如果这是正确的,那么在SQL中你将使用PIVOT。 快速搜索SO似乎表明MySql中没有PIVOT等价物。

答案 3 :(得分:0)

我写了一个简单的PHP脚本来概括斯宾塞的想法来解决我的问题。 这是代码:

<?php
    require_once('includes/db.config.php'); //this file performs connection to mysql

/*
 * Following function requires a table name ($table)
 * and a number of service fields ($num). Given those parameters
 * it returns the number of table fields (excluding service fields). 
 */

function get_fields_number($table,$num,$conn)
{   
    $query = "SELECT * FROM $table";
    $result = mysql_query($query,$conn);
    return mysql_num_fields($result)-$num; //remember there are $num service fields
}

/*
 * Following function requires a table name ($table) and an array
 * containing a list of service fields names. Given those parameters, 
 * it returns the list of field names. That list is contained within an array and
 * service fields are excluded.
 */

function get_fields_name($table,$service,$conn)
{
    $query = "SELECT * FROM $table";
    $result = mysql_query($query,$conn);
    $name = array(); //Array to be returned
    for ($i=0;$i<mysql_num_fields($result);$i++)
    {
        if(!in_array(mysql_field_name($result,$i),$service))
    {
       //currently selected field is not a service field
       $name[] = mysql_field_name($result,$i); 
    }
    }
    return $name;
}

//Below $conn is db connection created in 'db.config.php'

$query = "SELECT `name` FROM  `detail_arg` WHERE visibility = 0";
$res = mysql_query($query,$conn);
if($res===false)
{
    $err_msg = mysql_real_escape_string(mysql_error($conn));
    echo "{success:false,data:'".$err_msg."'}";
    die();
}
$arg = array(); //list of argument names
while($row = mysql_fetch_assoc($res))
{
    $arg[] = $row['name'];
}

//Following function writes the select subquery which is
    //necessary to build a column containing a single attribute.

function make_subquery($attribute) //$attribute contains attribute name
{
    $query = "";
    $query.="(SELECT incident_detail.arg_value ";
    $query.="FROM incident_detail ";
    $query.="INNER JOIN detail_arg ";
    $query.="ON incident_detail.arg_id = detail_arg.id AND      detail_arg.name='".$attribute."' ";
    $query.="WHERE incident.id = incident_detail.incident_id) ";
    $query.="AS $attribute";
    return $query;
}

/*

echo make_subquery("date"); //debug code

*/

$subquery = array(); //list of subqueries
for($i=0;$i<count($arg);$i++)
{
    $subquery[] = make_subquery($arg[$i]); 
}

$query = "SELECT "; //final query containing subqueries

$fields = get_fields_name("incident",array("id","visibility"),$conn); 
    //list of 'incident' table's fields

for($i=0;$i<count($fields);$i++)
{
    $query.="incident.".$fields[$i].", ";
}

//insert the subqueries

$sub = implode($subquery,", ");
$query .= $sub;

    $query.=" FROM incident ORDER BY incident.id";
echo $query;
?>