简介
维护数据库系统的性能是优化数据库支持应用程序响应速度的重要组成部分。这涉及管理服务器的硬件资源和软件配置,但也与你要求它执行的查询有关。
在之前的指南中,我们检查了如何在 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
语句接受一个 SELECT
、DELETE
、UPDATE
、INSERT
或 REPLACE
语句,并显示查询优化器将如何评估和执行给定的查询。输出显示了 MySQL 将如何连接表、选择字段、排序和过滤结果等。它还显示了诸如将评估多少行以及将使用哪些索引来加速过程等内容。
要了解此语句的工作原理,请要求 MySQL 解释它将如何执行查询 SELECT * FROM INFORMATION_SCHEMA.VIEWS;
EXPLAIN SELECT * FROM information_schema.views\G
*************************** 1. row ***************************id: 1select_type: SIMPLEtable: catpartitions: NULLtype: indexpossible_keys: PRIMARYkey: namekey_len: 194ref: NULLrows: 1filtered: 100.00Extra: Using index*************************** 2. row ***************************id: 1select_type: SIMPLEtable: vwpartitions: NULLtype: refpossible_keys: schema_id,type,view_client_collation_id,view_connection_collation_id,type_2key: typekey_len: 1ref: constrows: 100filtered: 100.00Extra: Using where*************************** 3. row ***************************id: 1select_type: SIMPLEtable: schpartitions: NULLtype: eq_refpossible_keys: PRIMARY,catalog_idkey: PRIMARYkey_len: 8ref: mysql.vw.schema_idrows: 1filtered: 100.00Extra: Using where*************************** 4. row ***************************id: 1select_type: SIMPLEtable: conn_collpartitions: NULLtype: eq_refpossible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.vw.view_connection_collation_idrows: 1filtered: 100.00Extra: NULL*************************** 5. row ***************************id: 1select_type: SIMPLEtable: client_collpartitions: NULLtype: eq_refpossible_keys: PRIMARY,character_set_idkey: PRIMARYkey_len: 8ref: mysql.vw.view_client_collation_idrows: 1filtered: 100.00Extra: NULL*************************** 6. row ***************************id: 1select_type: SIMPLEtable: cspartitions: NULLtype: eq_refpossible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.client_coll.character_set_idrows: 1filtered: 100.00Extra: NULL6 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`ASSELECT `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'))
有五个连接语句,这意味着将需要查询六个独立的表才能构建结果。这与我们在执行的 EXPLAIN
语句中返回的条目数量一致——每个表一个。
要解释 EXPLAIN
语句的结果,您需要了解每个单独的列代表什么。特别要注意 type
、key
、rows
和 filtered
列。
解读 type
列
type
列告诉您 MySQL 在查询中实现任何连接操作的方式。MySQL 文档对 可用的不同连接类型 以及它们如何影响性能进行了很好的描述。
type: const
例如,const
类型 将非常快,因为它意味着 MySQL 将唯一列与常量值进行比较,这最多应该返回一行。
type: eq_ref
eq_ref
类型 同样是用于将两个表组合起来的最快的连接操作,因为它指示查询使用完整的唯一非空索引值进行比较。
type: index
index
类型 用于需要全表扫描的情况,但查询不是扫描实际表,而是扫描索引树。这使其在查询范围内性能相对较低,但仍然有一些优化,因为它可以扫描较小的索引而不是实际表。
type: ref
ref
连接类型 意味着 MySQL 能够使用索引的引导列进行比较(当整个索引包含多个未用于比较的列时)。这意味着即使 MySQL 没有匹配整个比较,它也能有效地使用部分索引。
解读 key
列
key
列及其相关列(possible_keys
、key_length
和 ref
)可以帮助您了解哪些 索引 可用,哪些索引已使用以及索引使用效率如何。
. . .possible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.client_coll.character_set_id. . .
possible_keys
列 告诉我们哪些索引可用于要连接的表。它很有用,因为它向我们展示了 MySQL 在尝试找到适合满足查询的索引时从哪里开始。如果您在非主键上连接表以确保有可用的索引来加速该过程,这可能是您要查找的地方。
key
列 本身会告诉您 MySQL 决定使用哪个索引来运行查询。在某些情况下,这甚至可能包括不在 possible_keys
列中列出的结果。例如,当索引包含查询要求的所有列时,可以比关联的表更有效地扫描它,即使该索引不适合(并且不适用于)实际连接。
key_length
列 指示实际使用多少索引。对于多列索引,这可以帮助您了解为特定比较使用多少存储索引。 ref
列 也在其中发挥作用,它告诉您具体与特定索引进行比较的内容。这可以帮助您确定创建更优化的索引是否是个好主意。
解读评估的行数
rows
和 filtered
列很有用,因为它们告诉我们正在检查的表的范围。
rows: 1filtered: 100.00
rows
列 给出了 MySQL 认为执行查询的每个部分需要评估的行数。此处的数字受我们之前讨论的所有列的影响。它让我们了解了给定提供的语句和可用的索引,MySQL 能否有效地减少整个表。
filtered
列 估计在应用查询中的任何行过滤条件后将返回的评估行百分比。如果值为 100,则表示不会进行任何行过滤。低于 100 的值表示某些评估的行不会因连接条件或其他过滤而返回。
重写缓慢的查询以更快地执行
现在我们已经讨论了 MySQL 允许您评估单个语句性能的一些方法,我们可以谈谈如何提高这种性能。
您需要考虑的确切步骤在很大程度上取决于您在查询分析和 EXPLAIN
调查中发现的内容。我们将介绍以下一些优化查询性能的最常见方法。
创建其他索引
提高查询执行速度的最直接方法之一是确保您的表有合适的索引。您的索引应反映您查询数据的最常见方式,并且可能会随着时间的推移而改变,因为您的使用情况会发生变化或实施了其他查询。
要创建新的索引,您可以使用 CREATE INDEX
语句
CREATE INDEX <index_name> ON <table> (<columns_to_index>)
您可以根据要索引的列的数据类型、值的最大长度以及要使用的索引结构来创建许多不同类型的索引。如果您要索引的查询已经使用了一个索引,并且该索引的性能不佳,那么这些选项可能值得研究。但是,为了开始,您可以通过使用大多数默认值来保持相对简单。
您需要考虑的第一个问题是您想对哪些列进行索引。如果您经常在特定列上进行连接,那么确保这些列在您的索引中很重要。索引主要用于行查找,因此在没有索引的情况下,各种表中不同行的匹配非常昂贵。
如果您使用一个列来完全封装您通常如何查询表,则可以创建使用该列的索引。您还可以对多个列进行索引以使索引更有用。如果对多个列进行索引,则排序非常重要。第一列应该是查找匹配项的主要方式,而任何其他列都应该帮助您完善这些匹配项。
例如,如果您为表 person
创建 first_name
和 last_name
的索引,您几乎肯定需要使用类似以下内容定义索引
CREATE INDEX last_and_first_names ON person (last_name, first_name);
这里,last_name
列是索引中的第一个或最左边的列,这意味着它将快速查找 Alan Smith
或所有 Smith
条目。但它不会有效地查找所有 Alan
的实例,因为 first_name
,这可能很好,除非我们预计经常只按名字查询。
虽然创建索引可以帮助您提高数据集的读取性能,但重要的是不要过度索引您的表。索引对写入速度有成本,因为每次在表中记录新值时,与它关联的所有索引也必须更新。如果表的 primary keys 与用于访问它的查询模式非常匹配,那么表的工作效果最佳,因为它允许您从强大的索引性能中受益,而无需大量补充索引来适应不同的访问模式。
限制候选行
您可以加快查询速度的另一种方法是在可能的情况下限制语句中候选行和返回行的数量。这是一个非常具体的建议,因此在许多情况下都不适用,但如果您能够减少 MySQL 需要评估的行数,您可以显着提高性能。
您可以使用 LIMIT
子句 来做到这一点。例如,要仅返回五个结果,您可以使用类似以下格式
SELECT * FROM <table> LIMIT 5
在许多情况下(取决于查询中包含的其他子句),限制返回的行数可以帮助 MySQL 中断查询过程,比通常需要的时间更早停止。这有助于在早期节省时间,而不是在整个数据集上执行然后稍后截断结果。
这在连接多个表时尤其有用。如果可能,最好在连接发生 *之前* 限制查询的结果。这样,MySQL 就可以评估一小部分数据,而不是整个第一个表,然后再进行连接。
仅选择相关字段
提高查询性能的另一个方法是关注您实际返回的列。这可以通过多种方式帮助加快结果速度。
限制所选列对性能的主要影响是通过最小化网络负载。通过返回不需要的列,较大的查询会显着增加查询生成的网络流量。仅选择满足查询逻辑约束所需的列有助于通过减少通过网络传输的数据量来防止此问题。
选择性地返回列对性能产生的另一个影响与索引相关。如果 MySQL 能够将您返回的所有列与索引关联,则它可以潜在地对该较小、更组织的数据集进行操作,而不是对整个表进行操作。当您的索引与您的查询模式很好地匹配时,限制您查询的列允许这种情况发生。
删除带有前导通配符的比较
可能对查询性能产生负面影响的一件事是对通配符的过度依赖。在 SQL 中,%
字符在比较中用作通配符,表示可以替换任何值。这是一种在记录中查找数据的非常有效的方法,但它会对查询性能产生非常大的影响。
对于前导或前缀通配符来说尤其如此。前导通配符是检查列值是否与以通配符开头的模式匹配的比较。例如,检查列是否与 %Main St.
匹配。这会执行得特别糟糕,因为 MySQL 无法使用索引来发现匹配的结果。相反,它必须遍历整个表以查找匹配项,即使该列具有索引也是如此。
在这个特定示例中,如果您将定期按街道名称查询以发现该街道上的所有地址,那么将街道名称和街道号码拆分成两个单独的列可能更有意义。这将允许您使用索引快速查询“Main St.”上的地址,然后根据需要过滤这些结果(如果您正在查找特定地址)。使用街道名称作为第一个列,街道号码作为第二个列创建复合索引将使其更快。
结论
在本指南中,我们介绍了一些评估单个查询和获取有关可能导致其变慢的更多信息的简单方法。我们讨论了查询分析,并使用 MySQL 的 EXPLAIN
语句来获取有关查询计划程序如何评估查询的详细信息。然后,我们研究了一些通过修改查询使其更好地与数据库检索数据的方式相一致来避免这些延迟的方法。
数据库是负责对大量可能以多种不同格式存在的数据进行操作的复杂软件。除了简单地检索必要的数据之外,还期望数据库能够快速返回结果。MySQL 的查询计划程序和优化程序机制中内置了无数优化,以帮助最大限度地减少回答查询所需的时间。
但是,数据库只能在有限的范围内自动优化。作为数据库的用户,您还需要了解被访问的数据、对查询的要求以及哪些查询最需要优化。为此,重要的是了解如何评估您的标准以及在查询性能未达到预期水平时如何进行调整。
如果您在 MySQL 数据库中使用 Prisma,您可以在 文档的查询优化部分 中阅读有关优化查询的方法。这将帮助您了解在使用 Prisma 时各种查询结构如何影响数据库性能。