Time Complexity of SQL Cursor - sql-server

I am using CURSOR to implement the following in SQL Server, I am only iterating through the table - The time complexity will be O(n) I think (?). But everywhere I read about CURSOR, it says CURSOR is a bad practice. So is there a better way to implement the following ?
Existing Table
month value
1 92
4 20
9 92
New Table
month value
1 92
2 92
3 92
4 20
5 20
6 20
7 20
8 20
9 92
10 92
11 92
12 92

The use of cursor isn't (primarily) bad because it has poor time complexity, but because it is more error-prone and harder to read than a simple query. You are correct that iterating over a table via cursor is O(n).
On to your problem at hand. If you have the months (1..12) stored somewhere, say Months, then you can do it like this:
WITH matchingMonths AS (
SELECT m.month, MAX(mav.month) as matchedMonth
FROM Months m, MonthsAndValues mav
WHERE m.month >= mav.month
GROUP BY m.month
)
SELECT mm.month, mav.value
FROM matchingMonths mm
JOIN MonthsAndValues mav on mav.month = mm.matchedMonth
Without such a table Months, you could generate it on-the-fly:
WITH Months(month) AS (
SELECT 1
UNION ALL
SELECT month + 1 FROM Months WHERE month < 12
),
matchingMonths AS (
SELECT m.month, MAX(mav.month) as matchedMonth
FROM Months m, MonthsAndValues mav
WHERE m.month >= mav.month
GROUP BY m.month
)
SELECT mm.month, mav.value
FROM matchingMonths mm
JOIN MonthsAndValues mav on mav.month = mm.matchedMonth

Related

Accumulative Update for previous records

I have table that shows these information
Month NewClients OnHoldClients
5-2017 10 2
6-2017 16 4
7-2017 11 1
8-2017 15 6
9-2017 18 7
I am trying to find the accumulative total for each month
which is
(NewClients - OnHoldClients) + Previous Month Total
Something like this
Month NewClients OnHoldClients Total
5-2017 10 2 8
6-2017 16 4 20
7-2017 11 1 30
8-2017 15 6 39
9-2017 18 7 50
the query i tried to build was something like this but I think should be an easier way to do that
UPDATE MyTable
SET Total = (SELECT TOP 1 Total FROM MyTable B WHERE B.Month < A.Month) + NewClients - OnHoldClients
FROM MyTable A
Before we begin, note the mere fact that you're facing such calculative problem is a symptom that maybe you don't have the best possible design. Normally for this purpose calculated values are being stored along the way as the records are inserted. So i'd say you'd better have a total field to begin with and calculate it as records amass.
Now let's get down to the problem at hand. i composed a query which does that nicely but it's a bit verbose due to recursive nature of the problem. However, it yields the exact expected result:
DECLARE #dmin AS date = (SELECT min(mt.[Month]) from dbo.MyTable mt);
;WITH cte(_Month, _Total) AS (
SELECT mt.[Month] AS _Month, (mt.NewClients - mt.OnHoldClients) AS _Total
FROM dbo.MyTable mt
WHERE mt.[Month] = #dmin
UNION ALL
SELECT mt.[Month] AS _Month, ((mt.NewClients - mt.OnHoldClients) + ccc._Total) AS _Total
FROM dbo.MyTable mt
CROSS APPLY (SELECT cc._Total FROM (SELECT c._Total,
CAST((row_number() OVER (ORDER BY c._Month DESC)) AS int) as _Rank
FROM cte c WHERE c._Month < mt.[Month]) as cc
WHERE cc._Rank = 1) AS ccc
WHERE mt.[Month] > #dmin
)
SELECT c._Month, max(c._Total) AS Total
FROM cte c
GROUP BY c._Month
It is a recursive CTE structure that goes about each record all along the way to the initial month and adds up to the final Total value. This query only includes Month and Total fields but you can easily add the other 2 to the list of projection.
Try this
;WITH CTE([Month],NewClients,OnHoldClients)
AS
(
SELECT '5-2017',10,2 UNION ALL
SELECT '6-2017',16,4 UNION ALL
SELECT '7-2017',11,1 UNION ALL
SELECT '8-2017',15,6 UNION ALL
SELECT '9-2017',18,7
)
SELECT [Month],
NewClients,
OnHoldClients,
SUM(MonthTotal)OVER( ORDER BY [Month]) AS Total
FROM
(
SELECT [Month],
NewClients,
OnHoldClients,
SUM(NewClients-OnHoldClients)OVER(PArtition by [Month] Order by [Month]) AS MonthTotal
FROM CTE
)dt
Result,Demo:http://rextester.com/DKLG54359
Month NewClients OnHoldClients Total
--------------------------------------------
5-2017 10 2 8
6-2017 16 4 20
7-2017 11 1 30
8-2017 15 6 39
9-2017 18 7 50

SQL Server 2012 Computed column

ID Date Value Average
1 10/5/2017 15 15
2 10/6/2017 25 20
3 10/7/2017 35 25
4 10/8/2017 45 35
5 10/9/2017 55 45
6 10/10/2017 65 55
7 10/11/2017 75 65
If this is my table, I want average to be a computed column and its formula in general is average of previous 3 row's Value column.
(Ex. for 2nd row it is (25+15)/2 )
How can i do such a thing in computed column? Is there any better way to achieve this.
Thanks in advance.
i would go with a view and use avg windows function
select
id,
date,
value,
avg(value) over (order by id)
from table
Updated answer: you could use frames clause like below
Working Demo
;with cte(id,date,val)
as
(
select 1 ,'10/5/2017' , 15 UNION ALL
select 2 ,'10/6/2017' , 25 UNION ALL
select 3 ,'10/7/2017' , 35 UNION ALL
select 4 ,'10/8/2017' , 45 UNION ALL
select 5 ,'10/9/2017' , 55 UNION ALL
select 6 ,'10/10/2017', 65 UNION ALL
select 7 ,'10/11/2017', 75
)
SELECT *,avg(VAL) OVER (ORDER BY id rows between 2 PRECEDING and current row ) FROM CTE

SQL update table loop

I have this following query that gets me a small result set
SELECT
LOC, PLAN, FiscalYear, FiscalPeriod, SALES
FROM
#CurrentPrd PrdAg
WHERE
NOT EXISTS (SELECT AGE.ECPLAN
FROM ECPG_BAK AGE
WHERE PrdAg.LOC = AGE.STORE
AND PrdAg.PLAN = AGE.PLAN
AND PrdAg.FiscalYear = AGE.FiscalYear
AND PrdAg.FiscalPeriod = AGE.FiscalPeriod)
The result set looks like this:
LOC PLAN FiscalYear FiscalPeriod SALES
---------------------------------------------------
5 6 2031 5 -0.206232
12 6 2031 5 5.243052
12 8 2020 4 1.699716
12 8 2020 5 1.699716
14 6 2031 5 0.299972
19 6 2031 5 1.549812
19 8 2020 5 20.114116
33 6 2031 5 2.159767
33 8 2020 5 23.796883
34 6 2031 5 1.142360
34 8 2020 5 9.348583
................................................
Then I have this other query that gets me a number that I need to add to the SALES column. For example, the query below, I used fixed loc and plan to come up with a number:
select
(select SALES
from #TOT
where loc = 12 and PLAN = 6) - (select sum(sales)
from #CurrentPrd
where store = 12 and PLAN = 6) as Comp
Let's assume this query above gets me 10, then I need to add it to line 2 of the result set above, making it
LOC PLAN FiscalYear FiscalPeriod SALES
----------------------------------------------
12 6 2031 5 15.243052
My goal is to make it somewhat dynamic and do the whole process in a simple way, so for each LOC and PLAN combination, I would plug those values into the second select to retrieve the correct number to add to SALES, then update #CurrentPrd. Writing the new number to a new temp table is also an option.
I hope I was able to explain what I'm trying to do. Any help would be appreciated.
Thanks.
Without any actual test data, it's hard to say for sure but I think something like the following should work for you...
SELECT
PrdAg.LOC,
PrdAg.[PLAN],
PrdAg.FiscalYear,
PrdAg.FiscalPeriod,
SALES = PrdAg.SALES + (tx.SALES - cpx.SALES)
FROM
#CurrentPrd PrdAg
CROSS APPLY (SELECT TOP 1 T.SALES FROM #TOT T WHERE PrdAg.LOC = T.LOC AND PrdAg.[PLAN] = t.[PLAN]) tx
CROSS APPLY (SELECT SALES = SUM(CP.SALES) FROM #CurrentPrd CP WHERE PrdAg.LOC = CP.LOC AND PrdAg.[PLAN] = CP.[PLAN]) cpx
WHERE
NOT EXISTS (
SELECT 1
FROM
ECPG_BAK AGE
WHERE
PrdAg.LOC = AGE.STORE
AND PrdAg.[PLAN] = AGE.[PLAN]
AND PrdAg.FiscalYear = AGE.FiscalYear
AND PrdAg.FiscalPeriod = AGE.FiscalPeriod
);

Sql comparaison nested on one column

Imagine a table :
ID Month Year Value 1
1 May 17 58
2 June 09 42
3 December 18 58
4 December 18 58
5 September 10 84
6 May 17 42
7 January 16 3
I want to return all the data that shares the same month and year where Value 1 is different. So in our example, I want to return 1 and 6 only but not 3 and 4 or any of the other entries.
Is there a way to do this? I am thinking about a combination of distinct and group by but can't seem to come up with the right answer being new to SQL.
Thanks.
It could be done without grouping, but with simple self-join:
select distinct t1.*
from [Table] t1
inner join [Table] t2 on
t1.Month = t2.Month
and t1.Year = t2.Year
and t1.Value_1 <> t2.Value_1
You can find some information and self-join examples here and here.
For each row you can examine aggregates in its group with the OVER clause. eg:
create table #t(id int, month varchar(20), year int, value int)
insert into #t(id,month,year,value)
values
(1,'May' ,17, 58 ),
(2,'June' ,09, 42 ),
(3,'December' ,18, 58 ),
(4,'December' ,18, 58 ),
(5,'September',10, 84 ),
(6,'May' ,17, 42 ),
(7,'January' ,16, 3 );
with q as
(
select *,
min(value) over (partition by month,year) value_min,
max(value) over (partition by month,year) value_max
from #t
)
select id,month,year,value
from q
where value_min <> value_max;
If I understood your question correctly, you are looking for the HAVING keyword.
If you GROUP BY Month, Year, Value_1 HAVING COUNT(*) = 1, you get all combinations of Month, Year and Value_1 that have no other occurrence.

Return Maximum Value along with day + time value grouped by month

Using MS-SQL 2012. Having a real puzzle trying to retrieve specific datafields from a large climatology dataset.
I have stripped this large raw data file down to a temp table called #max_temp which correctly pulls back the max value for each day along with the time it occurred and day/month value for reference:
monthid month day time current_temp
1 12 24 12:45 9.1
1 12 25 12:25 8.3
1 12 26 23:55 8.6
1 12 27 00:00 8.6
1 12 28 13:15 5.9
1 12 29 12:50 5
1 12 30 13:32 6.3
1 12 31 12:49 6.9
2 1 1 23:59 12
2 1 2 01:12 12.7
2 1 3 03:55 6.2
What I want to retrieve is an output grouped by monthID, so returning:
monthid month day time current_temp
1 12 24 12:45 9.1
2 1 9 20:04 15.1 <<*not shown in above sample*>>
From looking at other similar questions I have tried the following the code but not getting to the end solution or the query fails.
select *
from (select t.*, ROW_NUMBER () over (partition by t.monthid, t.time order by t.current_temp desc) as rn
from #max_temp t) x
where rn=1
order by monthid asc
or
select monthid, day, time, current_temp
from #max_temp
where current_temp= (select max(current_temp) from #max_temp group by MonthID, day, time)
Thanks in advance for your help,
Elliot.
Remove t.time from the partition by like so:
select *
from (
select t.*, ROW_NUMBER () over (partition by t.monthid order by t.current_temp desc) as rn
from #max_temp t
) x
where rn=1
order by monthid asc
Having time in the partition would give you the greatest value for current_temp for each monthid and time, but since you just want the greatest current_temp for each monthid, remove time from that expression.

Resources