2

Given a table as below where fn contains the name of an existing table valued functions and param contains the param to be passed to the function

fn       | param
----------------
'fn_one' | 1001
'fn_two' | 1001
'fn_one' | 1002
'fn_two' | 1002

Is there a way to get a resulting table like this by using set-based operations? The resulting table would contain 0-* lines for each line from the first table.

param | resultval
---------------------------
1001  | 'fn_one_result_a'
1001  | 'fn_one_result_b'
1001  | 'fn_two_result_one'
1002  | 'fn_two_result_one'

I thought I could do something like (pseudo)

select t1.param, t2.resultval
from table1 t1
cross join exec sp_executesql('select * from '+t1.fn+'('+t1.param+')') t2

but that gives a syntax error at exec sp_executesql.

Currently we're using cursors to loop through the first table and insert into a second table with exec sp_executesql. While this does the job correctly, it is also the heaviest part of a frequently used stored procedure and I'm trying to optimize it. Changes to the data model would probably imply changes to most of the core of the application and that would cost more then just throwing hardware at sql server.

4
  • You can't use dynamic sql like that. It is also problematic if the functions that are being called are scalar functions which they appear to be. There really isn't a way to make this set based when the function itself is scalar. When you store the object names like this in your table you are going to have to use dynamic sql to execute it. Commented Jan 4, 2016 at 16:10
  • In my example fn_one returns for the same param two rows. I tried to show multiple combinations of results (from multiple, to one, to no results) Am I misunderstanding scalar? Commented Jan 4, 2016 at 16:20
  • This seems like a process that should be performed outside of SQL Server. Without knowing a lot of the details around the use case, stored procedure, etc. I can't really make a recommendation, but I would at least look closely at the architecture itself and see if this is really how this functionality should be handled. I'm also a little concerned at what looks like a suspicious functionality to begin with - storing function results in a table like that. Commented Jan 4, 2016 at 16:20
  • 1
    Thanks @TomH, I understand and share your concern. It is part of a really fine grained, user configurable, client-customizable authorization model in a 15 year old application. The procedure is used to cache a calculated model so we don't have to calculate it on every page request. Working with legacy is an interesting, yet seperate problem onto itself ;) Commented Jan 4, 2016 at 16:30

2 Answers 2

2

I believe that this should do what you need, using dynamic SQL to generate a single statement that can give you your results and then using that with EXEC to put them into your table. The FOR XML trick is a common one for concatenating VARCHAR values together from multiple rows. It has to be written with the AS [text()] for it to work.

--=========================================================
-- Set up
--=========================================================
CREATE TABLE dbo.TestTableFunctions (function_name VARCHAR(50) NOT NULL, parameter VARCHAR(20) NOT NULL)
INSERT INTO dbo.TestTableFunctions (function_name, parameter)
VALUES ('fn_one', '1001'), ('fn_two', '1001'), ('fn_one', '1002'), ('fn_two', '1002')

CREATE TABLE dbo.TestTableFunctionsResults (function_name VARCHAR(50) NOT NULL, parameter VARCHAR(20) NOT NULL, result VARCHAR(200) NOT NULL)
GO
CREATE FUNCTION dbo.fn_one
(
    @parameter VARCHAR(20)
)
RETURNS TABLE
AS
RETURN
    SELECT 'fn_one_' + @parameter AS result
GO
CREATE FUNCTION dbo.fn_two
(
    @parameter VARCHAR(20)
)
RETURNS TABLE
AS
RETURN
    SELECT 'fn_two_' + @parameter AS result
GO

--=========================================================
-- The important stuff
--=========================================================

DECLARE @sql VARCHAR(MAX)

SELECT @sql = 
(
    SELECT 'SELECT ''' + T1.function_name + ''', ''' + T1.parameter + ''', F.result FROM ' + T1.function_name + '(' + T1.parameter + ') F UNION ALL ' AS [text()]
    FROM
        TestTableFunctions T1
    FOR XML PATH ('')
)

SELECT @sql = SUBSTRING(@sql, 1, LEN(@sql) - 10)

INSERT INTO dbo.TestTableFunctionsResults
EXEC(@sql)

SELECT * FROM dbo.TestTableFunctionsResults

--=========================================================
-- Clean up
--=========================================================
DROP TABLE dbo.TestTableFunctions
DROP TABLE dbo.TestTableFunctionsResults
DROP FUNCTION dbo.fn_one
DROP FUNCTION dbo.fn_two
GO

The first SELECT statement (ignoring the setup) builds a string which has the syntax to run all of the functions in your table, returning the results all UNIONed together. That makes it possible to run the string with EXEC, which means that you can then INSERT those results into your table.

A couple of quick notes though... First, the functions must all return identical result set structures - the same number of columns with the same data types (technically, they might be able to be different data types if SQL Server can always do implicit conversions on them, but it's really not worth the risk). Second, if someone were able to update your functions table they could use SQL injection to wreak havoc on your system. You'll need that to be tightly controlled and I wouldn't let users just enter in function names, etc.

Sign up to request clarification or add additional context in comments.

3 Comments

It's a bit convoluted, but I think I understand what's happening. Going to see if I can apply it to my actual problem.
I'll add a bit more explanation to my answer
I put it in place today and reduced the number of selects from 300+ to 9. That's a win. It'll take a bit of comments and documentation, but it helped me
2

You cannot access objects by referencing their names in a SQL statement. One method would be to use a case statement:

select t1.*,
       (case when fn = 'fn_one' then dbo.fn_one(t1.param)
             when fn = 'fn_two' then dbo.fn_two(t1.param)
        end) as resultval
from table1 t1 ;

Interestingly, you could encapsulate the case as another function, and then do:

select t1.*, dbo.fn_generic(t1.fn, t1.param) as resultval
from table1 t1 ;

However, in SQL Server, you cannot use dynamic SQL in a user-defined function (defined in T-SQL), so you would still need to use case or similar logic.

Either of these methods is likely to be much faster than a cursor, because they do not require issuing multiple queries.

4 Comments

This would require us to maintain a switch case of all the available values contained in the fn column. The reason this is a string value is because we don't know what the values will be at the time of writing the query.
Indeed, the second suggestion was our guess too, but fn's can't have dynamic sql and sprocs can't be used to cross join on.
@BorisCallens . . . (1) It might be useful to have some control over the functions being called (although I appreciate the flexibility of a generic interface). (2) You can do what you want using a CLR (that is, writing the function in C# or some other language and then integrating it). I'm not advocating (2), just pointing out that it is possible.
2 will be out of the question with our DBA is my guess. (it was the last time I checked)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.