分享至

简介

维护数据库系统的性能是优化数据库支持应用程序响应能力的重要组成部分。这涉及到管理服务器的硬件资源和软件配置,但也与您要求它执行的查询有关。

在上一篇指南中,我们探讨了如何在 MySQL 中识别低性能和慢查询。在本指南中,我们将通过讨论如何评估和优化慢运行查询来迈出下一步。这将以上一篇文章的讨论为基础,为您提供一个识别和修复与低性能查询相关问题的完整策略。

启用查询分析以了解慢查询阶段

尝试优化慢查询时,您可能首先要做的事情之一是分析查询,以准确找出查询在哪一步花费了最多时间。

首先,通过输入以下命令检查查询分析是否已启用:

SELECT @@PROFILING;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set, 1 warning (0.00 sec)

0 表示当前分析已禁用。您可以通过输入以下命令启用分析:

SET PROFILING = 1;

启用分析后,您可以通过输入以下命令开始列出可用的配置文件:

SHOW PROFILES;
+----------+------------+--------------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------------+
| 1 | 3.00368075 | select sleep(3) |
| 2 | 0.00740700 | select * from mysql.user |
| 3 | 0.00075875 | select @@profiling |
+----------+------------+--------------------------+
3 rows in set, 1 warning (0.00 sec)

输出将显示自启用分析以来当前会话中执行的最新语句。每个查询都显示总执行时间,并有一个查询编号,可用于深入了解。

要显示最新查询的分析信息,请键入

SHOW PROFILE;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000144 |
| checking permissions | 0.000020 |
| Opening tables | 0.000024 |
| init | 0.000013 |
| optimizing | 0.000047 |
| executing | 0.000032 |
| end | 0.000010 |
| query end | 0.000016 |
| closing tables | 0.000012 |
| freeing items | 0.000367 |
| cleaning up | 0.000074 |
+----------------------+----------+
11 rows in set, 1 warning (0.00 sec)

默认情况下,输出将精确显示语句在查询处理的每个阶段所花费的时间。这可以帮助您准确识别查询的哪个部分性能不佳。

要显示与不同查询关联的结果,您可以添加 FOR QUERY <N> 子句以及 SHOW PROFILES 输出提供的查询 ID。

例如,要获取查询 1 的默认显示,您可以输入

SHOW PROFILE FOR QUERY 1;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.001419 |
| checking permissions | 0.000028 |
| Opening tables | 0.000030 |
| init | 0.000020 |
| optimizing | 0.000020 |
| executing | 0.000025 |
| User sleep | 3.000165 |
| end | 0.000043 |
| query end | 0.000019 |
| closing tables | 0.000012 |
| freeing items | 0.000540 |
| logging slow query | 0.001320 |
| cleaning up | 0.000041 |
+----------------------+----------+
13 rows in set, 1 warning (0.00 sec)

在这里,输出清楚地显示在 User sleep 状态下花费了很长时间,这直接管理了执行的函数。

您还可以更改输出以显示不同类型的信息。这通常在您查看默认输出后有助于检查更具体的信息。

例如,您可以通过键入以下内容查看第一个查询的块输入和输出信息:

SHOW PROFILE BLOCK IO FOR QUERY 1;
+----------------------+----------+--------------+---------------+
| Status | Duration | Block_ops_in | Block_ops_out |
+----------------------+----------+--------------+---------------+
| starting | 0.001419 | 184 | 0 |
| checking permissions | 0.000028 | 0 | 0 |
| Opening tables | 0.000030 | 0 | 0 |
| init | 0.000020 | 0 | 0 |
| optimizing | 0.000020 | 0 | 0 |
| executing | 0.000025 | 0 | 0 |
| User sleep | 3.000165 | 0 | 0 |
| end | 0.000043 | 0 | 0 |
| query end | 0.000019 | 0 | 0 |
| closing tables | 0.000012 | 0 | 0 |
| freeing items | 0.000540 | 0 | 0 |
| logging slow query | 0.001320 | 64 | 8 |
| cleaning up | 0.000041 | 0 | 0 |
+----------------------+----------+--------------+---------------+
13 rows in set, 1 warning (0.00 sec)

除了之前的信息,这还显示了 MySQL 必须与文件系统上的块数据交互的时间。

您可以指定的可用输出变体包括

  • ALL:显示所有可用信息
  • BLOCK IO:显示查询生成的输入和输出块数
  • CONTEXT SWITCHES:显示执行查询时发生的自愿和非自愿上下文切换次数
  • CPU:显示用户空间和系统类别的 CPU 时间
  • IPC:显示发送和接收的进程间消息数量
  • PAGE FAULTS:显示页面错误的数量,分为主要错误和次要错误
  • SOURCE:显示 MySQL 中实际执行的函数及其源文件和行号,以允许执行映射
  • SWAPS:显示查询所需的交换次数

要显示与查询关联的所有可能信息,请键入

SHOW PROFILE ALL FOR QUERY 1\G

我们使用 \G 语句终止序列触发替代输出格式,以使结果更具可读性,因为显示了大量额外输出。

一旦您使用分析信息对查询的哪个部分慢有了很好的了解,您就可以关闭分析以保存资源。

SET PROFILING = 0;

使用 EXPLAIN 了解查询性能

查询配置文件应能帮助您了解查询执行中耗时最长的阶段以及与查询相关的资源。然后,您可以使用 MySQL 的 EXPLAIN 语句来理解查询优化器对查询的评估。

EXPLAIN 语句接受 SELECTDELETEUPDATEINSERTREPLACE 语句,并显示查询优化器将如何评估和执行给定查询。输出显示 MySQL 将如何连接表、选择字段、排序和筛选结果等。它还显示诸如将评估多少行以及将查阅哪些索引以加快过程等信息。

为了了解此语句的工作原理,请让 MySQL 解释它将如何执行查询 SELECT * FROM INFORMATION_SCHEMA.VIEWS;

EXPLAIN SELECT * FROM information_schema.views\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: cat
partitions: NULL
type: index
possible_keys: PRIMARY
key: name
key_len: 194
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: vw
partitions: NULL
type: ref
possible_keys: schema_id,type,view_client_collation_id,view_connection_collation_id,type_2
key: type
key_len: 1
ref: const
rows: 100
filtered: 100.00
Extra: Using where
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: sch
partitions: NULL
type: eq_ref
possible_keys: PRIMARY,catalog_id
key: PRIMARY
key_len: 8
ref: mysql.vw.schema_id
rows: 1
filtered: 100.00
Extra: Using where
*************************** 4. row ***************************
id: 1
select_type: SIMPLE
table: conn_coll
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: mysql.vw.view_connection_collation_id
rows: 1
filtered: 100.00
Extra: NULL
*************************** 5. row ***************************
id: 1
select_type: SIMPLE
table: client_coll
partitions: NULL
type: eq_ref
possible_keys: PRIMARY,character_set_id
key: PRIMARY
key_len: 8
ref: mysql.vw.view_client_collation_id
rows: 1
filtered: 100.00
Extra: NULL
*************************** 6. row ***************************
id: 1
select_type: SIMPLE
table: cs
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: mysql.client_coll.character_set_id
rows: 1
filtered: 100.00
Extra: NULL
6 rows in set, 1 warning (0.01 sec)

上述输出显示,需要六个单独的查询才能将该查询的信息汇集起来。如果您查看表定义,您会明白原因。

SHOW CREATE TABLE information_schema.views\G

为了格式化,最终的表创建命令如下所示

CREATE algorithm=undefined definer=`mysql.infoschema`@`localhost` SQL security definer view `views`
AS
SELECT `cat`.`name` AS `table_catalog`,
`sch`.`name` AS `table_schema`,
`vw`.`name` AS `table_name`,
IF((can_access_view(`sch`.`name`,`vw`.`name`,`vw`.`view_definer`,`vw`.`options`) = TRUE),`vw`.`view_definition_utf8`,'') AS `view_definition`,
`vw`.`view_check_option` AS `check_option`,
`vw`.`view_is_updatable` AS `is_updatable`,
`vw`.`view_definer` AS `definer`,
IF((`vw`.`view_security_type` = 'DEFAULT'),'DEFINER',`vw`.`view_security_type`) AS `security_type`,
`cs`.`name` AS `character_set_client`,
`conn_coll`.`name` AS `collation_connection`
FROM (((((`mysql`.`tables` `vw`
JOIN `mysql`.`schemata` `sch`
ON ((
`vw`.`schema_id` = `sch`.`id`)))
JOIN `mysql`.`catalogs` `cat`
ON ((
`cat`.`id` = `sch`.`catalog_id`)))
JOIN `mysql`.`collations` `conn_coll`
ON ((
`conn_coll`.`id` = `vw`.`view_connection_collation_id`)))
JOIN `mysql`.`collations` `client_coll`
ON ((
`client_coll`.`id` = `vw`.`view_client_collation_id`)))
JOIN `mysql`.`character_sets` `cs`
ON ((
`cs`.`id` = `client_coll`.`character_set_id`)))
WHERE ((
0 <> can_access_table(`sch`.`name`,`vw`.`name`))
AND (
`vw`.`type` = 'VIEW'))

有五个 JOIN 语句,这意味着必须查询六个单独的表来构建结果。这与我们执行的 EXPLAIN 语句返回的条目数量相匹配——每个表一个。

要解释 EXPLAIN 语句的结果,您需要了解每个单独列的含义。其中特别重要的是 typekeyrowsfiltered 列。

解释 type

type 列告诉您 MySQL 实现查询中任何连接操作的方式。MySQL 文档对可用的不同连接类型以及它们如何影响性能有很好的描述。

type: const

例如,类型为 const 的查询将非常快,因为它意味着 MySQL 将唯一列与常量值进行比较,这最多只会返回一行。

type: eq_ref

eq_ref 类型是合并两个表的最快连接操作,因为它表示查询使用完整的唯一、非空索引值进行比较。

type: index

当需要进行全表扫描时,会使用index 类型,但查询不是扫描实际表,而是可以扫描索引树。这使得它在查询范围内性能相对较低,但由于它可以扫描较小的索引而不是实际表,因此仍然具有一定的优化。

type: ref

ref 连接类型表示 MySQL 能够使用索引的引导列进行比较(当整个索引包含多个未用于比较的列时)。这意味着即使它不匹配整个比较,MySQL 也能够有效地使用部分索引。

解释 key

key 列及其相关列(possible_keyskey_lengthref)帮助您了解哪些索引可用、使用了哪些索引以及索引使用效率如何。

. . .
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: mysql.client_coll.character_set_id
. . .

possible_keys告诉我们正在连接的表有哪些可用的索引。这很有用,因为它显示了 MySQL 在尝试查找适合满足查询的索引时从何开始。如果您在非主键上连接表,这可能是您需要检查的地方,以确保您有可用的索引来加速该过程。

key本身告诉您 MySQL 决定使用哪个索引来运行查询。在某些情况下,这甚至可能包括未在 possible_keys 列中列出的结果。例如,当索引包含查询要求的所有列时,它可以比关联的表更有效地扫描,即使该索引不适合(并且未用于)实际连接。

key_length表示实际使用了多少索引。对于多列索引,这可以帮助您了解存储索引的多少部分用于此特定比较。ref也在此处发挥作用,它告诉您具体将什么与特定索引进行比较。这可以帮助您确定是否值得创建更优化的索引。

解释评估的行数

rowsfiltered 列很有用,因为它们告诉我们正在检查的表的范围。

rows: 1
filtered: 100.00

rows给出 MySQL 认为执行查询的每个部分需要评估的行数。此处的数字受我们之前讨论的所有列的影响。它向我们表明 MySQL 在给定提供的语句和可用索引的情况下,能够以多高的效率来精简整个表。

filtered估计在应用查询中的任何行筛选条件后,将被返回的评估行数的百分比。如果值为 100,则表示不会发生行筛选。低于 100 的值表示由于连接条件或其他筛选,部分评估的行不会返回。

重写慢查询以加快执行速度

既然我们已经讨论了 MySQL 允许您评估单个语句性能的一些方法,我们就可以讨论如何提高该性能了。

您需要考虑的具体步骤在很大程度上取决于您通过查询分析和 EXPLAIN 调查发现的结果。我们将在下面介绍一些最常见的优化查询性能的方法。

创建额外的索引

提高查询执行速度最直接的方法之一是确保为表提供适当的索引。您的索引应反映您查询数据的最常用方式,并可能随着您的使用方式演变或新查询的实现而随时间变化。

要创建新索引,您可以使用 CREATE INDEX 语句

CREATE INDEX <index_name> ON <table> (<columns_to_index>)

您可以创建许多不同类型的索引,具体取决于您要索引的列的数据类型、值的最大长度以及要使用的索引结构。如果您的查询已经在使用索引且索引性能不佳,那么这些选项可能值得研究。但是,首先,您可以通过使用大多数默认值来保持相对简单。

您需要考虑的第一个因素是您希望索引哪些列。如果您经常在特定列上进行连接,那么确保这些列在您的索引中得到体现非常重要。索引主要用于行查找,因此如果没有索引,连接操作(将不同表中的各种行进行匹配)会非常昂贵。

如果单列完全封装了您通常查询表的方式,则可以创建单列索引。您还可以对多列进行索引,以使索引更具实用性。如果对多列进行索引,则顺序非常重要。第一列应作为查找匹配项的主要方式,而任何附加列应帮助您优化这些匹配项。

例如,如果您为 person 表创建 first_namelast_name 的索引,您几乎肯定会希望像这样定义索引:

CREATE INDEX last_and_first_names ON person (last_name, first_name);

在这里,last_name 列是索引中的第一个或最左边的列,这意味着查找 Alan Smith 或所有 Smith 条目都将很快。然而,它对于仅按 first_name 查找所有 Alan 实例将无效,这可能没关系,除非我们预计经常只按姓氏进行查询。

虽然创建索引可以帮助您提高数据集的读取性能,但重要的是不要过度索引您的表。索引会牺牲写入速度,因为每次在表中记录新值时,所有与它关联的索引也必须更新。如果表的主键与将用于它们的查询模式很好地匹配,则它们的效果最佳,因为它允许您从强大的索引性能中受益,而无需大量辅助索引来适应不同的访问模式。

限制候选行

另一种可以加快查询速度的方法是尽可能限制语句中候选行和返回行的数量。这是一个非常依赖上下文的建议,因此在许多情况下它可能不适用,但如果您能够减少 MySQL 需要评估的行数,则可以显著提高性能。

您可以使用 LIMIT 子句来做到这一点。例如,要只返回五个结果,您可以使用以下格式:

SELECT * FROM <table> LIMIT 5

在许多情况下(取决于查询中包含的其他子句),限制返回的行数可以帮助 MySQL 短路查询过程,比平时更早停止。这有助于尽早节省时间,而不是遍历整个数据集,然后再截断结果。

这在连接多个表时尤其有用。在任何可能的情况下,最好在连接发生之前限制查询结果。这允许 MySQL 评估少量数据,然后与其他连接进行关联,而不是评估整个第一个表。

仅选择相关字段

提高查询性能的另一种方法是关注您实际返回的列。这可以通过多种不同方式帮助加快结果。

限制所选列主要通过最小化网络负载来影响性能。通过返回不需要的列,大型查询会大幅增加查询产生的网络流量。只选择满足查询逻辑约束所需的列有助于通过精简网络传输的数据来避免此问题。

通过有选择地返回列影响性能的另一种方式与索引相关。如果 MySQL 能够将您返回的所有列与索引关联起来,它就可以潜在地对该更小、更有组织的数据集进行操作,而不是对整个表进行操作。当您的索引与查询模式很好地匹配时,限制您查询的列允许这种情况发生。

移除带有前导通配符的比较

可能对查询性能产生负面影响的一点是过度依赖通配符。在 SQL 中,% 字符在比较中用作通配符,表示可以替换任何值。这是在记录中查找数据的强大方式,但它可能对查询性能产生非常大的影响。

对于前导或前缀通配符尤其如此。前导通配符是检查列值是否与以通配符开头的模式匹配的比较。例如,检查列是否匹配 %Main St.。这种操作的性能特别差,因为 MySQL 无法使用索引来发现匹配的结果。相反,它必须遍历整个表以查找匹配项,即使该列具有索引。

在这个特定的例子中,如果您将经常按街道名称查询以发现该街道上的所有地址,那么将街道名称和街道号码分成两个单独的列可能更有意义。这将允许您使用索引快速查询“Main St.”上的地址,然后根据需要过滤这些结果,如果您正在寻找特定地址。创建一个复合索引,将街道名称放在首位,街道号码列放在第二位,将使这更快。

结论

在本指南中,我们探讨了一些评估单个查询和获取更多信息以了解其慢的原因的基本方法。我们讨论了分析查询和使用 MySQL 的 EXPLAIN 语句来获取有关查询规划器如何评估查询的详细信息。然后,我们研究了一些通过修改查询使其与数据库检索数据的方式更好地对齐来避免这些慢速的方法。

数据库是复杂的软件,负责处理大量可能以多种不同格式存在的数据。除了简单地检索必要数据之外,还期望数据库能够快速返回结果。MySQL 的查询规划器和优化器机制中内置了无数优化,以帮助最大限度地减少回答查询所需的时间。

然而,数据库只能在有限的范围内自动优化。作为数据库用户,您还需要能够理解正在访问的数据、您对查询的要求以及哪些查询最重要以进行优化。为此,了解如何评估您的标准并在查询性能未达到预期水平时进行调整非常重要。

关于作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以来一直撰写关于数据库、Linux、基础设施和开发者工具的文章。他目前与妻子和两只兔子住在柏林。他通常不必以第三人称写作,这对所有相关方来说都是一种解脱。
© . All rights reserved.