Concat multiple rows into a comma-delimited value during Update - sql-server-2000

I have a temporary table with a field called Method, thus:
DECLARE #CaseSites TABLE (
BriefID int,
Method varchar(60)
-- other fields
)
Method will be filled from several rows in another table - CaseEventTypeList.
Running
SELECT * FROM CaseEventTypeList WHERE RefID = 1
Gives
RefID TypeID
1 2
1 3
1 6
Turning this into a single comma delimited result is fairly trivial:
DECLARE #CETList varchar(30)
SELECT #CETList = COALESCE(#CETList + ',', '') + CAST(CETL.[TypeID] AS varchar)
FROM CaseEventTypeList CETL
WHERE CETL.RefID = 1
PRINT #CETList
Giving:
2,3,6
Now I need to expand this to take in the entire table. This is what I came up with:
UPDATE #CaseSites SET Method = COALESCE(Method + ',','') + CAST(CETL.TypeID AS VARCHAR)
FROM CaseEvents CE
JOIN CaseEventTypeList AS CETL ON CETL.RefID = CE.TypeListID
WHERE BriefID = CE.CaseID
However this only fills Method with the first value from each set of values.
I looked online and found this but would rather not use a udf - especially when the solution feels so close.
UPDATE: The data is fairly simple, the RefId is incremented for each case, the TypeID can be any number, though only 1 to 8 are modelled currently. Thus you might have:
RefID TypeID
12 2
12 7
13 1
14 1
14 3
14 6
And this will hopefully be modelled as
SELECT Method from #CaseSites
Method
...
12 2,7
13 1
14 1,3,6
...

I think your problem is because the update statment only evaluates the "SET Method = " once per row, hence you only get one value in the list.
A UDF would be the easy way to do this, but since you are using temporary tables this may not be an option and you wished to avoid them anyway. So you may need to use a cursor (not nice) but gets the job done the way you want.
Here's what I came up with based on your original sql.
DECLARE myCURSOR Cursor
FOR
Select BriefID
from #CaseSites
Open myCursor
DECLARE #BriefID int
DECLARE #CETList varchar(30)
Fetch NEXT FROM myCursor INTO #BriefID
While (##FETCH_STATUS <> -1)
BEGIN
IF (##FETCH_STATUS <> -2)
SET #CETList = ''
SELECT #CETList = COALESCE(#CETList + ',', '') + CAST(CETL.[TypeID] AS varchar)
FROM #CaseEventTypeList CETL
WHERE CETL.RefID = #BriefID
UPDATE #CaseSites
SET Method = #CETList
WHERE BriefID = #BriefID
Fetch NEXT FROM myCursor INTO #BriefID
END
CLOSE myCursor
DEALLOCATE myCursor

I have found a better answer than my first if you are ok with using xml:
A correlated subquery using xml.
UPDATE #CaseSites
SET Method = (
select cast([TypeID] as varchar(30))+ ','
from #CaseEventTypeList
where RefID = CE.CaseID
for xml path ('')
)
FROM #CaseEvents CE

Related

While Loop using Dynamic Text Using an XML column - need to break when a value is null

I am writing a while loop on an XML column in a SQL database.
It dynamically loops through the data using a repeating index, this could be 10 or 100. I want to break the loop when the returned value is null. The reason being its a while loop within a while loop and the first index runs to 91 (I know this as i checked but could be 1, or 50 depending on XML) and the second index varies so 99% of time its 1 coverholder to 1 binder, but sometimes it could be a coverholder has 2,3 or 4 binders (up to mostly around 1)
The script below works I have set #id <= 91 and returns 91 coverholders, and within them coverholders one of them has 10 binders so I have the #secondId <=10
This returns 910 rows when I really only need it to loop through roughly 180 times instead of the 910 so is becoming quite slow.
I need it to from break from #Id if coverholder column is null and #secondid if binderUMR is null?
Can someone assist, as I have no idea other than put it all in a table and remove from there at end.
-- LOOP THROUGH ID's
DECLARE #id INT
DECLARE #SecondID INT
DECLARE #SQL NVARCHAR(MAX)
SET #id = 1
WHILE (#id <= 10)
BEGIN
SET #SecondID = 1
WHILE #SecondID <=2
BEGIN
SET #SQL = 'DECLARE #SecondID INT
SELECT ' + CAST(#id AS varchar(MAX)) + ' AS ID,
' + CAST(#SecondID AS varchar(MAX)) + ' AS Second_ID,
audit.value(N''(rowdata[#REPEATINGINDEX="'+CAST(#id as varchar(MAX))+'"]/Name/text())[1]'', N''nvarchar(max)'') as [Coverholder],
audit.value(N''(rowdata[#REPEATINGINDEX="'+CAST(#id as varchar(MAX))+'"]/pxResults[#REPEATINGTYPE="PageList"]/rowdata[#REPEATINGINDEX="'+CAST(#SecondID as varchar(MAX))+'"]/BinderUMR/text())[1]'', N''nvarchar(max)'') as [BinderUMR]
FROM
#Dataset t
CROSS APPLY
TransXML.nodes(''pagedata/Audit_DirtyList/pxResults[#REPEATINGTYPE="PageList"]'') AS pagedata(audit);
'
EXEC (#SQL)
SET #SecondID = #SecondID + 1
END
SET #id = #id + 1
END
I can't show the XML as its got important information, and I can't make one as a, I don't know how and b, I'm not sure how I could create one which would be this unique so hoping someone can give me basic principles and I can replicate.

SQL server not returning all rows

Here is the procedure that I am using to search for skills from a single column. There are 3 variables that I need to pass to the SP and I need to get the results accordingly. I do understand that searching for multiple values from a single cell with a delimiter is prone to errors, but this query is not working after I tried to put the whole thing within another IF ELSE condition.
ALTER procedure [dbo].[spFilterThisResume]
#Skill varchar(100),
#Exp INT, #Dt date
AS
DECLARE #NoStart INT
IF (#Exp = '')
SET #Exp = NULL
IF (#Dt = '')
SET #NoStart = 1
BEGIN
DECLARE #SkillId varchar(100)
DECLARE MY_CURSOR CURSOR
LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
SELECT * FROM dbo.SplitStrings_CTE(#Skill,',')
OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO #SkillId
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#NoStart = 1)
BEGIN
SELECT * FROM tblResume
(Skills LIKE '%,'+(#SkillId)+',%' OR Skills LIKE (#SkillId)+',%' OR Skills LIKE '%,'+(#SkillId) OR Skills = (#SkillId)
AND (Experience LIKE #Exp))
END
ELSE
BEGIN
SELECT * FROM tblResume
(Skills LIKE '%,'+(#SkillId)+',%' OR Skills LIKE (#SkillId)+',%' OR Skills LIKE '%,'+(#SkillId) OR Skills = (#SkillId)
AND (Experience LIKE #Exp)
AND (CreatedDate LIKE #Dt))
END
END
PRINT #SkillId
FETCH NEXT FROM MY_CURSOR INTO #SkillId
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR
The result I got before I tried to put this into IF ELSE block was accurate. It returned even a single occurance of a single skill that I passed as parameter. But I do not understand which part of the query messed up the whole resultset that I got earlier. I unfortunately did not save the working SP into a notepad. So please help me to identify what mistake I made.
NOTE: SELECT * FROM dbo.SplitStrings_CTE(#Skill,',') is a function that I am using to split the input csv into its component arguments
What is going wrong is that your begin and end blocks are all out of line, and you end up in an infinite loop. If I remove actual queries, and add in a comment for each bit to shorten it, and label each begin/end with a number to tie them up you have:
ALTER procedure [dbo].[spFilterThisResume]
AS
-- DECLARE VARIABLES
BEGIN -- 1 BEGIN 1
-- DECLARE CURSOR AND OPEN IT
WHILE ##FETCH_STATUS = 0
BEGIN -- 2 START WHILE
IF (#NoStart = 1)
BEGIN -- 3 START IF
-- QUERY WITH NO DATE CHECK
END -- 3 END IF
ELSE
BEGIN -- 3 START IF
-- QUERY WITH DATE CHECK
END -- 3 END IF
END -- 2 END WHILE
PRINT #SkillId
FETCH NEXT FROM MY_CURSOR INTO #SkillId
END -- 1 END 1
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR
So your second FETCH NEXT FROM MY_CURSOR INTO #SkillId falls outside of the WHILE. So you never update ##FETCH_STATUS, it will always be 0 after the first fetch, and you will never exit the loop to get to the second FETCH, to correct it you would need to just put your second FETCH inside the loop.
However, this is needless use of a cursor, and as a general rule you should avoid cursors as much as possible. You could just use:
SELECT *
FROM tblResume AS r
WHERE r.Experience LIKE #Exp
AND EXISTS
( SELECT 1
FROM dbo.SplitStrings_CTE(#Skill,',') AS s
WHERE ',' + r.Skills + ',' LIKE '%,' + s.Value + ',%'
);
This uses your original split function still, but in a single query, so imagine, as from your previous question you have rows in Resume:
C,C++
P,H,D
ASP,.net,C,C#,C++,R+
C++
And you pass C++,C#, rather than running the query twice, once for C++ and once for C# you can run the query once checking for each value, the exists clause essentially expands out to:
SELECT
FROM tblResume
WHERE r.Experience = #Exp
AND ( ',' + r.Skills + ',' LIKE '%,C++,%'
OR ',' + r.Skills + ',' LIKE '%,C#,%'
)
Also, since #Dt is a DATE, the check #Dt = '' is really checking if `#Dt = '1900-01-01`` which is unlikely to be required behaviour? I suspect you want:
IF #dt IS NULL
BEGIN
SELECT *
FROM tblResume AS r
WHERE r.Experience LIKE #Exp
AND EXISTS
( SELECT 1
FROM dbo.SplitStrings_CTE(#Skill,',') AS s
WHERE ',' + r.Skills + ',' LIKE '%,' + s.Value + ',%'
);
END
ELSE
BEGIN
SELECT *
FROM tblResume AS r
WHERE r.Experience LIKE #Exp
AND r.CreatedDate = #Dt
AND EXISTS
( SELECT 1
FROM dbo.SplitStrings_CTE(#Skill,',') AS s
WHERE ',' + r.Skills + ',' LIKE '%,' + s.Value + ',%'
);
END
i.e. Checking where the date is not passed, rather than when it is 1900-01-01.
----
ADDENDUM
With regard to why you need to use the predicate as follows:
WHERE ',' + r.Skills + ',' LIKE '%,' + s.Value + ',%'
Again, using some of your previous example data (an one extra row):
1. C
2. ASP,.net,C,C#,C++,R+
3. C++
So if you were just looking for 'C', you might use:
WHERE r.Skills LIKE '%C%';
But as you noted in your previous question, this would also yield row 3 because C++ is like '%C%', which is not correct, it is therefore necessary to look for %,C,%, i.e. where there is a complete match on the skill:
WHERE r.Skills LIKE '%,C,%';
This would now mean that the first row is not returned, because C on it's own is not like %,C,%' - therefore we need to put the delimeter at the start and end ofr.Skills` to ensure that the first and last terms in the list are not excluded from the search.
If you pass NULL for #Exp you would get no rows returned since the predicate [Anything] = NULL yields NULL, so will never be true. This is not just the case in my answer, it is the case in your question too.
Your sql is returning incorrect results because you are using ( and ) to wrap both AND and OR together.
You need to structure your query like this.
SELECT
* FROM tblResume
WHERE
(
Skills LIKE '%,'+(#SkillId)+',%'
OR Skills LIKE (#SkillId)+',%'
OR Skills LIKE '%,'+(#SkillId)
OR Skills = (#SkillId)
)
AND (Experience LIKE #Exp)
Note:As mentioned in the answer by GarethD, you can avoid the use of cursor in such a case.

Aggregate sql resultset into HashBytes value

Is there an equivalent of
CHECKSUM_AGG(CHECKSUM(*))
for HashBytes?
I know you can do
SELECT
HashBytes('MD5',
CONVERT(VARCHAR,Field1) + '|'
+ CONVERT(VARCHAR,Field2) + '|'
+ CONVERT(VARCHAR,field3) + '|'
)
FROM MyTable
But I am not sure how to aggregate all calculated hashbyte records into a single value inside of SQL.
One reason I would want to do this is to determine if data has changed in the source table since the previous load before moving the data into my system.
You can loop through all records and combine hashes to one
declare #c cursor;
declare #data varchar(max);
declare #hash varchar(400) = '';
set #c = cursor fast_forward for
select cast(SomeINTData as varchar(50)) + SomeTextData
from TFact
where Year = #year
and Month = #month;
open #c
fetch next from #c into #data
while ##FETCH_STATUS = 0 begin
set #hash = HASHBYTES('sha1', #hash + #data)
fetch next from #c into #data
end
select #hash Ha
If you want to check if a given row has changed I strongly recomend you to use a "timestamp" column.
The value is automatically updated by Sql Server in every row modification.
Then if a row is changed, the value will be diferent after modification and you could notice it without implementing logic or querying the whole table.
But if you want to know if at least one row has been updated I recomend you to use:
DECLARE #Tablename sysname = 'MyTable';
SELECT modify_date FROM sys.tables WHERE name = #Tablename;
(If you are using .Net in your business layer it might be interesting for you to take a look on SqlDependency)
You can nest hashbytes, using a varbinary variable to accrue each row's inner hash results for the final outer hash.
My example below takes ~24 seconds against 870k rows on a mid-range Xeon. More columns and lots of null values will increase crunch time.
The Order by clause is essential to guaranteeing repeatable results.
Declare #TableHash varbinary(max) = 0x00;
Select #TableHash =
hashbytes('MD5', #TableHash +
hashbytes('MD5',
isnull(convert(nvarchar(max),Col1_int),'null') +
isnull(convert(nvarchar(max),Col2_int),'null') +
isnull(convert(nvarchar(max),Col3_int),'null') +
isnull(convert(nvarchar(max),Col4_int),'null') +
isnull(convert(nvarchar(max),Col5_nvmax),'null'))
)
From MyTable
Order by Col2_int,Col1_int;
Print convert(varchar(max), #TableHash, 1) +
Case #TableHash When 0x00 Then ' (Table has no data)' Else '' End;
Output:
0x2AF0A66411F23B67D3819AC407D3B8BD
With newer versions of SQL Server, you can use a combination of CONCAT and STRING_AGG to bung everything together, then hash the whole result.
SELECT
HASHBYTES('SHA2_512',
STRING_AGG(
CONCAT(
CAST(Field1 AS varchar(max)), -- at least one max
Field2,
field3
), ''
)
)
FROM MyTable;
Note that MD5 is deprecated, and would probably be at risk of hash collisions even in this case. You should use SHA2_512 or SHA2_256 instead.

Create a concatenation function in SQL

Suppose I have a table a with one column b and three rows(1,2,3), I would like to create a function that will return '1,2,3' that would be called like this : SELECT FUNC(f), ... FROM ...
In other words, I have a linked table that have more than one rows linked to each rows of the first table and would like to concatenate the content of one column from the second table. In this case, it's a list of names associated with a specific observation.
I was thinking of using a SQL function for that, but I can't remember how... :(
Thanks
Here is an example for SQL Server:
CREATE FUNCTION ConcatenateMyTableValues
(#ID int)
RETURNS varchar(max)
AS
BEGIN
declare #s as varchar(max);
select #s = isnull(#s + ',', '') + MyColumn from MyTable where ID = #ID;
return #s
end
And then you could use it like this:
select t.ID, t.Name, dbo.ConcatenateMyTableValues(t.ID)
from SomeTable t
you can use COALESCE to convert values in one column to csv
http://www.sqlteam.com/article/using-coalesce-to-build-comma-delimited-string
I am not sure how you will be able to create a function
unless you do not mind creating a dynamic sql which can accept a column name and then build sql accordingly at run time.
edit.
this works for SQL Server only.
didn't realize that you have not mentioned any db.
DECLARE #list AS varchar(MAX)
SELECT #list = ISNULL(#list + ',', '') + b
FROM MyTable
SELECT #list AS Result
Here a way to do this recursive (ms sql)
Depends on your technology.
If it is Oracle, check out the stragg function.
http://www.sqlsnippets.com/en/topic-11591.html
If it is MSSQL use the XML PATH trick
http://sqlblogcasts.com/blogs/tonyrogerson/archive/2009/03/29/creating-an-output-csv-using-for-xml-and-multiple-rows.aspx
I guess I'm going to answer my own question since I actually got a way to do it using CURSOR (that keyword I was missing in my thoughs..)
ALTER FUNCTION [dbo].[GET_NOM_MEDECIN_REVISEURS] (#NoAs810 int)
RETURNS NVARCHAR(1000)
AS
BEGIN
IF #NoAs810 = 0
RETURN ''
ELSE
BEGIN
DECLARE #NomsReviseurs NVARCHAR(1000)
DECLARE #IdReviseurs AS TABLE(IdReviseur int)
DECLARE #TempNomReviseur NVARCHAR(50)
SET #NomsReviseurs = ''
DECLARE CurReviseur CURSOR FOR
SELECT DISTINCT Nom FROM T_Ref_Reviseur R INNER JOIN T_Signature S ON R.IdReviseur = S.idReviseur WHERE NoAs810 = #NoAs810
OPEN CurReviseur
FETCH FROM CurReviseur INTO #TempNomReviseur
WHILE ##FETCH_STATUS = 0
BEGIN
SET #NomsReviseurs = #NomsReviseurs + #TempNomReviseur
FETCH NEXT FROM CurReviseur INTO #TempNomReviseur
IF ##FETCH_STATUS = 0
SET #NomsReviseurs = #NomsReviseurs + ' - '
END
CLOSE CurReviseur
RETURN #NomsReviseurs
END
RETURN ''
END

removing duplicates from table without using temporary table

I've a table(TableA) with contents like this:
Col1
-----
A
B
B
B
C
C
D
i want to remove just the duplicate values without using temporary table in Microsoft SQL Server. can anyone help me?
the final table should look like this:
Col1
-----
A
B
C
D
thanks :)
WITH TableWithKey AS (
SELECT ROW_NUMBER() OVER (ORDER BY Col1) As id, Col1 As val
FROM TableA
)
DELETE FROM TableWithKey WHERE id NOT IN
(
SELECT MIN(id) FROM TableWithKey
GROUP BY val
)
Can you use the row_number() function (http://msdn.microsoft.com/en-us/library/ms186734.aspx) to partition by the columns you're looking for dupes on, and delete where row number isn't 1?
I completely agree that having a unique identifier will save you a lot of time.
But if you can't use one (or if this is purely hypothetical), here's an alternative: Determine the number of rows to delete (the count of each distinct value -1), then loop through and delete top X for each distinct value.
Note that I'm not responsible for the number of kittens that are killed every time you use dynamic SQL.
declare #name varchar(50)
declare #sql varchar(max)
declare #numberToDelete varchar(10)
declare List cursor for
select name, COUNT(name)-1 from #names group by name
OPEN List
FETCH NEXT FROM List
INTO #name,#numberToDelete
WHILE ##FETCH_STATUS = 0
BEGIN
IF #numberToDelete > 0
BEGIN
set #sql = 'delete top(' + #numberToDelete + ') from #names where name=''' + #name + ''''
print #sql
exec(#sql)
END
FETCH NEXT FROM List INTO #name,#numberToDelete
END
CLOSE List
DEALLOCATE List
Another alternative would to be create a view with a generated identity. In this way you could map the values to a unique identifer (allowing for conventional delete) without making a permanent addition to your table.
Select grouped data to temp table, then truncate original, after that move back it to original.
Second solution, I am not sure will it work but you can try open table directly from SQL Management Studio and use CTRL + DEL on selected rows to delete them. That is going to be extremely slowly because you need to delete every single row by hands.
You can remove duplicate rows using a cursor and DELETE .. WHERE CURRENT OF.
CREATE TABLE Client ([name] varchar(100))
INSERT Client VALUES('Bob')
INSERT Client VALUES('Alice')
INSERT Client VALUES('Bob')
GO
DECLARE #history TABLE (name varchar(100) not null)
DECLARE #cursor CURSOR, #name varchar(100)
SET #cursor = CURSOR FOR SELECT name FROM Client
OPEN #cursor
FETCH NEXT FROM #cursor INTO #name
WHILE ##FETCH_STATUS = 0
BEGIN
IF #name IN (SELECT name FROM #history)
DELETE Client WHERE CURRENT OF #cursor
ELSE
INSERT #history VALUES (#name)
FETCH NEXT FROM #cursor INTO #name
END