如何改进这个Mysql查询?

时间:2013-07-22 19:26:29

标签: php mysql optimization query-optimization greatest-n-per-group

这个查询试图做一些mysql不容易做的事情,即限制每组的行数。 user_id's的列表将传递给查询,并返回一些但该组需要限制为每组4行。该查询有效,但根据Sequel Pro,有点慢200-500毫秒。

在标记之前继续阅读!!

SELECT id, user_id, article_id, row_number
FROM (
    SELECT a2.id, a2.user_id, a2.post_id,
        @num:= if(@group = a2.user_id, @num + 1, 1) as row_number
    FROM (
        SELECT a1.id, a1.user_id, a1.post_id
        FROM articles as a1
        WHERE a1.user_id IN (3,14,1,2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,38,39,13,114,1111,12,223,2234,225,226,227,228,229,2210)
        ORDER BY a1.date DESC
    ) as a2, 
    (SELECT @num := 0) t
) as f
WHERE row_number <= 4;

此查询的EXPLAIN是:

id  select_type table   type    possible_keys   key key_len ref rows    Extra
1   PRIMARY <derived2>  ALL         NULL    NULL    NULL    NULL    10516   Using where
2   DERIVED <derived4>  system      NULL    NULL    NULL    NULL    1   
2   DERIVED <derived3>  ALL         NULL    NULL    NULL    NULL    10516   
4   DERIVED NULL        NULL        NULL    NULL    NULL    NULL    NULL    No tables used
3   DERIVED s1          ALL         Reco... NULL    NULL    NULL    1180931 Using filesort

我考虑过将其分解为多个查询,但我似乎仍然遇到将每个组结果限制为4的问题。总而言之,我试图避免大量查询。昂贵的查询。

关于通过分解并将部分内容转移到应用程序中来提高查询速度的最佳方法的任何想法

3 个答案:

答案 0 :(得分:1)

要回答您的问题,我没有看到任何有效的方法来“分解”此查询。您仍然需要弄清楚来自那个user_id(@group)的文章是否按日期连续,而没有来自其他user_id之一的干预帖子。按日期排序所有行将是最好的方法。

如果要删除的行数是行的大部分,则在客户端过滤这些行将需要向客户端发送更大的结果集。但是如果它被过滤掉的一小部分行,则会将所有行(对于列表中的所有用户)传输到客户端以便处理更具吸引力。

SELECT a.id
     , a.user_id
     , a.post_id
  FROM articles a
 WHERE a.user_id IN (3,14,1,2,3,4,5,6,7,8,9,10,11,12,...)
 ORDER BY a.date DESC

然后客户端可以获取行,检查单个user_id(@group)的连续行序列,并忽略第五行,第六行等行,直到找到具有不同user_id的行。

如果结果集的规范不同,则可以将其分解。但是现在编写查询的方式,需要组合来自任何“分解”查询的结果集,以便获得当前查询当前返回的相同结果集。


(此查询与Marc B标记为可能重复的问题中的查询显着不同。)

这是一个奇怪的结果集;我们没有看到@group在语句中被赋值的任何地方,因此可能是在执行此语句之前设置的。所以,表达式

@group = a2.user_id

测试user_id是否等于常数。这意味着查询正在识别由单个user_id发布的articles中的行,并且每当该用户连续发布两个(或更多)文章时递增row_number,而没有任何其他user_id发布的干预文章IN列表(按DATE列的顺序)。另一个user_id(在IN列表中)发布的文章会将计数器重置为1.

净效果是此查询返回IN列表中指定的所有用户的所有文章,除了单个user_id(可能在列表中,也可能不在列表中)。每当有五篇或更多文章连续发布时一个单独的常量user_id,IN列表中没有来自另一个user_id的介入文章......每当发生这种情况时,查询只保留指定user_id中的前四个(最近四行)连续文章行。

如果date列是DATE数据类型,没有时间组件,那么您将更有可能拥有多个具有相同日期的行。并且在date列之外没有指定排序,因此结果集是不确定的。 (也就是说,可以存在满足ORDER BY的同一组行的多个序列。)它也与DATETIME不确定,但是如果这些值中的大多数包括唯一时间组件(即除了常量之外的其他组件)午夜),那不太可能是一个问题。

有什么奇怪之处在于,可以通过两种方式对同一组行进行排序,并给出不同的结果。假设@group识别用户'abc':

Date       user   id        Date       user   id
---------- ------ --        ---------- ------ --
2103-07-22 abc     1        2103-07-22 abc     1
2103-07-22 abc     2        2103-07-22 abc     2
2103-07-22 abc     3        2103-07-22 abc     3
2103-07-22 EFGHI   4        2103-07-22 abc     5
2103-07-22 abc     5        2103-07-22 abc     6
2103-07-22 abc     6        2103-07-22 abc     7
2103-07-22 abc     7        2103-07-22 EFGHI   4

7 rows selected.            5 rows selected.

两个结果集都与规范一致,因此可以返回任何一个。

返回结果集是没有错的。这有点奇怪。


就性能而言,具有前导列(user_id)的索引可能适用于WHERE子句中的谓词,如果这样可以消除大部分行。

或者,前导列为(date,user_id)的索引可能更合适,因为MySQL可以避免“使用filesort”操作,并按降序日期顺序检索行,然后使用在访问行时对user_id进行谓词。

实际上,列(date, user_id, post_id, id)上的覆盖索引可能更有用。

答案 1 :(得分:0)

这里有一点假设 - 如果您要为给定的用户列表中的每个用户列出最新的4篇文章,我认为您的查询可能会更好:

SET @gr=0, @row=0;
SELECT 
    id,user_id,post_id,row_number
FROM
    (SELECT 
        id,
            user_id,
            post_id,
            @row:=if(user_id <> @gr, 0, @row + 1) as row_number,
            @gr:=user_id
    FROM
        articles
    WHERE
        user_id IN (3 , 14, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 38, 39, 13, 114, 1111, 12, 223, 2234, 225, 226, 227, 228, 229, 2210)
    ORDER BY user_id , date DESC) as a1
WHERE
    row_number < 4

答案 2 :(得分:0)

可以避免使用变量。

加入表格,加入用户ID和日期,查找日期大于或等于的所有文章。然后获取按实际需要的字段分组的匹配文章的数量,并丢弃那些计数超过4的文章。

没有经过类似的测试。

SELECT a1.id, a1.user_id, a1.post_id, COUNT(a1_plus.id) AS other_count
FROM articles as a1
INNER JOIN articles a1_plus
ON a1.user_id = a1_plus.user_id
AND a1.date <= a1_plus.date
WHERE a1.user_id IN (3,14,1,2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,38,39,13,114,1111,12,223,2234,225,226,227,228,229,2210)
GROUP BY a1.id, a1.user_id, a1.post_id
HAVING other_count <= 4