0

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

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

与此处概述的类似

当然,子查询是必要的,以确保您只从 item_data 获得一行 - 最近的一行?

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

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

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

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

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

谢谢

最后更新

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

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

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

IE 完美运行。

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

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

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

再次感谢 Cryode !!

4

1 回答 1

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

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

可以进行的最大优化之一是将当前item_dataID 添加到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表被连接了两次——一次连接最近的行,再次从最近的行中获取其余数据(由于工作方式GROUP BY,我们可以'不要在一个连接中得到它)。如果我们已经存储了最新版本的 ID,我们根本不需要第一次加入,这将显着提高您的速度(以及正确的索引,但这是另一课)。

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

于 2013-04-08T17:55:26.787 回答