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

Luc*_*oli 2 mysql sql 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
Run Code Online (Sandbox Code Playgroud)

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

谢谢

vla*_*adr 5

方法1 - 改进原始查询

我很确定在这种情况下是一个INNER JOIN意志就足够了,没有理由做RIGHT JOIN(如果它中id存在的comm也存在comments). INNER JOINs 可以带来更好的性能.

而且,你真的想要推动LIMIT 10内部comm(顺便说一句,将它与它保持在一起ORDER BY):

  • 对于一个,保持LIMIT 10ORDER BY在一起不会得到你最近发布的十个主题(comm子查询的顺序不一定会保留到你正在的最终结果中LIMIT.)
  • 另外,强制执行最LIMIT里面的聚合子查询将鼓励基于成本的优化器支持嵌套循环(确切地说是10),而不是哈希合并连接(对于任何可以调整大小的表,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
Run Code Online (Sandbox Code Playgroud)

最后,用于EXPLAIN查看查询正在执行的操作.不要忘记检查是否已创建索引comments.id以帮助JOIN嵌套循环.

方法2 - 一种不同的方法

请注意,虽然上述查询仍然可能比原始查询更快,但如果最内层comm子查询导致全表扫描,则最内部子查询可能仍然是一个重要的瓶颈comments.这真的取决于数据库如何聪明是当它看到GROUP BY,ORDER 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
Run Code Online (Sandbox Code Playgroud)

在大多数情况下(也就是说,如果你的应用程序的数据库驱动程序在给你后退控制之前不会尝试将所有行缓冲到内存中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
Run Code Online (Sandbox Code Playgroud)

当您第一次运行查询时,请使用0for :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 10更经常远远INSERTS插入comments表中,考虑把更多的工作到INSERT做出SELECT更快.因此,您可以将索引updated_at列添加到topicsetc表中,并且每当您INSERTcomments表中的注释时,还要考虑将相应主题的updated_at值更新为NOW().然后,您可以轻松地选择10个最近更新的主题(一个简单的和短期索引扫描updated_at返回10行),与内侧连接comments表,以获得MAX(id)对那些10个主题(远比得到更有效的MAX(id)所有采摘十大之前的话题,就像在原始和方法1)中一样,然后再次内部连接comments以获得那些10的其余列值.

我想到方法4的总体性能可与方法2和3方法可比4将不得不使用,如果你需要得到任意主题(例如,通过它们拼版,LIMIT 10 OFFSET 50),或者如果主题或意见都可以在去除(必要无变化支持主题删除;为了正确支持注释删除,updated_at应该在注释INSERT和主题的最新未删除注释DELETEcreated_at值上更新主题.)