MySQL 未在子查询中使用 INDEX

Posted

技术标签:

【中文标题】MySQL 未在子查询中使用 INDEX【英文标题】:MySQL is not using INDEX in subquery 【发布时间】:2012-08-28 05:59:18 【问题描述】:

我有sqlfiddle 中定义的这些表和查询。

首先我的问题是将显示 LEFT JOINed 访问行的人与最新年份分组。我使用子查询解决了。

现在我的问题是该子查询没有使用在visits 表上定义的索引。这导致我的查询几乎无限期地在每个大约有 15000 行的表上运行。

这是查询。目标是在访问表中列出每个人的最新(按年份)记录。

不幸的是,在大型表上它变得非常缓慢,因为它没有在子查询中使用 INDEX。

SELECT *
FROM people
LEFT JOIN (
  SELECT *
  FROM visits
  ORDER BY visits.year DESC
) AS visits
ON people.id = visits.id_people
GROUP BY people.id

有谁知道如何强制 mysql 使用已在 visits 表上定义的 INDEX?

【问题讨论】:

【参考方案1】:

当您只需要一个用于连接的表名时,为什么还要有一个子查询?

我也不清楚为什么您的查询中包含GROUP BY 子句。 GROUP BY 通常与 MAXCOUNT 等聚合函数一起使用,但您没有这些。

这个怎么样?它可能会解决您的问题。

    SELECT people.id, people.name, MAX(visits.year) year
      FROM people
      JOIN visits ON people.id = visits.id_people
  GROUP BY people.id, people.name

如果您需要显示此人、最近一次访问以及最近一次访问的注释,您将不得不再次将访问表显式连接到摘要查询(虚拟表),如下所示。

SELECT a.id, a.name, a.year, v.note
  FROM (
         SELECT people.id, people.name, MAX(visits.year) year
          FROM people
          JOIN visits ON people.id = visits.id_people
      GROUP BY people.id, people.name
  )a
  JOIN visits v ON (a.id = v.id_people and a.year = v.year)

去小提琴:http://www.sqlfiddle.com/#!2/d67fc/20/0

如果您需要向从未访问过的人展示一些东西,您应该尝试将我声明中的JOIN 项目换成LEFT JOIN

正如其他人所写,子查询中的ORDER BY 子句不是标准的,并且会产生不可预测的结果。在您的情况下,它使优化器感到困惑。

编辑GROUP BY 是一把大锤子。除非你需要,否则不要使用它。而且,除非您在查询中使用聚合函数,否则不要使用它。

请注意,如果您在最近一年的某个人的访问次数超过一行,则此查询将为该人生成多行,针对该年的每次访问生成多行。如果您只需要每人一行,并且您不需要访问记录,那么第一个查询就可以解决问题。如果您在一年内对一个人进行了多次访问,而您只需要最近一次,则必须确定哪一行是最近一次。通常它将是具有最高 ID 号的那个,但只有您自己知道这一点。在这种情况下,我在您的小提琴中添加了另一个人。 http://www.sqlfiddle.com/#!2/4f644/2/0

这很复杂。但是:如果您的visits.id 编号是自动分配的,并且它们始终按时间顺序排列,则您可以简单地报告最高的访问 id,并保证您将获得最近的一年。这将是一个非常有效的查询。

SELECT p.id, p.name, v.year, v.note
  FROM (
         SELECT id_people, max(id) id
          FROM visits
      GROUP BY id_people
  )m
  JOIN people p ON (p.id = m.id_people)
  JOIN visits v ON (m.id = v.id)

http://www.sqlfiddle.com/#!2/4f644/1/0 但这不是您的示例设置方式。因此,您需要另一种方法来消除您最近访问的歧义,这样您每人只需获得一行。我们可以使用的唯一技巧是使用最大的 id 号。

因此,根据此定义,我们需要从您的表格中获取最新的 visit.id 编号列表。此查询执行此操作,其中 MAX(year)...GROUP BY(id_people) 嵌套在 MAX(id)...GROUP BY(id_people) 查询中。

  SELECT v.id_people,
         MAX(v.id) id
    FROM (
         SELECT id_people, 
                MAX(year) year
           FROM visits
          GROUP BY id_people
         )p
    JOIN visits v ON (p.id_people = v.id_people AND p.year = v.year)
   GROUP BY v.id_people

整体查询(http://www.sqlfiddle.com/#!2/c2da2/1/0)是这样的。

SELECT p.id, p.name, v.year, v.note
  FROM (
      SELECT v.id_people,
             MAX(v.id) id
        FROM (
             SELECT id_people, 
                    MAX(year) year
               FROM visits
              GROUP BY id_people
             )p
        JOIN visits v ON (     p.id_people = v.id_people 
                           AND p.year = v.year)
       GROUP BY v.id_people
      )m
   JOIN people p ON (m.id_people = p.id)
   JOIN visits v ON (m.id = v.id)

SQL 中的消歧是一门棘手的事情,因为您需要一些时间才能理解 DBMS 中的行没有固有顺序这一想法。

【讨论】:

那是因为我需要加入最近一年的访问。 MySQL GROUP BY 在找到的第一个行下进行分组。 您的查询工作正常。我什至不必创建另一个索引。困扰我的一件事是,有些人的行是两次结果。我最终通过在查询末尾添加一个额外的 GROUP BY people_id 来消除它,但这似乎会增加 CPU 负载。 检查我的编辑。对于某些人来说,您显然在同一年多次访问。【参考方案2】:

您的查询:

SELECT *
FROM people
LEFT JOIN (
  SELECT *
  FROM visits
  ORDER BY visits.year DESC
) AS visits
ON people.id = visits.id_people
GROUP BY people.id;

首先,使用非标准 SQL 语法(出现在 SELECT 列表中的项目不属于 GROUP BY 子句,不是聚合函数并且不附加在分组项目上)。这可能会给出不确定(半随机)的结果。

其次,(为了避免不确定的结果)您在子查询中添加了一个ORDER BY,该子查询(非标准或非标准)在 MySQL 文档中的任何地方都没有记录,它应该按预期工作。因此,当您升级到 MySQL 版本 X 时,它现在可能正在工作,但它可能不会在不久的将来工作(优化器将足够聪明地理解派生内部的 ORDER BY表是多余的,可以去掉)。

尝试使用此查询:

SELECT 
    p.*, v.*
FROM 
    people AS p
  LEFT JOIN 
        ( SELECT 
              id_people
            , MAX(year) AS year
          FROM
              visits
          GROUP BY
              id_people
         ) AS vm
      JOIN
          visits AS v
        ON  v.id_people = vm.id_people
        AND v.year = vm.year 
    ON  v.id_people = p.id;

该:SQL-fiddle

(id_people, year) 上的复合索引将有助于提高效率。


另一种方法。如果您先将人员限制在合理的限制(例如 30 人),然后加入 visits 表,则效果很好:

SELECT 
    p.*, v.*
FROM 
    ( SELECT *
      FROM people
      ORDER BY name
        LIMIT 30
    ) AS p
  LEFT JOIN 
    visits AS v
      ON  v.id_people = p.id
      AND v.year =
    ( SELECT 
          year
      FROM
          visits
      WHERE
          id_people = p.id
      ORDER BY
          year DESC
        LIMIT 1
     )  
ORDER BY name ;

【讨论】:

这似乎适用于 JOIN,但我需要使用 LEFT JOIN 来列出没有访问的人。如果我这样做,p 表将获得 NULL 键,我将永远等待。 仍然很慢,抱歉。不过,@Ollie Jones 的解决方案似乎运行良好。 是的。我已经在visits.id_people 甚至(id_people,year)上尝试过INDEX。没有任何帮助。 表是 MyISAM 还是 InnoDB? 让我们continue this discussion in chat

以上是关于MySQL 未在子查询中使用 INDEX的主要内容,如果未能解决你的问题,请参考以下文章

在子查询 JOIN 中引用外部查询

SQL Server - 在子查询中使用列别名

生产库中遇到mysql的子查询

如何以优化的方式在子查询中选择COUNT

SQL Server - 在子查询中使用主查询中的列

在子查询中使用聚合和窗口函数