0

I'll do my best to describe my problem, but please don't hesitate to comment with questions.

Imagine a dynamic query that needs to be executed in runtime with EXEC sp_executesql. It takes a list of ints as input, does a WHERE on that list, and it returns a table with a single column of Ints as output.

I then need to use those Ints once more as input in the previous query, n times.

The problems I am encountering:

I am not sure how to select into a list from a dynamic query, receive it as output, and re pass it in the same query... I have the rest of the recursion ready, but this bit is driving me insane. Any help at all would be appreciated - let me know if I need to include some basic tables of my DB to demonstrate; otherwise, if someone can come up with a simple table example that works I'd be forever in your debt.

--- UPDATE ---- Environment is MSSQLSERVER. Example:

A table with 3 columns : User1ID, User2ID, RelationID where each user is mapped to another by a relation.

Problem: Get all the users that satisfy a potential chain of relations X time. Example:

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

This will give me back a single table column of of userIDs. Now, if I want to run it again, I want that list of users as input for @OwnerID

And I want to be able to do this as many times as I like.

-- SECOND EDIT--

The tempTable comment seems to have steered me to SOME direction at least. Thank you for that. I'll try that, and monitor this thread for any further advice. Cheers guys.

1 Answers1

0

I'm assuming you're using SQL Server because this question is tagged with tsql (or usually t-sql) and that sp_executeSQL is a SQL Server stored procedure. Given that assumption, I don't think you really need to resort to using sp_executeSQL ... SQL Server 2000 added the ability to define local variables as table types, which makes it a bit easier to make something like this thread-safe.

Here's a script that creates a database with the kind of table you described, adds some data to that table, creates a stored procedure to get the list of ids like you want and returns it as a comma-delimited list in the same varchar input parameter. (Obviously this requires the permission to create and delete databases.) The stored procedure parameter @degrees lets you control the number of times you want to search for additional related users with a value of 0 causing it to search until no more are found. The last parameter, @relationid, if specified, will limit the search to a specific type of relation, so if you only wanted to see ancestors, you could specify @degrees = 0, @relationid = x where x is the id for a parent relationship and it would return parent of parents until it's found every user in that lineage.

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

When I execute this script, the output I get looks like:

@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

In most cases I personally find that taking the input as a string list of integers or returning it that way isn't actually necessary. If you turn that local table variable into a SQL server type (SQL Server 2008 or later), then you can pass a table of that type in as a parameter to the stored procedure, or return one back out, eliminating the need for all that extra string parsing and converting between int and varchar, and your procedure will undoubtedly execute faster that way.

Community
  • 1
  • 1
Samuel Dealey
  • 263
  • 1
  • 7
  • thank you for this ! My problem is, I do not understand much of what is going on in the block of code...SQL is really not my forte :( Could you add some comments? Also (this might boil down to my insufficient explanation), friends may well not be the only relation (more than one relation type ID). Additionally, the length of the relation is variable (could be friends, or friends of friends, or (friends of family of colleagues) and somethingelsehere. But I think what you propose is definitely on the right track with what I need ! – Evangelos Aktoudianakis Apr 09 '14 at 07:16
  • You're welcome. I added some comments and a few extra lines of code regarding your clarification about the types of relations and the depth of the search. – Samuel Dealey Apr 09 '14 at 16:13