【问题标题】:Better way to do SELECT with GROUP BY使用 GROUP BY 进行 SELECT 的更好方法
【发布时间】:2011-02-01 09:59:12
【问题描述】:

您好,我写了一个有效的查询:

SELECT `comments`.* FROM `comments` 
RIGHT JOIN (SELECT MAX( id ) AS id, core_id, topic_id 
FROM comments GROUP BY core_id, topic_id order by id desc) comm 
ON comm.id = comments.id LIMIT 10

我想知道是否可以(以及如何)重写它以获得更好的性能。

谢谢

【问题讨论】:

    标签: sql mysql performance join group-by


    【解决方案1】:

    方法一——改进原始查询

    我很确定在这种情况下 INNER JOIN 就足够了,没有理由做一个RIGHT JOIN(如果id 存在于comm 它也将存在于comments)。 INNER JOINs 可以导致better performance

    此外,你真的想把LIMIT 10 推到comm 里面(顺便说一下,把它和ORDER BY 放在一起):

    • 一方面,LIMIT 10ORDER BY放在一起不会为您提供十个最近发布的主题(comm的排序子查询不一定会保留在您LIMITing 的最终结果中。)
    • 此外,在最里面的聚合子查询中强制执行LIMIT 将鼓励基于成本的优化器偏爱nested loops(准确地说是10)而不是hashmerge joins(10 个嵌套循环是迄今为止最快的任何可观大小的comments表。)

    所以,你的查询应该重写为:

    SELECT `comments`.* FROM `comments` 
    INNER JOIN (
     SELECT MAX( id ) AS id, core_id, topic_id 
     FROM comments
     GROUP BY core_id, topic_id
     ORDER BY id DESC
     LIMIT 10
    ) comm 
    ON comm.id = comments.id
    ORDER BY comments.id
    

    最后,使用EXPLAIN 来查看查询在做什么。不要忘记检查您是否已在 comments.id 上创建索引以帮助处理 JOIN 嵌套循环。

    方法 2 - 不同的方法

    请注意,虽然上述查询仍可能比您的原始查询更快,最里面的comm 子查询可能仍然是一个重要的瓶颈,如果它导致@ 的全表扫描987654346@。这实际上取决于数据库在同时看到GROUP BYORDER BYLIMIT 时的智能程度。

    如果EXPLAIN表示子查询正在做表扫描,那么你可以尝试结合使用SQL和应用级逻辑以获得最佳性能假设我已经理解您的要求正确,并且您想确定十个不同主题中最近发布的十个 cmets

    # pseudo-code
    core_topics_map = { }
    command = "SELECT * FROM comments ORDER BY id DESC;"
    command.execute
    # iterate over the result set, betting that we will be able to break
    #  early, bringing only a handful of rows over from the database server
    while command.fetch_next_row do
      # have we identified our 10 most recent topics?
      if core_topics_map.size >= 10 then
        command.close
        break
      else
        core_topic_key = pair(command.field('core_id'), command.field('topic_id'))
        if not defined?(core_topics_map[core_topic_key]) then
          core_topics_map[core_topic_key] = command.field('id')
        end
      end
    done
    # sort our 10 topics in reverse chronological order
    sort_by_values core_topics_map
    

    在大多数情况下(即,假设您的应用程序的数据库驱动程序在将控制权从execute 交还给您之前不会尝试为您将所有行缓冲到内存中),上述内容只会获取少数行,始终使用一个索引,不涉及表扫描。

    方法 3 - 混合方法

    如果十秒前我知道最近的十个 cmets 是什么,我以后再问这个问题时能聪明点吗? 除非可以从数据库中删除cmets,那么答案是肯定的,因为我知道,当我再次提问时,所有评论ID都会大大大于或等于我得到的最早的评论ID在我的最后一个查询中。

    因此,我可以使用附加条件 WHERE id >= :last_lowest_id:

    将最里面的查询重写为更具选择性
    SELECT `comments`.* FROM `comments` 
    INNER JOIN (
     SELECT MAX( id ) AS id, core_id, topic_id 
     FROM comments
     WHERE id >= :last_lowest_id
     GROUP BY core_id, topic_id
     ORDER BY id DESC
     LIMIT 10
    ) comm 
    ON comm.id = comments.id
    ORDER BY comments.id
    

    当您第一次运行查询时,使用0 代替:last_lowest_id。该查询将按降序返回最多 10 行。在您的应用程序中,搁置最后一行的id,并在下次运行查询时将其值重新用作:last_lowest_id,然后重复(再次,搁置id最新查询返回的最后一行等。)这实质上会使查询增量且非常快

    例子:

    • 第一次运行查询,:last_lowest_id 设置为 0
      • 返回 10 行 ID:129, 100, 99, 88, 83, 79, 78, 75, 73, 70
      • 保存70
    • 第二次运行查询,:last_lowest_id 设置为70
      • 返回 10 行 ID:130, 129, 100, 99, 88, 83, 79, 78, 75, 73
      • 保存73

    方法 4 - 另一种方法

    如果您希望在comments 表中执行SELECT ... ORDER BY id DESC LIMIT 10INSERTs 更频繁,请考虑在INSERT 中添加更多工作以使SELECT 更快。因此,您可以将索引的updated_at 列添加到您的topics 等表中,并且每当您INSERTcomments 表进行评论时,请考虑将相应主题的updated_at 值更新为NOW()。然后,您可以轻松选择 10 个最近更新的主题(对 updated_at 进行简单而简短的索引扫描,返回 10 行),与 comments 表进行内部连接以获得这 10 个主题的 MAX(id)(比为 所有 主题获取 MAX(id),然后再选择 10 个最大的主题,就像在原始主题和方法 1 中一样),然后再次在 comments 上进行内部连接以获取这 10 个的其余列值。 p>

    我希望方法 4 的整体性能可以与方法 2 和 3 相媲美。如果您需要获取任意主题(例如,通过对它们进行分页,LIMIT 10 OFFSET 50)或者如果主题或 cmets 可以已删除(无需更改即可支持主题删除;为了支持正确删除评论,则应在评论 INSERTDELETE 上更新主题的 updated_at,并使用该主题的最新未删除评论的 created_at 值.)

    【讨论】:

      猜你喜欢
      • 2013-07-05
      • 2014-10-22
      • 1970-01-01
      • 2023-04-02
      • 2010-12-05
      • 1970-01-01
      • 1970-01-01
      • 2018-05-08
      • 1970-01-01
      相关资源
      最近更新 更多