【问题标题】:mySQL complicated conditional join queries for efficiency over multiple queries and PHPmySQL 复杂的条件连接查询以提高多个查询和 PHP 的效率
【发布时间】:2013-04-08 15:52:10
【问题描述】:

奇怪的是,我用mySQL做了很多开发,今天遇到的一些东西从来没有遇到过。

所以,我有一个 user_items 表

ID | name
---------
1  | test

然后我有一个 item_data 表

ID | item | added | info
-------------------------
1  | test | 12345 | important info
2  | test | 23456 | more recent important info

然后我有一个电子邮件表

ID | added | email
1  | 12345 | old@b.com
2  | 23456 | a@b.com
3  | 23456 | b@c.com

还有一个 emails_verified 表

ID | email
-----------
1  | a@b.com

现在我意识到这些表的设置可能效率不高,但这无法更改,而且比看起来要复杂得多。

我想要做的如下。我希望能够搜索用户项目并显示相关信息,以及相关的任何电子邮件,以及显示电子邮件是否已通过验证。

user_items.name = item_data.item
item_data.added = emails.added
emails.email = emails_verified.email

所以对于用户项目 1,测试。我希望能够返回其 ID、名称、最新信息、最新电子邮件及其验证状态。

所以我想回来

ID => 1
name => test
information => more recent important info
emails => array('0' => array('email' => 'a@b.com' , 'verified' => 'YES'),'1' => array('email' => 'b@c.com' , 'verified' => 'NO'))

现在我可以相对轻松地处理多个查询。然而,我的研究表明,与使用一个(尽管非常复杂)带有大量连接语句的 mysql 查询相比,这显着增加了资源/时间成本。

使用一个查询也很有用(我相信)是因为我可以相对轻松地添加搜索功能 - 添加到查询复杂的 where 语句中。

为了更复杂的问题,我正在使用 CodeIgniter。我不能太挑剔:) 所以任何无 CI 答案仍然非常有用。

到目前为止我得到的代码如下。然而,“我不太确定我在做什么”。

function test_search()
{
    $this->load->database();
    $this->db->select('user_items.*,item_data.*');
    $this->db->select('GROUP_CONCAT( emails.email SEPARATOR "," ) AS emails', FALSE);
    $this->db->select('GROUP_CONCAT( IF(emailed.email,"YES","NO") SEPARATOR "," ) AS emailed', FALSE);

    $this->db->where('user_items.name','test');
    $this->db->join('item_data','user_items.name = item_data.name','LEFT');
    $this->db->join('emails','item_data.added = emails.added','LEFT');
    $this->db->join('emailed','emails.email = emailed.email','LEFT');
    $this->db->group_by('user_items.name');
    $res = $this->db->get('user_items');

    print_r($res->result_array());
}

对此的任何帮助将不胜感激。

这是一个非常复杂的 sql - 这真的是实现此功能的最佳方式吗?

谢谢

更新

继续 Cryode 的出色回答。

唯一的问题是它只返回一封电子邮件。但是,通过使用 GROUP_CONCAT,我能够将所有电子邮件和所有 email_verified 状态转换为一个字符串,然后我可以使用 PHP 进行分解。

要澄清的是子查询,

SELECT item, MAX(added) AS added
            FROM item_data
            GROUP BY item

本质上是创建一个临时表?

类似于here的概述

肯定需要子查询来确保您只从 item_data 中获取一行 - 最新的一行?

最后回答关于设计不佳的数据库的注释。

数据库是这样设计的,因为 item_data 会定期更改,但我们希望保留历史记录。

电子邮件是商品数据的一部分,但因为可以有任意数量的电子邮件,并且我们希望它们可以被搜索到,所以我们选择了一个单独的表格。否则,电子邮件必须在 item_data 表中进行序列化。

emails_verified 表是独立的,因为一封电子邮件可以与多个项目相关联。

鉴于这一点,尽管(显然)查询很复杂,但它似乎仍然是一个合适的设置..?

谢谢

最终更新

Cryodes 的答案通常是与数据库架构相关的非常有用的答案。

稍微概念化一下,如果我们将版本 id 存储在 user_items 中,我们就不需要子查询了。

由于版本之间的数据不一定一致,我们将废弃他的建议项目表(对于这种情况)。 然后我们可以从 item_data 表中获取正确的版本 我们还可以根据版本 id 获取 items_version_emails 行,并从我们的“电子邮件”表中获取相应的电子邮件。

I.E 完美运行。

这样做的缺点是,当我在 item_data 中添加新版本数据时,我必须使用已插入的新版本更新 user_items 表。

这很好,但概括地说,什么更快? 我认为建议这种设置的原因是它更快 - 每次添加新数据时进行额外的更新是值得的,以在显示大量行时保存潜在的数百个子查询。尤其是考虑到我们显示的数据多于更新数据。

只是为了了解在未来设计数据库架构时是否有人提供任何链接/一般指导,说明什么更快以及为什么这样我们都可以制作更好的优化数据库。

再次感谢 Cryode !!

【问题讨论】:

  • 能否请您根据您的示例数据以表格形式发布此类查询的预期结果
  • 您应该尽可能在表之间使用整数键。它们比字符串更有效。表之间引用的键需要被索引。有了它,你应该能够成功地使用连接来获得你想要的。
  • It is however very much 'im not too sure what im doing'. 定义它 上面的代码有什么问题? 一个糟糕的设计仍然是
  • 预期的输出已经在上面发布了。我很欣赏整数键的效率——我将来会改变它。我知道它可以通过连接来完成,这很简单。我对上述代码的问题是,当它与 item_data 连接时,它会同时获取两行,从而连接旧 item_data 条目和新 item_data 条目的电子邮件。我只想要最新条目的数据。

标签: php mysql sql codeigniter join


【解决方案1】:

使用您的数据库结构,这是我想出的:

   SELECT ui.name, id.added, id.info, emails.email,
          CASE WHEN ev.id IS NULL THEN 'NO' ELSE 'YES' END AS email_verified
     FROM user_items AS ui
     JOIN item_data AS id ON id.item = ui.name
     JOIN (
            SELECT item, MAX(added) AS added
            FROM item_data
            GROUP BY item
        ) AS id_b ON id_b.item = id.item AND id_b.added = id.added
     JOIN emails ON emails.added = id.added
LEFT JOIN emails_verified AS ev ON ev.email = emails.email

但正如其他人指出的那样,数据库设计不佳。此查询在具有大量数据的表上表现不佳,因为没有用于此目的的聚合函数。我知道在某些情况下您几乎无法控制数据库设计,但如果您想真正创造最佳情况,您应该向任何可以控制它的人强调它可以改进。

可以进行的最大优化之一是将当前的item_data ID 添加到user_items 表中。这样子查询就不需要拉取了(因为现在我们基本上加入了item_data 两次)。

由于子查询,将其转换为 CI 的查询构建器有点让人头疼。假设您只使用 MySQL 数据库,请坚持使用 $this->db->query()

从您的编辑中添加:

此查询每行返回一封电子邮件,它不会将它们组合在一起。我把CONCAT 的东西留了出来,因为它还有一件事会减慢你的查询速度——你的PHP 可以更快地将电子邮件放在一起。

是的,子查询就是这一部分——查询中的查询(非常不言自明的名称:wink:)。我不会称它为创建临时表,因为这是您实际上可以做的事情。更像是检索表中的信息子集,并使用它有点像WHERE 子句。子查询是在 item_data 表中找到最新行的内容,因为我们必须自己弄清楚(同样,适当的数据库设计会消除这种情况)。

当我们说您可以优化您的数据库设计时,这并不意味着您不能以类似的方式对其进行设置。您说的好像数据库根本无法更改。就整体方案而言,您的想法是正确的,只是执​​行得很差。

数据库设计

这就是我的布局。请注意,在不了解项目的全部范围的情况下,这可能需要修改。也可能不是 100% 世界上最好的优化——我愿意接受改进建议。您的里程可能会有所不同。

用户项目

CREATE TABLE `users_items` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `item_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

定义基础项目和用户之间的关系。

项目

CREATE TABLE `items` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `item_name` varchar(50) NOT NULL DEFAULT '',
  `created_on` datetime NOT NULL,
  `current_version` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

您的项目表应该包含所有项目的基本信息——这些信息不会因每次修订而改变。请注意current_version 列 - 您将在此存储版本表中的 ID,指示哪个是最新的(因此我们不必自己弄清楚)。

项目版本(历史)

CREATE TABLE `items_versions` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `item_id` int(10) unsigned NOT NULL,
  `added` datetime NOT NULL,
  `info` text,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

您可以在此处存储项目的历史记录——每次更新都会在此处创建一个新行。请注意,item_id 列将这一行与特定的基础项目联系起来。

电子邮件

CREATE TABLE `emails` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(100) NOT NULL DEFAULT '',
  `verified` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

由于电子邮件可以在多个产品之间共享,我们最终将使用所谓的多对多关系。电子邮件可以绑定到多个产品,一个产品可以绑定到多个电子邮件。在这里,我们定义了我们的电子邮件,并包含一个 verified 列,以表明它是否已经过验证。

项目电子邮件

CREATE TABLE `items_versions_emails` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `version_id` int(11) NOT NULL,
  `email_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

假设电子邮件与项目版本而非基础项目相关联,这就是您想要的结构。不幸的是,如果您有大量版本并且从不更改电子邮件,这将导致大量重复数据。所以这里有优化的空间。如果您将电子邮件绑定到基本项目,您将获得更少的重复数据,但您会丢失历史记录。所以有这个选项。但目标是展示如何建立数据库关系,而不是 100% 完美。

这应该为您提供了一个如何更好地布置数据库结构的良好开端。

另一个更新

关于速度,插入一个新的项目版本,然后用新的版本 ID 更新相关的项目行将比要求子查询拉取最新更新提供更好的性能。您会在原始结构的解决方案中注意到,item_info 表被连接了两次——一次连接最近的行,再次从最近的行中获取其余数据(因为@ 987654338@ 有效,我们无法在单个连接中获得它)。如果我们已经存储了最新版本 ID,则根本不需要第一次加入,这将显着提高您的速度(以及正确的索引,但这是另一课)。

我不建议放弃基本的items 表,但这完全取决于您和您的应用程序的需求。如果没有基础项目,就没有真正的方法来跟踪该特定项目的历史。假设您要删除 item_id 列,则版本中没有任何内容显示共同的祖先/历史。

【讨论】:

  • 优秀的答案。非常感谢。我已经对最初的问题进行了修改,这可能会帮助其他有类似问题的人。如果您可以跟进/澄清这些要点,那将是很好的,但您的帮助/知识将不胜感激。
  • 已更新以回答您的问题并帮助您了解数据库设计。 :-p
  • 这太棒了。感谢您花时间让我更清楚!
  • 添加了更多关于通用数据库架构的信息。对不起。我保证我现在会让你一个人呆着。再次感谢。
  • 我不会删除基础项目,因为您会丢失该项目历史的参考。
猜你喜欢
  • 1970-01-01
  • 2015-07-20
  • 1970-01-01
  • 1970-01-01
  • 2012-11-29
  • 2018-10-14
  • 2012-06-30
  • 2022-01-11
  • 2013-06-21
相关资源
最近更新 更多