我的子查询将执行时间增加了 20 秒。我怎样才能加快速度?

Posted

技术标签:

【中文标题】我的子查询将执行时间增加了 20 秒。我怎样才能加快速度?【英文标题】:My sub query is adding 20 seconds to the execution time. How can I speed it up? 【发布时间】:2011-11-24 09:25:21 【问题描述】:

我有一个发送的 SMS 文本消息表,必须加入到送达回执表才能获得消息的最新状态。

已发送短信 997,148 条。

我正在运行这个查询:

SELECT
    m.id,
    m.user_id,
    m.api_key,
    m.to,
    m.message,
    m.sender_id,
    m.route,
    m.submission_reference,
    m.unique_submission_reference,
    m.reason_code,
    m.timestamp,
    d.id AS dlrid,
    d.dlr_status
FROM
    messages_sent m
LEFT JOIN
    delivery_receipts d
ON
    d.message_id = m.id
AND
    d.id = (SELECT MAX(id) FROM delivery_receipts WHERE message_id = m.id)

返回 997,148 个结果,包括每条消息的最新状态。

这需要 22.8688 秒来执行。

这是messages_sent 的 SQL:

CREATE TABLE IF NOT EXISTS `messages_sent` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`api_key` varchar(40) NOT NULL,
`to` varchar(15) NOT NULL,
`message` text NOT NULL,
`type` enum('sms','mms') NOT NULL DEFAULT 'sms',
`sender_id` varchar(15) NOT NULL,
`route` tinyint(1) unsigned NOT NULL,
`supplier` tinyint(1) unsigned NOT NULL,
`submission_reference` varchar(40) NOT NULL,
`unique_submission_reference` varchar(40) NOT NULL,
`reason_code` tinyint(1) unsigned NOT NULL,
`reason` text NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `api_key` (`api_key`),
KEY `sender_id` (`sender_id`),
KEY `route` (`route`),
KEY `submission_reference` (`submission_reference`),
KEY `reason_code` (`reason_code`),
KEY `timestamp` (`timestamp`),
KEY `to` (`to`),
KEY `unique_submission_reference` (`unique_submission_reference`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000342 ;

对于delivery_receipts

CREATE TABLE IF NOT EXISTS `delivery_receipts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`message_id` int(10) unsigned NOT NULL,
`dlr_id` bigint(20) unsigned NOT NULL,
`dlr_status` tinyint(2) unsigned NOT NULL,
`dlr_substatus` tinyint(2) unsigned NOT NULL,
`dlr_final` tinyint(1) unsigned NOT NULL,
`dlr_refid` varchar(40) NOT NULL,
`dlr_phone` varchar(12) NOT NULL,
`dlr_charge` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `message_id` (`message_id`),
KEY `dlr_status` (`dlr_status`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1468592 ;

这是一个EXPLAIN的SQL:

【问题讨论】:

看起来您正在执行greatest-n-per-group 查询。 Here's one highly voted approach。相当全面的解决方案here 【参考方案1】:

有个窍门。

而不是选择带有子查询的 MAX 元素,您可以像这样两次加入有趣的表:

SELECT
    m.id,
    m.user_id,
    m.api_key,
    m.to,
    m.message,
    m.sender_id,
    m.route,
    m.submission_reference,
    m.unique_submission_reference,
    m.reason_code,
    m.timestamp,
    d.id AS dlrid,
    d.dlr_status
FROM
    messages_sent m
JOIN
    delivery_receipts d
ON
    d.message_id = m.id
LEFT JOIN
    delivery_receipts d1
ON
    d1.message_id = m.id
    AND
    d1.id > d.id
WHERE
    d1.id IS NULL

加入第二个时间表有附加条件,即您要选择 MAX 的字段应高于第一个表中的字段。并过滤掉所有行,除了那些没有其他更高行的行。

这样只剩下最大行数。

我将您的 LEFT JOIN 更改为 JOIN。我不确定你是否需要 LEFT JOIN 那里。即使你应该仍然可以工作。

这比子查询快得多。

您可能想尝试相同想法的其他变体:

SELECT
    m.id,
    m.user_id,
    m.api_key,
    m.to,
    m.message,
    m.sender_id,
    m.route,
    m.submission_reference,
    m.unique_submission_reference,
    m.reason_code,
    m.timestamp,
    d.id AS dlrid,
    d.dlr_status
FROM
    messages_sent m
JOIN
(
SELECT d0.* FROM
    delivery_receipts d0
LEFT JOIN
    delivery_receipts d1
ON
    d1.message_id = d0.message_id
    AND
    d1.id > d0.id
WHERE
    d1.id IS NULL
) d
ON
    d.message_id = m.id

确保您在表 delivery_receipts 中具有字段 message_id 和 id 的多列索引:

ALTER TABLE  `delivery_receipts` 
ADD INDEX  `idx` (  `message_id` ,  `id` );

【讨论】:

我对这个方法很感兴趣,但是它一直挂在 phpMyAdmin 中并且不返回结果。 欣赏更新的答案,但即使添加了索引也需要 20.3334 秒! 加入messages_sent 和它们各自的delivery_recipts 需要多长时间(所有这些都不仅仅是最大值)?【参考方案2】:

减速似乎很大,但如果您需要坚持这个查询,恐怕没有太大的改进空间。

一个问题是d.dlr_status 的报告。尝试从报告的列列表中删除它,看看查询时间是否有所改善。

如果所有内容都存储在messages_sent 中,您将获得最佳性能。这将不再是 NF,但如果您需要性能,它是一个选项。为此,请在messages_sent 中创建iddlr_status 列,并将适当的INSERTUPDATEDELETE 触发器添加到delivery_receipts。触发器将更新messages_sent 中的相应列——这是查询时间和更新时间之间的权衡。

【讨论】:

删除 d.dlr_status 并没有改善查询 - 它实际上由于某种原因又增加了 3 秒!感谢您提供有关触发器的提示。重组数据库是我最后的手段,但我想我最终会这样做!【参考方案3】:

您可以在delivery_receipts 表中“缓存”部分计算,只需将is_last_status 布尔值添加到delivery_receipts 表中。使用简单的触发器,您可以在每次插入新收据时更改值。

比选择查询要简单得多:

SELECT
  m.id,
  m.user_id,
  m.api_key,
  m.to,
  m.message,
  m.sender_id,
  m.route,
  m.submission_reference,
  m.unique_submission_reference,
  m.reason_code,
  m.timestamp,
  d.id AS dlrid,
  d.dlr_status
FROM
  messages_sent m
LEFT JOIN
  delivery_receipts d
ON
  d.message_id = m.id
WHERE
  d.is_last_status = true

如果 mysql 支持部分索引,查询速度会更快。

【讨论】:

以上是关于我的子查询将执行时间增加了 20 秒。我怎样才能加快速度?的主要内容,如果未能解决你的问题,请参考以下文章

双连接查询需要 540 秒才能运行 - 我怎样才能加快速度?

SQL 查询优化需要 20 - 30 秒才能运行

MySQL中非常慢的子查询

我怎样才能让我的 caroutine 每 x 秒运行一次

我怎样才能增加分数?

查询需要很长时间才能执行大约 120 秒