缓慢的 MySQL“内部联接”

Posted

技术标签:

【中文标题】缓慢的 MySQL“内部联接”【英文标题】:slow MySQL "INNER JOIN" 【发布时间】:2012-02-22 04:40:42 【问题描述】:

在一个网站上,我正在使用 django 发出一些请求:

django 行:

CINodeInventory.objects.select_related().filter(ci_class__type='equipment',company__slug=self.kwargs['company'])

生成这样的 mysql 查询:

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `ci_cinodeclass` ON ( `inventory_cinodeinventory`.`ci_class_id` = `ci_cinodeclass`.`class_name` )
INNER JOIN `accounts_companyprofile` ON ( `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug` )
INNER JOIN `accounts_companysite` ON ( `inventory_cinodeinventory`.`company_site_id` = `accounts_companysite`.`slug` )
INNER JOIN `accounts_companyprofile` T5 ON ( `accounts_companysite`.`company_id` = T5.`slug` )
WHERE (
`ci_cinodeclass`.`type` = 'equipment'
AND `inventory_cinodeinventory`.`company_id` = 'thecompany'
)
ORDER BY `inventory_cinodeinventory`.`name` ASC

问题是主表中只有 40 000 个条目,处理时间为 0.5 秒。

我检查了所有索引,创建了排序或连接所需的索引:我仍然有问题。

有趣的是,如果我将最后一个 INNER JOIN 替换为 LEFT JOIN,请求速度会快 10 倍!不幸的是,由于我使用 django 进行请求,我无法访问它生成的 SQL 请求(我不想自己执行原始 SQL)。

对于最后一个作为“INNER JOIN”的连接,EXPLAIN 给出:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key                                | key_len | ref                                            | rows  | Extra                           |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
|  1 | SIMPLE      | accounts_companyprofile   | const  | PRIMARY                                                                                                  | PRIMARY                            | 152     | const                                          |     1 | Using temporary; Using filesort |
|  1 | SIMPLE      | inventory_cinodeinventory | range  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | inventory_cinodeinventory_543518c6 | 152     | NULL                                           | 42129 | Using where                     |
|  1 | SIMPLE      | T5                        | ALL    | PRIMARY                                                                                                  | NULL                               | NULL    | NULL                                           |     3 | Using join buffer               |
|  1 | SIMPLE      | accounts_companysite      | eq_ref | PRIMARY,accounts_companysite_543518c6                                                                    | PRIMARY                            | 152     | cidb.inventory_cinodeinventory.company_site_id |     1 | Using where                     |
|  1 | SIMPLE      | ci_cinodeclass            | eq_ref | PRIMARY                                                                                                  | PRIMARY                            | 92      | cidb.inventory_cinodeinventory.ci_class_id     |     1 | Using where                     |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+

对于最后一次加入“LEFT JOIN”,我得到了:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key     | key_len | ref                                            | rows | Extra       |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+
|  1 | SIMPLE      | accounts_companyprofile   | const  | PRIMARY                                                                                                  | PRIMARY | 152     | const                                          |    1 |             |
|  1 | SIMPLE      | inventory_cinodeinventory | index  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | name    | 194     | NULL                                           |  173 | Using where |
|  1 | SIMPLE      | accounts_companysite      | eq_ref | PRIMARY                                                                                                  | PRIMARY | 152     | cidb.inventory_cinodeinventory.company_site_id |    1 |             |
|  1 | SIMPLE      | T5                        | eq_ref | PRIMARY                                                                                                  | PRIMARY | 152     | cidb.accounts_companysite.company_id           |    1 |             |
|  1 | SIMPLE      | ci_cinodeclass            | eq_ref | PRIMARY                                                                                                  | PRIMARY | 92      | cidb.inventory_cinodeinventory.ci_class_id     |    1 | Using where |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+

似乎对于“INNER JOIN”的情况,MySQL 没有找到 T5 连接的任何索引:为什么?

分析给出:

starting                            0.000011
checking query cache for query  0.000086
Opening tables                  0.000014
System lock                     0.000005
Table lock                          0.000052
init                            0.000064
optimizing                          0.000021
statistics                          0.000180
preparing                           0.000024
Creating tmp table                  0.000308
executing                           0.000003
Copying to tmp table            0.353414   !!!
Sorting result                  0.037244
Sending data                    0.035168
end                             0.000005
removing tmp table                  0.550974   !!!
end                             0.000009
query end                           0.000003
freeing items                   0.000113
storing result in query cache   0.000009
logging slow query                  0.000002
cleaning up                     0.000004

看来,mysql有一个使用临时表的步骤。 此步骤不会发生在 LEFT JOIN 中,只会发生在 INNER JOIN 中。我试图通过在查询中包含“强制加入索引”来避免这种情况,但它没有帮助......

连接表是:

CREATE TABLE IF NOT EXISTS `accounts_companysite` (
  `slug` varchar(50) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `name` varchar(128) NOT NULL,
  `address` longtext NOT NULL,
  `city` varchar(64) NOT NULL,
  `zip_code` varchar(6) NOT NULL,
  `state` varchar(32) NOT NULL,
  `country` varchar(2) DEFAULT NULL,
  `phone` varchar(20) NOT NULL,
  `fax` varchar(20) NOT NULL,
  `more` longtext NOT NULL,
  PRIMARY KEY (`slug`),
  KEY `accounts_companysite_543518c6` (`company_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `accounts_companyprofile` (
  `slug` varchar(50) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `name` varchar(128) NOT NULL,
  `address` longtext NOT NULL,
  `city` varchar(64) NOT NULL,
  `zip_code` varchar(6) NOT NULL,
  `state` varchar(32) NOT NULL,
  `country` varchar(2) DEFAULT NULL,
  `phone` varchar(20) NOT NULL,
  `fax` varchar(20) NOT NULL,
  `contract_id` varchar(32) NOT NULL,
  `more` longtext NOT NULL,
  PRIMARY KEY (`slug`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `inventory_cinodeinventory` (
  `uuid` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `synopsis` varchar(64) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `root_id` varchar(36) DEFAULT NULL,
  `parent_id` varchar(36) DEFAULT NULL,
  `order` int(11) NOT NULL,
  `ci_class_id` varchar(30) NOT NULL,
  `data` longtext NOT NULL,
  `serial` varchar(64) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `company_site_id` varchar(50) NOT NULL,
  `vendor` varchar(48) NOT NULL,
  `type` varchar(64) NOT NULL,
  `model` varchar(64) NOT NULL,
  `room` varchar(30) NOT NULL,
  `rack` varchar(30) NOT NULL,
  `rack_slot` varchar(30) NOT NULL,
  PRIMARY KEY (`uuid`),
  KEY `inventory_cinodeinventory_1fb5ff88` (`root_id`),
  KEY `inventory_cinodeinventory_63f17a16` (`parent_id`),
  KEY `inventory_cinodeinventory_41ddcf59` (`ci_class_id`),
  KEY `inventory_cinodeinventory_543518c6` (`company_id`),
  KEY `inventory_cinodeinventory_14fe63e9` (`company_site_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

我还尝试通过添加 my.cnf 来调整 MySQL:

join_buffer_size        = 16M
tmp_table_size          = 160M
max_seeks_for_key       = 100

...但它没有帮助。

使用 django,使用 Postgresql 代替 Mysql 很容易,所以我试了一下:在 db 中使用相同的查询和相同的数据,postgres 比 Mysql 快得多:使用 INNER JOIN 时要快 10 倍(分析表明它使用与Mysql不同的索引)

你知道为什么我的 MySQL INNER JOIN 这么慢吗?

编辑 1:

经过一些测试,我将问题归结为这个请求:

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `accounts_companyprofile` ON `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug`
ORDER BY `inventory_cinodeinventory`.`name` ASC

这个请求很慢,我不明白为什么。 没有 'ORDER BY' 子句,它很快,但没有它, 虽然,名称索引已设置:

CREATE TABLE IF NOT EXISTS `inventory_cinodeinventory` (
  `uuid` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `synopsis` varchar(64) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `root_id` varchar(36) DEFAULT NULL,
  `parent_id` varchar(36) DEFAULT NULL,
  `order` int(11) NOT NULL,
  `ci_class_id` varchar(30) NOT NULL,
  `data` longtext NOT NULL,
  `serial` varchar(64) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `company_site_id` varchar(50) NOT NULL,
  `vendor` varchar(48) NOT NULL,
  `type` varchar(64) NOT NULL,
  `model` varchar(64) NOT NULL,
  `room` varchar(30) NOT NULL,
  `rack` varchar(30) NOT NULL,
  `rack_slot` varchar(30) NOT NULL,
  PRIMARY KEY (`uuid`),
  KEY `inventory_cinodeinventory_1fb5ff88` (`root_id`),
  KEY `inventory_cinodeinventory_63f17a16` (`parent_id`),
  KEY `inventory_cinodeinventory_41ddcf59` (`ci_class_id`),
  KEY `inventory_cinodeinventory_14fe63e9` (`company_site_id`),
  KEY `inventory_cinodeinventory_543518c6` (`company_id`,`name`),
  KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

编辑 2:

可以使用“FORCE INDEX FOR ORDER BY (name)”解决先前的请求。 不幸的是,这个提示不适用于我主题中的初始请求...

编辑 3:

我通过将 'uuid' 主键从 varchar 替换为整数来重建数据库:它根本没有帮助......坏消息。

编辑 4:

我试过 Mysql 5.5.20 :不是更好。对于这个特定请求,Postgresql 8.4 的速度提高了 10 倍。

我修改了一点resquest(删除了T5加入):

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `ci_cinodeclass` ON ( `inventory_cinodeinventory`.`ci_class_id` = `ci_cinodeclass`.`class_name` )
INNER JOIN `accounts_companyprofile` ON ( `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug` )
INNER JOIN `accounts_companysite` ON ( `inventory_cinodeinventory`.`company_site_id` = `accounts_companysite`.`slug` )
WHERE (
`ci_cinodeclass`.`type` = 'equipment'
AND `inventory_cinodeinventory`.`company_id` = 'thecompany'
)
ORDER BY `inventory_cinodeinventory`.`name` ASC

这工作正常,但我还有一些其他要求,只是在这个技巧不起作用的地方有点不同。

实际上,经过搜索,似乎只要您将两个具有“很多共同点”的表连接起来,也就是说,右表的一半行可以连接到左表的行(它是我的情况):Mysql 更喜欢使用表扫描而不是索引:我发现某处更快(!!)

【问题讨论】:

安装和配置django-debug-toolbar,你将很容易看到sql django正在生成pypi.python.org/pypi/django-debug-toolbar。您肯定需要 select_related 用于预期用途吗? 是的,我绝对需要 select_related,是的,我已经使用了 django-debug-toolbar :我给出的 SQL 请求来自这个工具。 (为了便于阅读,在请求中,我只是放了一个“*”而不是一长串请求的列) 您的解释说inventory_cinodeinventory.name 上有一个索引,但它不存在于您的架构中。有点不对劲。 @Marcus :即使没有写,我在写解释时也做了很多尝试。我确实尝试了 name 索引以及 (company_id, name) 索引。 顺便说一句,“复制到 tmp 表”并不是一件坏事。它通常可以参与快速查询,尤其是在涉及 UNION 的情况下。如果它复制大量行以执行手动 ORDER 或解析不使用索引的复杂 WHERE 子句,这只是一件坏事,这就是上面发生的情况。 【参考方案1】:

我已经为 Django ORM 实现了 INNER JOIN 的修复,如果使用 INNER JOIN 进行排序,它将使用 STRAIGHT_JOIN。我与 Django core-devs 进行了交谈,我们决定暂时将其作为单独的后端进行。所以你可以在这里查看:https://pypi.python.org/pypi/django-mysql-fix

【讨论】:

【参考方案2】:

将您的加入密钥设为 int 无符号

并将inventory_cinodeinventory.ci_class_id > 0 (ci_class_id__gt = 0)(与连接中的其余键相同)添加到 where

它会将 MySQL 指向您的密钥,使其保持 django 的 ORM 样式

【讨论】:

【参考方案3】:

您可以在访问数据时尝试使用视图:

CREATE VIEW v AS SELECT *
FROM inventory_cinodeinventory
LEFT JOIN ci_cinodeclass ON ( inventory_cinodeinventory.ci_class_id = ci_cinodeclass.class_name )
LEFT JOIN accounts_companyprofile ON ( inventory_cinodeinventory.company_id = accounts_companyprofile.slug )
LEFT JOIN accounts_companysite ON ( inventory_cinodeinventory.company_site_id = accounts_companysite.slug )
LEFT JOIN accounts_companyprofile T5 ON ( accounts_companysite.company_id = T5.slug )
ORDER BY inventory_cinodeinventory.name ASC

这里的缺点是你必须在服务器上写“纯sql”。 你必须为这个新视图创建一个模型。

编辑: 您还可以使用内部连接创建视图。这也可能比直接查询表更快。

CREATE VIEW v AS SELECT *
FROM inventory_cinodeinventory
INNER JOIN ci_cinodeclass ON ( inventory_cinodeinventory.ci_class_id = ci_cinodeclass.class_name )
INNER JOIN accounts_companyprofile ON ( inventory_cinodeinventory.company_id = accounts_companyprofile.slug )
INNER JOIN accounts_companysite ON ( inventory_cinodeinventory.company_site_id = accounts_companysite.slug )
INNER JOIN accounts_companyprofile T5 ON ( accounts_companysite.company_id = T5.slug )
ORDER BY inventory_cinodeinventory.name ASC

【讨论】:

是的,左连接工作得更好,就像我在问题中所说的那样。我只是期待使用“内连接”的工作速度与左连接一样快:但是如何......这是个问题...... 我的例子不是关于“左连接”。这是关于使用视图而不是直接查询视图。【参考方案4】:

我注意到您正在使用:

ENGINE=MyISAM

只是猜测,但您可以尝试将表引擎切换到 InnoDB。 如果与多个连接查询一起使用,它会更快。

ENGINE=InnoDB

InnoDB 引擎不能用于全文搜索,但整体性能有的差异。

【讨论】:

您是否有证据表明 InnoDB 通过多个 JOIN 查询更快?我知道对于写入量相当大的数据库来说它更快,但一般来说,使用 InnoDB 来自卸载数据库的单个查询会更慢,因为它会逐行锁定与 MyISAM 相比,对于可比较的查询,要么没有t 完全锁定或锁定整个表。 嗯,我不应该一概而论。在某些情况下它更快:link。在大多数情况下,整体索引搜索/比较更快:article 和 data。 不幸的是,我已经尝试过 InnoDB:仍在使用 tmp 表:慢【参考方案5】:

正如 Conspicuous Compiler 所指出的,我肯定会根据公司 id 和名称在您的第一个表上建立一个索引(因此名称部分针对 order by 子句进行了优化)。

虽然我也没有对 django 做过任何事情,但另一个优化 MySQL 关键字是“STRAIGHT_JOIN”,它告诉优化器按照您告诉它的顺序执行查询。例如:

SELECT STRAIGHT_JOIN * FROM ...

在您的“解释”查询的两个实例中,由于某种原因,它被困在 companyprofile 是一条记录这一事实上,并且可能会尝试将其用作连接的基础,否则会处理堆栈。通过做 straight_join,您告诉 MySQL 您知道主表是“Inventory_CINodeInventory”并首先使用它......其他表更像是您想要的其他简单元素的“查找”或“参考”表。我已经看到只有这个关键字使用了一个无法完全运行的查询(30 小时后终止任务)与 gov't 将超过 1400 万条记录的数据的合同数据缩短到不到 2 小时......查询中没有其他内容变了,就这一个KEYWORD。 (但如果还没有这样做,一定要包括其他索引)。

对问题的最新编辑评论...

您提到查询在 order by 下是 SLOW,但在没有它的情况下是 FAST。从结果集中实际返回了多少条目。我之前使用的另一种策略是将查询包装为选择以获取答案,然后将顺序应用于 OUTER 结果...类似

select *
   from 
      ( select your Entire Query
           from ...
           Without The Order by clause 
      ) as FastResults
   order by
      FastResults.Name

这可能超出了 django 自动构建 SQL 语句的过程,但值得一试以进行概念验证。你已经有了一个可以运行的有效语法,我会试一试。

【讨论】:

好主意,但请求仍然很慢。【参考方案6】:

您真正的问题在于您的第一个解释中的第二行:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key                                | key_len | ref                                            | rows  | Extra                           |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+   
|  1 | SIMPLE      | inventory_cinodeinventory | range  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | inventory_cinodeinventory_543518c6 | 152     | NULL                                           | 42129 | Using where                     |

您正在使用此 WHERE 子句分析 42129 行:

AND `inventory_cinodeinventory`.`company_id` = 'thecompany'

如果您还没有,您应该在inventory_cinodeinventory 上为(company_id, name) 建立一个索引

ALTER TABLE `inventory_cinodeinventory`
  ADD INDEX `inventory_cinodeinventory__company_id__name` (`company_id`, `name`);

这样您的 WHERE 和 ORDER BY 子句就不会发生冲突,从而导致错误的索引选择,这似乎正在发生。

如果您确实已经有这些列的索引,那么我建议您运行OPTIMIZE TABLE inventory_cinodeinventory; 以查看它是否让 MySQL 使用正确的索引。

一般来说,你有一个更大的问题(我认为这是由于 Django 的设计,但我缺乏使用该框架的经验),因为你有这些巨大的键。 EXPLAIN 中的所有键的长度均为 152 和 92 字节。这使得索引更大,这意味着更多的磁盘访问,这意味着更慢的查询。理想情况下,主键和外键是 ints 或非常短的 varchar 列(例如 varchar(10))。这些键的varchar(50) 将使您的数据库响应时间显着增加。

【讨论】:

Re: Django - 看起来 OP 专门覆盖了自动生成的 ID 字段,并使用 varchar(50) 'slug' 字段作为 PK。这在他的模型定义中绝对可以解决。 我需要这个“slug”作为 PK,由于某些原因,所有 PK 都必须是“自然的”,也就是 varchar... @Conspicuous Compiler :我尝试了您的索引并缩短了 slug,但 MySQL 继续在分析中“复制到 tmp 表”。 @Eric:等等,“索引”?我只列出了一个要添加的索引。如果您在每列上添加一个,那对您没有任何好处。您可以发布SHOW CREATE TABLE inventory_cinodeinventory 的结果吗? @Eric:就自然 ID 而言,我无法满足您的业务需求,但您可以为您的行和唯一的 slug 设置唯一的 int ID,但基于 int 进行连接。我强烈建议不要缩短现有列的长度,因为您很容易截断现有数据,从而失去参照完整性。如果您已经这样做了,我希望您事先备份数据,以便您可以撤消更改并恢复它。

以上是关于缓慢的 MySQL“内部联接”的主要内容,如果未能解决你的问题,请参考以下文章

mysql查询运行缓慢

mysql内部连接查询运行缓慢

mysql查询缓慢问题总结

Mysql“不是NULL”工作非常缓慢

缓慢的 MySQL UPDATE 查询

非常简单的 MySQL 索引查询运行非常缓慢