Call function for every row of a table without cross apply - tsql

I have this tsql function which inserts into a table variable:
create function fnListInfo(#id int) returns #res table(
[itemId] INT,
[name] NVARCHAR(255),
[type] NVARCHAR(20),
[unit] INT,
[order] INT
)
BEGIN
INSERT INTO #res
SELECT category.stockcat_id, category.stockcat_name, category.type, NULL /*unit*/, category.order
FROM tblStock stock
JOIN dbo.Map category on stock.id = category.itemId
WHERE stock.id = #id
insert into #res
SELECT t.*
FROM #res r
CROSS APPLY dbo.anotherFunction(r.itemId) AS t
WHERE r.type = 'parent'
RETURN
END
GO
In the end of fnListInfo, I want to add some more rows to the res table. If the row in #res is of type 'parent', I want to call another function (let's call it anotherFunction) which has the same return type as this one, and its input parameter is int (itemId from fnListInfo), and then I want to add the result from anotherFunction to #res in fnListInfo.
So basically I want to call anotherFunction for every row in #res which is of type 'parent' and append the result to the already existing #res.
I tried doing this:
insert into #res
SELECT t.*
FROM #res r
CROSS APPLY dbo.anotherFunction(r.itemId) AS t
WHERE r.type = 'parent'
and it works. The problem is that it's inefficient. Is there a better way?
I don't like using cursors.

Try changing your function to inline table valued function:
CREATE FUNCTION dbo.fnListInfo(#id int)
RETURNS TABLE
AS
RETURN (
SELECT t.*
FROM (
SELECT category.stockcat_id AS ItemId, category.stockcat_name AS name,
category.type AS [type], NULL AS [unit] , category.[order] AS [order]
FROM tblStock stock
JOIN dbo.Map category
ON stock.id = category.itemId
WHERE stock.id = #id) AS r
CROSS APPLY dbo.anotherFunction(r.itemId) AS t
WHERE r.type = 'parent'
UNION ALL
SELECT category.stockcat_id AS ItemId, category.stockcat_name AS name,
category.type AS [type], NULL AS [unit] , category.[order] AS [order]
FROM tblStock stock
JOIN dbo.Map category
ON stock.id = category.itemId
WHERE stock.id = #id
);
You should change dbo.anotherFunction to inline table valued function too if possible.
I suggest also reading about Inline vs Multistatement Table Function

Related

Run a stored procedure using select columns as input parameters?

I have a select query that returns a dataset with "n" records in one column. I would like to use this column as the parameter in a stored procedure. Below a reduced example of my case.
The query:
SELECT code FROM rawproducts
The dataset:
CODE
1
2
3
The stored procedure:
ALTER PROCEDURE [dbo].[MyInsertSP]
(#code INT)
AS
BEGIN
INSERT INTO PRODUCTS description, price, stock
SELECT description, price, stock
FROM INVENTORY I
WHERE I.icode = #code
END
I already have the actual query and stored procedure done; I just am not sure how to put them both together.
I would appreciate any assistance here! Thank you!
PS: of course the stored procedure is not as simple as above. I just choose to use a very silly example to keep things small here. :)
Here's two methods for you, one using a loop without a cursor:
DECLARE #code_list TABLE (code INT);
INSERT INTO #code_list SELECT code, ROW_NUMBER() OVER (ORDER BY code) AS row_id FROM rawproducts;
DECLARE #count INT;
SELECT #count = COUNT(*) FROM #code_list;
WHILE #count > 0
BEGIN
DECLARE #code INT;
SELECT #code = code FROM #code_list WHERE row_id = #count;
EXEC MyInsertSP #code;
DELETE FROM #code_list WHERE row_id = #count;
SELECT #count = COUNT(*) FROM #code_list;
END;
This works by putting the codes into a table variable, and assigning a number from 1..n to each row. Then we loop through them, one at a time, deleting them as they are processed, until there is nothing left in the table variable.
But here's what I would consider a better method:
CREATE TYPE dbo.code_list AS TABLE (code INT);
GO
CREATE PROCEDURE MyInsertSP (
#code_list dbo.code_list)
AS
BEGIN
INSERT INTO PRODUCTS (
[description],
price,
stock)
SELECT
i.[description],
i.price,
i.stock
FROM
INVENTORY i
INNER JOIN #code_list cl ON cl.code = i.code;
END;
GO
DECLARE #code_list dbo.code_list;
INSERT INTO #code_list SELECT code FROM rawproducts;
EXEC MyInsertSP #code_list = #code_list;
To get this to work I create a user-defined table type, then use this to pass a list of codes into the stored procedure. It means slightly rewriting your stored procedure, but the actual code to do the work is much smaller.
(how to) Run a stored procedure using select columns as input
parameters?
What you are looking for is APPLY; APPLY is how you use columns as input parameters. The only thing unclear is how/where the input column is populated. Let's start with sample data:
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL DROP TABLE dbo.Products;
IF OBJECT_ID('dbo.Inventory','U') IS NOT NULL DROP TABLE dbo.Inventory;
IF OBJECT_ID('dbo.Code','U') IS NOT NULL DROP TABLE dbo.Code;
CREATE TABLE dbo.Products
(
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Inventory
(
icode INT NOT NULL,
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Code(icode INT NOT NULL);
INSERT dbo.Inventory
VALUES (10,'',20.10,3),(11,'',40.10,3),(11,'',25.23,3),(11,'',55.23,3),(12,'',50.23,3),
(15,'',33.10,3),(15,'',19.16,5),(18,'',75.00,3),(21,'',88.00,3),(21,'',100.99,3);
CREATE CLUSTERED INDEX uq_inventory ON dbo.Inventory(icode);
The function:
CREATE FUNCTION dbo.fnInventory(#code INT)
RETURNS TABLE AS RETURN
SELECT i.[description], i.price, i.stock
FROM dbo.Inventory I
WHERE I.icode = #code;
USE:
DECLARE #code TABLE (icode INT);
INSERT #code VALUES (10),(11);
SELECT f.[description], f.price, f.stock
FROM #code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
Results:
description price stock
-------------- -------- -----------
20.10 3
40.10 3
Updated Proc (note my comments):
ALTER PROC dbo.MyInsertSP -- (1) Lose the input param
AS
-- (2) Code that populates the "code" table
INSERT dbo.Code VALUES (10),(11);
-- (3) Use CROSS APPLY to pass the values from dbo.code to your function
INSERT dbo.Products ([description], price, stock)
SELECT f.[description], f.price, f.stock
FROM dbo.code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
This ^^^ is how it's done.

Using CASE outside SELECT in table-valued function

I'm trying to select the same columns from a different table/view depending on the value of an argument (#ruleset). As it is not possible to pass the name of the table as a parameter nor to construct the name inside the function, used CASE structure outside the select statements. However, I get an error:
"Only one expression can be specified in the select list when the subquery is not introduced with EXISTS."
[Hope I get this right, it is my first question here.]
CREATE FUNCTION app.fgProduct
(
#ruleset nvarchar(50),
#matno nvarchar(50),
#datarevision int
)
RETURNS TABLE
AS
RETURN
(
SELECT
CASE WHEN #ruleset = 'G1' THEN
(
SELECT
#matno AS ProductId
,#datarevision AS DataRevision
,[ProductName]
FROM [ruleset].[g1gxProduct]
WHERE ProductId = #matno
)
WHEN #ruleset = 'G2' THEN
(
SELECT
#matno AS ProductId
,#datarevision AS DataRevision
,[ProductName]
FROM [ruleset].[g2gxProduct]
WHERE ProductId = #matno
)
END
)
There's a bunch of other views, so this whole issue cannot be solved in one procedure. Above is an example of a function which is used to generate new records based on various rule sets (= sets of views).
CASE is an expression and not for conditional flow. Use IF/ELSE instead.
The error is related to the select statements that are returning more than one column in the sub query. You can only return one column in the statements:
SELECT
#matno AS ProductId
,#datarevision AS DataRevision
,[ProductName]
The solution is to declare a result table and insert into it, just like #EzLo suggested in the comment.
CREATE FUNCTION app.fgProduct
(
#ruleset nvarchar(50),
#matno nvarchar(50),
#datarevision int
)
RETURNS #ret TABLE (
[ProductID] [nvarchar](50) NOT NULL,
[DataRevision] [int] NOT NULL,
[ProductName] [nvarchar](50) NULL
)
AS
BEGIN
IF #ruleset = 'G1'
INSERT INTO #ret
SELECT
#matno AS ProductId
,#datarevision AS DataRevision
,[ProductName]
FROM [ruleset].[g1gxProduct]
WHERE ProductId = #matno
ELSE
IF #ruleset = 'G2'
INSERT INTO #ret
SELECT
#matno AS ProductId
,#datarevision AS DataRevision
,[ProductName]
FROM [ruleset].[g2gxProduct]
WHERE ProductId = #matno
RETURN
END

SQL Server : Split string to row

How to turn data from below:
CODE COMBINATION USER
1111.111.11.0 KEN; JIMMY
666.778.0.99 KEN
888.66.77.99 LIM(JIM); JIMMY
To
CODE COMBINATION USER
1111.111.11.0 KEN
1111.111.11.0 JIMMY
666.778.0.99 KEN
888.66.77.99 LIM(JIM)
888.66.77.99 JIMMY
I know in SQL Server 2016 this can be done by split string function, but my production is SQL Server 2014.
With this TVF, you can supply the string to be split and delimiter. Furthermore, you get the sequence number which can be very useful for secondary processing.
Select [CODE COMBINATION]
,[USER] = B.RetVal
From YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.[USER],';') B
Returns
The Parse UDF
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>'+ Replace(#String,#Delimiter,'</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
Now, another option is the Parse-Row UDF. Notice we return the parsed string in one row. Currently 9 positions, but it is easy to expand or contract.
Select [CODE COMBINATION]
,B.*
From YourTable A
Cross Apply [dbo].[udf-Str-Parse-Row](A.[USER],';') B
Returns
The Parse Row UDF
CREATE FUNCTION [dbo].[udf-Str-Parse-Row] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select Pos1 = xDim.value('/x[1]','varchar(max)')
,Pos2 = xDim.value('/x[2]','varchar(max)')
,Pos3 = xDim.value('/x[3]','varchar(max)')
,Pos4 = xDim.value('/x[4]','varchar(max)')
,Pos5 = xDim.value('/x[5]','varchar(max)')
,Pos6 = xDim.value('/x[6]','varchar(max)')
,Pos7 = xDim.value('/x[7]','varchar(max)')
,Pos8 = xDim.value('/x[8]','varchar(max)')
,Pos9 = xDim.value('/x[9]','varchar(max)')
From (Select Cast('<x>' + Replace(#String,#Delimiter,'</x><x>')+'</x>' as XML) as xDim) A
)
--Select * from [dbo].[udf-Str-Parse-Row]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse-Row]('John Cappelletti',' ')
You need to use a UDF for splitting it on each row
CREATE FUNCTION [DBO].[FN_SPLIT_STR_TO_COL] (#T AS VARCHAR(4000) )
RETURNS
#RESULT TABLE(VALUE VARCHAR(250))
AS
BEGIN
SET #T= #T+';'
;WITH MYCTE(START,[END]) AS(
SELECT 1 AS START,CHARINDEX(';',#T,1) AS [END]
UNION ALL
SELECT [END]+1 AS START,CHARINDEX(';',#T,[END]+1)AS [END]
FROM MYCTE WHERE [END]<LEN(#T)
)
INSERT INTO #RESULT
SELECT SUBSTRING(#T,START,[END]-START) NAME FROM MYCTE;
RETURN
END
Now query on your table by calling above function with CROSS APPLY
SELECT [CodeCombination],FN_RS.VALUE FROM TABLE1
CROSS APPLY
(SELECT * FROM [DBO].[FN_SPLIT_STR_TO_COL] (User))
AS FN_RS
If your [USER] column only has one semicolon you don't need a "split string" function at all; you could use CROSS APPLY like this:
-- Your Sample data
DECLARE #table TABLE (CODE_COMBINATION varchar(30), [USER] varchar(100));
INSERT #table
VALUES ('1111.111.11.0', 'KEN; JIMMY'), ('666.778.0.99', 'XKEN'),
('888.66.77.99','LIM(JIM); JIMMY');
-- Solution using only CROSS APPLY
SELECT CODE_COMBINATION, [USER] = LTRIM(s.s)
FROM #table t
CROSS APPLY (VALUES (CHARINDEX(';',t.[USER]))) d(d)
CROSS APPLY
(
SELECT SUBSTRING(t.[USER], 1, ISNULL(NULLIF(d.d,0),1001)-1)
UNION ALL
SELECT SUBSTRING(t.[USER], d.d+1, 1000)
WHERE d.d > 0
) s(s);
If you do need a pre SQL Server 2016 "split string" function I would strongly suggest using Jeff Moden's DelimitedSplit8k or Eirikur Eiriksson's DelimitedSplit8K_LEAD. Both of these will outperform an XML-based or recursice CTE "split string" function.

Not sure how to call my function

When I select my data I can do a simple join to resolve the values of some of the columns but not all. Most of my columns have one value of data, like 10997, but other columns have multiple values of data, like 10997, 10889, 10123. I have created a function to resolve the three separate values in to the text I need but I am having trouble trying to figure out how to use it.
I have a basic join like this:
SELECT COLUMN1, COLUMN2, COLUMN3
FROM TABLE1 A
JOIN TABLE2 B ON A.ID = B.ID
The result of this will look like this:
Column1 Column2 Column3
1 11272, 11273, 11274, 11277 7712
The function I created uses declared variables and a table variable.
What I'd like to be able to do is something like this:
SELECT COLUMN1, dbo.MyFunction(COLUMN2), COLUMN3
FROM TABLE1 A
JOIN TABLE2 B ON A.ID = B.ID
Resulting in this:
Column1 Column2 Column3
1 Radio, Flyer, Internet, Bar 7712
The problem is that my function uses variables and I can't find where to put it in SQL (table valued function, stored proc, etc) so I can use it how I'd like. Each area has it's own limitation.
EDIT: Here is the code I created for my function, currently it's in a multi-statement table-valued function
DECLARE #looper INT, #res VARCHAR(100)
DECLARE #outList VARCHAR(300)
SET #outList = ''
DECLARE #tmpa TABLE (Item INT)
INSERT INTO #tmpa
SELECT Item
FROM fn_Split (', ', #input)
SELECT #looper = MIN(Item)
FROM #tmpa
WHILE #looper IS NOT NULL
BEGIN
SELECT #res = NAME
FROM FSM_CustomFormSelectOptions CFSO
WHERE ID = #looper
--print #res
SET #outList = #outList + #res + ', '
SELECT #looper = MIN(Item) FROM #tmpa WHERE Item > #looper
END
SET #outList = LEFT(#outList, LEN(#outList)-1)
INSERT INTO #Answers
VALUES(#outList)
RETURN
Sample call is:
select * from fn_GetAnswerText( '11273, 11274, 11275')
You are not returning anything
RETURN #outList

TSQL CTE: How to avoid circular traversal?

I have written a very simple CTE expression that retrieves a list of all groups of which a user is a member.
The rules goes like this, a user can be in multiple groups, and groups can be nested so that a group can be a member of another group, and furthermore, groups can be mutual member of another, so Group A is a member of Group B and Group B is also a member of Group A.
My CTE goes like this and obviously it yields infinite recursion:
;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
)
I can't find an easy solution to back-track those groups that I have already recorded.
I was thinking of using an additional varchar parameter in the CTE to record a list of all groups that I have visited, but using varchar is just too crude, isn't it?
Is there a better way?
You need to accumulate a sentinel string within your recursion. In the following example I have a circular relationship from A,B,C,D, and then back to A, and I avoid a loop with the sentinel string:
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;
Result:
Parent Child Sentinel
------ ----- --------
A B A
B C A|B
C D A|B|C
D A A|B|C|D
Instead of a sentinel string, use a sentinel table variable. Function will catch circular reference no matter how many hops the circle is, no issues with maximum length of nvarchar(max), easily modified for different data types or even multipart keys, and you can assign the function to a check constraint.
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