How to simulate STRING_AGG (contrary of split) function - sql-server

How to simulate string_agg function ?
I need get this
[value]
1
2
3
into this
1,2,3
I tried following
CREATE TYPE stringArray AS TABLE ([value] nvarchar(255))
GO
CREATE FUNCTION dbo.ufn_join
(
#table stringArray readonly,
#separator nvarchar(5) = ','
)
RETURNS nvarchar(max)
AS
BEGIN
RETURN stuff((select #separator + value from #table for xml path('')), 1, 1, '')
END
GO
SELECT dbo.ufn_join(
(
SELECT cast(1 as nvarchar(255)) as value
UNION
SELECT cast(2 as nvarchar(255)) as value
UNION
SELECT cast(3 as nvarchar(255)) as value
)
, DEFAULT
)
but I am getting an error
-- Error: Operand type clash: nvarchar is incompatible with stringArray
Only condition is that i do not want to use any kind of variables. CLR function is also totally fine, but there i have the same issue, how to insert return of select as a parameter to the function.

Normally I use this link when I want to concat rows. There are several options how to do it, so here you can find inspiration on which approach you like the most. Be aware of XML PATH since it uses all of your CPU Processes and can max out your CPU to 100%.
Different concat approaches
Example from the link:
CREATE FUNCTION dbo.udf_select_concat ( #c INT )
RETURNS VARCHAR(MAX) AS BEGIN
DECLARE #p VARCHAR(MAX) ;
SET #p = '' ;
SELECT #p = #p + ProductName + ','
FROM Northwind..Products
WHERE CategoryId = #c ;
RETURN #p
END
SELECT CategoryId, dbo.udf_select_concat( CategoryId )
FROM Northwind..Products
GROUP BY CategoryId ;

TVP issue aside, your function will be profoundly faster and more efficient by turning it into an inline table valued function (commonly referred as an inline scalar function (iSF)). This article explains what I'm saying in detail:
How to Make Scalar UDFs Run Faster (SQL Spackle)
CREATE FUNCTION dbo.ufn_join (#separator nvarchar(5))
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT concatinatedTxt =
stuff((select #separator + someTxt from dbo.someTable for xml path('')), 1, 1, '');

It's because you declared a type, set that type to a parameter and the tried to insert a table into this parameter (a different type).
Try this:
CREATE TYPE stringArray AS TABLE ([value] nvarchar(255))
GO
CREATE FUNCTION dbo.ufn_join
(
#table stringArray readonly,
#separator nvarchar(5) = ','
)
RETURNS nvarchar(max)
AS
BEGIN
RETURN stuff((select #separator + value from #table for xml path('')), 1, 1, '')
END
GO
DECLARE #table stringArray
INSERT INTO #Table
SELECT cast(1 as nvarchar(255)) as value
UNION
SELECT cast(2 as nvarchar(255)) as value
UNION
SELECT cast(3 as nvarchar(255)) as value
SELECT dbo.ufn_join(
#Table
, DEFAULT
)

Related

Query to select next column if the current column returns no rows

The question is quite extensive, please bear with me. I have a single mapping table with the following structure:
This particular table is used in the process of generating a hierarchy. The order and position of the columns in the table indicate the order of hierarchy (Organization, Category, Continent, Country.. etc.) Each entity in this hierarchy has a related table with associated Id and Name. For example, there is a Country table with CountryId and CountryName. Note that since the MappingTable's values are all nullable there are no foreign key constraints.
I want to generate a procedure that will do the following:
Based on conditions provided, retrieve values of the next entity in the hierarchy. For example, if the OrganizationId and CategoryId are given, the values of ContinentId that satisfy said condition need to be retrieved.
Also, if the value of ContinentId is NULL, then the values of CountryId need to be retrieved. Here, given the condition OrganizationId = 1 and CategoryId = 1 the procedure should return the list of RegionId.
In addition to retrieving the RegionId, the corresponding RegionName should be retrieved from the Region Table.
So far, the procedure looks something like this - just a few things to explain here.
ALTER PROCEDURE [dbo].[GetHierarchy]
(
#MappingTableName VARCHAR(30),
#Position VARCHAR(5),
-- Given in the form of Key-value pairs 'OrganizationId:1,CategoryId:1'
#InputData VARCHAR(MAX),
#Separator CHAR(1),
#KeyValueSeperator CHAR(1)
)
AS
BEGIN
DECLARE #Sql NVARCHAR(MAX)
DECLARE #Result NVARCHAR(MAX)
DECLARE #Sql1 NVARCHAR(MAX);
DECLARE #TableName NVARCHAR(30)
DECLARE #Exists bit
SELECT #TableName = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #MappingTableName AND ORDINAL_POSITION = #position
SET #TableName = SUBSTRING(#TableName,0,LEN(#TableName) - 1)
-- Returns a dynamic query like "SELECT ContinentId from Continent WHERE OrganizationId = 1 and CategoryId = 1".
SELECT #Sql = [dbo].[KeyValuePairs](#TableName, #InputData, #Separator, #KeyValueSeperator)
SET #Sql1 = N'SET #Exists = CASE WHEN EXISTS(' + #Sql + N' AND ' + #TableName + N'Id IS NOT NULL) THEN 1 ELSE 0 END'
PRINT #Sql
EXEC sp_executesql #Sql1,
N'#Exists bit OUTPUT',
#Exists = #Exists OUTPUT
IF(#Exists = 1)
BEGIN
SET #Sql1 = 'SELECT ' + #TableName + 'Id, ' + #TableName + 'Name FROM '+ #TableName+' WHERE ' + #TableName +'Id IN (' + #Sql + ')';
PRINT #Sql1
EXECUTE sp_executesql #SQL1
END
ELSE
BEGIN
--PRINT 'NOT EXISTS'
DECLARE #nextPosition INT
SELECT #nextPosition = CAST(#position AS INT)
SET #nextPosition = #nextPosition + 1
SET #Position = CONVERT(VARCHAR(5), CAST(#position AS INT))
EXEC [dbo].[GetHierarchy] #MappingTableName, #Position, #InputData, #Separator, #KeyValueSeperator
END
END
The logic of this procedure is such that, I get the name of the column at a particular position (based on the conditions here, it is Continent) and generate the dynamic query to retrieve the next column's values based on the condition of the input condition (I am using a separate function to do this for me).
Once retrieved, I run the query to check if it returns any rows. If the query returns rows, then I retrieve the corresponding ContinentName from the Continent table. If no rows are returns, I recursively call the procedure again with the next position as the input.
On the business side of things, it seems like a two step process. But, as a procedure it is quite complex, extensive and - not to mention, recursive. Is there an easier way to do this? I am not familiar with CTEs - can the same logic be implemented using CTEs?
This is quite similar to what is asked here: Working with a dynamic hierarchy SQL Server
Might be the little lengthy approach. Try this
DECLARE #T TABLE
(
SeqNo INT IDENTITY(1,1),
CatId INT,
Country INT,
StateId INT,
DistId INT
)
DECLARE #State TABLE
(
StateId INT,
StateNm VARCHAR(20)
)
DECLARE #Country TABLE
(
CountryId INT,
CountryNm VARCHAR(20)
)
INSERT INTO #State
VALUES(3,'FL')
INSERT INTO #Country
VALUES(2,'USA')
INSERT INTO #T(CatId)
VALUES(1)
INSERT INTO #T(CatId,Country)
VALUES(1,2)
INSERT INTO #T(CatId,StateId)
VALUES(1,3)
;WITH CTE
AS
(
SELECT
*,
IdVal = COALESCE(Country,StateId,DistId),
IdCol = COALESCE('Country '+CAST(Country AS VARCHAR(50)),'StateId '+CAST(StateId AS VARCHAR(50)),'DistId '+CAST(DistId AS VARCHAR(50)))
FROM #T
WHERE CatId = 1
),C2
AS
(
SELECT
SeqNo,
CatId,
Country,
StateId,
DistId,
IdVal,
IdCol = LTRIM(RTRIM(SUBSTRING(IdCol,1,CHARINDEX(' ',IdCol))))
FROM CTE
)
SELECT
C2.SeqNo,
C2.CatId,
S.StateNm,
C.CountryNm
FROM C2
LEFT JOIN #State S
ON C2.IdCol ='StateId'
AND C2.IdVal = S.StateId
LEFT JOIN #Country C
ON C2.IdCol ='Country '
AND C2.IdVal = c.CountryId

Multiple-value in one parameter

CREATE FUNCTION [dbo].[func_1]
(
#ListNum AS nvarchar(MAX)
)
RETURNS #t TABLE
(
col_1 nvarchar(MAX)
)
AS
BEGIN
INSERT #t
SELECT col_1
FROM table_name
WHERE col_2 IN (#ListNum)
RETURN
END
When I pass only one value in paramater (for example : 1) the function correctly works but how can I pass multiple value (for example : 1,2,3,4,5). I get the following error :
Procedure execution failed
42000 - [SQL Server]Error converting data type nvarchar to bigint.
Is there a simple way to solve this?
Hi you can try like this,
CREATE FUNCTION Splitstring (#Input NVARCHAR(MAX),
#Character CHAR(1))
RETURNS #Output TABLE (
Item NVARCHAR(1000))
AS
BEGIN
DECLARE #StartIndex INT,
#EndIndex INT
SET #StartIndex = 1
IF Substring(#Input, Len(#Input) - 1, Len(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE Charindex(#Character, #Input) > 0
BEGIN
SET #EndIndex = Charindex(#Character, #Input)
INSERT INTO #Output
(Item)
SELECT Substring(#Input, #StartIndex, #EndIndex - 1)
SET #Input = Substring(#Input, #EndIndex + 1, Len(#Input))
END
RETURN
END
GO
CREATE FUNCTION [dbo].[Func_1] (#ListNum AS NVARCHAR(MAX))
RETURNS #t TABLE (
col_1 NVARCHAR(MAX))
AS
BEGIN
INSERT #t
SELECT p.col1
FROM dbo.Splitstring(#ListNum, ',') s
JOIN Table_Name t
ON t.col2 = s.Item
RETURN
END
DECLARE #var VARCHAR(100)='1,2,3,4'
SELECT *
FROM dbo.Func_1(#var)
Introduce one more function called split string. It will return the comma separated list as a table. Join the comma separated table with your actual table. This will gives the result.
For versions 2008+ using table separated values can assist where the calling procedure can construct the table and you are able to create a table type. If you must pass comma (or other character separated values) in a single string then you will need to separate the comma delimited string into a result set of its own.
The XML method works well when your string doesn't contain any special XML characters such as angle brackets <> - how-to-split-a-comma-separated-value-to-columns
I think this will work for your adjusted function;
CREATE FUNCTION [dbo].[func_1]
(
#ListNum AS nvarchar(MAX)
)
RETURNS #t TABLE
(
col_1 nvarchar(MAX)
)
AS
BEGIN
DECLARE #S varchar(max),
#Split char(1),
#X xml
SELECT #S = #ListNum,
#Split = ','
SELECT #X = CONVERT(xml,' <root> <s>' + REPLACE(#S,#Split,'</s> <s>') + '</s> </root> ')
INSERT #t
SELECT col_1
FROM table_name
WHERE col_2 IN (SELECT [Value] = T.c.value('.','varchar(20)')
FROM #X.nodes('/root/s') T(c))
RETURN
END

Create string with embedded quotes in SQL

I run several queries that use a list of character values in the where clause, e.g.,
select *
from table1
where col1 in ('a','b','c')
The character list changes frequently, so I want to store the string in a variable and reference the variable in all of the queries instead of maintaining several copies of the string. I've tried the following but the query returns zero rows.
declare #str varchar(50)
select #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
select *
from table1
where col1 in (#str)
#str has the value 'a','b','c' but for some reason, SQL Server doesn't recognize it. How do I build a string and store it in a variable that works with the in keyword?
The IN construct in SQL as a set lookup, not a string lookup. Your single string value of "'a','b','c'" is exactly what it's looking for when you say where col1 in (#str)... as Fredou mentioned in comments.
Instead you want to pass in a set of values by using a table variable (or a temp table):
declare #tabIn table ( val varchar(10) )
insert #tabIn
(val) values
('a'), ('b'), ('c')
select *
from table1
where
col1 in (select val from #tabIn)
or, alternatively, just do a straight join:
declare #tabIn table ( val varchar(10) )
insert #tabIn
(val) values
('a'), ('b'), ('c')
select *
from table1 t1
join #tabIn t2 on
t1.col1 = t2.val
It is possible to create a string with embedded quotes. As Fredou and ChrisS mentioned, #str is considered a single string. If the #str value is concatenated with the rest of your select statement and then executed, you will achieve the your desired results. SQL Fiddle example.
declare #str varchar(50)
declare #sql varchar(MAX)
select #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
Select #sql = 'SELECT * FROM table1 WHERE col1 IN (' + #str + ')'
Exec(#sql)
Results using #str = '''a''' + ',' + '''b'''+ ',' + '''c'''
Results using #str = '''a''' + ',' + '''b'''

How to parse a comma-delimited string variable in SQL

I am working in SQL Server 2008. I have a stored proc that takes a parameter, called #test. This parameter is varchar(255). In this stored proc, I need to parse this string, convert each value into a string itself (there will be a variable number of values), and build a list to use in a NOT IN statement.
For example, suppose #test = 'a, b, c, d'. I need to send this parameter into my stored proc. There is a SELECT query in my stored proc that uses a NOT IN statement. For this example, I need this NOT IN statement to read NOT IN('a', 'b', 'c', 'd').
How do I accomplish this? Or, is this a bad practice?
Use Split function something along with NOT EXISTS operator almost always faster than NOT IN operator
Split Function
CREATE FUNCTION [dbo].[split]
(
#delimited NVARCHAR(MAX),
#delimiter NVARCHAR(100)
)
RETURNS #t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<t>' + REPLACE(#delimited,#delimiter,'</t><t>') + '</t>'
INSERT INTO #t(val)
SELECT r.value('.','varchar(MAX)') as item
FROM #xml.nodes('/t') as records(r)
RETURN
END
Test Data
DECLARE #Table TABLE (Vals INT)
INSERT INTO #Table VALUES (1), (2), (3), (4)
DECLARE #test VARCHAR(256) = '3,4,5,6'
SELECT * FROM #Table
WHERE NOT EXISTS (SELECT 1
FROM [dbo].[split](#test , ',')
WHERE val = Vals)
Result
Vals
1
2
You can use dynamic sql for this. Use the replace function to get the correct NOT IN() argument:
DECLARE #test VARCHAR(10) = 'a,b,c,d'
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = 'SELECT *
FROM #temp
WHERE label NOT IN (''' + REPLACE( #test ,',',''',''') + ''')'
EXEC sp_executesql #sql
you can find the char or string in the whole string having delimiter as comma (,)
DECLARE #code VARCHAR(50) = 'c', #text varchar(100) = 'a,b,c,d,e'
SELECT CHARINDEX(',' + #code + ',', ',' + #text + ',') > 0

I need to flatten out SQL Server rows into columns using pivot

I've been trying for a while to use SQL Server pivot but I just don't seem to be getting it right. I've read a bunch of SO answers, but don't understand how pivot works.
I'm writing a stored procedure. I have Table 1 (received as a TVP), and need to make it look like Table 2 (see this image for tables).
Important: the values in Table1.valueTypeID cannot be hard coded into the logic because they can always change. Therefore, the logic must be super dynamic.
Please see the code below. The pivot is at the end of the stored procedure.
-- Create date: 12/10/2013
-- Description: select all the contacts associated with received accountPassport
-- =============================================
ALTER PROCEDURE [dbo].[selectContactsPropsByAccountPassport]
-- Add the parameters for the stored procedure here
#accountPassport int,
#valueTypeFiltersTVP valueTypeFiltersTVP READONLY
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
DECLARE #accountID int;
DECLARE #contactsAppAccountPassport int;
DECLARE #searchResults TABLE
(
resultContactID int
);
DECLARE #resultContactID int;
DECLARE #contactsPropsForReturn TABLE
(
contactID int,
valueTypeID int,
value varchar(max)
);
create table #contactsPropsForReturnFiltered(contactID int,valueTypeID int, value varchar(max))
/*
DECLARE #contactsPropsForReturnFiltered TABLE
(
contactID int,
valueTypeID int,
value varchar(max)
);
*/
--2. get #contactsAppAccountPassport associated with recieved #accountPassport
-- go into dbo.accounts and get the #accountID associated with this #accountPassport
SELECT
#accountID = ID
FROM
dbo.accounts
WHERE
passport = #accountPassport
-- go into dbo.accountsProps and get the value (#contactsAppAccountPassport) where valueType=42 and accountID = #accountID
SELECT
#contactsAppAccountPassport = value
FROM
dbo.accountsProps
WHERE
(valueTypeID=42) AND (accountID = #accountID)
--3. get all the contact ID's from dbo.contacts associated with #contactsAppAccountPassport
INSERT INTO
#searchResults
SELECT
ID
FROM
dbo.contacts
WHERE
contactsAppAccountPassport = #contactsAppAccountPassport
--4. Get the props of all contact ID's from 3.
--start for each loop....our looping object is #resultContactID row. if there are more rows, we keep looping.
DECLARE searchCursor CURSOR FOR
SELECT
resultContactID
FROM
#searchResults
OPEN searchCursor
FETCH NEXT FROM searchCursor INTO #resultContactID
WHILE (##FETCH_STATUS=0)
BEGIN
INSERT INTO
#contactsPropsForReturn
SELECT
contactID,
valueTypeID,
value
FROM
dbo.contactsProps
WHERE
contactID = #resultContactID
FETCH NEXT FROM searchCursor INTO #resultContactID
END --end of WHILE loop
--end of cursor (both CLOSE and DEALLOCATE necessary)
CLOSE searchCursor
DEALLOCATE searchCursor
-- select and return only the props that match with the requested props
-- (we don't want to return all the props, only the ones requested)
INSERT INTO
#contactsPropsForReturnFiltered
SELECT
p.contactID,
p.valueTypeID,
p.value
FROM
#contactsPropsForReturn as p
INNER JOIN
#valueTypeFiltersTVP as f
ON
p.valueTypeID = f.valueTypeID
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(ValueTypeId)
FROM #contactsPropsForReturnFiltered
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'');
set #query = 'SELECT contactid, ' + #cols + ' from
(
select contactid
, Value
,ValueTypeId
from #contactsPropsForReturnFiltered
) x
pivot
(
min(Value)
for ValueTypeId in (' + #cols + ')
) p ';
execute(#query);
END
You need to use dynamic pivot in your case. Try the following
create table table1
(
contactid int,
ValueTypeId int,
Value varchar(100)
);
insert into table1 values (56064, 40, 'Issac');
insert into table1 values (56064, 34, '(123)456-7890');
insert into table1 values (56065, 40, 'Lola');
insert into table1 values (56065, 34, '(123)456-7832');
insert into table1 values (56068, 40, 'Mike');
insert into table1 values (56068, 41, 'Gonzalez');
insert into table1 values (56068, 34, '(123)456-7891');
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(ValueTypeId)
FROM table1
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'');
set #query = 'SELECT contactid, ' + #cols + ' from
(
select contactid
, Value
,ValueTypeId
from table1
) x
pivot
(
min(Value)
for ValueTypeId in (' + #cols + ')
) p ';
execute(#query);
drop table table1
Why do you need to present the data in this way?
In many cases, clients are better at pivoting than the database engine. For example, SQL Server Reporting Services easily does this with the matrix control. Similarly, if you are coding a web page in, say, Asp.Net, you can run through the recordset quickly to pass your data into a new data representation (meanwhile collecting unique values) and then in a single pass through the new data object spit out the HTML to render the result.
If at all possible, have your client do the pivoting instead of the server.
UPDATE:
If you really want to use table variables in dynamic SQL, you can just fine in SQL Server 2008 and up. Here's an example script:
USE tempdb
GO
CREATE TYPE IDList AS TABLE (
ID int
);
GO
DECLARE #SQL nvarchar(max);
SET #SQL = 'SELECT * FROM #TransactionIDs WHERE ID >= 4;'
DECLARE #TransactionIDs IDLIst;
INSERT #TransactionIDs VALUES (1), (2), (4), (8), (16);
EXEC sp_executesql #SQL, N'#TransactionIDs IDList READONLY', #TransactionIDs;
GO
DROP TYPE IDList;

Resources