Two tables as parameters of user defined function - sql-server

I am trying to create a function in SQL SERVER which I can use to compare two tables, to check if they are identical. I do that with two excepts.
The Tables are supposed to be exactly the same, with the same data formats and column names as well as all values identical in both tables. This will be a manual check, so if differences are there, a thrown error is not a problem. The aim is just to see if two approaches of creating the tables leads to the same tables.
I am really new to functions in SQL, so I am not sure how to solve the problem.
I want to pass both tables as parameters to the function, to get something like this:
CREATE FUNCTION DIFFERING_ROWS
(#TABLE1, #TABLE2)
RETURNS TABLE
AS
RETURN (
SELECT *, 'A_not_B' as [Difference] FROM #TABLE1
except
SELECT *, 'A_not_B' as [Difference] FROM #TABLE2
union all
SELECT *, 'B_not_A' as [Difference] FROM #TABLE2
except
SELECT *, 'B_not_A' as [Difference] FROM #TABLE1
)
END
How is this implemented correctly?
Can anybody help me?

You cannot do this in a function. The only way you can pass table names as parameters is to use Dynamic SQL, and Dynamic SQL is not allowed in functions. You CAN do it with a stored procedure.

You can create this stored procedure that counts if the tables have the same column_names:
CREATE PROCEDURE checkEqualTables
#table1 varchar(100),
#table2 varchar(100)
AS
BEGIN
DECLARE #xCount int;
(SELECT #xCount = COUNT(*) from (SELECT column_name FROM information_schema.COLUMNS WHERE table_name=#table1) base
where column_name not in (SELECT column_name FROM information_schema.COLUMNS WHERE table_name=#table2))
IF(#xCount <= 0)
print 'Tables are equal!';
ELSE
print 'Tables are not equal!'
END

Ok I took the information from the answers and comments and researched about how to put this into procedures, and this is what I built:
I think this does what I want:
CREATE PROCEDURE checkEqualTables
#table1 nvarchar(100),
#table2 nvarchar(100)
AS
BEGIN
DECLARE #SQL nvarchar(max);
SET #SQL = 'SELECT * FROM ' + #TABLE1 +
'except
SELECT * FROM ' + #TABLE2 +
'union all
SELECT * FROM ' + #TABLE2 +
'except
SELECT * FROM ' + #TABLE1
EXECUTE sp_executesql #SQL
END

Related

Accumulating (concatenate) values in variable does not work

I'm trying to accumulate values into a variable in SQL Server>=2012.
It works in case 1 below, but in case 2 I get the answer ",CD" instead of the expected ",EF,AB,CD" Why?
In MMS:
USE MyDB
GO
-- Create a simple table
CREATE TABLE Tbl1 (Code VARCHAR(2), So TINYINT NULL)
INSERT INTO Tbl1 VALUES('AB', 10)
INSERT INTO Tbl1 VALUES('CD', NULL)
INSERT INTO Tbl1 VALUES('EF', 5)
GO
-- Case 1
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar=#MyVar + ',' + Code FROM Tbl1 ORDER BY So
SELECT #MyVar
GO
-- Case 2
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar=#MyVar + ',' + Code FROM Tbl1 ORDER BY ISNULL(So, 255)
SELECT #MyVar
GO
The explanation is in the documentation:
Don't use a variable in a SELECT statement to concatenate values (that
is, to compute aggregate values). Unexpected query results may occur.
Because, all expressions in the SELECT list (including assignments)
aren't necessarily run exactly once for each output row.
There are opinions (but not in the official docs), stating that without an ORDER BY clause (and/or a DISTINCT clause) the aggregation works as you expect.
If you are using SQL Server 2017+, you may use STRING_AGG() to build the expected output:
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar = STRING_AGG(Code, ',') WITHIN GROUP (ORDER BY ISNULL(So, 255))
FROM Tbl1
SELECT #MyVar

How to create comma delimited list from table with dynamic columns

I want to be able to grab all of the records in a table into a comma delimited list that I can then use to insert into a table on another database. Due to permission restrictions on the customer's server I cannot access any of the options when right-clicking on the database name, and all of the solutions I've found so far involve having permission to do so (e.g. Tasks > Export Data...)
I have tried using COALESCE to do this, however the problem is that my table could have any number of columns. Columns can be added/deleted at any time through the UI by the users and therefore I cannot hard code the columns in my select statement.
Here is what I have written so far, using a simple CTE statement where there are three columns (RowCode, RowOrd, RowText) and concatenating them into a variable that I print out. I just want to find a way to grab these column names dynamically instead of hard coding them. I'll also need to account for various types of column names by casting them each as varchar in the variable.
DECLARE #listStr VARCHAR(MAX)
;WITH tableData AS
(
SELECT *
FROM tableRows
)
SELECT
#listStr = ISNULL(#listStr + 'select ','select ') + '''' + RowCode + ''',''' + cast(RowOrd as varchar) + ''',''' + RowText + '''' + Char(13)
FROM
tableData
PRINT #listStr
The tableRows table contains the following records
RowCode RowOrd RowText
-----------------------
RowA 1 Row A
RowB 2 Row B
And the variable #listStr is currently printing this, which is correct
select 'RowA','1.00','Row A'
select 'RowB','2.00','Row B'
Thanks in advance!
With a bit of XML you can dynamically gather and "stringify" your values
Declare #tableRows table (RowCode varchar(50), RowOrd int, RowText varchar(50))
Insert Into #tableRows values
('RowA',1,'Row A'),
('RowB',2,'Row B')
Declare #listStr VARCHAR(MAX) = ''
Select #listStr = #listStr + C.String + char(13)
From #tableRows A
Cross Apply (Select XMLData = cast((Select A.* for XML RAW) as xml)) B
Cross Apply (
Select String = 'select '+Stuff((Select ',' +Value
From (
Select Value = ''''+attr.value('.','varchar(max)')+''''
From B.XMLData.nodes('/row') as A(r)
Cross Apply A.r.nodes('./#*') AS B(attr)
) X
For XML Path ('')),1,1,'')
) C
Select #listStr
Returns
select 'RowA','1','Row A'
select 'RowB','2','Row B'

How to pass an array of integer values from a table to a stored procedure?

I have a stored proc using dynamic sql that updates a few columns based on the value passed to it. I am trying to test it out for multiple values without having to enter those manually. These values are to be taken from a table. Is there a way to pass all these values in the table and have it go through the proc? Just like in your regular programming language where you would run through an array. I am doing this in sql server 2012.
Code is something like this
CREATE PROCEDURE sp1 #enteredvalue int
AS
BEGIN
UPDATE table1
SET column1 = 'some var char value',
column2 = 'some integer values'
WHERE xid = #enteredvalue
END
I want to enter the values for that integer parameter (#enteredvalue) from a table that has different values.
Perhaps a little more dynamic SQL will do the trick (along with a parser)
Declare #String varchar(max) = '1,25,659'
Declare #SQL varchar(max) = ''
Select #SQL = #SQL + concat('Exec [dbo].[sp1] ',Key_Value,';',char(13))
From (Select * from [dbo].[udf-Str-Parse-8K](#String,',')) A
Select #SQL
--Exec(#SQL)
Returns
Exec [dbo].[sp1] 1;
Exec [dbo].[sp1] 25;
Exec [dbo].[sp1] 659;
The UDF if needed (super fast!)
CREATE FUNCTION [dbo].[udf-Str-Parse-8K](#String varchar(8000), #Delimiter varchar(50))
Returns Table
As
--Usage: Select * from [dbo].[udf-Str-Parse-8K]('Dog,Cat,House,Car',',')
-- Select * from [dbo].[udf-Str-Parse-8K]('John||Cappelletti||was||here','||')
-- Select * from [dbo].[udf-Str-Parse-8K]('The quick brown fox',' ')
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 a, cte1 b, cte1 c, cte1 d) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter)) = #Delimiter),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter,#String,s.N),0)-S.N,8000) From cte3 S)
Select Key_PS = Row_Number() over (Order By A.N)
,Key_Value = Substring(#String, A.N, A.L)
,Key_Pos = A.N
From cte4 A
)
Another approach is (without Dynamic SQL):
1) Create a new SP where input parameter is a table
https://msdn.microsoft.com/en-us/library/bb510489.aspx
2) In that procedure, create a WHILE loop to go through each row and execute your existing SP for each individual row value
Example of WHILE loop is here:
SQL Call Stored Procedure for each Row without using a cursor
To pass a table into an SP, consider creating a User-Defined Table type. Example:
create type ArrayOfInt as table (IntVal int)
go
create proc SumArray(#IntArray ArrayOfInt readonly)
as
select sum(IntVal) from #IntArray
go
declare #IntArray ArrayOfInt
insert #IntArray values (1), (2), (3)
select * from #IntArray
exec SumArray #IntArray
drop proc SumArray
drop type ArrayOfInt

dynamic filters and sp_executesql

I have a stored procedure which has a parameter #SomeFilterIds, which takes in comma separated integer ids. If this parameter is not NULL it is translated into something like this:
AND [X] IN(1, 2, 4)
and assigned to #SomeFilter
I then used something along those lines:
SET #Sql = N' ...WHERE
c.SomeDate >= #SomeDate
' + #SomeFilter
and:
SET #ParameterDefinition = N'#SomeDate DateTime';
EXEC sp_executesql
#Sql
,#ParameterDefinition
,#SomeDate = #SomeDate
I would think that this is not best practice and opens potential security holes. Is this correct? Can this be improved? Thanks.
I think instead of #SomeFilterIds as varchar parameter you can use XML type variable and then use Inner join your main table with this XML variable.
This will avoid dynamic query execution and will be be safer.
Example:
--Instead of comma separated ID use below XML
declare #xml xml = '<row><ID>1</ID></row><row><ID>3</ID></row>'
--Assume this is your other table
declare #YourTable table (ItemId int, ColA varchar(20))
insert #YourTable
select 1, 'Hello World'
--Joining both the tables
select col.value('data(ID[1])', 'int') as ID
from #xml.nodes('/row') tbl(col)
inner join #YourTable t2
on t2. ItemId=tbl.col.value('data(ID[1])', 'int')
--WHERE c.SomeDate >= #SomeDate

Select (Select field from FieldTable) from Table

I'm using MSQL 2005. I have 2 table.A and B
Table A
- ID DOVKOD
- 1 KURSATIS
Table B
- ID KURALIS KURSATIS
- 1 2,2522 2,2685
- 2 2,4758 2,4874
Table A has only 1 record
When I execute Select (Select DOVKOD from Table A) from Table B I want to get same result as Select KURSATIS from Table B
I am gonna use it in a view. How can I do that. Thanks..
You can simply use a CASE expression:
SELECT CASE WHEN (SELECT DOVKOD FROM A) = 'KURSATIS' THEN KURSATIS
ELSE KURALIS
END
FROM B
SQL Fiddle Demo here
You must use Dynamic TSQL
SELECT #column=DOVKOD from Table A
EXEC ('Select ' + #column + ' from Table B')
If I understood you right then in table A you have the name of the column that you want to return. Then your solution is bad at all. I'll rather do something like that:
CREATE TABLE #TableA
(
ID INT, DOVKOD VARCHAR(100)
);
INSERT INTO #TableA VALUES (1, 'KURSATIS');
CREATE TABLE #TableB
(
ID INT, Value DECIMAL (18,2),Name VARCHAR(100)
);
INSERT INTO #TableB VALUES (1, 2.2522 , 'KURALIS');
INSERT INTO #TableB VALUES (2, 2.4758 , 'KURSATIS');
SELECT #TableB.* FROM #TableB JOIN #TableA ON #TableA.DOVKOD = #TableB.Name
The only way how to do this in MySQL is using Prepared statements. Dynamic pivot tables (transform rows to columns) is a good article about this.
SET #sql = NULL;
Select DOVKOD INTO #sql
FROM from Table A;
SET #sql = CONCAT('SELECT ', #sql, 'FROM Table B');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

Resources