SQL查询变得指数级变慢

10

我有一个关于联系人消息系统的查询,随着我进行更多的连接,它变得指数级缓慢。

表结构基本上是一个联系人表和一个联系人字段表。

查询多次连接联系人字段表,每次连接所需时间都是前一次的两倍。

这是查询代码。

SELECT  SQL_CALC_FOUND_ROWS
    `contact_data`.`id`,
    `contact_data`.`name`,
    `fields0`.`value` AS `fields0`,
    `fields1`.`value` AS `fields1`,
    `fields2`.`value` AS `fields2`,
    ...etc...
    CONTACT_DATA_TAGS(
        GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`),
        GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`),
        GROUP_CONCAT(DISTINCT `contact_data_read`.`user`)
    ) AS `tags`,
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`,
    `contact_data`.`updated`,
    `contact_data`.`created`
FROM
    `contact_data`
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data`
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data`
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data`
LEFT JOIN contact_data_fields AS fields0 ON contact_data.`id` = fields0.`contact_data_id` AND fields0.`key` = :field1
LEFT JOIN contact_data_fields AS fields1 ON contact_data.`id` = fields1.`contact_data_id` AND fields1.`key` = :field2
LEFT JOIN contact_data_fields AS fields2 ON contact_data.`id` = fields2.`contact_data_id` AND fields2.`key` = :field3
...etc...
GROUP BY contact_data.`id`
ORDER BY `id` DESC

这是表格的结构:

CREATE TABLE IF NOT EXISTS `contact_data` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) NOT NULL,
  `format` varchar(50) NOT NULL,
  `fields` longtext NOT NULL,
  `url` varchar(2000) NOT NULL,
  `referer` varchar(2000) DEFAULT NULL,
  `ip` varchar(40) NOT NULL,
  `agent` varchar(1000) DEFAULT NULL,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  `updater` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`),
  KEY `url` (`url`(333)),
  KEY `ip` (`ip`),
  KEY `created` (`created`),
  KEY `updated` (`updated`),
  KEY `updater` (`updater`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `contact_data_assignment` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `user` int(10) unsigned NOT NULL,
  `data` int(10) unsigned NOT NULL,
  `created` datetime NOT NULL,
  `updater` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_assignment` (`user`,`data`),
  KEY `user` (`user`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `contact_data_fields` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `contact_data_id` int(10) unsigned NOT NULL,
  `key` varchar(200) NOT NULL,
  `value` text NOT NULL,
  `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `contact_data_id` (`contact_data_id`),
  KEY `key` (`key`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `contact_data_read` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `user` int(10) unsigned NOT NULL,
  `data` int(10) unsigned NOT NULL,
  `type` enum('admin','email') NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user` (`user`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `contact_data_tags` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) NOT NULL,
  `data` int(10) unsigned NOT NULL,
  `created` datetime NOT NULL,
  `updater` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_tag` (`name`,`data`),
  KEY `name` (`name`),
  KEY `data` (`data`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

DELIMITER $$
CREATE FUNCTION `contact_data_tags`(`tags` TEXT, `assigned` BOOL, `read` BOOL) RETURNS text CHARSET latin1
BEGIN
    RETURN CONCAT(
        ',',
        IFNULL(`tags`, ''),
        ',',
        IF(`tags` IS NULL OR FIND_IN_SET('Closed', `tags`) = 0, 'Open', ''),
        ',',
        IF(`assigned` IS NULL, 'Unassigned', ''),
        ',',
        IF(`read` IS NULL, 'New', ''),
        ','
    );
END$$

DELIMITER ;

有人知道它为什么运行速度很慢吗?我该怎么做才能让它更快?我需要调整查询吗(我宁愿不改变结构)?是否有任何配置选项可以加快速度?

另外奇怪的是,它似乎在我的Windows开发机上比我的Debain生产服务器上运行得更快(几乎瞬间完成,而不是30多秒钟)。

但Windows机器比Debain服务器弱得多(8核Xeon,32GB RAM)。

在Debian上运行MySQL 5.1.49(我无法更新),在Windows上运行5.5.28。

因此,阅读EAV在RDBMS中表现不佳(或者至少在我的情况下如此),是否有一种配置选项可以增加以使其运行更快(例如,我可以随意加更多RAM)?


4
啊,实体属性值模型真是太棒了。但在关系数据库中却不太可行。关系型数据库需要提前知道字段。你是否需要将所有这些可配置的联系人数据字段暴露给数据库(以进行查询)?如果不需要,可以将其存储为CLOB中的JSON格式。或者使用NoSQL文档数据库。如果它们不可配置,只需使用“常规”列即可。 - Thilo
1
更正:在某些数据库中,使用正确的键和/或聚集表结构,EAV 运作得非常好。 - wildplasser
2
EAV在正确索引的情况下效率很高。请参见DBA.SE:子查询单独运行非常快,但连接后非常慢中类似的问题。没有索引-> 300秒。添加索引-> 30毫秒。 - ypercubeᵀᴹ
我看不到(key,contact_data_id,value)上的索引。 - ypercubeᵀᴹ
1
你很可能还需要在其他表中添加更多的索引,比如contact_data_read表。这取决于你如何连接这些表以及在SELECT列表中使用哪些列。 - ypercubeᵀᴹ
显示剩余8条评论
4个回答

6

加快查询的一种方法是仅在contact_data.id = contact_data_fields.contact_data_id时链接到contact_data_fields,并将fields列更改为max表达式 - 如下所示:

SELECT  SQL_CALC_FOUND_ROWS
    `contact_data`.`id`,
    `contact_data`.`name`,
    MAX(CASE WHEN fields.`key` = :field1 THEN fields.`value` END) AS `fields0`,
    MAX(CASE WHEN fields.`key` = :field2 THEN fields.`value` END) AS `fields1`,
    MAX(CASE WHEN fields.`key` = :field3 THEN fields.`value` END) AS `fields2`,
    ...etc...
    CONTACT_DATA_TAGS(
        GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`),
        GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`),
        GROUP_CONCAT(DISTINCT `contact_data_read`.`user`)
    ) AS `tags`,
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`,
    `contact_data`.`updated`,
    `contact_data`.`created`
FROM
    `contact_data`
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data`
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data`
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data`
LEFT JOIN contact_data_fields AS fields
       ON contact_data.`id` = fields.`contact_data_id` 
...etc...
GROUP BY contact_data.`id`
ORDER BY `id` DESC

+1,我想建议这作为额外的奖励,以防他的“contact_data_fields”表被完全转储,但我怀疑这种情况会发生 =) - newtover
这个很好用,谢谢。我会添加另一个赏金来奖励你。 - Petah

3

很遗憾,您的查询存在许多低效率。我不认为您只通过调整一些参数和增加更多的RAM就能解决问题:

  • 首先,我们不知道您的表的大小,为什么需要转储整个表contact_data。没有额外的条件和限制(这通常很重要)。
  • 同样,我们也不知道在给定的contact_data.id下是否可以有几个具有相同(contact_data_id、key)的记录。我认为可能会有{0, 1}个记录,并且如果您有相应的唯一索引(最终需要作为查询的索引以提高效率),这可以明确地表示出来。
  • 如果您使用LIMIT,SQL_CALC_FOUND_ROWS是一个附加的杀手,因为它使MySQL计算并扫描整个结果以计算行数(我将仅使用单独的查询计算行数,获取裸ID并缓存其结果。如果表不经常更改,则MySQL的自己的查询缓存可能足够)。

一旦您在(contact_data_id、key)上添加索引,我会将分组和排序隔离到子查询中,然后LEFT JOIN contact_data_fields (没有任何排序)。您当前的查询在将所有结果与contact_datacontact_data_tagscontact_data_assignmentcontact_data_read的乘积中分组之前进行相同的LEFT JOIN比较(更不用说您的服务器在将所有数据分组和去重之前存储了整个中间结果)。


数据大小约为contact_data中的20,000行和contact_data_fields中的200,000行。其他连接关系几乎没有意义,如果需要,可能可以删除。没有额外的where语句,有时不会有限制(导出到xls)。是的,一个contact_data_id可以有多个相同的键。需要SQL_CALC_FOUND_ROWS,因为我需要"从100,000行中筛选出100行显示10,000行"的统计信息。 - Petah

2
我会把这些有趣的评论和我的实体-属性-值模型查询与MySQL的经验整合在一起。
首先,不要忘记MySQL对连接数量有低限制61 joins. 最初看起来是一个很大的数字。但是,使用这个模型时,它可能轻松地使您的查询崩溃,并出现一个漂亮的SQLSTATE[HY000]: General error: 1116错误。
我也经历了这些指数级的减速。当我们第一次用50个连接的查询查询50,000行表格时,我们发现查询需要超过20秒,其中14.5秒花费在查询优化器上——似乎是在尝试猜测这50个连接的最佳连接顺序——。因此,我们简单地在SELECT关键字后面添加了STRAIGHT_JOIN关键字,然后回到了正常的时间。当然,这样做意味着您必须得到一个良好的索引方案,并且必须以聪明的连接顺序编写查询(具有最佳索引和最佳人口减少的表格应该首先出现)。
SELECT STRAIGHT_JOIN (...)

请注意,此关键字也可用于JOIN语法。
STRAIGHT_JOIN强制优化器按照FROM子句中列出的顺序连接表。如果优化器以非最佳顺序连接表,则可以使用此选项来加快查询速度。
我会补充说“或者如果猜测连接顺序占用了95%的查询时间”:-)
还可以在查询中直接检查其他查询优化器设置,请参阅this page
然后,您需要注意5.1和5.5之间的差异......这两个版本之间有太多的差异,就像使用两个不同的数据库服务器一样。您应该真正考虑在生产中使用5.5,以获得速度改进(还要检查Percona),但也为事务和锁定改进提供支持。如果您需要一个理由,那就是在生产环境中会出现您在开发中没有遇到的错误。
这些包含许多连接的查询将会对服务器造成压力。您需要通过my.cnf文件进行一些微调以控制服务器行为。例如,尽量避免创建临时表(检查查询的explain输出)。一个2秒的查询可能会变成120秒的查询,只是因为您达到了极限并且需要使用临时文件来管理20或30个连接、排序和分组操作。相比内存处理,将数据放在磁盘上非常慢。这两个设置特别重要:
tmp_table_size = 1024M
max_heap_table_size = 1024M

在这里,我们说“如果内存使用量不超过1Go,则将请求的工作保存在内存中”。当然,如果您这样做,可以避免同时运行500个并行脚本来处理这些请求——如果您需要对大量并行请求进行定期处理,请考虑避免使用此数据方案。
这也引出了一个重要的问题。您正在接近一个请求的复杂性边界。SQL服务器通常比您的应用程序更快地聚合数据以生成结果。但是,当数据量很大,并且您在查询中添加了许多索引(至少每个连接一个),以及排序、分组,甚至使用group_contact聚合结果时,MySQL几乎肯定会使用临时文件,速度会变慢。通过使用几个短查询(一个没有group by的主查询,然后再进行10或200个查询,以获取例如group_contact字段的内容),您可能会通过避免使用临时文件而更快。

感谢提供信息。关于升级生产环境,我注意到我的5.5安装比5.1更为严格,因此它破坏了我的一些代码。 - Petah
^Petah。另外我忘记补充一点,要检查只有数字用于索引,文本列索引的性能表现非常差,并且使用更多的空间。 - regilero

1
根据Mark Bannister的查询,可能需要使用类似以下方式返回字段/值细节作为分隔符列表:-
SELECT  SQL_CALC_FOUND_ROWS
    `contact_data`.`id`,
    `contact_data`.`name`,
    GROUP_CONCAT(CONCAT_WS(',', contact_data_fields.`key`, contact_data_fields.`value`)),
    CONTACT_DATA_TAGS(
        GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`),
        GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`),
        GROUP_CONCAT(DISTINCT `contact_data_read`.`user`)
    ) AS `tags`,
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`,
    `contact_data`.`updated`,
    `contact_data`.`created`
FROM
    `contact_data`
LEFT JOIN contact_data_tags ON contact_data.`id` = contact_data_tags.`data`
LEFT JOIN contact_data_assignment ON contact_data.`id` = contact_data_assignment.`data`
LEFT JOIN contact_data_read ON contact_data.`id` = contact_data_read.`data`
LEFT JOIN contact_data_fields ON contact_data.`id` = contact_data_fields.`contact_data_id` 
WHERE contact_data_fields.`key` IN (:field1, :field2, :field3, etc)
GROUP BY contact_data.`id`
ORDER BY `id` DESC

根据contact_data_tags、contact_data_assignment和contact_data_read表中匹配行的数量(以及每个contact_data.id可能的中间行数),从子查询中获取联系人键/值详细信息可能更快。

SELECT  SQL_CALC_FOUND_ROWS
    `contact_data`.`id`,
    `contact_data`.`name`,
    Sub1.ContactKeyValue,
    CONTACT_DATA_TAGS(
        GROUP_CONCAT(DISTINCT `contact_data_tags`.`name`),
        GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`),
        GROUP_CONCAT(DISTINCT `contact_data_read`.`user`)
    ) AS `tags`,
    GROUP_CONCAT(DISTINCT `contact_data_assignment`.`user`) AS `assignments`,
    `contact_data`.`updated`,
    `contact_data`.`created`
FROM
    `contact_data`
LEFT JOIN contact_data_tags ON contact_data.id = contact_data_tags.`data`
LEFT JOIN contact_data_assignment ON contact_data.id = contact_data_assignment.`data`
LEFT JOIN contact_data_read ON contact_data.id = contact_data_read.`data`
LEFT JOIN (SELECT contact_data_id, GROUP_CONCAT(CONCAT_WS(',', contact_data_fields.`key`, contact_data_fields.`value`)) AS ContactKeyValue FROM contact_data_fields 
WHERE fields.`key` IN (:field1, :field2, :field3, etc) GROUP BY contact_data_id) Sub1 ON contact_data.id = Sub1.contact_data_id
GROUP BY contact_data.id
ORDER BY `id` DESC

连接的值的问题在于它无法进行搜索和排序。或者是我漏掉了什么? - Petah
不理想的是搜索它们可能会很慢,可能会消耗任何性能节省,但这取决于您想如何搜索它们。在读取此SQL的代码中,这可能很容易,但无法根据此处发布的信息进行评论。在SQL中更难,尽管如果需要,您可以使用“find in set”。您还可以省略连接的值,并为field1、field2等的每个字段设置一行,在之后的代码中处理它。 - Kickstart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接