Slow MSSQL stored procedure in processes excel files with only 30,000 rows - sql

I have a web applciation with an iterface that users can uplaod files on. The data form the excel file is collected, concatenated and passed to
a stored procedure which process and returns data.
A brief explanation of the stored procedure.
The stored Procedure collects the string, break it down using a delimeter and stores it in a temp variable table.
Another process is run trough the temp table, where a count is done to find the exact match count and approximate match count by comparing each string
agains a view which contains
all the names to compare against for each row in the first
An exact match count is where the eact string is found in the view for example.. (Bobby Bolonski )
An approximate match is done using a levenshtein distance algorithm database function with a frequency of 2.
temo table #temp1.
The result (name, exactmatch count and approximate match count) are stored in the final temp table.
a select statement is run on the last temp table to return all the data to the application..
MY problem is that, when i passed huge files like and excel file with 27000 names. IT took like 2 hours to process and return data from the database.
I have checked both servers where the application is on and where the database is on.
On the application server. Both memory and cpu usage are less than 15 %
On the database server. both memory and cpu usage are also less than 15 %.
Am looking for advice on what improvements i can do to make the process faster.
Below is the copy of the stored procedure as it is doing all the work and returning the results to the web application.
CREATE PROCEDURE [dbo].[FindMatch]
#fullname varchar(max),#frequency int,
#delimeter varchar(max) AS
set #frequency = 2
declare #transID bigint
SELECT #transID = ABS(CAST(CAST(NEWID() AS VARBINARY(5)) AS Bigint))
DECLARE #exactMatch int = 99
DECLARE #approximateMatch int = 99
declare #name varchar(50)
DECLARE #TEMP1 TABLE (fullname varchar(max),approxMatch varchar(max), exactmatch varchar(max))
DECLARE #ID varchar(max)
--declare a temp table
DECLARE #TEMP TABLE (ID int ,fullname varchar(max),approxMatch varchar(max), exactmatch varchar(max))
--split and store the result in the #temp table
insert into #TEMP (ID,fullname) select * from fnSplitTest(#fullname, #delimeter)
--loop trough the #temp table
WHILE EXISTS (SELECT ID FROM #TEMP)
BEGIN
SELECT Top 1 #ID = ID FROM #TEMP
select #name = fullname from #TEMP where id = #ID
--get the exact match count of the first row from the #temp table and so on until the loop ends
select #exactMatch = count(1) from getalldata where replace(name,',','') COLLATE Latin1_general_CI_AI = #name COLLATE Latin1_general_CI_AI
--declare temp #TEMP3
DECLARE #TEMP3 TABLE (name varchar(max))
--insert into #temp 3 only the data that are similar to our search name so as not to loop over all the data in the view
INSERT INTO #TEMP3(name)
select name from getalldata where SOUNDEX(name) LIKE SOUNDEX(#name)
--get the approximate count using the [DEMLEV] function.
--this function uses the Damerau levenshtein distance algorithm to calculate the distinct between the search string
--and the names inserted into #temp3 above. Uses frequency 2 so as to eliminate all the others
select #approximateMatch = count(1) from #TEMP3 where
dbo.[DamLev](replace(name,',',''),#name,#frequency) <= #frequency and
dbo.[DamLev](replace(name,',',''),#name,#frequency) > 0 and name != #name
--insert into #temp1 at end of every loop results
insert into #TEMP1 (fullname,approxMatch, exactmatch) values(#name,#approximateMatch,#exactMatch)
insert into FileUploadNameInsert (name) values (#name + ' ' +cast(#approximateMatch as varchar) + ' ' + cast(#exactMatch as varchar) + ', ' + cast(#transID as varchar) )
DELETE FROM #TEMP WHERE ID= #ID
delete from #TEMP3
END
--Return all the data stored in #temp3
select fullname,exactmatch,approxMatch, #transID as transactionID from #TEMP1
GO

In my opinion,
Use Openrowset to directly read the records into a pre-defined, properly indexed table of your database.
Now, perform your operations using this table at back-end using pre-defined Stored Procedures.
It should take around 15 minutes for 30,000 rows.

Related

SQL: efficiently append incremental number to string, avoiding duplicates

I have a set of records (table [#tmp_origin]) containing duplicate entries in a string field ([Names]). I would like to insert the whole content of [#tmp_origin] into the destination table [#tmp_destination], that does NOT allow duplicates and may already contain items.
If the string in the origin table does not exist in the destination table, then in is simply inserted in the destination table, as is.
If an entry in the destination table already exists with the same value of the entry in the original table, a string-ified incremental number must be appended to the string, before it is inserted in the destination table.
The process of moving data in this way has been implemented with a cursor, in this sample script:
-- create initial situation (origin and destination table, both containing items)
-- Begin
CREATE TABLE [#tmp_origin] ([Names] VARCHAR(10))
CREATE TABLE [#tmp_destination] ([Names] VARCHAR(10))
CREATE UNIQUE INDEX [IX_UniqueName] ON [#tmp_destination]([Names] ASC)
INSERT INTO [#tmp_origin]([Names]) VALUES ('a')
INSERT INTO [#tmp_origin]([Names]) VALUES ('a')
INSERT INTO [#tmp_origin]([Names]) VALUES ('b')
INSERT INTO [#tmp_origin]([Names]) VALUES ('c')
INSERT INTO [#tmp_destination]([Names]) VALUES ('a')
INSERT INTO [#tmp_destination]([Names]) VALUES ('a_1')
INSERT INTO [#tmp_destination]([Names]) VALUES ('b')
-- create initial situation - End
DECLARE #Name VARCHAR(10)
DECLARE NamesCursor CURSOR LOCAL FORWARD_ONLY FAST_FORWARD READ_ONLY FOR
SELECT [Names]
FROM [#tmp_origin];
OPEN NamesCursor;
FETCH NEXT FROM NamesCursor INTO #Name;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #finalName VARCHAR(10)
SET #finalName = #Name
DECLARE #counter INT
SET #counter = 1
WHILE(1=1)
BEGIN
IF NOT EXISTS(SELECT * FROM [#tmp_destination] WHERE [Names] = #finalName)
BREAK;
SET #finalName = #Name + '_' + CAST(#counter AS VARCHAR)
SET #counter = #counter + 1
END
INSERT INTO [#tmp_destination] ([Names]) (
SELECT #finalName
)
FETCH NEXT FROM NamesCursor INTO #Name;
END
CLOSE NamesCursor;
DEALLOCATE NamesCursor;
SELECT *
FROM [#tmp_destination]
/*
Expected result:
a
a_1
a_2
a_3
b
b_1
c
*/
DROP TABLE [#tmp_origin]
DROP TABLE [#tmp_destination]
This works correctly, but its performance drastically slows down when the number of items to insert increases.
Any idea to speed it up?
thanks
Using a windowing function allows the duplicates to be numbered. You can also get the count from the destination table (will need where condition to strip off the suffix you've added):
select orig.names,
row_number() over (partition by orig.names order by orig.names) as rowNo,
dest.count
from ##tmp_origin orig
cross apply (select count(1) from #tmp_destination where names = orig.names) as dest
An insert can be built from the above (new suffix is rowNo + dest.count -1 if greater than zero).
Suggest you refactor the destination temporary table to include the name and suffix as separate columns – this might mean having a new intermediate stage – because this will make the matching logic much simpler.
Something like this:
insert [#tmp_destination]
select CASE WHEN row_number() over(partition by Names order by Names) > 1 THEN Names + '_' + CONVERT(VARCHAR(10), row_number() over(partition by Names order by Names)) ELSE Names END
from [#tmp_origin]
I wouldn't use a cursor in that case. Instead, I would build the query using ROW_NUMBER(). This way you add a counter in your original table, and then use this counter to append to your [Names]:
SELECT [Names], ROW_NUMBER() OVER (PARTITION BY [Names] ORDER BY [Names]) - 1 AS [counter]
INTO #tmp_origin_with_counter
FROM #tmp_origin
SELECT CONCAT([Names], IIF([counter] = 0, '', '_'+ CAST([counter] AS NVARCHAR)))
INTO #tmp_destination
FROM #tmp_origin_with_counter

SSMS Temp Table / Parameter Issue

I have a temp table with 2 columns, each column is a parameter I've declared. I've done so using this sql.
Declare
#SourceKey varchar(40) = '1109'
,#Department Key varchar(1500) = '14,55
The table is then populated using the following sql:
if OBJECT_ID('Tempdb..#Department','U') is not null
drop table #Department
CREATE TABLE #Department
(DepartmentKey int
,BaseTerm varchar(5))
INSERT INTO #Department
SELECT value
,skt.Key from YYY.ParseList(#Department,',')
join #SourceKeyTable skt
on skt.Key = skt.key
If I select * From #Department I get these results:
Department Key | SourceKey
14 | 1109
55 | 1109
Thats what I expect. I then join the temp table to my main query like so
JOIN #Department d
on Table.rKey = d.DepartmentKey
I need to have a temp table to allow for a multi-select in the visual studio report. However, with the department key equal to 14 AND 55 its skewing my results. I need 1 value passed 14 OR 55 not both. But the temp table is neccessary for the multi-select.
Any suggestions on how to pass only 1 value while still having set up mentioned previously?
I'll do my best to answer questions as I might not have explained this question well enough for some.
I reckon you need to parse your list into a temporary table or table variable and then do whatever needs to be done with it.
It's difficult to see from your code exactly what that would involve but the code below should illustrate the idea sufficiently.
I create a table variable. Insert the parsed list values and then cycle through them printing the values to output
--Create a table variable
DECLARE #Departments TABLE(DepartmentOrder int identity, RKey nvarchar(40) NOT NULL)
--Create variables to loop through table variable
DECLARE #DepartmentOrder int
DECLARE #RKey nvarchar(40)
--Populate table variable with the parsed list values
INSERT #Departments (RKey) SELECT RKey from YYY.ParseList(#Department,',')
--Get the first list entry
SELECT #DepartmentOrder = min(DepartmentOrder ) FROM #Departments
--While we've not reached the end
WHILE #DepartmentOrder IS NOT NULL
BEGIN
--Get the Department key for this entry
SET #RKey = (SELECT RKey FROM #Departments WHERE DepartmentOrder = #DepartmentOrder )
--Use the values
PRINT '#DepartmentOrder = '+CONVERT(nvarchar(9),#DepartmentOrder )
PRINT '#RKey = '''+#RKey +''''
--Get the next list entry
SET #DepartmentOrder = (SELECT MIN(DepartmentOrder ) FROM #Departments WHERE DepartmentOrder > #DepartmentOrder )
END

How to get query result with stored procedure (convert item quantity from one table into my unit defined in second table)

I have two MSSQL2008 tables like this:
I have problem on the unit conversion logic.
The result I expect like this :
1589 cigar = 1ball, 5slop, 8box, 2pcs
52 pen = 2box, 12pcs
Basically I'm trying to take number (qty) from one table and to convert (split) him into the units which I defined in other table!
Note : Both table are allowed to add new row and new data (dinamic)
How can I get these results through a SQL stored procedure?
i totally misunderstand the question lest time so previous answer is removed (you can see it in edit but it's not relevant for this question)... However i come up with solution that may solve your problem...
NOTE: one little think about this solution, if you enter the value in second table like this
+--------+-------+
| Item | qty |
+--------+-------+
| 'cigar'| 596 |
+--------+-------+
result for this column will be
598cigar = 0ball, 5slop, 8box, 0pcs
note that there is a ball and pcs is there even if their value is 0, that probably can be fix if you don't want to show that value but I let you to play with it...
So let's back to solution and code. Solution have two stored procedures first one is the main and that one is the one you execute. I call it sp_MainProcedureConvertMe. Here is a code for that procedure:
CREATE PROCEDURE sp_MainProcedureConvertMe
AS
DECLARE #srcTable TABLE(srcId INT IDENTITY(1, 1), srcItem VARCHAR(50), srcQty INT)
DECLARE #xTable TABLE(xId INT IDENTITY(1, 1), xVal1 VARCHAR(1000), xVal2 VARCHAR(1000))
DECLARE #maxId INT
DECLARE #start INT = 1
DECLARE #sItem VARCHAR(50)
DECLARE #sQty INT
DECLARE #val1 VARCHAR(1000)
DECLARE #val2 VARCHAR(1000)
INSERT INTO #srcTable (srcItem, srcQty)
SELECT item, qty
FROM t2
SELECT #maxId = (SELECT MAX(srcId) FROM #srcTable)
WHILE #start <= #maxId
BEGIN
SELECT #sItem = (SELECT srcItem FROM #srcTable WHERE srcId = #start)
SELECT #sQty = (SELECT srcQty FROM #srcTable WHERE srcId = #start)
SELECT #val1 = (CAST(#sQty AS VARCHAR) + #sItem)
EXECUTE sp_ConvertMeIntoUnit #sItem, #sQty, #val2 OUTPUT
INSERT INTO #xTable (xVal1, xVal2)
VALUES (#val1, #val2)
SELECT #start = (#start + 1)
CONTINUE
END
SELECT xVal1 + ' = ' + xVal2 FROM #xTable
GO
This stored procedure have two variables as table #srcTable is basically your second table but instead of using id of your table it's create new srcId which goes from 1 to some number and it's auto_increment it's done because of while loop to avoid any problems when there is some deleted values etc. so we wanna be sure that there wont be any skipped number or something like that.
There is few more variables some of them is used to make while loop work other one is to store data. I think it's not hard to figure out from code what are they used for...
While loop iterate throughout all rows from #srcTable take values processing them and insert them into #xTable which basically hold result.
In while loop we execute second stored procedure which have a task to calculate how many unit of something is there in specific number of item. I call her sp_ConvertMeIntoUnit and here is a code for her:
CREATE PROCEDURE sp_ConvertMeIntoUnit
#inItemName VARCHAR(50),
#inQty INT,
#myResult VARCHAR(5000) OUT
AS
DECLARE #rTable TABLE(rId INT IDENTITY(1, 1), rUnit VARCHAR(50), rQty INT)
DECLARE #yTable TABLE(yId INT IDENTITY(1, 1), yVal INT, yRest INT)
DECLARE #maxId INT
DECLARE #start INT = 1
DECLARE #quentity INT = #inQty
DECLARE #divider INT
DECLARE #quant INT
DECLARE #rest INT
DECLARE #result VARCHAR(5000)
INSERT INTO #rTable(rUnit, rQty)
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
SELECT #maxId = (SELECT MAX(rId) FROM #rTable)
WHILE #start <= #maxId
BEGIN
SELECT #divider = (SELECT rQty FROM #rTable WHERE rId = #start)
SELECT #quant = (#quentity / #divider)
SELECT #rest = (#quentity % #divider)
INSERT INTO #yTable(yVal, yRest)
VALUES (#quant, #rest)
SELECT #quentity = #rest
SELECT #start = (#start + 1)
CONTINUE
END
SELECT #result = COALESCE(#result + ', ', '') + CAST(y.yVal AS VARCHAR) + r.rUnit FROM #rTable AS r INNER JOIN #yTable AS y ON r.rId = y.yId
SELECT #myResult = #result
GO
This procedure contain three parametars it's take two parameters from the first one and one is returned as result (OUTPUT). In parameters are Item and Quantity.
There are also two variables as table #rTable we stored values as #rId which is auto increment and always will go from 1 to some number no matter what is there Id's in the first table. Other two values are inserted there from the first table based on #inItemName parameter which is sanded from first procedure... From the your first table we use unit and quantity and stored them with rId into table #rTable ordered by Qty from biggest number to lowest. This is a part of code for that
INSERT INTO #rTable(rUnit, rQty)
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
Then we go into while loop where we do some maths. Basically we store into variable #divider values from #rTable. In the first iteration we take the biggest value calculate how many times it's contain into the number (second parameter we pass from first procedure is qty from the yours second table) and store it into #quant than we also calculate modulo and store it into variable #rest. This line
SELECT #rest = (#quentity % #divider)
After that we insert our values into #yTable. Before we and with iteration in while loop we assign #quentity variable value of #rest value because we need to work just with the remainder not with whole quantity any more. In second iteration we take next (the second greatest number in our #rTable) number and procedure repeat itself...
When while loop finish we create a string. This line here:
SELECT #result = COALESCE(#result + ', ', '') + CAST(y.yVal AS VARCHAR) + r.rUnit FROM #rTable AS r INNER JOIN #yTable AS y ON r.rId = y.yId
This is the line you want to change if you want to exclude result with 0 (i talk about them at the beginning of answer)...
And at the end we store result into output variable #myResult...
Result of this stored procedure will return string like this:
+--------------------------+
| 1ball, 5slop, 8box, 2pcs |
+--------------------------+
Hope I didn't miss anything important. Basically only think you should change here is the name of the table and their columns (if they are different) in first stored procedure instead t2 here
INSERT INTO...
SELECT item, qty
FROM t2
And in second one instead of t1 (and column if needed) here..
INSERT INTO...
SELECT unit, qty
FROM t1
WHERE item = #inItemName
ORDER BY qty DESC
Hope i help a little or give you an idea how this can be solved...
GL!
You seem to want string aggregation – something that does not have a simple instruction in Transact-SQL and is usually implemented using a correlated FOR XML subquery.
You have not provided names for your tables. For the purpose of the following example, the first table is called ItemDetails and the second one, Items:
SELECT
i.item,
i.qty,
details = (
SELECT
', ' + CAST(d.qty AS varchar(10)) + d.unit
FROM
dbo.ItemDetails AS d
WHERE
d.item = i.item
FOR XML
PATH (''), TYPE
).value('substring(./text()[1], 3)', 'nvarchar(max)')
FROM
dbo.Items AS i
;
For the input provided in the question, the above query would return the following output:
item qty details
----- ----------- ------------------------------
cigar 1598 1pcs, 1000ball, 12box, 100slop
pen 52 1pcs, 20box
You can further arrange the data into strings as per your requirement. I would recommend you do it in the calling application and use SQL only as your data source. However, if you must, you can do the concatenation in SQL as well.
Note that the above query assumes that the same unit does not appear more than once per item in ItemDetails. If it does and you want to aggregate qty values per unit before producing the detail line, you will need to change the query a little:
SELECT
i.item,
i.qty,
details = (
SELECT
', ' + CAST(SUM(d.qty) AS varchar(10)) + d.unit
FROM
dbo.ItemDetails AS d
WHERE
d.item = i.item
GROUP BY
d.unit
FOR XML
PATH (''), TYPE
).value('substring(./text()[1], 3)', 'nvarchar(max)')
FROM
dbo.Items AS i
;

Removing ' ' from sql query and stored proc

I have a table in which a column named data is of type varbinary . If I do a simple query
select * from tab where data = 1 then it works but if I do select * from tab where data = '1' then it does not return any row. The issue comes when I create a stored proc to retrieve data from this table and it converts the query and adds ' ' in the parameter when querying and so I am not able to retrieve any data. Can some one please tell me how to get around this issue.
Parameters
#ID INT = NULL
,#Data varchar(100) = NULL
CREATE TABLE #Results (
ID INT
,Data varchar(100)
)
BEGIN
INSERT #Results (
ID
,Data
)
SELECT
SK.ID
,SK.Data
FROM dbo.tab SK
where SK.ID = #ID And SK.data = #data
END
SELECT #TotalRows = COUNT(*)
FROM #Results
SELECT #TotalRows TotalRows
Now from the code when I execute this statement
oReader = ExecuteReader(oConn, CommandType.StoredProcedure, "Proc", New SqlParameter("#ID", Request.ID), _
New SqlParameter("#Data", Request.Data))
I see in SQL Profiler that it runs the query as 'data'
which does not return any rows
Thanks
Since you have said that you have written an SP, I think the inpput parameter is specified as NVARCHAR or VARCHAR
Below is one way of doing but i'm guessing that the column called data will only have integer values in the first solution.
DECLARE #X VARCHAR(5)
SET #X = '1 '
SELECT CAST(#X AS INT)
The above is only if the Data column specified above is Integer.
If the same is string (VARCHAR) you can write a User defined function to do the same.
CREATE FUNCTION dbo.TRIM(#string VARCHAR(8000))
RETURNS VARCHAR(8000)
BEGIN
RETURN LTRIM(RTRIM(#string))
END
SELECT dbo.TRIM('1 ')
I hope the above was useful, I did get the idea rather copied the function from here
http://blog.sqlauthority.com/2007/04/24/sql-server-trim-function-udf-trim/

Pass table as parameter into sql server UDF

I'd like to pass a table as a parameter into a scaler UDF.
I'd also prefer to restrict the parameter to tables with only one column. (optional)
Is this possible?
EDIT
I don't want to pass a table name, I'd like to pass the table of data (as a reference I presume)
EDIT
I would want my Scaler UDF to basically take a table of values and return a CSV list of the rows.
IE
col1
"My First Value"
"My Second Value"
...
"My nth Value"
would return
"My First Value, My Second Value,... My nth Value"
I'd like to do some filtering on the table though, IE ensuring that there are no nulls and to ensure there are no duplicates. I was expecting something along the lines of:
SELECT dbo.MyFunction(SELECT DISTINCT myDate FROM myTable WHERE myDate IS NOT NULL)
You can, however no any table. From documentation:
For Transact-SQL functions, all data
types, including CLR user-defined
types and user-defined table types,
are allowed except the timestamp data
type.
You can use user-defined table types.
Example of user-defined table type:
CREATE TYPE TableType
AS TABLE (LocationName VARCHAR(50))
GO
DECLARE #myTable TableType
INSERT INTO #myTable(LocationName) VALUES('aaa')
SELECT * FROM #myTable
So what you can do is to define your table type, for example TableType and define the function which takes the parameter of this type. An example function:
CREATE FUNCTION Example( #TableName TableType READONLY)
RETURNS VARCHAR(50)
AS
BEGIN
DECLARE #name VARCHAR(50)
SELECT TOP 1 #name = LocationName FROM #TableName
RETURN #name
END
The parameter has to be READONLY. And example usage:
DECLARE #myTable TableType
INSERT INTO #myTable(LocationName) VALUES('aaa')
SELECT * FROM #myTable
SELECT dbo.Example(#myTable)
Depending on what you want achieve you can modify this code.
EDIT:
If you have a data in a table you may create a variable:
DECLARE #myTable TableType
And take data from your table to the variable
INSERT INTO #myTable(field_name)
SELECT field_name_2 FROM my_other_table
Unfortunately, there is no simple way in SQL Server 2005. Lukasz' answer is correct for SQL Server 2008 though and the feature is long overdue
Any solution would involve temp tables, or passing in xml/CSV and parsing in the UDF. Example: change to xml, parse in udf
DECLARE #psuedotable xml
SELECT
#psuedotable = ...
FROM
...
FOR XML ...
SELECT ... dbo.MyUDF (#psuedotable)
What do you want to do in the bigger picture though? There may be another way to do this...
Edit: Why not pass in the query as a string and use a stored proc with output parameter
Note: this is an untested bit of code, and you'd need to think about SQL injection etc. However, it also satisfies your "one column" requirement and should help you along
CREATE PROC dbo.ToCSV (
#MyQuery varchar(2000),
#CSVOut varchar(max)
)
AS
SET NOCOUNT ON
CREATE TABLE #foo (bar varchar(max))
INSERT #foo
EXEC (#MyQuery)
SELECT
#CSVOut = SUBSTRING(buzz, 2, 2000000000)
FROM
(
SELECT
bar -- maybe CAST(bar AS varchar(max))??
FROM
#foo
FOR XML PATH (',')
) fizz(buzz)
GO
Step 1: Create a Type as Table with name TableType that will accept a table having one varchar column
create type TableType
as table ([value] varchar(100) null)
Step 2: Create a function that will accept above declared TableType as Table-Valued Parameter and String Value as Separator
create function dbo.fn_get_string_with_delimeter (#table TableType readonly,#Separator varchar(5))
returns varchar(500)
As
begin
declare #return varchar(500)
set #return = stuff((select #Separator + value from #table for xml path('')),1,1,'')
return #return
end
Step 3: Pass table with one varchar column to the user-defined type TableType and ',' as separator in the function
select dbo.fn_get_string_with_delimeter(#tab, ',')
Cutting to the bottom line, you want a query like SELECT x FROM y to be passed into a function that returns the values as a comma separated string.
As has already been explained you can do this by creating a table type and passing a UDT into the function, but this needs a multi-line statement.
You can pass XML around without declaring a typed table, but this seems to need a xml variable which is still a multi-line statement i.e.
DECLARE #MyXML XML = (SELECT x FROM y FOR XML RAW);
SELECT Dbo.CreateCSV(#MyXml);
The "FOR XML RAW" makes the SQL give you it's result set as some xml.
But you can bypass the variable using Cast(... AS XML). Then it's just a matter of some XQuery and a little concatenation trick:
CREATE FUNCTION CreateCSV (#MyXML XML)
RETURNS VARCHAR(MAX)
BEGIN
DECLARE #listStr VARCHAR(MAX);
SELECT
#listStr =
COALESCE(#listStr+',' ,'') +
c.value('#Value[1]','nvarchar(max)')
FROM #myxml.nodes('/row') as T(c)
RETURN #listStr
END
GO
-- And you call it like this:
SELECT Dbo.CreateCSV(CAST(( SELECT x FROM y FOR XML RAW) AS XML));
-- Or a working example
SELECT Dbo.CreateCSV(CAST((
SELECT DISTINCT number AS Value
FROM master..spt_values
WHERE type = 'P'
AND number <= 20
FOR XML RAW) AS XML));
As long as you use FOR XML RAW all you need do is alias the column you want as Value, as this is hard coded in the function.
PASSING TABLE AS PARAMETER IN STORED PROCEDURE
Step 1:
CREATE TABLE [DBO].T_EMPLOYEES_DETAILS
(
Id int,
Name nvarchar(50),
Gender nvarchar(10),
Salary int
)
Step 2:
CREATE TYPE EmpInsertType AS TABLE
(
Id int,
Name nvarchar(50),
Gender nvarchar(10),
Salary int
)
Step 3:
/* Must add READONLY keyword at end of the variable */
CREATE PROC PRC_EmpInsertType
#EmployeeInsertType EmpInsertType READONLY
AS
BEGIN
INSERT INTO [DBO].T_EMPLOYEES_DETAILS
SELECT * FROM #EmployeeInsertType
END
Step 4:
DECLARE #EmployeeInsertType EmpInsertType
INSERT INTO #EmployeeInsertType VALUES(1,'John','Male',50000)
INSERT INTO #EmployeeInsertType VALUES(2,'Praveen','Male',60000)
INSERT INTO #EmployeeInsertType VALUES(3,'Chitra','Female',45000)
INSERT INTO #EmployeeInsertType VALUES(4,'Mathy','Female',6600)
INSERT INTO #EmployeeInsertType VALUES(5,'Sam','Male',50000)
EXEC PRC_EmpInsertType #EmployeeInsertType
=======================================
SELECT * FROM T_EMPLOYEES_DETAILS
OUTPUT
1 John Male 50000
2 Praveen Male 60000
3 Chitra Female 45000
4 Mathy Female 6600
5 Sam Male 50000
I've been dealing with a very similar problem and have been able to achieve what I was looking for, even though I'm using SQL Server 2000. I know it is an old question, but think its valid to post here the solution since there should be others like me that use old versions and still need help.
Here's the trick: SQL Server won't accept passing a table to a UDF, nor you can pass a T-SQL query so the function creates a temp table or even calls a stored procedure to do that. So, instead, I've created a reserved table, which I called xtList. This will hold the list of values (1 column, as needed) to work with.
CREATE TABLE [dbo].[xtList](
[List] [varchar](1000) NULL
) ON [PRIMARY]
Then, a stored procedure to populate the list. This is not strictly necessary, but I think is very usefull and best practice.
-- =============================================
-- Author: Zark Khullah
-- Create date: 20/06/2014
-- =============================================
CREATE PROCEDURE [dbo].[xpCreateList]
#ListQuery varchar(2000)
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM xtList
INSERT INTO xtList
EXEC(#ListQuery)
END
Now, just deal with the list in any way you want, using the xtList. You can use in a procedure (for executing several T-SQL commands), scalar functions (for retrieving several strings) or multi-statement table-valued functions (retrieves the strings but like it was inside a table, 1 string per row). For any of that, you'll need cursors:
DECLARE #Item varchar(100)
DECLARE cList CURSOR DYNAMIC
FOR (SELECT * FROM xtList WHERE List is not NULL)
OPEN cList
FETCH FIRST FROM cList INTO #Item
WHILE ##FETCH_STATUS = 0 BEGIN
<< desired action with values >>
FETCH NEXT FROM cList INTO #Item
END
CLOSE cList
DEALLOCATE cList
The desired action would be as follows, depending on which type of object created:
Stored procedures
-- =============================================
-- Author: Zark Khullah
-- Create date: 20/06/2014
-- =============================================
CREATE PROCEDURE [dbo].[xpProcreateExec]
(
#Cmd varchar(8000),
#ReplaceWith varchar(1000)
)
AS
BEGIN
DECLARE #Query varchar(8000)
<< cursor start >>
SET #Query = REPLACE(#Cmd,#ReplaceWith,#Item)
EXEC(#Query)
<< cursor end >>
END
/* EXAMPLES
(List A,B,C)
Query = 'SELECT x FROM table'
with EXEC xpProcreateExec(Query,'x') turns into
SELECT A FROM table
SELECT B FROM table
SELECT C FROM table
Cmd = 'EXEC procedure ''arg''' --whatchout for wrong quotes, since it executes as dynamic SQL
with EXEC xpProcreateExec(Cmd,'arg') turns into
EXEC procedure 'A'
EXEC procedure 'B'
EXEC procedure 'C'
*/
Scalar functions
-- =============================================
-- Author: Zark Khullah
-- Create date: 20/06/2014
-- =============================================
CREATE FUNCTION [dbo].[xfProcreateStr]
(
#OriginalText varchar(8000),
#ReplaceWith varchar(1000)
)
RETURNS varchar(8000)
AS
BEGIN
DECLARE #Result varchar(8000)
SET #Result = ''
<< cursor start >>
SET #Result = #Result + REPLACE(#OriginalText,#ReplaceWith,#Item) + char(13) + char(10)
<< cursor end >>
RETURN #Result
END
/* EXAMPLE
(List A,B,C)
Text = 'Access provided for user x'
with "SELECT dbo.xfProcreateStr(Text,'x')" turns into
'Access provided for user A
Access provided for user B
Access provided for user C'
*/
Multi-statement table-valued functions
-- =============================================
-- Author: Zark Khullah
-- Create date: 20/06/2014
-- =============================================
CREATE FUNCTION [dbo].[xfProcreateInRows]
(
#OriginalText varchar(8000),
#ReplaceWith varchar(1000)
)
RETURNS
#Texts TABLE
(
Text varchar(2000)
)
AS
BEGIN
<< cursor start >>
INSERT INTO #Texts VALUES(REPLACE(#OriginalText,#ReplaceWith,#Item))
<< cursor end >>
END
/* EXAMPLE
(List A,B,C)
Text = 'Access provided for user x'
with "SELECT * FROM dbo.xfProcreateInRow(Text,'x')" returns rows
'Access provided for user A'
'Access provided for user B'
'Access provided for user C'
*/
To obtain the column count on a table, use this:
select count(id) from syscolumns where id = object_id('tablename')
and to pass a table to a function, try XML as show here:
create function dbo.ReadXml (#xmlMatrix xml)
returns table
as
return
( select
t.value('./#Salary', 'integer') as Salary,
t.value('./#Age', 'integer') as Age
from #xmlMatrix.nodes('//row') x(t)
)
go
declare #source table
( Salary integer,
age tinyint
)
insert into #source
select 10000, 25 union all
select 15000, 27 union all
select 12000, 18 union all
select 15000, 36 union all
select 16000, 57 union all
select 17000, 44 union all
select 18000, 32 union all
select 19000, 56 union all
select 25000, 34 union all
select 7500, 29
--select * from #source
declare #functionArgument xml
select #functionArgument =
( select
Salary as [row/#Salary],
Age as [row/#Age]
from #source
for xml path('')
)
--select #functionArgument as [#functionArgument]
select * from readXml(#functionArgument)
/* -------- Sample Output: --------
Salary Age
----------- -----------
10000 25
15000 27
12000 18
15000 36
16000 57
17000 44
18000 32
19000 56
25000 34
7500 29
*/
create table Project (ProjectId int, Description varchar(50));
insert into Project values (1, 'Chase tail, change directions');
insert into Project values (2, 'ping-pong ball in clothes dryer');
create table ProjectResource (ProjectId int, ResourceId int, Name varchar(15));
insert into ProjectResource values (1, 1, 'Adam');
insert into ProjectResource values (1, 2, 'Kerry');
insert into ProjectResource values (1, 3, 'Tom');
insert into ProjectResource values (2, 4, 'David');
insert into ProjectResource values (2, 5, 'Jeff');
SELECT *,
(SELECT Name + ' ' AS [text()]
FROM ProjectResource pr
WHERE pr.ProjectId = p.ProjectId
FOR XML PATH (''))
AS ResourceList
FROM Project p
-- ProjectId Description ResourceList
-- 1 Chase tail, change directions Adam Kerry Tom
-- 2 ping-pong ball in clothes dryer David Jeff
The following will enable you to quickly remove the duplicate,null values and return only the valid one as list.
CREATE TABLE DuplicateTable (Col1 INT)
INSERT INTO DuplicateTable
SELECT 8
UNION ALL
SELECT 1--duplicate
UNION ALL
SELECT 2 --duplicate
UNION ALL
SELECT 1
UNION ALL
SELECT 3
UNION ALL
SELECT 4
UNION ALL
SELECT 5
UNION
SELECT NULL
GO
WITH CTE (COl1,DuplicateCount)
AS
(
SELECT COl1,
ROW_NUMBER() OVER(PARTITION BY COl1 ORDER BY Col1) AS DuplicateCount
FROM DuplicateTable
WHERE (col1 IS NOT NULL)
)
SELECT COl1
FROM CTE
WHERE DuplicateCount =1
GO
CTE are valid in SQL 2005 , you could then store the values in a temp table and use it with your function.
you can do something like this
/* CREATE USER DEFINED TABLE TYPE */
CREATE TYPE StateMaster AS TABLE
(
StateCode VARCHAR(2),
StateDescp VARCHAR(250)
)
GO
/*CREATE FUNCTION WHICH TAKES TABLE AS A PARAMETER */
CREATE FUNCTION TableValuedParameterExample(#TmpTable StateMaster READONLY)
RETURNS VARCHAR(250)
AS
BEGIN
DECLARE #StateDescp VARCHAR(250)
SELECT #StateDescp = StateDescp FROM #TmpTable
RETURN #StateDescp
END
GO
/*CREATE STORED PROCEDURE WHICH TAKES TABLE AS A PARAMETER */
CREATE PROCEDURE TableValuedParameterExample_SP
(
#TmpTable StateMaster READONLY
)
AS
BEGIN
INSERT INTO StateMst
SELECT * FROM #TmpTable
END
GO
BEGIN
/* DECLARE VARIABLE OF TABLE USER DEFINED TYPE */
DECLARE #MyTable StateMaster
/* INSERT DATA INTO TABLE TYPE */
INSERT INTO #MyTable VALUES('11','AndhraPradesh')
INSERT INTO #MyTable VALUES('12','Assam')
/* EXECUTE STORED PROCEDURE */
EXEC TableValuedParameterExample_SP #MyTable
GO
For more details check this link: http://sailajareddy-technical.blogspot.in/2012/09/passing-table-valued-parameter-to.html