Làm thế nào để đệ quy SQL thực sự hoạt động?


19

Đến với SQL từ các ngôn ngữ lập trình khác, cấu trúc của truy vấn đệ quy trông khá kỳ quặc. Đi qua nó từng bước một, và nó dường như sụp đổ.

Hãy xem xét ví dụ đơn giản sau:

CREATE TABLE #NUMS
(N BIGINT);

INSERT INTO #NUMS
VALUES (3), (5), (7);

WITH R AS
(
    SELECT N FROM #NUMS
    UNION ALL
    SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Hãy đi qua nó.

Đầu tiên, thành viên neo thực thi và tập kết quả được đưa vào R. Vì vậy, R được khởi tạo thành {3, 5, 7}.

Sau đó, việc thực thi giảm xuống dưới UNION ALL và thành viên đệ quy được thực thi lần đầu tiên. Nó thực thi trên R (nghĩa là trên R mà chúng ta hiện có trong tay: {3, 5, 7}). Điều này dẫn đến {9, 25, 49}.

Nó làm gì với kết quả mới này? Nó có nối {9, 25, 49} vào {3, 5, 7} hiện tại, gắn nhãn kết quả R kết hợp, và sau đó tiếp tục với đệ quy từ đó không? Hoặc nó xác định lại R chỉ là kết quả mới này {9, 25, 49} và thực hiện tất cả các kết hợp sau?

Không có sự lựa chọn nào có ý nghĩa.

Nếu R bây giờ là {3, 5, 7, 9, 25, 49} và chúng tôi thực hiện lần lặp tiếp theo của phép đệ quy, thì chúng tôi sẽ kết thúc với {9, 25, 49, 81, 625, 2401} và chúng tôi mất {3, 5, 7}.

Nếu R bây giờ chỉ là {9, 25, 49}, thì chúng ta có vấn đề gắn nhãn sai. R được hiểu là sự kết hợp của tập kết quả thành viên neo và tất cả các tập kết quả thành viên đệ quy tiếp theo. Trong khi {9, 25, 49} chỉ là một thành phần của R. Nó không phải là R đầy đủ mà chúng tôi đã tích lũy cho đến nay. Do đó, để viết thành viên đệ quy như chọn từ R không có ý nghĩa gì.


Tôi chắc chắn đánh giá cao những gì @Max Vernon và @Michael S. đã nêu chi tiết dưới đây. Cụ thể, (1) tất cả các thành phần được tạo ra đến giới hạn đệ quy hoặc tập hợp null, và sau đó (2) tất cả các thành phần được liên kết với nhau. Đây là cách tôi hiểu đệ quy SQL để thực sự hoạt động.

Nếu chúng ta thiết kế lại SQL, có thể chúng ta sẽ thực thi một cú pháp rõ ràng và rõ ràng hơn, đại loại như thế này:

WITH R AS
(
    SELECT   N
    INTO     R[0]
    FROM     #NUMS
    UNION ALL
    SELECT   N*N AS N
    INTO     R[K+1]
    FROM     R[K]
    WHERE    N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Sắp xếp giống như một bằng chứng quy nạp trong toán học.

Vấn đề với đệ quy SQL như hiện tại là nó được viết theo một cách khó hiểu. Cách nó được viết nói rằng mỗi thành phần được hình thành bằng cách chọn từ R, nhưng điều đó không có nghĩa là toàn bộ R đã được (hoặc, dường như đã được) xây dựng cho đến nay. Nó chỉ có nghĩa là thành phần trước đó.


"Nếu R bây giờ là {3, 5, 7, 9, 25, 49} và chúng tôi thực hiện lần lặp tiếp theo của đệ quy, thì chúng tôi sẽ kết thúc với {9, 25, 49, 81, 625, 2401} và chúng tôi ' đã mất {3, 5, 7}. " Tôi không thấy bạn mất {3,5,7} như thế nào nếu nó hoạt động như vậy.
ypercubeᵀᴹ

@ yper-crazyhat-cubeᵀᴹ - Tôi đã theo dõi từ giả thuyết đầu tiên mà tôi đề xuất, cụ thể là, nếu R trung gian là sự tích lũy của tất cả mọi thứ đã được tính đến thời điểm đó thì sao? Sau đó, ở lần lặp tiếp theo của thành viên đệ quy, mọi phần tử của R được bình phương. Do đó, {3, 5, 7} trở thành {9, 25, 49} và chúng tôi không bao giờ có {3, 5, 7} trong R. Nói cách khác, {3, 5, 7} bị mất từ ​​R.
UnLogicGuys

Câu trả lời:


26

Mô tả BOL của CTE đệ quy mô tả ngữ nghĩa của thực thi đệ quy như sau:

  1. Chia biểu thức CTE thành neo và các thành viên đệ quy.
  2. Chạy (các) thành viên neo tạo tập lệnh đầu tiên hoặc kết quả cơ sở (T0).
  3. Chạy (các) thành viên đệ quy với Ti làm đầu vào và Ti + 1 làm đầu ra.
  4. Lặp lại bước 3 cho đến khi một bộ trống được trả về.
  5. Trả về tập kết quả. Đây là ĐOÀN TẤT CẢ T0 đến Tn.

Vì vậy, mỗi cấp chỉ có đầu vào ở mức trên chứ không phải toàn bộ tập kết quả được tích lũy cho đến nay.

Trên đây là cách nó hoạt động hợp lý . Các CTE đệ quy vật lý hiện luôn được triển khai với các vòng lặp lồng nhau và một bộ đệm ngăn xếp trong SQL Server. Điều này được mô tả ở đâyở đây và có nghĩa là trong thực tế, mỗi phần tử đệ quy chỉ hoạt động với hàng cha từ cấp trước chứ không phải toàn bộ cấp. Nhưng các hạn chế khác nhau về cú pháp cho phép trong CTE đệ quy có nghĩa là phương pháp này hoạt động.

Nếu bạn xóa ORDER BYkhỏi truy vấn của mình, kết quả sẽ được sắp xếp như sau

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

Điều này là do kế hoạch thực hiện hoạt động rất giống với sau đây C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1: Như trên vào thời điểm đứa con đầu tiên của thành viên mỏ neo 3đang được xử lý tất cả thông tin về anh chị em của nó, 57, con cháu của họ, đã bị loại khỏi ống chỉ và không thể truy cập được nữa.

NB2: C # ở trên có ngữ nghĩa tổng thể tương tự như kế hoạch thực hiện nhưng luồng trong kế hoạch thực hiện không giống nhau, vì ở đó các toán tử làm việc theo kiểu xuất phát theo đường ống. Đây là một ví dụ đơn giản để chứng minh ý chính của phương pháp này. Xem các liên kết trước đó để biết thêm chi tiết về bản thân kế hoạch.

NB3: Bản thân bộ đệm ngăn xếp được triển khai rõ ràng là một chỉ mục cụm không duy nhất với cột chính của mức đệ quy và các bộ duy nhất được thêm vào khi cần ( nguồn )


6
Các truy vấn đệ quy trong SQL Server luôn được chuyển đổi từ đệ quy sang lặp (có xếp chồng) trong quá trình phân tích cú pháp. Quy tắc thực hiện cho phép lặp là IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv). Chỉ cần FYI. Câu trả lời tuyệt vời.
Paul White nói GoFundMonica

Ngẫu nhiên, UNION cũng được phép thay vì UNION ALL nhưng SQL Server sẽ không làm điều đó.
Joshua

5

Đây chỉ là một phỏng đoán (bán) có giáo dục, và có lẽ là hoàn toàn sai. Bằng cách này, câu hỏi thú vị.

T-SQL là ngôn ngữ khai báo; có lẽ một CTE đệ quy được dịch thành một thao tác kiểu con trỏ trong đó các kết quả từ phía bên trái của UNION ALL được thêm vào một bảng tạm thời, sau đó phía bên phải của UNION ALL được áp dụng cho các giá trị ở phía bên trái.

Vì vậy, trước tiên, chúng tôi chèn đầu ra của phía bên trái của UNION ALL vào tập kết quả, sau đó chúng tôi chèn kết quả của phía bên phải của UNION ALL được áp dụng cho phía bên trái và chèn nó vào tập kết quả. Phía bên trái sau đó được thay thế bằng đầu ra từ phía bên phải và phía bên phải được áp dụng lại cho phía bên trái "mới". Một cái gì đó như thế này:

  1. {3,5,7} -> tập kết quả
  2. báo cáo đệ quy áp dụng cho {3,5,7}, đó là {9,25,49}. {9,25,49} được thêm vào tập kết quả và thay thế phía bên trái của UNION ALL.
  3. báo cáo đệ quy áp dụng cho {9,25,49}, đó là {81,625,2401}. {81,625,2401} được thêm vào tập kết quả và thay thế phía bên trái của UNION ALL.
  4. báo cáo đệ quy áp dụng cho {81,625,2401}, đó là {6561,390625,5764801}. {6561,390625,5764801} được thêm vào tập kết quả.
  5. Con trỏ đã hoàn thành, vì lần lặp tiếp theo dẫn đến mệnh đề WHERE trả về false.

Bạn có thể thấy hành vi này trong kế hoạch thực hiện cho CTE đệ quy:

nhập mô tả hình ảnh ở đây

Đây là bước 1 ở trên, trong đó phía bên trái của UNION ALL được thêm vào đầu ra:

nhập mô tả hình ảnh ở đây

Đây là phía bên phải của UNION ALL nơi đầu ra được nối với tập kết quả:

nhập mô tả hình ảnh ở đây


4

Tài liệu SQL Server , đề cập đến T iT i + 1 , không dễ hiểu lắm, cũng không phải là một mô tả chính xác về việc triển khai thực tế.

Ý tưởng cơ bản là phần đệ quy của truy vấn xem xét tất cả các kết quả trước đó, nhưng chỉ một lần .

Có thể hữu ích để xem cách các cơ sở dữ liệu khác thực hiện điều này (để có được kết quả tương tự ). Các tài liệu Postgres nói:

Đánh giá truy vấn đệ quy

  1. Đánh giá các thuật ngữ không đệ quy. Đối với UNION(nhưng không UNION ALL), loại bỏ các hàng trùng lặp. Bao gồm tất cả các hàng còn lại trong kết quả của truy vấn đệ quy và cũng đặt chúng trong một bảng làm việc tạm thời .
  2. Miễn là bàn làm việc không trống, hãy lặp lại các bước sau:
    1. Đánh giá thuật ngữ đệ quy, thay thế các nội dung hiện tại của bảng làm việc để tự tham khảo đệ quy. Đối với UNION(nhưng không UNION ALL), loại bỏ các hàng và hàng trùng lặp trùng lặp bất kỳ hàng kết quả trước đó. Bao gồm tất cả các hàng còn lại trong kết quả của truy vấn đệ quy và cũng đặt chúng trong một bảng trung gian tạm thời .
    2. Thay thế nội dung của bàn làm việc bằng nội dung của bảng trung gian, sau đó làm trống bảng trung gian.

Lưu ý
Nói đúng ra, quá trình này không lặp lại, nhưng RECURSIVElà thuật ngữ được lựa chọn bởi ủy ban tiêu chuẩn SQL.

Các tài liệu SQLite gợi ý tại một thực hiện hơi khác nhau, và thuật toán này một hàng-at-a-thời gian có thể là đơn giản nhất để hiểu:

Thuật toán cơ bản để tính toán nội dung của bảng đệ quy như sau:

  1. Chạy initial-selectvà thêm kết quả vào hàng đợi.
  2. Trong khi hàng đợi không trống:
    1. Trích xuất một hàng từ hàng đợi.
    2. Chèn hàng đơn đó vào bảng đệ quy
    3. Giả sử rằng hàng duy nhất vừa trích xuất là hàng duy nhất trong bảng đệ quy và chạy recursive-select, thêm tất cả kết quả vào hàng đợi.

Quy trình cơ bản ở trên có thể được sửa đổi bởi các quy tắc bổ sung sau:

  • Nếu một toán tử UNION kết nối initial-selectvới recursive-select, thì chỉ thêm các hàng vào hàng đợi nếu trước đó không có hàng giống hệt nào được thêm vào hàng đợi. Các hàng lặp lại được loại bỏ trước khi được thêm vào hàng đợi ngay cả khi các hàng lặp lại đã được trích xuất từ ​​hàng đợi bằng bước đệ quy. Nếu toán tử là UNION ALL, thì tất cả các hàng được tạo bởi cả initial-selectvà và recursive-selectluôn được thêm vào hàng đợi ngay cả khi chúng được lặp lại.
    [Càng]

0

Kiến thức của tôi là đặc biệt về DB2 nhưng nhìn vào các sơ đồ giải thích dường như giống với SQL Server.

Kế hoạch đến từ đây:

Xem nó trên Dán Kế hoạch

Kế hoạch giải thích máy chủ SQL

Trình tối ưu hóa không thực sự chạy một liên minh tất cả cho mỗi truy vấn đệ quy. Nó lấy cấu trúc của truy vấn và gán phần đầu tiên của liên minh cho một "thành viên neo", sau đó nó sẽ chạy qua nửa sau của liên minh tất cả (được gọi là "thành viên đệ quy" cho đến khi đạt đến giới hạn được xác định. đệ quy hoàn tất, trình tối ưu hóa kết hợp tất cả các bản ghi lại với nhau.

Trình tối ưu hóa chỉ lấy nó làm gợi ý để thực hiện thao tác được xác định trước.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.