Removal of Cursor in SQL - sql

Its always been discouraged to use cursor, which have been extensively used in our current stored procedures and replace them with set based queries. But this particular scenario is one, where I did not get solution to use set based query and forced to keep using the cursor. I am providing below sample code which represents the scenario:
DECLARE #temp varchar(10), #continuechar varchar(10)
DECLARE #table1 table (col1 varchar(10))
insert into #table1
select 'A' UNION
select 'B' UNION
select 'C' UNION
select 'D' UNION
select 'E' UNION
select 'F' UNION
select 'G'
DECLARE Cursor1 CURSOR for select Col1 from #table1
open Cursor1
FETCH NEXT from Cursor1 into #temp
WHILE ##FETCH_STATUS = 0
BEGIN
if #temp='A'
BEGIN
set #continuechar=#temp
END
if #temp='C'
BEGIN
set #continuechar=#temp
END
select #continuechar, #temp
FETCH NEXT from Cursor1 into #temp
END
CLOSE cursor1;
deallocate cursor1
Here in above sample code #continuechar variable is not getting set, every time cursor is getting executed. If #continuechar is getting set, then following select statement is providing result set with current value of #continuechar:
select #continuechar, #temp
if its not getting set, then its using the previously set value to provide result set.
Can we have set based queries to remove cursor from such scenario.

First I would add some id column to get stable sort. Then simply use windowed functions:
SUM() OVER() to calculate groups
FIRST_VALUE() OVER() to propagate first value across group
(present from SQL Server 2012, you could exchange it with MAX(continuechar) OVER(PARTITION BY grp) if necessary)
DECLARE #table1 table (id INT IDENTITY(1,1), col1 varchar(10))
insert into #table1
select 'A' UNION
select 'B' UNION
select 'C' UNION
select 'D' UNION
select 'E' UNION
select 'F' UNION
select 'G';
WITH cte AS (
SELECT id
,col1
,CASE WHEN col1 IN('A', 'C') THEN col1 END AS continuechar
,SUM(CASE WHEN col1 IN ('A', 'C') THEN 1 ELSE 0 END)
OVER(ORDER BY id) AS grp
FROM #table1
)
SELECT id, col1,
FIRST_VALUE(continuechar) OVER(PARTITION BY grp ORDER BY id) AS continuechar
FROM cte
ORDER BY id;
DBFiddle Demo
EDIT:
Quirky update This is for pure demo only. Do not use this method on production system:
DECLARE #table1 table (id INT IDENTITY(1,1) PRIMARY KEY, col1 varchar(10),
continue_char VARCHAR(10));
DECLARE #temp VARCHAR(10);
insert into #table1(col1)
select 'A' UNION
select 'B' UNION
select 'C' UNION
select 'D' UNION
select 'E' UNION
select 'F' UNION
select 'G';
UPDATE #table1
SET #temp = CASE WHEN col1 IN ('A','C') THEN col1 ELSE #temp END
,continue_char = #temp
OPTION(MAXDOP 1);
SELECT *
FROM #table1;
DBFiddle Demo2

Related

Passing Value to parameter in stored procedure

I have a list of value which is more than 500, and every time I have to pass those value to the below stored procedure. Will it be possible to call those value dynamically?
RDBMS: SQL Server 2014
small set of #value example below.
declare #valuetable table
(
value varchar(50)
)
insert #valuetable
select video union
select audio union
select hayward union
select abott union
select gsk
Code:
DECLARE #value VARCHAR(24) SET #value = 'video'
DECLARE #DAYS INT SET #DAYS = -30
SELECT * INTO #XTP1 FROM (
SELECT DISTINCT 'START' AS DTT, DATEADD(D,#DAYS,DATEACTIONED) AS DT FROM NEWREPORTS
WHERE value = #value
UNION
SELECT DISTINCT 'CHANGE' AS DTT, DATEACTIONED AS DT FROM NEWREPORTS
WHERE value = #value
)r
Thanks
So what you need to do is to add one column to #XTP1 to store the value name. Then you can do:
declare #valuetable table
(
value varchar(50)
)
insert #valuetable
select video union
select audio union
select hayward union
select abott union
select gsk
SELECT * INTO #XTP1 FROM (
SELECT DISTINCT v.value, 'START' AS DTT, DATEADD(D,#DAYS,DATEACTIONED) AS DT
FROM NEWREPORTS n INNER JOIN #valuetable v ON n.value = v.value
UNION
SELECT DISTINCT v.value, 'CHANGE' AS DTT, DATEACTIONED AS DT
FROM NEWREPORTS n INNER JOIN #valuetable v ON n.value = v.value
)r
Now you can retrieve everything from #XTP1 in one go. You will probably want to include "value" in your ORDER BY.

SQL - populate new column according to data in row above

I need to populate a new column in a table known as RowType, where if the ID column contains the same ID value as the one above RowType is populated with 'D', if the value is new then RowType is populate with 'H', how would the SQL code look to be able to do this?
I.e should look something like below:
RowType (to be populated), ID (already there)
H, 1
D, 1
D, 1
H, 2
D, 2
H, 3
D, 3
D, 3
Thanks
You can use Row_Number and case
select *, RowType = case when Row_Number() over (partition by id order by id) = 1 then 'H' else 'D' End from #yourid
Your input table:
create table #yourId (id int)
insert into #yourid (id) values
(1)
,(1)
,(1)
,(2)
,(2)
,(3)
,(3)
,(3)
Use ROW_NUMER concept :
CREATE TABLE #table(Id INT)
INSERT INTO #table(Id)
SELECT 1 UNION ALL
SELECT 1 UNION ALL
SELECT 1 UNION ALL
SELECT 2 UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 3 UNION ALL
SELECT 3
SELECT CASE WHEN RowType = 1 THEN 'H' ELSE 'D' END RowType , Id
FROM
(
SELECT ROW_NUMBER() OVER (PARTITION BY Id ORDER BY id) RowType , Id
FROM #table
) A
Please try...
UPDATE tableName
SET RowType = CASE
WHEN ( ID = LAG( ID ) OVER ( ORDER BY ID ) ) THEN 'D'
ELSE 'H'
END
If you have any questions or comments, then please feel free to post a Comment accordingly.
Further Reading
https://learn.microsoft.com/en-us/sql/t-sql/functions/lag-transact-sql (for information on LAG()).
It may not be the best solution, however it can point you somewhere, and it works.
Go through the code carfuly and make sure you understand this.
create table yourTable (RowType char, id int)
insert into yourTable (RowType, id) values
('',1)
,('',1)
,('',1)
,('',2)
,('',2)
,('',3)
,('',3)
,('',3)
select
row_number() over (order by id) as rowNumber,
RowType,
id
into #tempTable
from yourTable
declare #maxRow int = (select max(rowNumber) from #tempTable)
declare #currentRow int = 1
while (#currentRow <= #maxRow)
begin
if (#currentRow = 1)
begin
update #tempTable
set RowType = 'H'
where rowNumber = #currentRow
end
else
begin
if (select id from #tempTable where rowNumber = #currentRow) = (select id from #tempTable where rowNumber = #currentRow - 1)
begin
update #tempTable
set RowType = 'D'
where rowNumber = #currentRow
end
else
begin
update #tempTable
set RowType = 'H'
where rowNumber = #currentRow
end
end
set #currentRow = #currentRow +1
end
-- update data in actual table, you can do below if only those two columns exist in table !!!
delete from yourTable
-- insert into table from updated temp table
insert into yourTable
select RowType, ID
from #tempTable
select * from yourTable
select * from #tempTable
-- drop temp table
drop table #tempTable

Get top number of rows anyway

My requirement is like select top 5 rows from sql, if it contains only 2 rows then by default it shows some text say 'no data' in remaining 3 rows. Same for all conditions.
For i.e. Select top 5 rows, but it contains only 3 rows then query will return
Row1
Row2
Row3
No Data
No Data
Please try:
select top 5 Col
from(
select 0 srt, Col from YourTable
union all
select 1 srt, 'No Data' Col union all
select 1 srt, 'No Data' Col union all
select 1 srt, 'No Data' Col union all
select 1 srt, 'No Data' Col union all
select 1 srt, 'No Data' Col
)x
order by srt
this will be much more dynamic
declare #t TABLE (id int,TerminalID varchar(15))
declare #top int
set #top = 5
declare #c int --Holds Total
set #c = 0
while (#c < #top) begin
insert into #t (id, TerminalID) values (#c, 'No Data')
set #c = #c + 1
end
select top 5 * from
(
select top 5 TerminalID from AccessLog-- [where colo0 = ???]
union all
select TerminalID from #t
) x
/* assuming the column width is 8 characters and datatype is varchar */
DECLARE #NoDataTable AS TABLE(column1 VARCHAR(8))
DECLARE #i AS INT
SET #i = 0
WHILE(#i<5)
BEGIN
insert into #NoDataTable (column1)
values('No Data');
set #i = #i+1
end
select top 5 *
from
(
select column1 from TestTable
union all
select column1 from #NoDataTable
) as T
WITH cte AS
(
SELECT YourColumn FROM YourTable
UNION ALL
SELECT TOP 5 NULL FROM YourTable
)
SELECT TOP 5 COALESCE(YourColumn, 'No Data')
FROM cte

Writing CASE Statement in SQL

I have a requirement to display two columns in a report,whose values will be determined using the same input expression as following:
SELECT
CASE WHEN id>10
THEN 'AAAA'
ELSE
'BBBB'
END as 'FirstColumn',
CASE WHEN id>10
THEN
'BBBB'
ELSE
'DDDD' END as 'SecondColumn'
Can I construct this expression without repeating input expression twice as they are same?
You will need to repeat the CASE statment for each field as while the condition might be the same teh results are differnt. The only other alternative is to use a UNion all statement whenre the first select takes the records WHERE id<=10 and the other one does the WHERE ID>10. This can bea viable alternative but it is a littel hareder to maintain, so if the performance is good enough, Iwoudl stick to repeating teh CASE condition.
If some sophisticated is supposed instead of id>10 then, to make it a bit shorter and a bit more readable:
select
IIF(p.b=1, 'AAA', 'BBB') [Col1],
IIF(p.b=1, 'BBB', 'DDD') [Col2]
from
TableName t
outer apply (select cast(case when t.id>10 then 1 else NULL end as bit) as b) p
However, this is only available in SqlServer 2012. In earlier versions, parhaps, you will have to write your own IIF-like scalar function:
create function dbo.IIF (#b bit, #ifValue varchar(50), #elseValue varchar(50))
returns varchar(50)
as begin
return (select case when #b = 1 then #ifValue else #elseValue end)
end
GO
select
dbo.IIF(p.b, 'AAA', 'BBB') [Col1],
dbo.IIF(p.b, 'BBB', 'DDD') [Col2]
from
TableName t
outer apply (select cast(case when t.id>10 then 1 else NULL end as bit) as b) p
If it's worth the trouble, you could created a User-defined function (UDF) that would do this transforamation logic:
CREATE FUNCTION dbo.fColTransform(#id Int)
RETURNS Varchar(20)
AS BEGIN
DECLARE #ret Varchar(20)
SET #ret =
CASE
WHEN #id IS NULL THEN 'Unknown'
WHEN #id > 10 THEN 'BBB'
ELSE 'DDD'
END
RETURN #ret
END
Then, in your SELECT, you could structure it like this:
SELECT dbo.fColTransform(id1) AS FirstColumn,
dbo.fColTransform(id2) AS SecondColumn
FROM MyTable
You can store it in a variable and use Execute to call it:
Declare #foo as nvarchar(max)
set #foo='MyTable'
execute ('Select * from ' + #foo)
The downside is that your entire query would be all red (since it's now a string).
Try with this query, maybe will be useful (in the case if you do not use a function):
WITH temp AS (
SELECT
CASE WHEN x.ID > 10
THEN 'AAAA'
ELSE 'BBBB'
END as Col
FROM (
SELECT 1 AS ID
UNION
SELECT 11 AS ID
UNION
SELECT 13 AS ID
UNION
SELECT 9 AS ID
UNION
SELECT 7 AS ID
) x
)
SELECT temp.Col AS FirstColumn, temp.Col AS SecondColumn
FROM temp
Basically:
With name As (
/*yourquery with one CASE-COLUMN*/
)
SELECT name.COL AS COLUMN1, name.COL AS COLUMN2 FROM name
You can try this here

T-Sql count string sequences over multiple rows

How can I find subsets of data over multiple rows in sql?
I want to count the number of occurrences of a string (or number) before another string is found and then count the number of times this string occurs before another one is found.
All these strings can be in random order.
This is what I want to achieve:
I have one table with one column (columnx) with data like this:
A
A
B
C
A
B
B
The result I want from the query should be like this:
2 A
1 B
1 C
1 A
2 B
Is this even possible in sql or would it be easier just to write a little C# app to do this?
Since, as per your comment, you can add a column that will unambiguously define the order in which the columnx values go, you can try the following query (provided the SQL product you are using supports CTEs and ranking functions):
WITH marked AS (
SELECT
columnx,
sortcolumn,
grp = ROW_NUMBER() OVER ( ORDER BY sortcolumn)
- ROW_NUMBER() OVER (PARTITION BY columnx ORDER BY sortcolumn)
FROM data
)
SELECT
columnx,
COUNT(*)
FROM marked
GROUP BY
columnx,
grp
ORDER BY
MIN(sortcolumn)
;
You can see the method in work on SQL Fiddle.
If sortcolumn is an auto-increment integer column that is guaranteed to have no gaps, you can replace the first ROW_NUMBER() expression with just sortcolumn. But, I guess, that cannot be guaranteed in general. Besides, you might indeed want to sort on a timestamp instead of an integer.
I dont think you can do it with a single select.
You can use AdventureWorks cursor:
create table my_Strings
(
my_string varchar(50)
)
insert into my_strings values('A'),('A'),('B'),('C'),('A'),('B'),('B') -- this method will only work on SQL Server 2008
--select my_String from my_strings
declare #temp_result table(
string varchar(50),
nr int)
declare #myString varchar(50)
declare #myLastString varchar(50)
declare #nr int
set #myLastString='A' --set this with the value of your FIRST string on the table
set #nr=0
DECLARE string_cursor CURSOR
FOR
SELECT my_string as aux_column FROM my_strings
OPEN string_cursor
FETCH NEXT FROM string_cursor into #myString
WHILE ##FETCH_STATUS = 0 BEGIN
if (#myString = #myLastString) begin
set #nr=#nr+1
set #myLastString=#myString
end else begin
insert into #temp_result values (#myLastString, #nr)
set #myLastString=#myString
set #nr=1
end
FETCH NEXT FROM string_cursor into #myString
END
insert into #temp_result values (#myLastString, #nr)
CLOSE string_cursor;
DEALLOCATE string_cursor;
select * from #temp_result
Result:
A 2
B 1
C 1
A 1
B 2
Try this :
;with sample as (
select 'A' as columnx
union all
select 'A'
union all
select 'B'
union all
select 'C'
union all
select 'A'
union all
select 'B'
union all
select 'B'
), data
as (
select columnx,
Row_Number() over(order by (select 0)) id
from sample
) , CTE as (
select * ,
Row_Number() over(order by (select 0)) rno from data
) , result as (
SELECT d.*
, ( SELECT MAX(ID)
FROM CTE c
WHERE NOT EXISTS (SELECT * FROM CTE
WHERE rno = c.rno-1 and columnx = c.columnx)
AND c.ID <= d.ID) AS g
FROM data d
)
SELECT columnx,
COUNT(1) cnt
FROM result
GROUP BY columnx,
g
Result :
columnx cnt
A 2
B 1
C 1
A 1
B 2