【问题标题】:Optimising MySQL LEFT JOIN query for simple three table schema?为简单的三表模式优化 MySQL LEFT JOIN 查询?
【发布时间】:2019-06-11 22:42:01
【问题描述】:

我用 PHP 编写了一个带有 MySQL 数据库的工作请求系统,但我遇到了查询速度慢的问题。

我的架构(简化)如下:

tbl_job
工作编号
job_desc requester_user_id

tbl_user
用户ID
用户名

tbl_workermap
workermap_id
工作编号
worker_user_id

一个包含作业的表、一个用于可能工作人员的用户表和一个用于将工作人员映射到工作的表。一个工作可以有一个或多个工人,一个工人可以有一个或多个工作。

tbl_user 包含请求工作的用户和从事工作的用户,因此用户 ID 存储在 tbl_workermap 中的 worker_user_id 和 tbl_job 中的 requester_user_id 下

当一个作业被记录时,它会在 tbl_job 中创建一个条目,但在 tbl_workermap 中没有任何内容,直到有人专门分配了一个工人。这意味着当我查询作业时,我会使用左连接来执行此操作,因为 tbl_workermap 中没有每个作业的条目:

SELECT 
job.job_id,
job.job_desc,
workermap.worker_user_id,
worker.worker_name

FROM tbl_job AS job

LEFT JOIN tbl_workermap AS workermap
ON job.job_id = workermap.job_id

LEFT JOIN tbl_user AS worker
ON workermap.worker_user_id = worker.user_id

系统已经使用了一段时间,现在我在 tbl_job 中有大约 8000 个条目,在 tbl_workermap 中有 7000 个条目,检索所有结果需要 4 秒以上。 EXPLAIN 查询显示 tbl_workermap 连接返回大约 7000 行和“使用 where;使用连接缓冲区(块嵌套循环)”。

有什么办法可以加快速度吗?

编辑:添加表格信息
我简化了一些事情来解释,但这是实际的表结构。还有更多连接,但 tbl_workermap 是唯一有问题的连接:

CREATE TABLE `tbl_job` (
  `job_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `job_title` varchar(100) DEFAULT NULL,
  `job_description` text,
  `job_added_datetime` int(11) DEFAULT '0',
  `job_due_datetime` int(11) NOT NULL DEFAULT '0',
  `job_time_estimate` int(11) DEFAULT NULL,
  `job_additional_fields` text,
  `addedby_user_id` int(11) NOT NULL DEFAULT '0',
  `requester_user_id` int(11) NOT NULL DEFAULT '0',
  `worker_user_id` int(11) NOT NULL DEFAULT '0',
  `job_active` tinyint(4) NOT NULL DEFAULT '1',
  `site_id` tinyint(4) NOT NULL DEFAULT '1',
  `status_id` int(11) NOT NULL DEFAULT '1',
  `estimategroup_id` int(11) DEFAULT '1',
  `brand_id` int(11) DEFAULT '1',
  `job_isproject` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`job_id`),
  FULLTEXT KEY `job_title` (`job_title`,`job_description`,`job_additional_fields`)
) ENGINE=MyISAM AUTO_INCREMENT=8285 DEFAULT CHARSET=latin1



CREATE TABLE `tbl_user` (
  `user_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_shortname` varchar(30) DEFAULT NULL,
  `user_name` varchar(30) DEFAULT NULL,
  `user_password` varchar(50) DEFAULT NULL,
  `user_password_reset_uuid` varchar(50) DEFAULT NULL,
  `user_email` varchar(50) DEFAULT NULL,
  `user_description` text,
  `user_sortorder` int(11) NOT NULL DEFAULT '0',
  `user_isworker` tinyint(4) NOT NULL DEFAULT '0',
  `user_active` tinyint(4) NOT NULL DEFAULT '1',
  `site_id` tinyint(4) NOT NULL DEFAULT '0',
  `user_avatar_file_id` int(11) DEFAULT NULL,
  `user_avatar_hub_url` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=MyISAM AUTO_INCREMENT=917 DEFAULT CHARSET=latin1


CREATE TABLE `tbl_workermap` (
  `workermap_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `job_id` int(11) DEFAULT NULL,
  `workermap_datetime_added` int(11) DEFAULT NULL,
  `workermap_datetime_removed` int(11) DEFAULT NULL,
  `worker_user_id` int(11) DEFAULT NULL,
  `addedby_user_id` int(11) DEFAULT NULL,
  `removedby_user_id` int(11) DEFAULT NULL,
  `site_id` int(11) DEFAULT NULL,
  `workermap_isassigned` int(11) DEFAULT NULL,
  `workermap_active` int(11) NOT NULL DEFAULT '1',
  PRIMARY KEY (`workermap_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7145 DEFAULT CHARSET=latin1

显示索引

+---------+---+-----------+---+-----------------------+------+------+------+------+-----+----------+--+--+
| tbl_job | 0 |  PRIMARY  | 1 |        job_id         |  A   | 8283 | NULL | NULL |     |  BTREE   |  |  |
+---------+---+-----------+---+-----------------------+------+------+------+------+-----+----------+--+--+
| tbl_job | 1 | job_title | 1 | job_title             | NULL |    1 | NULL | NULL | YES | FULLTEXT |  |  |
| tbl_job | 1 | job_title | 2 | job_description       | NULL |    1 | NULL | NULL | YES | FULLTEXT |  |  |
| tbl_job | 1 | job_title | 3 | job_additional_fields | NULL |    1 | NULL | NULL | YES | FULLTEXT |  |  |
+---------+---+-----------+---+-----------------------+------+------+------+------+-----+----------+--+--+

+----------+---+---------+---+---------+---+-----+------+------+--+-------+--+--+
| tbl_user | 0 | PRIMARY | 1 | user_id | A | 910 | NULL | NULL |  | BTREE |  |  |
+----------+---+---------+---+---------+---+-----+------+------+--+-------+--+--+

+---------------+---+---------+---+--------------+---+------+------+------+--+-------+--+--+
| tbl_workermap | 0 | PRIMARY | 1 | workermap_id | A | 7184 | NULL | NULL |  | BTREE |  |  |
+---------------+---+---------+---+--------------+---+------+------+------+--+-------+--+--+

解释查询

+---+--------+----------------+--------+---------+---------+------+-------------------------------+------+----------------------------------------------------+
| 1 | SIMPLE |      job       |  ALL   |  NULL   |  NULL   | NULL |             NULL              | 8283 |    Using where; Using temporary; Using filesort    |
+---+--------+----------------+--------+---------+---------+------+-------------------------------+------+----------------------------------------------------+
| 1 | SIMPLE | estimategroup  | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.estimategroup_id     |    1 | Using where                                        |
| 1 | SIMPLE | brand          | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.brand_id             |    1 | Using index condition                              |
| 1 | SIMPLE | site           | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.site_id              |    1 | Using where                                        |
| 1 | SIMPLE | addedby        | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.addedby_user_id      |    1 | Using index condition                              |
| 1 | SIMPLE | requester      | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.requester_user_id    |    1 | Using index condition                              |
| 1 | SIMPLE | worker         | eq_ref | PRIMARY | PRIMARY | 4    | jobq.job.worker_user_id       |    1 | Using index condition                              |
| 1 | SIMPLE | status         | ALL    | PRIMARY | NULL    | NULL | NULL                          |    6 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | workermap      | ALL    | NULL    | NULL    | NULL | NULL                          | 7184 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | user_workermap | eq_ref | PRIMARY | PRIMARY | 4    | jobq.workermap.worker_user_id |    1 | Using where                                        |
| 1 | SIMPLE | categorymap    | ALL    | NULL    | NULL    | NULL | NULL                          |    1 | Using where; Using join buffer (Block Nested Loop) |
| 1 | SIMPLE | category       | eq_ref | PRIMARY | PRIMARY | 4    | jobq.categorymap.category_id  |    1 | Using where                                        |
+---+--------+----------------+--------+---------+---------+------+-------------------------------+------+----------------------------------------------------+

【问题讨论】:

  • 请发布 SHOW CREATE TABLE tbl_job 的 TEXT 结果;和 tbl_worker;和 tbl_workermap;以及 SHOW INDEX FROM tbl_job;和 tbl_worker;和 tbl_workermap;进行分析。另外,请发布 EXPLAIN .... 结果。
  • @WilsonHauck 我已经添加了该信息,谢谢。
  • 目前,您需要两个索引来涵盖 JOIN 的左右对象的基本规则 = 需要一个索引。 1. ALTER TABLE tbl_workermap ADD INDEX idx_t_w_map_job_id (job_id) 2. ALTER TABLE tbl_workermap ADD INDEX idx_t_w_map_wrk_user_id (worker_user_id) 创建后运行EXPLAIN .....查看新的执行计划
  • @WilsonHauck 谢谢,我已经添加了这两个索引,这就成功了,现在几乎是即时的。如果您可以将该信息添加为答案,我可以将其标记为已接受,以便您获得信用。
  • @WilsonHauck 我很乐意这样做,并且刚刚这样做了 :) 再次感谢。

标签: mysql join query-optimization


【解决方案1】:

2019 年 1 月 18 日 13:43 目前,您需要两个索引来涵盖 JOIN 的左右对象的基本规则 = 需要一个索引。 1. ALTER TABLE tbl_workermap ADD INDEX idx_t_w_map_job_id (job_id) 2. ALTER TABLE tbl_workermap ADD INDEX idx_t_w_map_wrk_user_id (worker_user_id) 创建后运行EXPLAIN .....查看新的执行计划。

【讨论】:

  • 这解决了这个问题。我在 tbl_workermap 上为 job_idworker_user_id 添加了索引,并重新运行了 EXPLAIN 查询。它不再从 tbl_workermap 检索 7000 多行,并且查询现在几乎是即时的。谢谢!
【解决方案2】:

如果您还没有,请通过setting a primary key 创建一个聚集索引(假设表已正确规范化)。 (如果你还没有,你可能也想setup foreign key constraints。)

如果这个问题涉及 Microsoft SQL Server,我建议创建一个存储过程,特别是如果这是作为某种常规过程频繁运行的查询。然而,正如 this answer 中所述,此类简单查询的主要性能优势将来自表设计和索引。

【讨论】:

  • 谢谢保罗。我正在使用主键,我已经编辑了我的帖子以显示它们是如何设置的。
  • @Adrian,看起来你的问题比我想象的/希望的要有趣得多。关于我的存储过程注释,我错了。如果您使用的是 SQL Server,存储过程可能会为您省去一些麻烦,但 MySQL 似乎并非如此。
【解决方案3】:

“一个工作可以有一个或多个工人。”反之亦然(一个工人可以从事多项工作)?如果没有,那么你只有 1:many,你不应该用那个额外的表来实现它。

假设它确实是 many:many,here 有几个优化该表的技巧。

不要使用LEFT,除非您期望“正确”表缺少所需的行。

风格提示:去掉tbl_user_user_id 除外)等。也就是说,名称上的前缀在上下文中是混乱和冗余的。在“用户”与“工人”上保持一致。

将 many:many 表命名为两个目标(例如,worker_job)。但是,我现在看到它不仅仅是一个简单的多:多映射表;它更像是一个表格,用于分配和跟踪谁在随着时间的推移在做什么?

如果您需要两者了解谁从事过什么工作的历史谁从事什么工作的当前状态,请考虑使用两张表。历史不断发展; “当前”不断变化。

使用合适的数据类型,例如DATEDATETIME

使用 InnoDB 代替 MyISAM。

【讨论】:

  • 谢谢瑞克,是的,一个工人可以有多个工作,所以它是多:多。一项工作可能有一个、多个或没有分配的工作人员,因此 tbl_workermap 中可能有也可能没有任何给定工作的条目。我已经编辑了我的原始帖子以澄清。我现在将阅读该链接。
  • @Adrian - 我添加了几个 cmets。
  • 感谢您的提示,user_idworker_user_id 的原因是 users 表同时包含工人和其他用户。分配工作的人不一定是工人。我已经使用上面 WilsonHauck 的建议解决了性能问题,但是如果随着数据库大小的增长,这成为一个问题,您建议的两个表听起来是一个很好的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-10-06
  • 1970-01-01
  • 1970-01-01
  • 2014-05-23
  • 1970-01-01
  • 1970-01-01
  • 2011-02-14
相关资源
最近更新 更多