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.
Related
I need to perform a set of queries to extract the distinct values of a group of fields from different tables,
so far I managed to create the cursor that makes the loop as expected, but I'm not able to create the query in the loop assigning the values as I need.
please look at below example:
DECLARE #sourcetablename NVARCHAR(250),
#targettablename NVARCHAR(250),
#sourcefieldname NVARCHAR(250);
DECLARE DMTCursor CURSOR FOR
SELECT
SOURCETABLE, TARGETTABLE, ENTITYFIELD
FROM DMTSOURCE ;
OPEN DMTCursor ;
FETCH NEXT FROM DMTCursor INTO
#sourcetablename,
#targettablename,
#sourcefieldname;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #sourcetablename + ' ' + #targettablename+ ' ' + #sourcefieldname;
FETCH NEXT FROM DMTCursor INTO
#sourcetablename,
#targettablename,
#sourcefieldname;
END;
CLOSE DMTCursor ;
DEALLOCATE DMTCursor ;
in this example I used a print function to test the code and it actually shows the values that I want to use as parameters for the query, but I cannot figure out how to use them in the select, let's make for instance that the first row coming from the cursor retrieves:
#sourcetablename = sourcetable1
#targettablename = targettable1
#sourcefieldname = sourcefield1
I want the loop query (replacing the PRINT line) be like:
select distinct
'targettable1' AS ENTITY,
'sourcefield1' AS SOURCE_FIELD,
sourcefield1 AS DISTINCTVALUE
from targettable1
just replacing the variables in the query does not work, it shows the error about the table (targettable1) not declared as a variable and I need a way to set the first 2 parameters as "fixed" strings (that's why I wrote them among '' in above example) in the query.
thanks in advance for help,
thanks shawnnt00,
Dynamic sql helped me to figure out the SELECT inside the loop:
SET #SQL=N'
select distinct ''' +
#targettablename + ''' AS ENTITY, ''' +
#sourcefieldname + ''' AS SOURCE_FIELD, ' +
#sourcefieldname + ' AS VALUE
from ' + #targettablename ;
EXEC sp_executesql #SQL;
the point of discussion was not about the cursor but just on the parameters management,
thanks everybody for support
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.
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
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
I have the following TSQL codes:
-- 1. define a cursor
DECLARE c_Temp CURSOR FOR
SELECT name FROM employees;
DECLARE #name varchar(100);
-- 2. open it
OPEN c_Temp;
-- 3. first fetch
FETCH NEXT FROM c_Temp INTO #name;
WHILE ##FETCH_STATUS = 0
BEGIN
print #name;
FETCH NEXT FROM c_Temp INTO #name; -- fetch again in a loop
END
-- 4. close it
....
I use the name value only in a loop block. Here I have to
define a cursor variable,
open it,
fetch twice and
close it.
In PL/SQL, the loop can be like this:
FOR rRec IN (SELECT name FROM employees) LOOP
DBMS_OUTPUT.put_line(rRec.name);
END LOOP;
It is much simpler than my TSQL codes. No need to define a cursor. It is created dynamically which is accessible within a loop block (much like C# for loop). Not sure if there something similar like this in TSQL?
Something along these lines might work for you, although it depends on having an ID column or some other unique identifier
Declare #au_id Varchar(20)
Select #au_id = Min(au_id) from authors
While #au_id IS NOT NULL
Begin
Select au_id, au_lname, au_fname from authors Where au_id = #au_id
Select #au_id = min(au_id) from authors where au_id > #au_id
End
Cursors are evil in Sql Server as they can really degrade performance - my favoured approach is to use a Table Variable (>= Sql Server 2005) with an auto inc ID column:
Declare #LoopTable as table (
ID int identity(1,1),
column1 varchar(10),
column2 datetime
)
insert into #LoopTable (column1, column2)
select name, startdate from employees
declare #count int
declare #max int
select #max = max(ID) from #LoopTable
select #count = 1
while #count <= #max
begin
--do something here using row number '#count' from #looptable
set #count = #count + 1
end
It looks pretty long winded however works in any situation and should be far more lightweight than a cursor
Since you are coming from an Oracle background where cursors are used frequently, you may not be aware that in SQl Server cursors are performance killers. Depending on what you are actually doing (surely not just printing the variable), there may be a much faster set-based solution.
In some cases, its also possible to use trick like this one:
DECLARE #name VARCHAR(MAX)
SELECT #name = ISNULL(#name + CHAR(13) + CHAR(10), '') + name
FROM employees
PRINT #name
For a list of employee names.
It can also be used to make comma-separated string, just replace + CHAR(13) + CHAR(10) with + ', '
Why not simply just return the recordset using a select statement. I assume the object is to copy and paste the values in the UI (based on the fact that you are simply printing the output)? In Management studio you can copy and paste from the grid, or press +T and then run the query and return the results as part of the messages tab in plain text.
If you were to run this via an application, the application wouldn't be able to access the printed statements as they are not being returned within a recordset.