converting multiline Table valued function to inline in SQL - sql

Considering the performance issues, I wanted to make my multiline Table valued function, an inline TVF.
Here is the sample code for Multiline TVF:
CREATE FUNCTION MatchAptNumber (#AptNumberFromUser nvarchar(20))
RETURNS #MatchedData Table
(
RowNumber int null ,
PercentMatch int null
)
AS
Begin
Insert into #MatchedData(RowNumber) select dbo.Patients.Rowid from dbo.Patients where dbo.Patients.Aptnumber = #AptNumberFromUser
update #MatchedData set PercentMatch= 100
RETURN;
END;
Go
Here is how I use it:
select #constVal = FunctionWeight from dbo.FunctionWeights where FunctionWeights.FunctionName = 'MatchAptNumber';
INSERT INTO #Temp2(RowNumber, ValFromFunc, FuncWeight, percentage)
SELECT RowNumber, PercentMatch, #constVal, PercentMatch * #constVal
from dbo.MatchAptNumber(#Aptnumber);
Is it possible to convert it into an inline TVF and use it as mentioned above? I do know the syntactic differences between two but not sure how it be possible to use it the same way? Can I get some pointers on same?

You can get the '100' as a constant in the SELECT so the function becomes;
CREATE FUNCTION MatchAptNumber (#AptNumberFromUser nvarchar(20))
RETURNS TABLE AS RETURN
SELECT
p.Rowid AS RowNumber ,
CAST(100 AS INT) AS PercentMatch
FROM
dbo.Patients p
WHERE
p.Aptnumber = #AptNumberFromUser
GO

Related

How can I write SQL select statement inside scalar type user-defined function?

I am trying to create a function in SQL Server using the following, but I think I am missing something in either in syntax or query
CREATE FUNCTION DEMO.Get_Rate_For_Absence
(#company_id_ VARCHAR,
#emp_no_ VARCHAR,
#account_date_ DATE)
RETURN DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE_ DECIMAL(10, 2)
SET #RATE_ = SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = '#company_id_ '
AND Emp_no = '#emp_no_ '
AND ORG_CODE = '#wage_code_'
AND ACCOUNT_DATE = '#account_date_'
RETURN #RATE
END
The SQL statement that I am trying to write inside function code block is:
SELECT DISTINCT rate
FROM DEMO.Employee
WHERE Company_ID = #company_id_
AND EMP_NO = #emp_no_
AND ACCOUNT_DATE = #account_date_
Something like:
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence
(#company_id VARCHAR(200),
#emp_no VARCHAR(200),
#account_date DATE)
RETURNS DECIMAL(10, 2) AS
BEGIN
DECLARE #RATE DECIMAL(10, 2)
SET #RATE = (
SELECT rate
FROM DEMO.Employee
WHERE COMPANY_ID = #company_id
AND Emp_no = #emp_no
AND ACCOUNT_DATE = #account_date
)
RETURN #RATE
END
Perhaps you actually want to return a whole resultset rather than just a single value.
Then you should use an inline Table Valued Function (of the form RETURNS TABLE AS RETURN SELECT ...) which in any case performs much better than a scalar function.
Variables don't go in quotes so you just do COMPANY_ID = #company_id_.
Always declare varchar with a length.
CREATE OR ALTER FUNCTION DEMO.Get_Rate_For_Absence (
#company_id_ VARCHAR(100),
#emp_no_ VARCHAR(100),
#wage_code_ VARCAHR(100),
#account_date_ DATE
)
RETURNS TABLE AS RETURN
SELECT e.rate
FROM DEMO.Employee e
WHERE e.COMPANY_ID = #company_id_
AND e.Emp_no = #emp_no_
AND e.ORG_CODE = #wage_code_
AND e.ACCOUNT_DATE = #account_date_;
You use it slightly differently than scalar functions, as it goes in the FROM part
SELECT r.rate
FROM DEMO.Get_Rate_For_Absence('a', 'b', 'c', GETDATE()) r;
Or
SELECT r.rate
FROM SomeTable t
CROSS APPLY DEMO.Get_Rate_For_Absence(t.a, t.b, t.c, t.date) r;

Replacing function call with procedure execution in sql

I had a function called dbo.Match.It was an inline TVF that I replaced with a procedure called dbo.Match that has a select statement at its end to select rows from a table so that I can direct the results of select query when I execute dbo.Match to a temporary table called #Temp.
Now if it was a function, I was using this query :
if #MotherFN is not null
begin
SELECT #constVal = FunctionWeight
FROM dbo.FunctionWeights
WHERE FunctionWeights.FunctionId = 20;
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,ValFromFunc,FuncWeight,percentage)
SELECT RowId,
#MotherFN ,
'mothersfirstname'
,PercentMatch,
#constVal,
PercentMatch * #constVal
FROM dbo.Match(#MotherFN)
end
Now, I need to execute dbo.Match procedure instead of dbo.Match function.How I may make this execution call and insert data in #Temp table like I was doing with function call ?
Problem : Calculating PercentMatch * #constVal and inserting in #Temp in same step efficiently. Procedure dbo.Match would return rowId and PercentMatch only. I need to insert values of RowId and PercentMatch in #Temp along with value of #constVal and a value for result of multiplication of PercentMatch and #constval
I would make the procedure accept these following parameters
#MotherFN , #constVal
And do the following inside the procedure , in select statement that returns the procedure's result set.
SELECT RowId,
#MotherFN , --<-- In proc definition
'mothersfirstname'
,PercentMatch,
#constVal, --<-- In proc definition
PercentMatch * #constVal --<-- In proc definition
And for insert simply do
INSERT INTO #TemP (RowNumber,ValFromUser,ColumnName
,ValFromFunc,FuncWeight,percentage)
Exec dbo.Match(#MotherFN , #constVal)
You options are more limited with a procedure.
You can use insert into ... exec ... to insert the results of the procedure into a temporary table, but you can't really combine it with another query. (Well you could use openrowset with dynamic SQL, but that will get nasty very quickly).
For example something like:
if #MotherFN is not null
begin
select
#constVal = FunctionWeight
from
dbo.FunctionWeights
where
FunctionWeights.FunctionId = 20;
insert into #Temp2 (
RowId, ColumnName, ValFromFunc
) exec
dbo.Match(#MotherFN);
update
#Temp2
set
ValFromUser = #MotherFN,
FuncWeight = #constVal,
percentage = PercentMatch * #constVal;
end;

Selecting multiple ints in query from a varchar

Consider the following table:
DECLARE #tmp_Example
TABLE (
TestInt int,
TestName varchar(max)
);
INSERT INTO #tmp_Example (TestInt, TestName)
VALUES (22,'Bob'),
(23,'James'),
(24,'Joe');
SELECT * from #tmp_Example
WHERE TestInt = 23;
I am getting into situations where somebody might want to select multiple results for TestInt, and I am receiving it like so:
('23,24')
Without changing the schema of the table, how can I support such a query? I fiddled with splitting it like so:
DECLARE #testEx as varchar(max) = '23,24';
SELECT * from #tmp_Example
WHERE TestInt = CAST(dbo.Split(#testEx,',') AS int);
Assuming dbo.Split is what I think it is, a table-valued function that will return integers 23 and 24 in their own rows:
SELECT t.TestInt, t.TestName
FROM #tmp_Example AS t
INNER JOIN dbo.Split(#testEx, ',') AS s
ON t.TestInt = s.OutputColumnName;
SQLfiddle demo
This approach works also (assuming you handle the case where someone has spaces in their list, e.g. 23, 24):
SELECT TestInt, TestName
FROM #tmp_Example
WHERE ','+REPLACE(#testEx, ' ', '')+','
LIKE '%,'+cast(TestInt as varchar(255))+',%';
SQLfiddle demo

Dynamic sql using table variable -TSQL

My problem is using a table variable in a exec.
declare #sort_col nvarchar(1000) = 'itm_id'
declare #sort_dir nvarchar(4) = 'desc'
declare #filters nvarchar(1000) = ' and itm_name like ''%aa%'''
declare #temp table
(
itm_id int
)
insert into #temp
EXEC('select itm_id from Tblitm where itm_name not like ''%aa%''')
EXEC('select * from (select (ROW_NUMBER() OVER (ORDER BY '+#sort_col+' '+#sort_dir+')) row_num, * FROM (select itm_id, itm_name,
dbo.fnItmsHistory(itm_id) itm_history
from dbo.Tblitm as itm
left outer join '+#temp+' as temp on itm.itm_id = temp.itm_id
where itm_id=itm_id and temp.itm_id = null '+#filters+') as x) as tmp')
It says Must declare the scalar variable "#temp" when the temp table is declared i tried using original temp table and it worked, but i had problems when trying to update my entity model.So is there any solution for this problem?
Note:
I must use exec because in filters i store string for the where clause.
Try moving the table variable inside the dynamic statement.
EXEC('
declare #temp table
(
itm_id int
)
insert into #temp
select itm_id from Tblitm where itm_name not like ''%aa%''
select * from (select (ROW_NUMBER() OVER (ORDER BY '+#sort_col+' '+#sort_dir+')) row_num, * FROM (select itm_id, itm_name,
dbo.fnItmsHistory(itm_id) itm_history
from dbo.Tblitm as itm
left outer join #temp as temp on itm.itm_id = temp.itm_id
where itm_id=itm_id and temp.itm_id = null '+#filters+') as x) as tmp')
For solution i had to use a temp table and then on the start of my stored procedure i used the if condition from the EF can't infer return schema from Stored Procedure selecting from a #temp table anwser.
It's the best solution for this scenario i think.

Define variable to use with IN operator (T-SQL)

I have a Transact-SQL query that uses the IN operator. Something like this:
select * from myTable where myColumn in (1,2,3,4)
Is there a way to define a variable to hold the entire list "(1,2,3,4)"? How should I define it?
declare #myList {data type}
set #myList = (1,2,3,4)
select * from myTable where myColumn in #myList
DECLARE #MyList TABLE (Value INT)
INSERT INTO #MyList VALUES (1)
INSERT INTO #MyList VALUES (2)
INSERT INTO #MyList VALUES (3)
INSERT INTO #MyList VALUES (4)
SELECT *
FROM MyTable
WHERE MyColumn IN (SELECT Value FROM #MyList)
DECLARE #mylist TABLE (Id int)
INSERT INTO #mylist
SELECT id FROM (VALUES (1),(2),(3),(4),(5)) AS tbl(id)
SELECT * FROM Mytable WHERE theColumn IN (select id from #mylist)
There are two ways to tackle dynamic csv lists for TSQL queries:
1) Using an inner select
SELECT * FROM myTable WHERE myColumn in (SELECT id FROM myIdTable WHERE id > 10)
2) Using dynamically concatenated TSQL
DECLARE #sql varchar(max)
declare #list varchar(256)
select #list = '1,2,3'
SELECT #sql = 'SELECT * FROM myTable WHERE myColumn in (' + #list + ')'
exec sp_executeSQL #sql
3) A possible third option is table variables. If you have SQl Server 2005 you can use a table variable. If your on Sql Server 2008 you can even pass whole table variables in as a parameter to stored procedures and use it in a join or as a subselect in the IN clause.
DECLARE #list TABLE (Id INT)
INSERT INTO #list(Id)
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
SELECT
*
FROM
myTable
JOIN #list l ON myTable.myColumn = l.Id
SELECT
*
FROM
myTable
WHERE
myColumn IN (SELECT Id FROM #list)
Use a function like this:
CREATE function [dbo].[list_to_table] (#list varchar(4000))
returns #tab table (item varchar(100))
begin
if CHARINDEX(',',#list) = 0 or CHARINDEX(',',#list) is null
begin
insert into #tab (item) values (#list);
return;
end
declare #c_pos int;
declare #n_pos int;
declare #l_pos int;
set #c_pos = 0;
set #n_pos = CHARINDEX(',',#list,#c_pos);
while #n_pos > 0
begin
insert into #tab (item) values (SUBSTRING(#list,#c_pos+1,#n_pos - #c_pos-1));
set #c_pos = #n_pos;
set #l_pos = #n_pos;
set #n_pos = CHARINDEX(',',#list,#c_pos+1);
end;
insert into #tab (item) values (SUBSTRING(#list,#l_pos+1,4000));
return;
end;
Instead of using like, you make an inner join with the table returned by the function:
select * from table_1 where id in ('a','b','c')
becomes
select * from table_1 a inner join [dbo].[list_to_table] ('a,b,c') b on (a.id = b.item)
In an unindexed 1M record table the second version took about half the time...
I know this is old now but TSQL => 2016, you can use STRING_SPLIT:
DECLARE #InList varchar(255) = 'This;Is;My;List';
WITH InList (Item) AS (
SELECT value FROM STRING_SPLIT(#InList, ';')
)
SELECT *
FROM [Table]
WHERE [Item] IN (SELECT Tag FROM InList)
Starting with SQL2017 you can use STRING_SPLIT and do this:
declare #myList nvarchar(MAX)
set #myList = '1,2,3,4'
select * from myTable where myColumn in (select value from STRING_SPLIT(#myList,','))
DECLARE #myList TABLE (Id BIGINT) INSERT INTO #myList(Id) VALUES (1),(2),(3),(4);
select * from myTable where myColumn in(select Id from #myList)
Please note that for long list or production systems it's not recommended to use this way as it may be much more slower than simple INoperator like someColumnName in (1,2,3,4) (tested using 8000+ items list)
slight improvement on #LukeH, there is no need to repeat the "INSERT INTO":
and #realPT's answer - no need to have the SELECT:
DECLARE #MyList TABLE (Value INT)
INSERT INTO #MyList VALUES (1),(2),(3),(4)
SELECT * FROM MyTable
WHERE MyColumn IN (SELECT Value FROM #MyList)
No, there is no such type. But there are some choices:
Dynamically generated queries (sp_executesql)
Temporary tables
Table-type variables (closest thing that there is to a list)
Create an XML string and then convert it to a table with the XML functions (really awkward and roundabout, unless you have an XML to start with)
None of these are really elegant, but that's the best there is.
If you want to do this without using a second table, you can do a LIKE comparison with a CAST:
DECLARE #myList varchar(15)
SET #myList = ',1,2,3,4,'
SELECT *
FROM myTable
WHERE #myList LIKE '%,' + CAST(myColumn AS varchar(15)) + ',%'
If the field you're comparing is already a string then you won't need to CAST.
Surrounding both the column match and each unique value in commas will ensure an exact match. Otherwise, a value of 1 would be found in a list containing ',4,2,15,'
As no one mentioned it before, starting from Sql Server 2016 you can also use json arrays and OPENJSON (Transact-SQL):
declare #filter nvarchar(max) = '[1,2]'
select *
from dbo.Test as t
where
exists (select * from openjson(#filter) as tt where tt.[value] = t.id)
You can test it in
sql fiddle demo
You can also cover more complicated cases with json easier - see Search list of values and range in SQL using WHERE IN clause with SQL variable?
This one uses PATINDEX to match ids from a table to a non-digit delimited integer list.
-- Given a string #myList containing character delimited integers
-- (supports any non digit delimiter)
DECLARE #myList VARCHAR(MAX) = '1,2,3,4,42'
SELECT * FROM [MyTable]
WHERE
-- When the Id is at the leftmost position
-- (nothing to its left and anything to its right after a non digit char)
PATINDEX(CAST([Id] AS VARCHAR)+'[^0-9]%', #myList)>0
OR
-- When the Id is at the rightmost position
-- (anything to its left before a non digit char and nothing to its right)
PATINDEX('%[^0-9]'+CAST([Id] AS VARCHAR), #myList)>0
OR
-- When the Id is between two delimiters
-- (anything to its left and right after two non digit chars)
PATINDEX('%[^0-9]'+CAST([Id] AS VARCHAR)+'[^0-9]%', #myList)>0
OR
-- When the Id is equal to the list
-- (if there is only one Id in the list)
CAST([Id] AS VARCHAR)=#myList
Notes:
when casting as varchar and not specifying byte size in parentheses the default length is 30
% (wildcard) will match any string of zero or more characters
^ (wildcard) not to match
[^0-9] will match any non digit character
PATINDEX is an SQL standard function that returns the position of a pattern in a string
DECLARE #StatusList varchar(MAX);
SET #StatusList='1,2,3,4';
DECLARE #Status SYS_INTEGERS;
INSERT INTO #Status
SELECT Value
FROM dbo.SYS_SPLITTOINTEGERS_FN(#StatusList, ',');
SELECT Value From #Status;
Most of these seem to focus on separating-out each INT into its own parenthetical, for example:
(1),(2),(3), and so on...
That isn't always convenient. Especially since, many times, you already start with a comma-separated list, for example:
(1,2,3,...) and so on...
In these situations, you may care to do something more like this:
DECLARE #ListOfIds TABLE (DocumentId INT);
INSERT INTO #ListOfIds
SELECT Id FROM [dbo].[Document] WHERE Id IN (206,235,255,257,267,365)
SELECT * FROM #ListOfIds
I like this method because, more often than not, I am trying to work with IDs that should already exist in a table.
My experience with a commonly proposed technique offered here,
SELECT * FROM Mytable WHERE myColumn IN (select id from #mylist)
is that it induces a major performance degradation if the primary data table (Mytable) includes a very large number of records. Presumably, that is because the IN operator’s list-subquery is re-executed for every record in the data table.
I’m not seeing any offered solution here that provides the same functional result by avoiding the IN operator entirely. The general problem isn’t a need for a parameterized IN operation, it’s a need for a parameterized inclusion constraint. My favored technique for that is to implement it using an (inner) join:
DECLARE #myList varchar(50) /* BEWARE: if too small, no error, just missing data! */
SET #myList = '1,2,3,4'
SELECT *
FROM myTable
JOIN STRING_SPLIT(#myList,',') MyList_Tbl
ON myColumn = MyList_Tbl.Value
It is so much faster because the generation of the constraint-list table (MyList_Tbl) is executed only once for the entire query execution. Typically, for large data sets, this technique executes at least five times faster than the functionally equivalent parameterized IN operator solutions, like those offered here.
I think you'll have to declare a string and then execute that SQL string.
Have a look at sp_executeSQL