【问题标题】:Optimizing a PHP page: MySQL bottleneck优化 PHP 页面:MySQL 瓶颈
【发布时间】:2009-05-06 06:08:09
【问题描述】:

我有一个页面需要 37 秒才能加载。在加载时,它将 MySQL 的 CPU 使用率固定在最高点。这个页面的代码不是我写的,而且相当复杂,所以瓶颈的原因对我来说不是很明显。

我对其进行了分析(使用 kcachegrind),发现页面上的大部分时间都花在了 MySQL 查询上(90% 的时间花在了 25 个不同的 mysql_query 调用上)。

查询采用以下形式,tag_id 在 25 个不同调用中的每一个都发生变化:

SELECT * FROM tbl_news WHERE news_id
 IN(选择 news_id 来自
 tbl_tag_relations WHERE tag_id = 20)

每个查询大约需要 0.8 秒才能完成,但需要更长的延迟才能更好地衡量......因此需要 37 秒才能完全加载页面。

我的问题是,导致问题的嵌套选择是查询格式化的方式吗?或者它可能是一百万个其他事物中的任何一个?任何有关如何解决这种缓慢问题的建议都值得赞赏。

在查询上运行 EXPLAIN 给了我这个(但我不清楚这些结果的影响......主键上的 NULL 看起来会很糟糕,是吗?返回的结果数量对我来说似乎很高以及最后只返回少数结果):

1 PRIMARY tbl_news ALL NULL NULL NULL NULL 1318 使用 where 2 DEPENDENT SUBQUERY tbl_tag_relations ref FK_tbl_tag_tags_1 FK_tbl_tag_tags_1 4 const 179 使用 where

【问题讨论】:

  • 您可以在 tbl_news 和 tbl_tag_relations 表上发布索引吗?解释说明将使用哪些索引,但没有说明它们的组件是什么。我猜这是一个“缺少索引”的问题(尽管运行 25 个单独的查询也不好)
  • 你是对的,乔纳森。这是一个缺少索引的问题。 Cletus 指出了哪一个和他的建议导致了 10 倍的加速。

标签: php optimization mysql


【解决方案1】:

我已在 Database Development Mistakes Made by AppDevelopers 中解决了这一点。基本上,赞成加入聚合。 IN 本身不是聚合,但同样的原则适用。良好的优化将使这两个查询在性能上相当:

SELECT * FROM tbl_news WHERE news_id
 IN (select news_id from
 tbl_tag_relations WHERE tag_id = 20)

SELECT tn.*
FROM tbl_news tn
JOIN tbl_tag_relations ttr ON ttr.news_id = tn.news_id
WHERE ttr.tag_id = 20

我相信 Oracle 和 SQL Server 都可以,但 MySQL 没有。第二个版本基本上是即时的。我在我的机器上做了数十万行的测试,并通过添加适当的索引使第一个版本的性能达到亚秒级。带有索引的连接版本基本上是即时的,但即使没有索引也可以执行。

顺便说一句,我使用的上述语法是您应该更喜欢进行连接的语法。这比将它们放在WHERE 子句中(正如其他人所建议的那样)更清楚,并且上面可以使用 WHERE 条件不能的左外连接以 ANSI SQL 方式执行某些操作。

所以我会在以下内容上添加索引:

  • tbl_news (news_id)
  • tbl_tag_relations (news_id)
  • tbl_tag_relations (tag_id)

查询将几乎立即执行。

最后,不要使用 * 来选择您想要的所有列。明确命名它们。稍后添加列时,您会遇到更少的麻烦。

【讨论】:

  • +1 你的观点很好,但我认为 cletus 从来没有使用过 disabuse 之类的词:)
  • 我会说上面的语法更明确,不一定更清楚。
  • 这是个好词。 :) 唉,自从我去检查后,我不得不删除它,令人惊讶的是,MySQL 确实将 IN 与 join 区别对待(我不相信 Oracle 会这样做)。相应地进行了编辑。
  • 有趣。除了 tbl_tag_relations 上的 news_id 之外,已经为所有这些设置了键(通过 tbl_rows,我假设您的意思是 tbl_news)。仅添加该索引可将页面加载时间减半……现在为 16 秒。我很惊讶如此微小的变化会对性能产生如此巨大的影响。非常感谢...我要去阅读一下索引。
  • 呃,是的,让我的电线与其他东西交叉。固定的。我很惊讶您的查询仍然需要在秒范围内的时间。 tbl_news 和 tbl_tag_relations 有多少行?我尝试了相当于 100k 和 600k 的记录,并在 0.7 秒内得到它。当然,连接是即时的。
【解决方案2】:

SQL 查询本身绝对是您的瓶颈。该查询中有一个子查询,它是代码的 IN(...) 部分。这实际上是一次运行两个查询。您可以使用 JOIN(类似于 d03boy 上面提到的内容)或更有针对性的 SQL 查询将 SQL 时间减半(或更多!)。一个例子可能是:

SELECT * 
FROM tbl_news, tbl_tag_relations 
WHERE tbl_tag_relations.tag_id = 20 AND
tbl_news.news_id = tbl_tag_relations.news_id 

为了帮助 SQL 更快地运行,您还想尽量避免使用 SELECT *,而只选择您需要的信息;最后还要加上限制性声明。例如:

SELECT news_title, news_body 
... 
LIMIT 5;

您还需要查看数据库架构本身。确保您正在索引所有通常引用的列,以便查询运行得更快。在这种情况下,您可能需要检查 news_id 和 tag_id 字段。

最后,您需要查看 PHP 代码,看看是否可以创建一个包罗万象的 SQL 查询,而不是遍历多个单独的查询。如果您发布更多代码,我们可以提供帮助,这可能是为您发布的问题节省的最大时间。 :)

【讨论】:

  • 这不是问题。您的查询将与原始查询一样进行优化和执行。您刚刚将 IN 重写为连接,这可能是错误的,因为根本没有选择第二个表。
  • Cletus 建议的索引确实起到了作用。您对 SELECT * 的看法是正确的,我正在尝试找到一种简单的方法来解决这个问题,但是代码非常狂野,因此更改它可能会影响我尚未想象的查询...我必须小心。
【解决方案3】:

如果我理解正确,这只是列出一组特定标签的新闻报道。

  1. 首先,你真的不应该 曾经SELECT *

  2. 其次,这可能是
    在单个查询中完成, 从而降低管理费用
    多个查询。好像是 得到相当微不足道的数据,所以 它可以在一个 单个调用而不是 20 个。

  3. 使用IN 的更好方法可能是使用JOINWHERE 条件。使用IN 时,基本上会有很多OR 语句。
  4. 您的tbl_tag_relations 绝对应该在tag_id 上有一个索引

【讨论】:

  • 如果你写出这样的陈述作为答案,你应该解释原因(至少用几句话)或提供更好的例子。不是每个人都知道这些事情,OP 明确表示他/她对数据库的了解并不多。
  • 我不喜欢给出例子,坦率地说,我可以不列出字段。
  • SELECT * 不受欢迎有什么具体原因吗? SELECT * 与显式列出表中的每个字段有何不同?
  • 没有什么不同。但是,如果不查看表格,就很难知道这些字段是什么。这是糟糕的文档。这可能会导致发送更多数据,影响速度等。
【解决方案4】:
select * 
 from tbl_news, tbl_tag_relations 
 where 
      tbl_tag_relations.tag_id = 20 and 
      tbl_news.news_id = tbl_tag_relations.news_id 
 limit 20

我认为这给出了相同的结果,但我不能 100% 确定。有时简单地限制结果会有所帮助。

【讨论】:

  • 它会给出 tbl_tag_relations 中的所有字段,这在技术上不会是相同的结果,但无论如何可能只有两个字段
  • 限制结果会减少结果。
  • 哦,等等,我忘了,一开始可能没有 20 个结果,这当然是可能的,所以可能是一样的。 :)
  • 这确实提供了相同的结果并且启动速度更快!
【解决方案5】:

不幸的是,MySQL 不能很好地处理不相关的子查询,如您的案例所示。该计划基本上是说对于外部查询的每一行,都将执行内部查询。这将很快失控。正如其他人提到的那样,重写为普通的旧连接将解决该问题,但可能会导致重复行的不良影响。

例如,原始查询将为 tbl_news 表中的每个符合条件的行返回 1 行,但此查询:

SELECT news_id, name, blah
FROM tbl_news n
JOIN tbl_tag_relations r ON r.news_id = n.news_id
WHERE r.tag_id IN (20,21,22)

将为每个匹配的标签返回 1 行。您可以将 DISTINCT 粘贴在那里,根据数据集的大小,它对性能的影响应该很小。

不要太糟糕,但大多数其他数据库(PostgreSQL、Firebird、Microsoft、Oracle、DB2 等)会将原始查询作为有效的半联接处理。就我个人而言,我发现子查询语法更具可读性和更易于编写,尤其是对于较大的查询。

【讨论】:

  • 我用过 PostgreSQL,它很强大,但改变数据库根本不是一种选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-04
  • 2019-08-03
  • 1970-01-01
相关资源
最近更新 更多