MySQL 查询性能 - 查询/架构/索引?
Posted
技术标签:
【中文标题】MySQL 查询性能 - 查询/架构/索引?【英文标题】:MySQL Query Performance - Query/Schema/Indexes? 【发布时间】:2013-10-16 15:58:16 【问题描述】:基本上有一些查询性能问题,主要是我保存呼叫数据的最大表。
主查询包含相当多的左连接和子选择,但在我运行查询的情况下,我希望返回 130 万次调用,但查询并没有这样做。必须在 7 分钟停止它意味着肯定有问题。
我已经缩小了主查询范围并测试了最简单的子选择连接
SELECT
DateStart,
ID,
NumbID,
EffectiveFlag,
OrigNumber
FROM calls
WHERE
DateStart <= '2013-12-31'
AND DateStart >= '2013-01-01'
AND CallLength >= '00:00:00'
AND Direction = '1'
AND CustID IN (474,482,250,268,197,604,132,359,279,441,118,448,152,133,380,162,249,679,226,259,2450,2408,2451,2453,2439,2454,2444,2445,2452)
即使是那个查询也需要 4.5 秒 - 所以当它是一个带有其他连接和子选择的查询中的子选择时,我可以想象为什么整个查询是不可用的。
上述查询的解释语句是
+----+-------------+-------+-------+-------------------------------------------------------------------------------------------------------+----------------------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+-------------------------------------------------------------------------------------------------------+----------------------+---------+------+---------+-------------+
| 1 | SIMPLE | calls | range | idx_CustID,idx_DateStart,idx_CustID_DateStart,idx_CustID_TermNumber,idx_Direction | idx_CustID_DateStart | 7 | NULL | 1660009 | Using where |
+----+-------------+-------+-------+-------------------------------------------------------------------------------------------------------+----------------------+---------+------+---------+-------------+
调用表的数据库架构是
+-------------------+-------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+-------------+------+-----+---------------------+----------------+
| ID | int(11) | NO | PRI | NULL | auto_increment |
| CustID | int(11) | NO | MUL | 0 | |
| CarrID | int(11) | NO | MUL | NULL | |
| TariID | int(11) | NO | MUL | 0 | |
| CarrierRef | varchar(30) | NO | MUL | | |
| NumbID | int(11) | NO | MUL | 0 | |
| VlviID | int(11) | NO | MUL | NULL | |
| VcamID | int(11) | NO | MUL | NULL | |
| SomeID | int(11) | NO | MUL | NULL | |
| VlnsID | int(11) | NO | MUL | NULL | |
| NGNumber | varchar(12) | NO | | | |
| OrigNumber | varchar(16) | NO | MUL | NULL | |
| CLIRestrictedFlag | int(2) | NO | | NULL | |
| OrigLocality | varchar(11) | NO | MUL | | |
| OrigAreaCode | varchar(11) | NO | MUL | | |
| TermNumber | varchar(16) | NO | MUL | NULL | |
| BatchNumber | varchar(10) | NO | MUL | | |
| DateStart | date | NO | MUL | 0000-00-00 | |
| DateClear | date | NO | | 0000-00-00 | |
| TimeStart | time | NO | | 00:00:00 | |
| TimeClear | time | NO | | 00:00:00 | |
| CallLength | time | NO | | 00:00:00 | |
| RingLength | time | NO | | 00:00:00 | |
| EffectiveFlag | smallint(1) | NO | MUL | NULL | |
| UnansweredFlag | smallint(1) | NO | MUL | NULL | |
| EngagedFlag | smallint(1) | NO | | NULL | |
| RecID | int(11) | NO | MUL | NULL | |
| CreatedUserID | int(11) | NO | | 0 | |
| CreatedDatetime | datetime | NO | MUL | 0000-00-00 00:00:00 | |
| Direction | int(1) | NO | MUL | NULL | |
+-------------------+-------------+------+-----+---------------------+----------------+
调用表上的索引是
+-------+------------+---------------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-------+------------+---------------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+
| calls | 0 | PRIMARY | 1 | ID | A | 23905312 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CustID | 1 | CustID | A | 1685 | NULL | NULL | | BTREE | |
| calls | 1 | idx_NumbID | 1 | NumbID | A | 37765 | NULL | NULL | | BTREE | |
| calls | 1 | idx_OrigNumber | 1 | OrigNumber | A | 5976328 | NULL | NULL | | BTREE | |
| calls | 1 | idx_OrigLocality | 1 | OrigLocality | A | 45019 | NULL | NULL | | BTREE | |
| calls | 1 | idx_OrigAreaCode | 1 | OrigAreaCode | A | 846 | NULL | NULL | | BTREE | |
| calls | 1 | idx_TermNumber | 1 | TermNumber | A | 232090 | NULL | NULL | | BTREE | |
| calls | 1 | idx_DateStart | 1 | DateStart | A | 4596 | NULL | NULL | | BTREE | |
| calls | 1 | idx_EffectiveFlag | 1 | EffectiveFlag | A | 2 | NULL | NULL | | BTREE | |
| calls | 1 | idx_UnansweredFlag | 1 | UnansweredFlag | A | 2 | NULL | NULL | | BTREE | |
| calls | 1 | idx_EngagedFlag | 1 | UnansweredFlag | A | 2 | NULL | NULL | | BTREE | |
| calls | 1 | idx_TariID | 1 | TariID | A | 110 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CustID_DateStart | 1 | CustID | A | 1685 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CustID_DateStart | 2 | DateStart | A | 919435 | NULL | NULL | | BTREE | |
| calls | 1 | idx_NumbID_DateStart | 1 | NumbID | A | 37765 | NULL | NULL | | BTREE | |
| calls | 1 | idx_NumbID_DateStart | 2 | DateStart | A | 5976328 | NULL | NULL | | BTREE | |
| calls | 1 | idx_RecID | 1 | RecID | A | 288015 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CarrierRef | 1 | CarrierRef | A | 7968437 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CustID_CallTermNumber | 1 | CustID | A | 1685 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CustID_CallTermNumber | 2 | TermNumber | A | 246446 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CreatedDatetime | 1 | CreatedDatetime | A | 771139 | NULL | NULL | | BTREE | |
| calls | 1 | idx_Direction | 1 | Direction | A | 2 | NULL | NULL | | BTREE | |
| calls | 1 | idx_VlviID | 1 | VlviID | A | 50539 | NULL | NULL | | BTREE | |
| calls | 1 | idx_SomeID | 1 | SomeID | A | 30 | NULL | NULL | | BTREE | |
| calls | 1 | idx_VcamID | 1 | VcamID | A | 64 | NULL | NULL | | BTREE | |
| calls | 1 | idx_VlnsID | 1 | VlnsID | A | 191 | NULL | NULL | | BTREE | |
| calls | 1 | idx_CarrID | 1 | CarrID | A | 4 | NULL | NULL | | BTREE | |
| calls | 1 | idx_BatchNumber | 1 | BatchNumber | A | 271651 | NULL | NULL | | BTREE | |
+-------+------------+---------------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+
据我了解,可能会导致性能下降的原因是基数较低的列上的索引。我知道诸如 Direction 之类的基数为 2 的列实际上可能比使用索引的性能更差,但仅凭这一点不应该使语句变得如此缓慢。
就拥有有价值的索引的基数要求而言,与总表记录相比,在索引提高性能和何时降低性能时,是否存在一般的基数百分比?
我知道没有人能够向我抛出一个将查询时间从 4.5 秒更改为 0.01 秒的答案,但是对于查询本身、表架构、索引或硬件将不胜感激。
更新:
@Sebas “请您重新运行查询并解释不包括以下部分的计划:AND CallLength >= '00:00:00' AND Direction = '1' please?”
+----+-------------+-------+-------+---------------------------------------------------------------------+----------------------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------------------------------------------------------------+----------------------+---------+------+--------+-------------+
| 1 | SIMPLE | calls | range | idx_CustID,idx_DateStart,idx_CustID_DateStart,idx_CustID_TermNumber | idx_CustID_DateStart | 7 | NULL | 724813 | Using where |
+----+-------------+-------+-------+---------------------------------------------------------------------+----------------------+---------+------+--------+-------------+
【问题讨论】:
能否请您重新运行查询并解释没有该部分的计划:AND CallLength >= '00:00:00' AND Direction = '1'
好吗?
出于好奇,存储引擎是什么?
@DavidPope 存储引擎是 MyISAM
@Sebas - 请查看更新后的帖子
@stanyer 查询完成得更快吗?
【参考方案1】:
您的“DateStart”是截断的日期时间 - 仅保留日期吗?如果没有,您可能希望构建一个截断值(按天或按小时)的数据,并使用 int 数据类型,这将使索引更小,以便更快地查询。
或者,另一种优化方式(黄金法则 #1 不要这样做,#2 现在不要这样做)。
当且仅当您的日期和 PK 顺序同步时,您可以构建 StartDate ID (PK) 范围的外部索引。
并使用以下模式
SELECT @start:=ID_START FROM ANOTHER_TABLE WHERE StartDate='2013-01-01'
SELECT @end:=ID_END FROM ANOTHER_TABLE WHERE StartDate='2013-12-31'
SELECT * FROM calls WHERE ID BETWEEN @start and @end AND CustId in (xxxxx) ....
通过使用上述模式,mysql 将知道是否只需要扫描一段表。
【讨论】:
【参考方案2】:就像 Darhazer 说的那样,您的索引太多了,首先删除所有索引,然后根据您的需要重新构建它们。
对于这个特定的查询,创建 一个 INDEX,其中包含这些字段:
DateStart
CallLength
Direction
CustID
将AND Direction = '1'
更改为AND Direction = 1
(去掉引号,你是在比较一个整数,而不是一个字符串)
看看这对您的查询时间有什么影响。如果一切顺利,添加子查询,用 EXPLAIN 再次检查,添加所需的索引等等。
【讨论】:
@Darhazer 我知道它看起来像很多索引,但我们可能有 200 多个使用调用表的查询,它们都使用不同的表搜索表,因此大多数索引都是用过。 我认为建议的复合索引(从 DateStart 开始)在这里没有多大帮助。索引中的辅助字段(CallLength、Direction 和 CustID)只会对具有相同 DateStart 值的记录起作用,并且在搜索一系列 DateStart 值时根本没有帮助。 @jokeeffe 我希望最高选择性的索引是最有用的,可以尽早去除尽可能多的数据(至少其他一些系统是这样工作的,我我对 MySQL 不太熟悉)。 MySQL 的工作方式是否不同?【参考方案3】:您的查询应该达到的最佳索引是idx_CustID_DateStart
。 IN
声明正在防止这种情况发生。如果CustID
列表来自表格,我建议将JOIN
放入其中,而不是枚举。
【讨论】:
【参考方案4】:当您担心需要 5 秒的子查询时,我不确定需要 7 分钟以上的原始查询是否正确编写(希望它不会针对每一行执行)。但是无论如何,如果你想加快这个速度,你应该阅读一些索引是如何工作的。我会推荐 this article 开始。
基本上,您在 4 个字段上有条件,在两个字段上是范围条件。如果你读过这篇文章,你就会知道在满足第一个范围条件之前,索引是有效使用的。不过,索引中的其余数据可用于索引扫描。因此,您需要选择哪种条件可以更好地缩小结果集:DateStart
或 CallLength
。
无论如何,您需要一个以(CustID, Direction ...
开头的复合索引。我的感觉是 DateStart 上的条件更好。所以我会从(CustID, Direction, DateStart, CallLength)
开始,然后和(CustID, Direction, DateStart)
比较,因为最后一个字段可能不会带来足够的性能提升,但会占用内存资源。
虽然我仍然认为,当专注于子查询时,应该确保查询的其余部分正确编写。可能有一种更合适的方式来组织查询,因此这种优化将变得无关紧要。
【讨论】:
【参考方案5】:4.5s 对于返回的 160 万行来说并不算多,我很确定这一切都花在了 IO 操作上。然后几乎没有任何优化空间。您最好向我们展示您的原始查询,也许我们可以提供更好的帮助。
这 160 万人口占总数的百分比是多少?如果索引用于返回数据集的最小部分,则它们很好,但由于它们的data access pattern with mrr is random reads,有时在表上使用全扫描更有效。当然,这取决于如何将数据添加到表中以及如何在磁盘上分配空间。
另外,您可能会发现使用 MySQL performance schema 监控性能很有用,请查看 here 了解详细信息。
【讨论】:
【参考方案6】:您的索引过多。例如,您不需要单独的 CustID 索引,因为它在 CustID,DateStart 的最左侧。您在 UnansweredFlag 上有 2 个索引。你真的需要所有这些索引吗?这不仅会减慢插入/更新速度,还会减慢优化器的速度,并可能欺骗优化器选择不太好的索引。
现在,关于具体查询。您需要查看哪些字段或组合对查询的限制最大(因为它现在扫描 1,6M 行!)并强制它使用该索引。因此,使用指定的 DateStart 对每个 where 子句(方向、调用长度)运行 SELECT COUNT(*) 查询(您总是希望基于此进行限制)。也许您只需要在索引中添加方向。
另外,在 MySQL 5.6 之前,WHERE 子句中的子查询没有优化,所以也许你应该重写整个查询以使用 join 而不是 subselect,而不是优化特定查询
【讨论】:
'(现在它扫描了 1.6M 行!)' - 这实际上是它将返回的行数(1.6m) 此外,在 where 语句中不使用子查询 - 仅用作连接。例如。LEFT JOIN ( SELECT * FROM x ) ON x.a = y.a
@Stanyer 尝试将 Direction 和 CallLength 添加到索引中以删除 Using where
部分。
当基数如此之低时,向索引添加方向有什么意义吗? (2,在大约 30m 行的桌子上)。据我了解,低基数列在不被索引的情况下表现更好?以上是关于MySQL 查询性能 - 查询/架构/索引?的主要内容,如果未能解决你的问题,请参考以下文章
建议收藏15755字,讲透MySQL性能优化(包含MySQL架构存储引擎调优工具SQL索引建议等等)