这里有很多类似的问题,但我认为没有一个能充分回答这个问题。

如果可以的话,我将从当前最流行的问题继续,用他们的例子。

这个实例中的任务是获取数据库中每个作者的最新文章。

示例查询产生了不可用的结果,因为它并不总是返回的最新帖子。

SELECT wp_posts.* FROM wp_posts
    WHERE wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
    GROUP BY wp_posts.post_author           
    ORDER BY wp_posts.post_date DESC

目前公认的答案是

SELECT
    wp_posts.*
FROM wp_posts
WHERE
    wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author
HAVING wp_posts.post_date = MAX(wp_posts.post_date) <- ONLY THE LAST POST FOR EACH AUTHOR
ORDER BY wp_posts.post_date DESC

不幸的是,这个答案显然是错误的,在许多情况下产生的结果不如原始查询稳定。

我的最佳解决方案是使用表单的子查询

SELECT wp_posts.* FROM 
(
    SELECT * 
    FROM wp_posts
    ORDER BY wp_posts.post_date DESC
) AS wp_posts
WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author 

我的问题很简单: 有没有办法在分组之前对行进行排序而不诉诸子查询?

编辑:这个问题是另一个问题的延续,我的具体情况略有不同。您可以(也应该)假设还有一个wp_posts。Id,是该特定帖子的唯一标识符。


当前回答

不确定这是否已经被建议,但你现在可以使用SQL窗口函数:

SELECT * FROM (
      SELECT wp_posts.*, ROW_NUMBER() OVER (PARTITION BY wp_posts.post_author ORDER BY post_date DESC) rank
      FROM wp_posts
      WHERE wp_posts.post_status = 'publish'
      AND wp_posts.post_type = 'post'
  ) AS T
WHERE rank = 1

所有行都被“排名”,然后你只需要选择每个第一行。

我承认我对表现没有任何了解,但据我所知,这应该是可以接受的。

其他回答

以下是我使用用户定义变量获得一致结果的解决方案,甚至没有GROUP BY。我们的目标是获取整个行,而不仅仅是一行中一个单元格的最大值。请看下面的例子:

SET @product_id := 0;

SELECT
    products.order_code,
    purchases.`date`,
    purchases.price
FROM products
LEFT JOIN (       
    SELECT
        purchases.`date`,
        purchases.price,
        IF(@product_id = purchases.product_id, 0, 1) AS is_last,
        @product_id := purchases.product_id AS product_id
    FROM purchases
    ORDER BY purchases.product_id ASC, purchases.id DESC
) purchases ON products.id = purchases.product_id
WHERE purchases.is_last = 1
ORDER BY products.order_code ASC;

我不确定性能如何,但在50000行购买表上它是0.1秒。如果我能做些什么改进,请告诉我。

不确定这是否已经被建议,但你现在可以使用SQL窗口函数:

SELECT * FROM (
      SELECT wp_posts.*, ROW_NUMBER() OVER (PARTITION BY wp_posts.post_author ORDER BY post_date DESC) rank
      FROM wp_posts
      WHERE wp_posts.post_status = 'publish'
      AND wp_posts.post_type = 'post'
  ) AS T
WHERE rank = 1

所有行都被“排名”,然后你只需要选择每个第一行。

我承认我对表现没有任何了解,但据我所知,这应该是可以接受的。

你将要读到的内容相当俗气,所以不要在家尝试!

在SQL中,你的问题的答案通常是NO,但由于GROUP BY的放松模式(@bluefeet提到过),在MySQL中答案是YES。

假设,你有一个BTREE索引(post_status, post_type, post_author, post_date)。索引在引擎盖下看起来如何?

(post_status='publish', post_type='post', post_author='user A', post_date='2012-12-01') (post_status='publish', post_type='post', post_author='user A', post_date='2012-12-31') (post_status='publish', post_type='post', post_author='user B', post_date='2012-10-01') (post_status='publish', post_type='post', post_author='user B', post_date='2012-12-01')

也就是说,数据是由所有这些字段按升序排序的。

当你在默认情况下执行GROUP BY时,它会根据分组字段(在我们的例子中是post_author;post_status, post_type是WHERE子句所需要的),如果有匹配的索引,它将按升序获取每条第一个记录的数据。也就是说,该查询将获取以下内容(每个用户的第一篇文章):

(post_status='publish', post_type='post', post_author='user A', post_date='2012-12-01') (post_status='publish', post_type='post', post_author='user B', post_date='2012-10-01')

但是MySQL中的GROUP BY允许显式地指定顺序。当你以降序请求post_user时,它会以相反的顺序遍历我们的索引,仍然取每个组的第一条记录,实际上是最后一条。

这是

...
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC

会给我们

(post_status='publish', post_type='post', post_author='user B', post_date='2012-12-01') (post_status='publish', post_type='post', post_author='user A', post_date='2012-12-31')

现在,当您按照post_date对分组结果进行排序时,您将得到所需的数据。

SELECT wp_posts.*
FROM wp_posts
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC
ORDER BY wp_posts.post_date DESC;

NB:

对于这个特定的查询,我不建议使用这种方法。在这种情况下,我会使用@bluefeet建议的稍微修改版本。但是这个技巧可能非常有用。看一下我的回答:检索每个组中的最后一条记录

缺陷:这种方法的缺点是

查询的结果取决于索引,这违背了SQL的精神(索引应该只加速查询); 索引不知道它对查询的影响(您或其他人将来可能会发现索引太消耗资源,并以某种方式更改它,破坏查询结果,而不仅仅是它的性能) 如果您不理解查询是如何工作的,很可能在一个月内就会忘记解释,查询将使您和您的同事感到困惑。

其优势在于在困难情况下的性能。在这种情况下,查询的性能应该与@bluefeet的查询相同,因为涉及到排序的数据量(所有数据都加载到一个临时表中,然后排序;顺便说一句,他的查询需要(post_status, post_type, post_author, post_date)索引以及)。

我的建议:

正如我所说,这些查询使MySQL在临时表中浪费时间对潜在的大量数据进行排序。如果需要分页(即涉及到LIMIT),大部分数据甚至会被丢弃。我要做的是最小化排序数据的数量:即对子查询中的数据进行排序和限制,然后连接回整个表。

SELECT * 
FROM wp_posts
INNER JOIN
(
  SELECT max(post_date) post_date, post_author
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) p2 USING (post_author, post_date)
WHERE post_status='publish' AND post_type='post';

同样的查询,使用上面描述的方法:

SELECT *
FROM (
  SELECT post_id
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author DESC
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) as ids
JOIN wp_posts USING (post_id);

所有这些查询及其在SQLFiddle上的执行计划。

**子查询在使用大型数据集时可能会对性能产生不良影响**

原始查询

SELECT wp_posts.*
FROM   wp_posts
WHERE  wp_posts.post_status = 'publish'
       AND wp_posts.post_type = 'post'
GROUP  BY wp_posts.post_author
ORDER  BY wp_posts.post_date DESC; 

修改查询

SELECT p.post_status,
       p.post_type,
       Max(p.post_date),
       p.post_author
FROM   wp_posts P
WHERE  p.post_status = "publish"
       AND p.post_type = "post"
GROUP  BY p.post_author
ORDER  BY p.post_date; 

因为我在选择子句中使用Max ==> Max (p.post_date),可以避免子选择查询和按组后的Max列排序。

简单回顾一下,标准的解决方案使用了一个不相关的子查询,看起来像这样:

SELECT x.*
  FROM my_table x
  JOIN (SELECT grouping_criteria,MAX(ranking_criterion) max_n FROM my_table GROUP BY grouping_criteria) y
    ON y.grouping_criteria = x.grouping_criteria
   AND y.max_n = x.ranking_criterion;

如果你正在使用一个古老版本的MySQL,或者一个相当小的数据集,那么你可以使用以下方法:

SELECT x.*
  FROM my_table x
  LEFT
  JOIN my_table y
    ON y.joining_criteria = x.joining_criteria
   AND y.ranking_criteria < x.ranking_criteria
 WHERE y.some_non_null_column IS NULL;