MySQL 中不允许在递归公用表表达式中使用 LIMIT

Posted

技术标签:

【中文标题】MySQL 中不允许在递归公用表表达式中使用 LIMIT【英文标题】:Use of LIMIT in Recursive Common Table Expression is not allowed in MySQL 【发布时间】:2020-10-07 06:34:51 【问题描述】:

我的目标是使用最新的 mysql 的 WITH RECURSIVE 方法构造一棵树。

我的表名为categories,有 2 行。 IDparentID row

我的分类表:

 . ID . | ParentID   
--------|----------
 . 1  . | null  
 . 2  . |  1
 . 3  . |  1  
 . 4  . |  1
 . 6  . |  1
 . 7  . |  1
 . 8  . |  1
 . 9  . |  1
 . 10 . |  1
 . 11 . |  13
 . 12 . |  14
 . 13 . |  12     
 .... . | ...

从 2 到 9 的 ID 具有相同的父级,即 ID = 1 的父级。这是我试图通过在递归公用表表达式的第二个 SELECT 查询中提供“LIMIT 5”来限制的。

上表在树中的光学表示如下所示: 我的问题是限制同一​​级别的孩子的数量(在下图中标记为项目 Y)。

+ Item X .............. (level 1)       
  + Item Y .............. (level 2)  
  + Item Y .............. (level 2)   
  + Item Y .............. (level 2) 
  + .... LIMIT to 5 Items 
+ Item X
    + Item X
      + Item X
         + Item X
             + Item X  
+ Item X

这是我的 mySQL Recursive Common Table Expression 使用 LIMIT 子句导致问题的查询:

WITH RECURSIVE cte AS
(
  SELECT ID, 0 AS depth, CAST(ID AS CHAR(200)) AS path
    FROM categories WHERE parentID = 1
  UNION ALL
  SELECT c.ID, cte.depth+1, CONCAT(cte.path, ',', c.ID)
    FROM categories c 
    JOIN cte ON cte.ID = c.parentID
    WHERE FIND_IN_SET(c.ID, cte.path)=0 AND depth <= 10
    LIMIT 5
)

 SELECT * FROM cte

从逻辑上讲,我希望通过在 CTE 的第二个 Select 部分中使用 LIMIT 子句来限制第二个 SELECT 语句返回的行数来对我的问题进行排序。但它给了我一个错误:

This version of MySQL doesn't yet support 'ORDER BY / LIMIT over UNION in recursive Common Table Expression'

请注意,我使用的是 MySQL 8.0 + 版本。我明白错误很明显。但是,如果我在同一个父母之下有 100 万个孩子呢?它会冻结系统!

我将不胜感激。

谢谢。

【问题讨论】:

好问题! +1 虽然 SQL 标准可以实现你想要的高效检索,但遗憾的是 MySQL 还没有实现这个功能。 【参考方案1】:

如果我正确地关注了你,row_number() 可以做你想做的事。这个想法是在递归部分枚举categories 行,然后过滤前5个:

with recursive cte as (
    select id, 0 as depth, cast(id as char(200)) as path
    from categories 
    where parentid = 1
    union all
    select c.id, cte.depth+1, concat(cte.path, ',', c.id)
    from cte
    inner join (
        select c.*, row_number() over(partition by c.parentid order by c.id) rn
        from categories c 
    ) c on cte.id = c.parentid
    where find_in_set(c.id, cte.path) = 0 and depth <= 10 and c.rn <= 5
)
select * from cte

您可以通过对数据集进行预过滤来稍微优化一下:

with recursive 
    cats as (
        select *
        from (
            select c.*, row_number() over(partition by parentid order by id) rn
            from categories c 
        ) t
        where rn <= 5
    ),
    cte as (
        select id, 0 as depth, cast(id as char(200)) as path
        from cats 
        where parentid = 1
        union all
        select c.id, cte.depth+1, concat(cte.path, ',', c.id)
        from cte
        inner join cats c on cte.id = c.parentid
        where find_in_set(c.id, cte.path) = 0 and depth <= 10 and c.rn <= 5
    )
select * from cte

【讨论】:

虽然我同意这可行,但我怀疑它的效率。 @TheImpaler:我添加了另一个带有预过滤的选项。 find_in_set() 条件应用在错误的级别。 @GMB 不幸的是我不能接受这个答案。原因是它会遍历每一个条目以将其标记为“ over(partition by parentid order by id)”。如果表中有 100 万行,即使您想 LIMIT 只返回最多 10 行(最新的 MySQL 版本允许 LIMIT 递归 cte),它也会遍历 100 万行。

以上是关于MySQL 中不允许在递归公用表表达式中使用 LIMIT的主要内容,如果未能解决你的问题,请参考以下文章

MySQL基础--09---MySQL8新特性简述----公用表表达式

使用公用表表达式避免重复递归

SQL Server 公用表表达式(CTE)实现递归

SQL Server 公用表表达式(CTE)实现递归

SQL Server中公用表表达式(CTE)递归的生成帮助数据

SQL With (递归CTE查询)