使用GROUP BY进行SELECT的更好方法

时间:2010-03-26 23:29:19

标签: sql mysql performance join group-by

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

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

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

由于

1 个答案:

答案 0 :(得分:5)

方法1 - 改进原始查询

我很确定在这种情况下 INNER JOIN就足够了,没有理由做RIGHT JOIN(如果id存在comm它也会存在comments)。 INNER JOIN s 可能会产生better performance

此外,你真的想推动LIMIT 10comm(顺便提一下,与ORDER BY保持一致):

  • 为一个,LIMIT 10ORDER BY放在一起将为您提供最近发布的十个主题(排序comm子查询不一定会保留到LIMIT的最终结果中。)
  • 此外,在最里面的聚合子查询中强制执行LIMIT将鼓励基于成本的优化器优先于nested loopshash merge joins(确切地说是10) (对于任何大小合适的comments表,10个嵌套循环到目前为止最快。)

因此,您的查询应该重写为:

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子查询可能仍然会成为一个重大瓶颈,如果它导致全表扫描comments。这实际上取决于数据库在看到GROUP BYORDER BYLIMIT时的智能程度。

如果EXPLAIN表示子查询正在进行表扫描,那么您可以尝试 SQL和应用程序级逻辑的组合以获得最佳性能假设我已正确理解您的要求,并且您希望确定在十个不同主题中发布的十条最新评论

# 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 - 混合方法

如果十秒前我知道最近的十条评论是什么,当我稍后再问这个问题时,我能否明智一点? 除非可以从数据库中删除评论,否则答案是肯定的,因为我知道,当我再次提出问题时,所有评论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 - 又一种方法

如果您希望SELECT ... ORDER BY id DESC LIMIT 10INSERT表更频繁地执行comments,请考虑在INSERT中加入更多工作来制作SELECT updated_at 1}}更快。因此,您可以在topics等表格中添加已编入索引的INSERT列,并且只要commentsupdated_at表发表评论,也请考虑更新相应主题的NOW() 1}}值为updated_at。然后,您可以轻松选择10个最近更新的主题(comments返回10行的简单和短索引扫描),内部加入MAX(id)表以获取这些主题的MAX(id)主题(在选择十个最好的主题之前获得所有主题的comments效率更高,比如在原始和方法1中),然后再次在LIMIT 10 OFFSET 50上进行内部联接以获得那些10的其余列值。

我希望方法4的整体性能与方法2和3相当。如果您需要获取任意主题(例如,通过对它们进行分页,updated_at)或主题或评论,则必须使用方法4可以删除(不支持删除主题所需的更改;为了正确支持删除评论,应该使用INSERT值更新评论DELETEcreated_at上的主题{{1}}该主题的最新未删除评论。)