mySQL复杂的条件连接查询提高了多个查询和PHP的效率

时间:2013-04-08 15:52:11

标签: php mysql sql codeigniter join

奇怪的是我用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 !!

1 个答案:

答案 0 :(得分:2)

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

   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

但正如其他人所指出的那样,数据库的设计很差。此查询在具有大量数据的表上执行效果不佳,因为没有用于此目的的聚合函数。我知道在某些情况下你几乎无法控制数据库设计,但如果你想真正创造最好的情况,那么你应该强调可以控制它可以改进它。< / p>

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

由于子查询,将其转换为CI的查询构建器是一种痛苦。假设您只使用MySQL DB,只需坚持使用$this->db->query()

从您的修改中添加

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

是的,子查询就是那个部分 - 查询中的查询(非常不言自明的名字:wink :)。我不会称之为创建临时表,因为这是你可以实际做的事情。更像是检索表中信息的一个子集,并使用它类似于WHERE子句。子查询是查找item_data表中最新行的内容,因为我们必须自己弄清楚(再次,正确的数据库设计会消除这种情况)。

当我们说您可以优化数据库设计时,并不意味着您不能以类似的方式设置它。你让它听起来像是无法改变数据库 at all 。对于整体方案,你有正确的想法,你只是执行得不好。

数据库设计

我将如何解决这个问题。请注意,如果不知道项目的整个范围,可能需要修改。也许不是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表连接两次 - 一次连接最近的行,再次从最近的行中获取其余数据(因为方式GROUP BY有效,我们无法在单个连接中获取它。如果我们已经存储了最近的版本ID,我们根本不需要第一次加入,这将显着提高您的速度(以及正确的索引,但这是另一个教训)。

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