面试问题:一个语句中的SQL递归

时间:2010-11-15 02:32:53

标签: sql

我认识的人去参加面试并得到以下问题要解决。我已经考虑了几个小时,并且相信不可能没有使用一些特定于数据库的扩展或来自最近标准的功能,这些扩展或功能还没有得到广泛的支持。

我不记得所代表的背后的故事,但它并不重要。简单来说,您试图表示唯一数字链:

chain 1: 1  -> 2  -> 3
chain 2: 42 -> 78
chain 3: 4
chain 4: 7  -> 8  -> 9
...

此信息已在下表结构中存储:

id | parent
---+-------
1  | NULL
2  | 1
3  | 2
42 | NULL
78 | 42
4  | NULL
7  | NULL
8  | 7
9  | 8

可能有数百万个这样的链,每个链可以有无限数量的条目。目标是创建一个包含完全相同信息的第二个表,但是第三个列包含链的起始点:

id | parent | start
---+--------+------
1  | NULL   | 1
2  | 1      | 1
3  | 2      | 1
42 | NULL   | 42
78 | 42     | 42
4  | NULL   | 4
7  | NULL   | 7
8  | 7      | 7
9  | 8      | 7

声明(由采访者提出)是只需2个SQL查询即可实现。他们提供的提示是首先使用根元素填充目标表(我称之为dst),如下所示:

INSERT INTO dst SELECT id, parent, id FROM src WHERE parent IS NULL

这将为您提供以下内容:

id | parent | start
---+--------+------
1  | NULL   | 1
42 | NULL   | 42
4  | NULL   | 4
7  | NULL   | 7

他们说你现在可以再执行一次查询来达到上面显示的目标。

在我看来,你可以做两件事之一。在源表中使用递归到达每个链的前面,或者在每次更新到dst之后连续执行某个版本的SELECT start FROM dst WHERE dst.id = src.parent(即无法缓存结果)。

我不认为MySQL,PostgreSQL,SQLite等常见数据库都支持这些情况。我知道在PostgreSQL 8.4中你可以使用WITH RECURSIVE查询实现递归,而在Oracle中你有START WITHCONNECT BY条款。关键是这些东西特定于数据库类型和版本。

有没有办法在一个查询中使用常规SQL92来实现所需的结果?我能做的最好的事情是使用以下内容填充第一个孩子的开始列(也可以使用LEFT JOIN来实现相同的结果):

INSERT INTO dst
    SELECT s.id, s.parent,
        (SELECT start FROM dst AS d WHERE d.id = s.parent) AS start 
    FROM src AS s
    WHERE s.parent IS NOT NULL

如果在每次插入到dst之后有某种方法重新执行内部select语句,那么问题就会解决。

3 个答案:

答案 0 :(得分:4)

它不能在遵循ANSI SQL 92的任何静态SQL中实现。

但正如你所说,使用oracle的CONNECT BY

可以很容易地实现它
    SELECT id,
           parent,
           CONNECT_BY_ROOT id
      FROM table
START WITH parent IS NULL
CONNECT BY PRIOR id = parent

答案 1 :(得分:2)

在SQL Server中,您将使用公用表表达式(CTE)。

为了复制存储的数据,我创建了一个临时表

-- Create a temporary table
CREATE TABLE #SourceData
(
    ID INT
    , Parent INT
)

-- Setup data (ID, Parent, KeyField)
INSERT INTO #SourceData VALUES (1,  NULL);
INSERT INTO #SourceData VALUES (2,  1);
INSERT INTO #SourceData VALUES (3,  2);
INSERT INTO #SourceData VALUES (42, NULL);
INSERT INTO #SourceData VALUES (78, 42);
INSERT INTO #SourceData VALUES (4,  NULL);
INSERT INTO #SourceData VALUES (7,  NULL);
INSERT INTO #SourceData VALUES (8,  7);
INSERT INTO #SourceData VALUES (9,  8);

然后我创建CTE来编译数据结果:

-- Perform CTE
WITH RecursiveData (ID, Parent, Start) AS
(
    -- Base query
    SELECT  ID, Parent, ID AS Start
    FROM    #SourceData
    WHERE   Parent IS NULL

    UNION ALL

    -- Recursive query
    SELECT  s.ID, s.Parent, rd.Start
    FROM    #SourceData AS s
            INNER JOIN RecursiveData AS rd ON s.Parent = rd.ID
)
SELECT * FROM RecursiveData WHERE Parent IS NULL

将输出以下内容:

id | parent | start
---+--------+------
1  | NULL   | 1
42 | NULL   | 42
4  | NULL   | 4
7  | NULL   | 7

然后我清理:)

-- Clean up
DROP TABLE #SourceData

答案 2 :(得分:1)

ANSI-92中没有递归查询支持,因为它是在ANSI-99中添加的。自v2以来,Oracle已经拥有了自己的递归查询语法(CONNECT BY)。虽然Oracle从9i开始支持WITH子句,但SQL Server是我所知道的第一个支持递归WITH / CTE语法的方法 - Oracle直到11gR2才启动。 PostgreSQL在8.4+中增加了支持。自2006年以来,MySQL已收到WITH支持请求,我非常怀疑你会在SQLite中看到它。

您提供的示例只有两个级别,因此您可以使用:

INSERT INTO dst
   SELECT a.id,
          a.parent,
          COALESCE(c.id, b.id) AS start
     FROM SRC a
LEFT JOIN SRC b ON b.id = a.parent
LEFT JOIN SRC c ON c.id = b.parent
    WHERE a.parent IS NOT NULL

您必须为深度级别添加LEFT JOIN,并以适当的顺序将它们添加到COALESCE函数中。