具有动态查询的复杂SQL递归

时间:2014-04-08 13:01:12

标签: sql tsql

我会尽力描述我的问题,但请不要犹豫,对问题发表评论。

想象一下需要在运行时使用EXEC sp_executesql执行的动态查询。它将一个int列表作为输入,在该列表上执行WHERE,并返回一个表,其中包含一列Ints作为输出。

然后我需要再次使用那些Ints作为上一个查询的输入,n次。

我遇到的问题:

我不确定如何select into来自动态查询的列表,将其作为输出接收,然后在同一查询中重新传递...我已准备好其余的递归,但这一位是让我疯了。任何帮助都将不胜感激 - 如果我需要包含我的数据库的一些基本表来证明,请告诉我。否则,如果有人能想出一个有效的简单表格,那么我将永远为你负债。

---更新---- 环境是MSSQLSERVER。例如:

包含3列的表:User1ID,User2ID,RelationID,其中每个用户通过关系映射到另一个用户。

问题:让所有满足潜在关系链的用户X时间。例如:

Dynamic query generated by .NET - SELECT URX.User2ID from UserRelations UR1
JOIN UR1.User2ID ON UR2.User1ID
JOIN UR2.User1ID ON UR3.User2ID
....
JOIN URX-1.User2ID ON URX.UserID1
WHERE RelationID = 1 OR RelationID = 2
AND OwnerID = @OwnerID

这将返回userIDs的单个表列。现在,如果我想再次运行它,我希望用户列表作为@OwnerID

的输入

我希望能够多次这样做。

- 第二次编辑 -

tempTable评论似乎至少引导我走向了一些方向。谢谢你。我会尝试,并监视此线程以获得任何进一步的建议。干杯。

1 个答案:

答案 0 :(得分:0)

我假设你正在使用SQL Server,因为这个问题用tsql(或通常是t-sql)标记,而sp_executeSQL是SQL Server存储过程。鉴于这种假设,我认为你真的不需要求助sp_executeSQL ... SQL Server 2000增加了将局部变量定义为表类型的能力,这使得制作类似的东西变得更容易一些这个线程安全。

这里是一个脚本,它使用您描述的那种表创建数据库,向该表添加一些数据,创建存储过程以获取您想要的ID列表并将其作为逗号分隔列表返回在相同的varchar输入参数中。 (显然这需要创建和删除数据库的权限。)存储过程参数@degrees允许您控制要搜索值为0的其他相关用户的次数,使其搜索直到不再有找到。最后一个参数@relationid,如果指定,将限制搜索到特定类型的关系,因此如果您只想查看祖先,可以指定@degrees = 0, @relationid = x其中x是父关系的id它将返回父母的父母,直到找到该血统中的每个用户。

create database testme 
go

use testme 
go

/* this suppresses output of statement results 
which can sometimes interfere with other code */
set nocount on 

/* this just creates some dummy data to test with - modify it however you need */
create table UserRElations (id int identity primary key, user1id int, user2id int, relationid int) 
insert into UserRelations (user1id, user2id, relationid) values (1,2, 1)
insert into UserRelations (user1id, user2id, relationid) values (1,3, 3)
insert into UserRelations (user1id, user2id, relationid) values (2,3, 8)
insert into UserRelations (user1id, user2id, relationid) values (3,5, 2)
insert into UserRelations (user1id, user2id, relationid) values (3,7, 17)
insert into UserRelations (user1id, user2id, relationid) values (4,7, 6)
insert into UserRelations (user1id, user2id, relationid) values (4,8, 1)
insert into UserRelations (user1id, user2id, relationid) values (5,10, 3)
insert into UserRelations (user1id, user2id, relationid) values (5,13, 1)
go

create procedure pGetFriendsOfFriends
    @list nvarchar(1000) output, -- comma delimited list of int values from the user1id column of UserRelations 
    @degrees tinyint = 0, -- the number of times to search for more relations - 0 means no limit
    @relationid int = null -- set this only if you want a specific type of relation 
as 

/* see above comment about nocount */
set nocount on

/* create a local variable that is a table 
with one int column named id as the primary key */
declare @utbl table (id int primary key) 

/* additional temp variables for later use */
declare @tmp nvarchar(5) -- if you have user1ids of more than 5 digits you might need this larger 
declare @next int, @i int 

set @list = @list + ',' -- make sure there's a comma after every id in the list 

/* move the first item in the list to the table until the list is empty */
while (len(@list) > 0) begin 
    set @next = charindex(',', @list) -- find the first comma in the list 

    if (@next > 0) begin 
        /* we found a comma, insert an id into the temp table utbl */
        set @tmp = ltrim(rtrim(left(@list, @next-1))) -- get a string up to the comma 

        if (isnumeric(@tmp) = 1) begin -- make sure the string is numeric 
            set @i = cast(@tmp as int) -- convert the string to an int 
            if (not exists (select id from @utbl where id = @i)) -- make sure we dont insert the same id twice 
                insert into @utbl values (@i) -- self explanatory 
        end 

        /* remove the id we just inserted into the table from the string */
        set @list = substring(@list,@next+1,len(@list)) 
    end else set @list = '' -- there weren't any more commas, we're done 
end

/* now we're going to loop over the search for relations */
set @i = 1 -- set our temp variable to 1 to start the loop 

while (@i <= @degrees -- loop from 1 to a specified number of @degrees of separation 
    or /* the keyword "exists" below will stop the select statement and return true as soon as a matching row is found */
    (@degrees = 0 and exists 
        ( -- !!NOTE!! this select statement must have the same where clause as below - otherwise you risk an infinite loop 
        select distinct urx.user2id 
        from UserRelations urx 
        where urx.user1id in (select id from @utbl) 
        and urx.user2id not in (select id from @utbl)
        and (@relationid is null or urx.relationid = @relationid) 
        ) 
    )) 
begin
    /* add more relations for the current pass */
    insert into @utbl 
    select distinct urx.user2id -- distinct ensures all the returned user2id values are unique 
    from UserRelations urx 
    where urx.user1id in (select id from @utbl) -- find relations for users already in the @utbl temp table 
    and urx.user2id not in (select id from @utbl) -- ignore any that are already in the @utbl temp table 
    and (@relationid is null or urx.relationid = @relationid) -- allow the caller to declare a specific relation type - null will return all types 

    set @i = @i + 1 -- !!NOTE!! if @degrees is not 0, the counter must be incremented (or you'll get an infinite loop)
end

set @list = '' -- we'll return the list parameter to the caller, but start with an empty string 

/* create a cursor to loop over the ids we found */
declare cFoF cursor local fast_forward for  
select id from @utbl 

open cFoF -- open the cursor 
fetch next from cFoF into @i -- get the first item from the cursor 

while (@@FETCH_STATUS = 0) begin -- while there are more records in the cursor 
    set @list = ',' + cast(@i as nvarchar) + @list -- add the current item to the list 

    fetch next from cFoF into @i -- get the next item from the cursor 
end

set @list = substring(@list, 2, len(@list)) -- remove the leading comma from the list 

return -- make sure the @list parameter goes back to the caller 
go

declare @list nvarchar(1000) = '1,2' 
exec pGetFriendsOfFriends @list out, 2 
print '' 
print '@list=1,2, @depth=2 => @list=' + @list 

set @list = '2'
exec pGetFriendsOfFriends @list out, 1
print ''
print '@list=2, @depth=1 => @list=' + @list 

set @list = '1'
exec pGetFriendsOfFriends @list out, 0
print ''
print '@list=1, @depth=0 => @list=' + @list 

set @list = '1'
exec pGetFriendsOfFriends @list out, 0, 1
print ''
print '@list=1, @depth=0, @relationid=1 => @list=' + @list 
go

use master 
go

drop database testme 
go

当我执行此脚本时,我得到的输出如下:

@list=1,2, @depth=2 => @list=7,5,3,2,1

@list=2, @depth=1 => @list=3,2

@list=1, @depth=0 => @list=13,10,7,5,3,2,1

@list=1, @depth=0, @relationid=1 => @list=2,1

在大多数情况下,我个人发现将输入作为整数的字符串列表或以这种方式返回它实际上并不是必需的。如果将该本地表变量转换为SQL服务器typeSQL Server 2008 or later),则可以将该类型的表作为参数传递给存储过程,或者将其返回一个,从而消除需要在intvarchar之间进行所有额外的字符串解析和转换,并且您的程序无疑会以这种方式执行得更快。