低效的 SQL 查询

Posted

技术标签:

【中文标题】低效的 SQL 查询【英文标题】:Inefficient SQL Query 【发布时间】:2011-07-28 14:58:12 【问题描述】:

我正在构建一个简单的网络应用程序,我有一天会开源。就目前而言,导航是在每次页面加载时生成的(有一天会更改为缓存),但目前,它是使用下面的代码制作的。使用 php 5.2.6 和 mysqli 5.0.7.7,下面的代码效率能提高多少?我认为加入可能会有所帮助,但我正在寻求建议。任何提示将不胜感激。

<?php
    $navQuery = $mysqli->query("SELECT id,slug,name FROM categories WHERE live=1 ORDER BY name ASC") or die(mysqli_error($mysqli));
    while($nav = $navQuery->fetch_object()) 
        echo '<li>';
            echo '<a href="/'. $nav->slug .'">'. $nav->name .'</a>';
            echo '<ul>';
                $subNavQuery = $mysqli->query("SELECT id,name FROM snippets WHERE category='$nav->id' ORDER BY name ASC") or die(mysqli_error($mysqli));
                while($subNav = $subNavQuery->fetch_object()) 
                    echo '<li>';
                        echo '<a href="/'. $nav->slug .'/'. $subNav->name .'">'. $subNav->name .'</a>';
                    echo '</li>';
                
            echo '</ul>';
        echo '</li>';
    
?>

【问题讨论】:

+1 用于发布此问题;带有查询的循环很糟糕。我不擅长 JOIN 来帮助您,但还有很多其他人可以! 【参考方案1】:

它应该打印与您的示例完全相同的代码

$navQuery = $mysqli->query("SELECT t1.id AS cat_id,t1.slug,t1.name AS cat_name,t2.id,t2.name
    FROM categories AS t1
    LEFT JOIN snippets AS t2 ON t1.id = t2.category
    WHERE t1.live=1
    ORDER BY t1.name ASC, t2.name ASC") or die(mysqli_error($mysqli));

$current = false;

while($nav = $navQuery->fetch_object()) 
    if ($current != $nav->cat_id) 
        if ($current) echo '</ul>';
        echo '<a href="/'. $nav->slug .'">'. $nav->cat_name .'</a><ul>';
        $current = $nav->cat_id;
    

    if ($nav->id)  //check for empty category
        echo '<li><a href="/'. $nav->slug .'/'. $nav->name .'">'. $nav->name .'</a></li>';
    


//last category
if ($current) echo '</ul>';

【讨论】:

【参考方案2】:

除了单个组合查询之外,您还可以使用两个单独的查询。

这里有一个基本的树结构,其中包含分支元素(类别表)和叶元素(sn-ps 表)。单查询解决方案的缺点是您会为每个叶元素重复获取所有者分支元素。这是冗余信息,取决于叶子的数量以及您从每个分支元素查询的信息量,可能会产生大量额外的流量。

两个查询的解决方案如下:

$navQuery = $mysqli->query ("SELECT id, slug, name FROM categories WHERE live=1 ORDER BY name")
    or die (mysqli_error ($mysqli));
$subNavQuery = $mysqli->query ("SELECT c.id AS cid, s.id, s.name FROM categories AS c LEFT JOIN snippets AS s ON s.category=c.id WHERE c.live=1 ORDER BY c.name, s.name")
    or die (mysqli_error ($mysqli));

$sub = $subNavQuery->fetch_object ();    // pre-reading one record
while ($nav = $navQuery->fetch_object ()) 

    echo '<li>';
    echo '<a href="/'. $nav->slug .'">'. $nav->name .'</a>';
    echo '<ul>';

    while ($sub->cid == $nav->id) 

        echo '<li>';
        echo '<a href="/'. $nav->slug .'/'. $sub->name .'">'. $sub->name .'</a>';
        echo '</li>';

        $sub = $subNavQuery->fetch_object ();
     

    echo '</ul>';

【讨论】:

【参考方案3】:

您可以运行此查询:

SELECT c.id AS cid, c.slug AS cslug, c.name AS cname,
    s.id AS sid, s.name AS sname
FROM categories AS c
    LEFT JOIN snippets AS s ON s.category = c.id
WHERE c.live=1
ORDER BY c.name, s.name

然后遍历结果以创建正确的标题,例如:

// last category ID
$lastcid = 0;
while ($r = $navQuery->fetch_object ()) 

    if ($r->cid != $lastcid) 
        // new category

        // let's close the last open category (if any)
        if ($lastcid)
            printf ('</li></ul>');

        // save current category
        $lastcid = $r->cid;

        // display category
        printf ('<li><a href="/%s">%s</a>', $r->cslug, $r->cname);

        // display first snippet
        printf ('<li><a href="/%s/%s">%s</a></li>', $r->cslug, $r->sname, $r->sname);

     else 

        // category already processed, just display snippet

        // display snippet
        printf ('<li><a href="/%s/%s">%s</a></a>', $r->cslug, $r->sname, $r->sname);
    


// let's close the last open category (if any)
if ($lastcid)
    printf ('</li></ul>');

请注意,我使用了 printf,但您应该使用您自己的函数来代替 printf,但通过参数运行 htmlspecialchars(当然第一个除外)。

免责声明:我不一定鼓励使用&lt;ul&gt;s。

这段代码只是为了展示处理通过一次查询获得的分层数据的基本思想。

【讨论】:

不得不调整 UL 和 LI 标签,但理论是完美的。导航现在加载速度提高了 2 到 3 倍。谢谢,本斯! :) 为什么这个问题有赏金? @JohnP 我对其他(也许更好)解决方案感兴趣。我发布了两个方法,但它们也都有缺点。如果有更多方法,我真的很感兴趣。【参考方案4】:

首先,您不应该在视图中查询数据库。那将混合您的业务逻辑和您的表示逻辑。只需将查询结果分配给控制器中的变量并遍历它。

至于查询,是的,连接可以在 1 个查询中完成。

SELECT * -- Make sure you only select the fields you want. Might need to use aliases to avoid conflict
FROM snippets S LEFT JOIN categiries C ON S.category = C.id
WHERE live = 1
ORDER BY S.category, C.name

这将为您提供初始结果集。但这不会像您期望的那样为您提供有序排列的数据。您需要使用一些 PHP 将其分组为一些可以在循环中使用的数组。

类似

$categories = array();
foreach ($results as $result) 
   $snippet = array();
   //assign all the snippet related data into this var

  if (isset($categories[$result['snippets.category']])) 

    $categories[$result['snippets.category']]['snippet'][] = $snippet;
   else 
    $category = array();
    //assign all the category related data into this var;

    $categories[$result['snippets.category']]['snippet']  = array($snippet);
    $categories[$result['snippets.category']]['category'] = $category;
  

这应该为您提供一个类别数组,其中包含一个数组中的所有相关 sn-ps。您可以简单地循环遍历此数组来重现您的列表。

【讨论】:

MVC 不是唯一可能的方式。 是的,但是将数据层分开总是一个好主意。 @vbence 您认为这部分代码是独立的是非常危险的。 OP 明确表示他正在构建一个网络应用程序。正是受益的项目类型。 ..adding to the equation the probability of this part of DB to change.. 正是您应该有摘要的原因。否则,您最终会因为顺序更改或类似的事情而浏览所有页面。 @PaulAdamDavis 不,不,不。在使用任何类型的框架之前,您绝对必须使用 php 的“本机”工具创建一个项目。然后当出现问题时,您就会知道后台发生了什么。 我个人同意@JohnP 的解决方案,但我缺少的一件事是没有人建议为列使用正确的索引,到目前为止,请务必在实时列和类别中设置索引, @PaulAdamDavis 还请记住,如果您进行连接,则用于连接的列的顺序必须与索引定义中的列顺序匹配【参考方案5】:

我会试试这个:

SELECT
    c.slug,c.name,s.name
FROM
    categories c
LEFT JOIN snippets s
    ON s.category = c.id 
WHERE live=1 ORDER BY c.name, s.name

不过,我没有测试它。还可以使用EXPLAIN 语句检查索引,这样 MySQL 就不会对表进行完整扫描。

使用这些结果,您可以在 PHP 中循环结果并检查类别名称何时更改,并根据需要构建输出。

【讨论】:

以上是关于低效的 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

通过EXPLAIN分析低效SQL的执行计划

使用MySQL的慢查询日志找到低效的SQL语句

低效能的”where1=1”

MySql 定位和分析执行效率的方法

MYSQL诊断分析低效SQL方法

如何查找MySQL中查询慢的SQL语句