Need Help in creating Dynamic SQL Query - sql

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 ;-)

Related

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...

Scaler Function in Where Clause Really Slow? How to use Cross Apply Instead?

I have some data, some of it was imported with different separators such as * - . or a space...some of it was removed on import, some was not. Some of the external values being compared to it has the same issue. So we remove all separators and compare that way, I don't want to just update the columns yet as the data isn't "mine".
So since I see this over and over in the code I am moving to stored procedures, I wrote a stored function to do it for me.
ALTER FUNCTION [dbo].[fn_AccountNumber_Format2]
(#parAcctNum NVARCHAR(50))
RETURNS NVARCHAR(50)
AS
BEGIN
SET #parAcctNum = REPLACE(REPLACE(REPLACE(REPLACE(#parAcctNum, '.', ''), '*', ''), '-', ''), ' ', '');
RETURN #parAcctNum
END
Normally the queries looked something like this and it takes less than a second to run on a few millions rows :
SELECT name1, accountID FROM tblAccounts WHERE (Replace(Replace(Replace(accountnumber, '.', ''), '*', ''), '-', '') = Replace(Replace(Replace('123-456-789', '.', ''), '*', ''), '-', ''));
So my first attempt with it like this takes 24 seconds to excecute:
SELECT name1, accountID FROM tblAccounts WHERE (dbo.fn_AccountNumber_Format2 ([accountnumber])) = Replace(Replace(Replace('123-456-789', '.', ''), '*', ''), '-', '');
This one 43 seconds:
SELECT name1, accountID FROM tblAccounts WHERE (dbo.fn_AccountNumber_Format2(accountnumber)) = (dbo.fn_AccountNumber_Format2 ('123-456-789'));
So the drastic slow down came as a complete shock to me as I expected the user defined function to run just the same as the system function REPLACE... After some research on stackexchange and google it seems that using Cross Apply and creating a table with the function may be a better solution but I have no idea how that works, can anyone help me with that?
Inline Function
CREATE FUNCTION [dbo].[uspAccountNumber_Format3]
(
#parAcctNum NVARCHAR(50))
RETURNS TABLE
AS
RETURN
(
SELECT REPLACE(REPLACE(REPLACE(REPLACE(#parAcctNum, '.', ''), '*', ''),'-', ''), ' ', '') AS AccountNumber
)
Usage
SELECT name1 ,
accountID
FROM tblAccounts
CROSS APPLY dbo.uspAccountNumber_Format3(accountnumber) AS a
CROSS APPLY dbo.uspAccountNumber_Format3('123-456-789') AS b
WHERE a.AccountNumber = b.AccountNumber

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

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

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

sql query complex

I have table where in a table called test which have 4 fields.one field named as listing, I have 1,2,3,4,5,6 multiple values separated by comma, I need to check whether in that table and in that particular field an id say 4 is there or not.. by a sql query.
You database design is wrong, that's why you have problems querying the data. You should have the values in a separate table, so that teach value is in it's own field. Then it would be easy to find the records:
select t.testId
from test t
inner join listing l on l.testId = t.testId
where l.id = 4
Now you have to use some ugly string comparison to find the records:
select testId
from test
where ','+listing+',' like '%,4,%'
You can try
SELECT *
FROM YourTable
WHERE REPLACE(Col, ' ', '') LIKE '4,%' --Starts with
OR REPLACE(Col, ' ', '') LIKE '%,4' --Ends with
OR REPLACE(Col, ' ', '') LIKE '%,4,%' --Contains
OR REPLACE(Col, ' ', '') = '4' --Equals
Just as a matter of interest, have a look at this
DECLARE #delimiter NVARCHAR(5),
#Val INT
SELECT #Val = 40
SELECT #delimiter = ','
DECLARE #YourTable TABLE(
ID INT,
Vals VARCHAR(50)
)
INSERT INTO #YourTable (ID,Vals) SELECT 1, '1,2,3,4,5,6,7,8'
DECLARE #TempTable TABLE(
ID INT,
Vals XML
)
INSERT INTO #TempTable
SELECT ID,
CAST('<d>' + REPLACE(Vals, #delimiter, '</d><d>') + '</d>' AS XML)
FROM #YourTable
SELECT *
FROM #TempTable tt
WHERE EXISTS(
SELECT T.split.value('.', 'nvarchar(max)') AS data
FROM tt.Vals.nodes('/d') T(split)
WHERE T.split.value('.', 'nvarchar(max)') = #Val
)
The common approach is to parse the list into a table variable or table-valued function, then either join against the table, or use an EXISTS sub-query.
There are lots of examples on how to do this:
http://www.bing.com/search?setmkt=en-US&q=SQL+parse+list+into+table
You could use an instring function in the where clause and in the select clause:
Oracle:
select substr(column, instr(column, '1', 1), 1)
where instr(column, '1', 1) > 0
works if you want a single value. Alternatively you can use a combination of case or decode statements to create a single column for each possible value:
select
decode(instr(column, '1', 1), 0, substr(column, instr(column, '1', 1), 1), null) c1,
decode(instr(column, '2', 1), 0, substr(column, instr(column, '2', 1), 1), null) c2,
decode(instr(column, '3', 1), 0, substr(column, instr(column, '3', 1), 1), null) c3
The beauty of this approach for such a poorly normalised set of data is you can save this as a view and then run SQL on that, so if you save the above you could use:
select c1, c2 from view where c1 is not null or c2 is not null
NB. In other dbms you might have to use different syntax, possibly the case rather decode statement
If you need to find 4 and only 4 (ie not 14 or 24 or 40 etc) you should use
SELECT * FROM foo WHERE col LIKE '%, 4,%'
or
SELECT * FROM foo WHERE col LIKE '%,4,%'
if there are no spaces between the commas and numbers
How about this?
Select * From Foo Where Col like '%4%'