2012-06-14 23 views
11

Tôi đã viết một biểu thức CTE rất đơn giản để truy xuất danh sách tất cả các nhóm trong đó người dùng là thành viên.TSQL CTE: Cách tránh truyền tải ngang?

Quy tắc như thế này, người dùng có thể ở nhiều nhóm và nhóm có thể lồng nhau để nhóm có thể là thành viên của một nhóm khác, và hơn nữa, nhóm có thể là thành viên lẫn nhau của nhóm khác, vì vậy nhóm A là một thành viên của nhóm B và B cũng là một thành viên của Tập đoàn A.

CTE của tôi đi như thế này và rõ ràng nó mang lại đệ quy vô hạn:

  ;WITH GetMembershipInfo(entityId) AS(-- entity can be a user or group 
       SELECT k.ID as entityId FROM entities k WHERE k.id = @userId 
       UNION ALL 
       SELECT k.id FROM entities k 
       JOIN Xrelationships kc on kc.entityId = k.entityId 
       JOIN GetMembershipInfo m on m.entityId = kc.ChildID 
      ) 

tôi không thể tìm thấy một giải pháp dễ dàng để back- theo dõi những nhóm mà tôi đã ghi lại.

Tôi đã nghĩ đến việc sử dụng tham số varchar bổ sung trong CTE để ghi lại danh sách tất cả các nhóm mà tôi đã truy cập, nhưng việc sử dụng varchar quá thô, phải không?

Có cách nào tốt hơn không?

+0

Bạn có chắc là nó đang đệ quy vĩnh viễn không? Mặc định máy chủ là 100 lần lặp. Thử đọc gợi ý 'MAXRECURSION' trên [MSDN] (http://msdn.microsoft.com/en-us/library/ms175972.aspx). – Bridge

+0

Đầu tiên lo lắng về hiệu quả, * sau đó * lo lắng về sự thô lỗ, nếu thời gian cho phép :) – AakashM

+0

nó không recurse mãi mãi vì nó ném một lỗi sau 100 cuộc gọi đệ quy. Tha thứ từ ngữ của tôi. – Haoest

Trả lời

25

Bạn cần phải tích luỹ một chuỗi sentinel trong đệ quy của mình. Trong ví dụ sau Tôi có một mối quan hệ hình tròn từ A, B, C, D, và sau đó trở lại A, và tôi tránh một vòng lặp với chuỗi sentinel:

DECLARE @MyTable TABLE(Parent CHAR(1), Child CHAR(1)); 

INSERT @MyTable VALUES('A', 'B'); 
INSERT @MyTable VALUES('B', 'C'); 
INSERT @MyTable VALUES('C', 'D'); 
INSERT @MyTable VALUES('D', 'A'); 

; WITH CTE (Parent, Child, Sentinel) AS (
    SELECT Parent, Child, Sentinel = CAST(Parent AS VARCHAR(MAX)) 
    FROM @MyTable 
    WHERE Parent = 'A' 
    UNION ALL 
    SELECT CTE.Child, t.Child, Sentinel + '|' + CTE.Child 
    FROM CTE 
    JOIN @MyTable t ON t.Parent = CTE.Child 
    WHERE CHARINDEX(CTE.Child,Sentinel)=0 
) 
SELECT * FROM CTE; 

Kết quả:

Parent Child Sentinel 
------ ----- -------- 
A  B  A 
B  C  A|B 
C  D  A|B|C 
D  A  A|B|C|D 
+1

Tôi thích giải pháp của bạn vì nó hoạt động. Nhưng có cách nào để làm điều này mà không có một chuỗi sentinel? Tôi cảm thấy nó sặc sỡ và trùng lặp mà chúng tôi phải thêm một số loại dấu phân cách xung quanh mỗi mục được gửi, nói Sentinel = '<' + CAST (Parent AS VARCHAR (MAX)) + '>' Sau đó chúng ta phải làm tương tự trong hàm CharIndex(), bởi vì không có các dấu phân tách có thể có các kết quả dương tính giả. Và điều gì xảy ra nếu chuỗi sentinel quá lớn đến nỗi nó vượt quá chiều dài của varchar (max)? – Haoest

+2

Tôi rất vui khi biết điều này hoạt động. Đó là một chút của một hack, và tôi thành thật không thể nghĩ ra một cách "sạch" hơn. Tuy nhiên, hãy nhớ rằng các sentinel phát triển dọc theo mỗi nhánh đệ quy một cách độc lập, và do đó sẽ chỉ nhận được lớn như độ sâu tối đa lần mỗi chuỗi, cộng với dấu phân cách. VARCHAR (MAX) có giới hạn 2 GB, trong khi độ sâu tối đa có thể được mở rộng, nếu cần thiết, tối đa là 32767. Vì vậy, rất khó có khả năng bạn sẽ tràn VARCHAR (MAX). Hầu hết các công việc đệ quy có thể có vài nghìn cây, nhưng độ sâu hiếm khi vượt quá 5 hoặc hơn. Vì vậy, các chuỗi sentinel của bạn nói chung sẽ vẫn còn khá nhỏ. –

+0

tốt để biết, cảm ơn. – Haoest

2

Thay vì một chuỗi sentinel, sử dụng một biến bảng sentinel. Hàm sẽ bắt tham chiếu vòng tròn cho dù có bao nhiêu bước nhảy vòng tròn, không có vấn đề với độ dài tối đa của nvarchar (max), dễ dàng sửa đổi cho các kiểu dữ liệu khác nhau hoặc thậm chí nhiều khóa, và bạn có thể gán hàm cho ràng buộc kiểm tra.

CREATE FUNCTION [dbo].[AccountsCircular] (@AccountID UNIQUEIDENTIFIER) 
RETURNS BIT 
AS 
BEGIN 
    DECLARE @NextAccountID UNIQUEIDENTIFIER = NULL; 
    DECLARE @Sentinel TABLE 
    (
     ID UNIQUEIDENTIFIER 
    ) 
    INSERT INTO  @Sentinel 
       ([ID]) 
    VALUES   (@AccountID) 
    SET @NextAccountID = @AccountID; 

    WHILE @NextAccountID IS NOT NULL 
    BEGIN 
     SELECT @NextAccountID = [ParentAccountID] 
     FROM [dbo].[Accounts] 
     WHERE [AccountID] = @NextAccountID; 
     IF EXISTS(SELECT 1 FROM @Sentinel WHERE ID = @NextAccountID) 
      RETURN 1; 
     INSERT INTO @Sentinel 
       ([ID]) 
     VALUES  (@NextAccountID) 
    END 
    RETURN 0; 
END 
Các vấn đề liên quan