Using dynamic SQL to create single 'cell' string. Returning this string via SELECT clause - sql

The following should ultimately return a single scalar value that is a string separated list of account numbers for supplier with ID 1179. With the LEFT function, this works as intended. Without the LEFT function, I see only 'Command(s) completed successfully'.
I've looked at the QUOTENAME function documentation and it says it is only capable of returning a 128 character string. Is that why I can't see the full list of accounts (easily in excess of 128 characters)?
Is there some way to achieve what I want using another method?
Code:
DECLARE #str1 VARCHAR(MAX)
DECLARE #str2 VARCHAR(MAX)
SET #str1 = LEFT(STUFF(
(
SELECT DISTINCT ', ' + cpl.clplAcNo
FROM tblSuppliers s
JOIN tblClientPriceLists cpl ON (cpl.clplSupplierId = supRowID)
WHERE s.supRowID = 1179
AND clplAcNo IS NOT NULL AND clplAcNo <> ''
FOR XML PATH('')
), 1, 1, ''), 300)
SET #str2 = 'SELECT ' + QUOTENAME(#str1, CHAR(39))
EXEC(#str2)

Why not just do:
Select SupplierID + ',' anothercolumn + ',' + LEFT(STUFF(
(
SELECT DISTINCT '', '' + cpl.clplAcNo
FROM tblSuppliers s
JOIN tblClientPriceLists cpl ON (cpl.clplSupplierId = supRowID)
WHERE s.supRowID = 1179
AND clplAcNo IS NOT NULL AND clplAcNo <> ''''
FOR XML PATH('''')
), 1, 1, ''''), 300)

See the MSDN docs for QUOTENAME, note the sentence about the first parameter "Is a string of Unicode character data. character_string is sysname and is limited to 128 characters. Inputs greater than 128 characters return NULL."
As mentioned above, dynamic SQL is best avoided if you can. You could use this instead of dynamic SQL, and you can easily add a FROM clause to bring back other data.
SELECT (LEFT(STUFF((SELECT DISTINCT ', ' + cpl.clplAcNo
FROM tblSuppliers s
JOIN tblClientPriceLists cpl ON (cpl.clplSupplierId = supRowID)
WHERE s.supRowID = 1179
AND clplAcNo IS NOT NULL AND clplAcNo <> ''
FOR XML PATH('')
),
1, 1, ''
), 300
)
) as AcNos

Related

How Do I Split a Delimited String without creating a function or using the STRING_SPLIT function

Need to split a delimited string in the code without using the STRING_SPLIT function.
This is in SQL Server 2016 running in compatibility mode 110 (SQL Server 2012), this unfortunately cannot be changed.
SELECT
rsys.Netbios_Name0 As Name,
(SELECT bg.Name
FROM vSMS_BoundaryGroup bg
WHERE bg.GroupID = value) As 'Boundary Group',
rsys.Full_Domain_Name0 AS Domain
FROM
v_R_System rsys
INNER JOIN
v_GS_BOUNDARYGROUPCACHE bgc ON bgc.ResourceID = rsys.ResourceID
OUTER APPLY
STRING_SPLIT(bgc.BoundaryGroupIDs0, ',')
I get this error:
Msg 208, Level 16, State 1, Line 1
Invalid object name 'STRING_SPLIT'
Created the following function:
Using XML
create a table valued function like the below which is using Sql XML feature to split the string.
CREATE FUNCTION [dbo].StringSplitXML
(
#String VARCHAR(MAX), #Separator CHAR(1)
)
RETURNS #RESULT TABLE(Value VARCHAR(MAX))
AS
BEGIN
DECLARE #XML XML
SET #XML = CAST(
('<i>' + REPLACE(#String, #Separator, '</i><i>') + '</i>')
AS XML)
INSERT INTO #RESULT
SELECT t.i.value('.', 'VARCHAR(MAX)')
FROM #XML.nodes('i') AS t(i)
WHERE t.i.value('.', 'VARCHAR(MAX)') <> ''
RETURN
END
Below example shows how we can use the above function to split the comma delimited string
SELECT *
FROM StringSplitXML('Basavaraj,Kalpana,Shree',',')
With my query
SELECT
rsys.Netbios_Name0 As Name
, (SELECT
bg.Name
FROM vSMS_BoundaryGroup bg
WHERE bg.GroupID = value) As 'Boundary Group',
rsys.Full_Domain_Name0 AS Domain
FROM v_R_System rsys
INNER JOIN v_GS_BOUNDARYGROUPCACHE bgc ON bgc.ResourceID = rsys.ResourceID
OUTER APPLY StringSplitXML(bgc.BoundaryGroupIDs0, ',')
order by rsys.Netbios_Name0
Thank you
https://sqlhints.com/tag/split-delimited-string-in-sql/

Need Help in creating Dynamic SQL Query

I am using a SQL Server database. I have a SQL query which I have to write inside a stored procedure using SQL string and I am unable to write it.
The SQL query is
SELECT TOP (1000)
[OfficeNo], [CustNo], [SAPNo],
[Name1], [Name2],
[HomePhone], [OtherPhone], [FaxPhone], [cellPhone], [workPhone]
FROM
[dbo].[tblCustomers]
WHERE
OfficeNo = '1043'
AND (REPLACE(REPLACE(REPLACE(REPLACE(HomePhone,'(',''),' ',''),'-',''),')','') = '6147163987' )
OR (REPLACE(REPLACE(REPLACE(REPLACE(OtherPhone,'(',''),' ',''),'-',''),')','') = '6147163987'
OR (REPLACE(REPLACE(REPLACE(REPLACE(FaxPhone,'(',''),' ',''),'-',''),')','') = '6147163987'
OR (REPLACE(REPLACE(REPLACE(REPLACE(cellPhone,'(',''),' ',''),'-',''),')','') = '6147163987'
OR (REPLACE(REPLACE(REPLACE(REPLACE(workPhone,'(',''),' ',''),'-',''),')','') = '6147163987'))))
The above SQL query works, but I am unable to convert the above REPLACE statements inside a dynamic SQL string due to lot of single quotes and colons. And it is throwing errors.
Here is another option. This is using an inline table valued function which is a whole lot better for performance than a scalar function. There are several ways this could work but I chose to pass in both the stored (or formatted) value in addition to the desired clean value. This lets us use cross apply to filter out those rows that don't match.
create function PhoneNumberCheck
(
#StoredValue varchar(20)
, #CleanValue varchar(20)
) returns table as return
select CleanValue = #CleanValue
where #CleanValue = REPLACE(REPLACE(REPLACE(REPLACE(#StoredValue, '(', ''),' ', ''), '-', ''), ')', '')
Then to use this function we simply need to call it for each column of phone number values. One thing I should mention is in your original query you have top 1000 but you do not have an order by. This means you have no way of ensuring which rows you get back. If you use top you almost always need to include an order by.
SELECT TOP (1000) [OfficeNo]
,[CustNo]
,[SAPNo]
,[Name1]
,[Name2]
,[HomePhone]
,[OtherPhone]
,[FaxPhone]
,[cellPhone]
,[workPhone]
FROM [dbo].[tblCustomers] c
cross apply dbo.PhoneNumberCheck(HomePhone, '6147163987') hp
cross apply dbo.PhoneNumberCheck(OtherPhone, '6147163987') op
cross apply dbo.PhoneNumberCheck(FaxPhone, '6147163987') fp
cross apply dbo.PhoneNumberCheck(cellPhone, '6147163987') cp
cross apply dbo.PhoneNumberCheck(workPhone, '6147163987') wp
where OfficeNo = '1043'
--order by ???
Depending on what version of SQL server you are using there are better ways to do this now, but here is a function I have to clean phones for 2012 and earlier.
Create FUNCTION [dbo].[fn_CleanPhone] (
#phone VARCHAR(20))
RETURNS VARCHAR(10)
AS
BEGIN
RETURN CASE WHEN ISNUMERIC(LEFT(NULLIF(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(#phone,
'`', 1), '{', ''), '}', ''),'_', ''), ' ', ''), '-', ''), '.', ''), '(', ''), ')', ''), '/', ''), ''), 10)) = 1
THEN LEFT(NULLIF(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(#phone,
'`', 1), '{', ''), '}', ''), '_', ''), ' ', ''), '-', ''), '.', ''), '(', ''), ')', ''), '/', ''), ''), 10)
ELSE NULL
END
END
Then call the function like this in place of all your NULL and IF statements and LEFT statements as all of them are done in the function above
SELECT dbo.fn_CleanPhone('1234-22(23)')
so this in your where statement:
where OfficeNo = '1043'
AND (
dbo.fn_CleanPhone(HomePhone) = '6147163987' )
OR dbo.fn_CleanPhone(OtherPhone) = '6147163987' )
OR dbo.fn_CleanPhone(FaxPhone) = '6147163987' )
OR dbo.fn_CleanPhone(cellPhone) = '6147163987' )
OR dbo.fn_CleanPhone(workPhone) = '6147163987' )
) -- end for and
create a function to return the numbers of the input(sorry for bad naming):
CREATE FUNCTION GETCleand(#INPUT VARCHAR(MAX))
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #INDEX INT = 0
DECLARE #CLEANED VARCHAR(MAX)=''
WHILE(#INDEX<LEN(#INPUT))
BEGIN
IF(ISNUMERIC(SUBSTRING(#INPUT,#INDEX,1))=1)
SET #CLEANED = #CLEANED + SUBSTRING(#INPUT,#INDEX,1)
SET #INDEX = #INDEX + 1
END
RETURN #CLEANED
END
SELECT TOP (1000) [OfficeNo]
,[CustNo]
,[SAPNo]
,[Name1]
,[Name2]
,[HomePhone]
,[OtherPhone]
,[FaxPhone]
,[cellPhone]
,[workPhone]
FROM [dbo].[tblCustomers]
where OfficeNo = '1043' and
(GetCleaned(HomePhone) = '6147163987'
or GetCleaned(OtherPhone) = '6147163987'
or GetCleand(FaxPhone) = '6147163987'
or GetCleand(cellPhone) = '6147163987'
or GetCleand(workPhone) = '6147163987')
when you have some OR and AND conditions on your where you should use parentheses on OR ones
Perhaps the easiest way is to let TSQL do the loop without the performance hit when using a Row-By-Row query.
I have created a little test query hoping it will be easier for you to implement it in your case.
declare #table table
(
id int
,PhoneNr nvarchar(18)
)
insert into #table
values(1,'(123) 4567')
,(2,'123, 4567')
,(3,'123 4567')
,(4,'123 - 4567');
;with t1 as
(
select PhoneNr, id from #table
union all
select cast(replace(PhoneNr, substring(PhoneNr, PatIndex('%[^a-z0-9]%', PhoneNr), 1), '') as nvarchar(18)), id
from t1
where PatIndex('%[^a-z0-9]%', PhoneNr) > 0
)
select t1.PhoneNr from t1
where PatIndex('%[^a-z0-9]%', t1.PhoneNr) = 0
option (maxrecursion 0)
I would:
Replace the values entered in my test table with your test cases
Run the query and alter the regex if needed
integrate it into your table structure and cast the table phone column to in, if no error you will have achieved your transformation.
When you have your 'set up working' then compare the execution plans and you can pick your winner ;-)

SQL Server - COALESCE WHEN NOTHING RETURNS , GET DEFAULT VALUE

I'm trying to use Coalesce function in SQL Server to concatente multiple names. But when the conditon in the query returns no rows or nothing, I need to return a default value. I tried some condition using case statement but I can't figure it out what I missed.
declare #Names varchar(max) = '',
#Key varchar(max) = 'ABC'
select #Names = COALESCE(#Names, '') + isnull(T0.A, #Key) + ', '
from TData P
left join TNames T0 on T0.C + '\' + T0.D = P.#Key
where OBID=581464
and ID < 1432081
select #Names
You can do it with 2 minor changes to your current code, but I suspect this is an XYProblem, and you might benefit more from editing your question to include sample data and desired results (so perhaps we can suggest a better solution).
Anyway, what I had in mind is this:
declare #Names varchar(max), -- remove the = '', so that #Names starts as null
#Key varchar(max) = 'ABC'
select #Names = COALESCE(#Names, '') + isnull(T0.A, #Key) + ', '
from TData P
left join TNames T0 on T0.C + '\' + T0.D = P.#Key -- ' (This is just to fix the coding colors)
where OBID=581464
and ID < 1432081
select COALESCE(#Names, 'default value') -- since #Names started as null, and the query did not return any results, it's still null...

Replace with wildcard, in SQL

I know MS T-SQL does not support regular expression, but I need similar functionality. Here's what I'm trying to do:
I have a varchar table field which stores a breadcrumb, like this:
/ID1:Category1/ID2:Category2/ID3:Category3/
Each Category name is preceded by its Category ID, separated by a colon. I'd like to select and display these breadcrumbs but I want to remove the Category IDs and colons, like this:
/Category1/Category2/Category3/
Everything between the leading slash (/) up to and including the colon (:) should be stripped out.
I don't have the option of extracting the data, manipulating it externally, and re-inserting back into the table; so I'm trying to accomplish this in a SELECT statement.
I also can't resort to using a cursor to loop through each row and clean each field with a nested loop, due to the number of rows returned in the SELECT.
Can this be done?
Thanks all - Jay
I think your best bet is going to be to use a recursive user-defined function (UDF). I've included some code here that you can use to pass in a string to achieve the results you're looking for.
CREATE FUNCTION ufn_StripIDsFromBreadcrumb (#cIndex int, #breadcrumb varchar(max), #theString varchar(max))
RETURNS varchar(max)
AS
BEGIN
DECLARE #nextColon int
DECLARE #nextSlash int
SET #nextColon = CHARINDEX(':', #theString, #cIndex)
SET #nextSlash = CHARINDEX('/', #theString, #nextColon)
SET #breadcrumb = #breadcrumb + SUBSTRING(#theString, #nextColon + 1, #nextSlash - #nextColon)
IF #nextSlash != LEN(#theString)
BEGIN
exec #breadcrumb = ufn_StripIDsFromBreadcrumb #cIndex = #nextSlash, #breadcrumb = #breadcrumb, #theString = #theString
END
RETURN #breadcrumb
END
You could then execute it with:
DECLARE #myString varchar(max)
EXEC #myString = ufn_StripIDsFromBreadcrumb 1, '/', '/ID1:Category1/ID2:Category2/ID3:Category3/'
PRINT #myString
This works for SQL Server 2005 and up.
create table strings (
string varchar(1000)
)
insert into strings values( '/ID1:Category1/ID2:Category2/ID3:Category3/' )
insert into strings values( '/ID4:Category4/ID5:Category5/ID8:Category6/' )
insert into strings values( '/ID7:Category7/ID8:Category8/ID9:Category9/' )
go
with
replace_with_wildcard ( restrung ) as
(
select replace( string, '', '' )
from strings
union all
select
replace( restrung, substring( restrung, patindex( '%ID%', restrung ), 4 ), '' )
from replace_with_wildcard
where patindex( '%ID%', restrung ) > 0
)
select restrung
from replace_with_wildcard
where charindex( ':', restrung ) = 0
order by restrung
drop table strings
You might be able to do this using a Split function. The following split function relies on the existence of a Numbers table which literally contains a sequential list of numbers like so:
Create Table dbo.Numbers( Value int not null primary key clustered )
GO
With Nums As
(
Select ROW_NUMBER() OVER( Order By o.object_id ) As Num
From sys.objects as o
cross join sys.objects as o2
)
Insert dbo.Numbers( Value )
Select Num
From Nums
Where Num Between 1 And 10000
GO
Create Function [dbo].[udf_Split] (#DelimitedList nvarchar(max), #Delimiter nvarchar(2) = ',')
Returns #SplitResults TABLE (Position int NOT NULL PRIMARY KEY, Value nvarchar(max))
AS
/*
PURPOSE: to split the #DelimitedList based on the #Delimter
DESIGN NOTES:
1. In general the contents of the next item is: NextDelimiterPosition - CurrentStartPosition
2. CurrentStartPosition =
CharIndex(#Delimiter, A.list, N.Value) = Current Delimiter position
+ Len(#Delimiter) + The number of delimiter characters
+ 1 + 1 since the text of the item starts after the delimiter
3. We need to calculate the delimiter length because the LEN function excludes trailing spaces. Thus
if a delimiter of ", " (a comma followed by a space) is used, the LEN function will return 1.
4. The DataLength function returns the number of bytes in the string. However, since we're using
an nvarchar for the delimiter, the number of bytes will double the number of characters.
*/
Begin
Declare #DelimiterLength int
Set #DelimiterLength = DataLength(#Delimiter) / 2
If Left(#DelimitedList, #DelimiterLength) <> #Delimiter
Set #DelimitedList = #Delimiter + #DelimitedList
If Right(#DelimitedList, #DelimiterLength) <> #Delimiter
Set #DelimitedList = #DelimitedList + #Delimiter
Insert #SplitResults(Position, Value)
Select CharIndex(#Delimiter, A.list, N.Value) + #DelimiterLength
, Substring (
A.List
, CharIndex(#Delimiter, A.list, N.Value) + #DelimiterLength
, CharIndex(#Delimiter, A.list, N.Value + 1)
- ( CharIndex(#Delimiter, A.list, N.Value) + #DelimiterLength )
)
From dbo.Numbers As N
Cross Join (Select #DelimitedList As list) As A
Where N.Value > 0
And N.Value < LEN(A.list)
And Substring(A.list, N.Value, #DelimiterLength) = #Delimiter
Order By N.Value
Return
End
You then might be able to run a query like so where you strip out the prefixes:
Select Table, Substring(S.Value, CharIndex(':', S.Value) + 1, Len(S.Value))
From Table
Cross Apply dbo.udf_Split(Table.ListColumn, '/') As S
This would give you values like:
Category1
Category2
Category3
You could then use FOR XML PATH to combine them again:
Select Table.PK
, Stuff( (
Select '/' + Substring(S.Value, CharIndex(':', S.Value) + 1, Len(S.Value))
From Table As Table1
Cross Apply dbo.udf_Split(Table.ListColumn, '/') As S1
Where Table1.PK = Table.PK
Order By S1.Position
For Xml Path('')
), 1, 1, '') As BreadCrumb
From Table
For SQL Server 2005+, you can get regex support by:
Enabling CLR (doesn't require instance restart)
Uploading your CLR functionality (in this case, regex replace)
Using native TSQL, you'll need to define REPLACE statements for everything you want to remove:
SELECT REPLACE(
REPLACE(
REPLACE(''/ID1:Category1/ID2:Category2/ID3:Category3/'', 'ID1:', ''),
'ID2:', ''),
'ID3:', '')
Regex or otherwise, you need to be sure these patterns don't appear in the actual data.
You can use SQL CLR. Here's an MSDN article:
declare #test1 nvarchar(max)
set #test1='/ID1:Category1/ID2:Category2/ID3:Category3/'
while(CHARINDEX('ID',#test1)<>0)
Begin
select #test1=REPLACE(#test1,SUBSTRING(#test1,CHARINDEX('ID',#test1),CHARINDEX(':',#test1)-
CHARINDEX('ID',#test1)+1),'')
End
select #test1

What is happening in this T-SQL code? (Concatenting the results of a SELECT statement)

I'm just starting to learn T-SQL and could use some help in understanding what's going on in a particular block of code. I modified some code in an answer I received in a previous question, and here is the code in question:
DECLARE #column_list AS varchar(max)
SELECT #column_list = COALESCE(#column_list, ',') +
'SUM(Case When Sku2=' + CONVERT(varchar, Sku2) +
' Then Quantity Else 0 End) As [' +
CONVERT(varchar, Sku2) + ' - ' +
Convert(varchar,Description) +'],'
FROM OrderDetailDeliveryReview
Inner Join InvMast on SKU2 = SKU and LocationTypeID=4
GROUP BY Sku2 , Description
ORDER BY Sku2
Set #column_list = Left(#column_list,Len(#column_list)-1)
Select #column_list
----------------------------------------
1 row is returned:
,SUM(Case When Sku2=157 Then Quantity Else 0 End) As [157 -..., SUM(Case ...
The T-SQL code does exactly what I want, which is to make a single result based on the results of a query, which will then be used in another query.
However, I can't figure out how the SELECT #column_list =... statement is putting multiple values into a single string of characters by being inside a SELECT statement. Without the assignment to #column_list, the SELECT statement would simply return multiple rows. How is it that by having the variable within the SELECT statement that the results get "flattened" down into one value? How should I read this T-SQL to properly understand what's going on?
In SQL Server:
SELECT #var = #var + col
FROM TABLE
actually concatenates the values. It's a quirks mode (and I am unable at this time to find a reference to the documentation of feature - which has been used for years in the SQL Server community). If #var is NULL at the start (i.e. an uninitialized value), then you need a COALESCE or ISNULL (and you'll often use a separator):
SELECT #var = ISNULL(#var, '') + col + '|'
FROM TABLE
or this to make a comma-separated list, and then remove only the leading comma:
SELECT #var = ISNULL(#var, '') + ',' + col
FROM TABLE
SET #var = STUFF(#var, 1, 1, '')
or (courtesy of KM, relying on NULL + ',' yielding NULL to eliminate the need for STUFF for the first item in the list):
SELECT #var = ISNULL(#var + ',', '') + col
FROM TABLE
or this to make a list with a leading, separated and trailing comma:
SELECT #var = ISNULL(#var, ',') + col + ','
FROM TABLE
You will want to look into the COALESCE function. A good article describing what is happening can be seen here.