T-SQL: Compute Subtotals For A Range Of Rows - sql

MSSQL 2008. I am trying to construct a SQL statement which returns the total of column B for all rows where column A is between 2 known ranges. The range is a sliding window, and should be recomputed as it might be using a loop.
Here is an example of what I'm trying to do, much simplified from my actual problem. Suppose I have this data:
table: Test
Year Sales
----------- -----------
2000 200
2001 200
2002 200
2003 200
2004 200
2005 200
2006 200
2007 200
2008 200
2009 200
2010 200
2011 200
2012 200
2013 200
2014 200
2015 200
2016 200
2017 200
2018 200
2019 200
I want to construct a query which returns 1 row for every decade in the above table, like this:
Desired Results:
DecadeEnd TotalSales
--------- ----------
2009 2000
2010 2000
Where the first row is all the sales for the years 2000-2009, the second for years 2010-2019. The DecadeEnd is a sliding window that moves forward by a set ammount for each row in the result set. To illustrate, here is one way I can accomplish this using a loop:
declare #startYear int
set #startYear = (select top(1) [Year] from Test order by [Year] asc)
declare #endYear int
set #endYear = (select top(1) [Year] from Test order by [Year] desc)
select #startYear, #endYear
create table DecadeSummary (DecadeEnd int, TtlSales int)
declare #i int
-- first decade ends 9 years after the first data point
set #i = (#startYear + 9)
while #i <= #endYear
begin
declare #ttlSalesThisDecade int
set #ttlSalesThisDecade = (select SUM(Sales) from Test where(Year <= #i and Year >= (#i-9)))
insert into DecadeSummary values(#i, #ttlSalesThisDecade)
set #i = (#i + 9)
end
select * from DecadeSummary
This returns the data I want:
DecadeEnd TtlSales
----------- -----------
2009 2000
2018 2000
But it is very inefficient. How can I construct such a query?

How about something like
SELECT (Year / 10) * 10,
SUM(Sales)
FROM #Table
GROUP BY (Year / 10) * 10
Have a look at the example here
DECLARE #Table TABLE(
Year INT,
Sales FLOAT
)
INSERT INTO #Table SELECT 2000,200
INSERT INTO #Table SELECT 2001,200
INSERT INTO #Table SELECT 2002,200
INSERT INTO #Table SELECT 2003,200
INSERT INTO #Table SELECT 2004,200
INSERT INTO #Table SELECT 2005,200
INSERT INTO #Table SELECT 2006,200
INSERT INTO #Table SELECT 2007,200
INSERT INTO #Table SELECT 2008,200
INSERT INTO #Table SELECT 2009,200
INSERT INTO #Table SELECT 2010,200
INSERT INTO #Table SELECT 2011,200
INSERT INTO #Table SELECT 2012,200
INSERT INTO #Table SELECT 2013,200
INSERT INTO #Table SELECT 2014,200
INSERT INTO #Table SELECT 2015,200
INSERT INTO #Table SELECT 2016,200
INSERT INTO #Table SELECT 2017,200
INSERT INTO #Table SELECT 2018,200
INSERT INTO #Table SELECT 2019,200
SELECT (Year / 10) * 10,
SUM(Sales)
FROM #Table
GROUP BY (Year / 10) * 10
OUTPUT
Decade SumOfSales
----------- ----------------------
2000 2000
2010 2000

How about:
select sum(sales) as TotalSales, max([year]) as DecadeEnd from Test
group by year / 10
You don't have to do (year / 10) * 10 as long as Year is an integer.
Edit: If year is a float, and the interval is 2.5 years rather than 10
select sum(sales) as TotalSales, max([year]) as DecadeEnd from Test
group by convert(integer, (year * 10)) / 25

IMHO for complex operations you should use .NET method from assembly registered in SQL server.
Since SQL 2005 you can register .NET assembly and call its method from SQL server.
Managed code in SQL

Related

DateDiff for each month

My current query give my this as a result;
Address PK StartDate EndDate Rent Cost NoDays
1 water lane 3435 01/04/2018 12/02/2020 500 11210.95 682
7 get road 5456 14/06/2019 01/02/2020 700 5339.18 232
I want to outline how many days per month/ or how much per month spent.
this is what i want to see after NoDays or even on a new query result.
04/2018 05/2018 06/2018 07/2018 so on ....
30 31 30 31 so on ....
0 0 0 0 so on ....
or
04/2018 05/2018 06/2018 07/2018 so on ....
500 500 500 500 so on ....
0 0 0 0 so on ....
Here a solution which generates a calendar using recursion
then adding columns and updating them using cursor and dynamic script
set dateformat dmy
declare #table as table(pk int, startdate date,enddate date,rent int,cost float)
insert into #table values(3435,'01/04/2018','12/02/2020',500,11210.95),(5456,'14/06/2019','01/02/2020',700,5339.18)
declare #table2 as table(pk int)
insert into #table2 select distinct(pk) from #table
declare #calendar as table (date date)
declare #mindate as date
declare #maxdate as date
select #mindate=min(startdate) from #table
select #maxdate=max(enddate) from #table;
with cte as(select #mindate as mydate union all select dateadd(day,1,mydate) from cte
where mydate < #maxdate)
insert into #calendar select * from cte
option(maxrecursion 0);
declare #tabresultsrows as table(pk int,MO varchar(7),N int,M int,Y int);
declare #tabmonths as table(Mo varchar(7),M int,Y int);
with cal as(
select t2.pk,c.date ,t.startdate,t.enddate ,month(date) M, year(date) y ,concat(RIGHT('00' + CONVERT(NVARCHAR(2), month(date)), 2),'/', year(date)) Mo,
case when c.date >= t.startdate and c.date <=t.enddate then 1 else 0 end N from #calendar c
cross join #table2 t2
inner join #table t on t2.pk=t.pk),
caltab as(select pk,Mo,sum(N) N ,Y,M from cal group by pk,Y,M,Mo )
insert into #tabresultsrows select pk,MO,N,M,Y from caltab order by pk,Y,M
insert into #tabmonths select distinct(MO),M,Y from #tabresultsrows
IF OBJECT_ID('tempdb..#tabresultscolumns') IS NOT NULL DROP TABLE #tabresultscolumns
select * into #tabresultscolumns from #table
declare #script as varchar(max)
declare mycursor cursor for select mo from #tabmonths order by Y,M
declare #mo as varchar(7)
open mycursor
fetch mycursor into #mo
while ##fetch_status=0
begin
set #script='alter table #tabresultscolumns add ['+#mo+'] int'
print #script
exec(#script)
fetch mycursor into #mo
end
close mycursor
deallocate mycursor
declare secondcursor cursor for select pk,Mo,N from #tabresultsrows
declare #PK AS INT
declare #n as int
open secondcursor
fetch secondcursor into #pk,#mo,#n
while ##fetch_status=0
begin
set #script=concat('update #tabresultscolumns set ['+#mo+']=',#n,' where pk=',#pk )
print #script
exec(#script)
fetch secondcursor into #pk,#mo,#n
end
close secondcursor
deallocate secondcursor
select * from #tabresultscolumns
Try something like this:
-- You can set the variables to get as an input the StartDate, EndDate from your table
DECLARE #StartDate DATE = '20200101'
, #EndDate DATE = '20200331'
;with datecreator as (
SELECT DATEADD(DAY, nbr - 1, #StartDate) as dates
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.PK ) AS Nbr
FROM Test c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
)
,CTE AS
(
select distinct Month(dates) rnk,Convert(char(3), dates, 0) MM from datecreator
)
,CTE3 AS
(
SELECT T.* , rnk
FROM Test T INNER JOIN CTE c ON C.Mm = T.Months
)
,CTE4 AS
(
SELECT Years,[1] Jan ,[2] Feb ,[3] Mar FROM CTE3
PIVOT
(SUM(Rent) FOR rnk IN ([1],[2],[3])) p
)
SELECT Years , SUM(Jan) Jan , SUM(Feb) Feb , SUM(Mar) Mar FROM CTE4 GROUP BY Years
You'll get the result :
Year1 Jan Feb Mar
2013 3000 3000 3000
2014 3500 3500 3500
As you can see above I used only the first Quarter (First 3 Months) but you can use the whole year.
Please let me know if you have any questions/Feedbacks :)

SQL sum a particular row based on condition?

I am using MS SQL Server. This is the table I have:
Create table tblVal
(
Id int identity(1,1),
Val NVARCHAR(100),
startdate datetime,
enddate datetime
)
--Inserting Records--
Insert into tblVal values(500,'20180907','20191212')
Insert into tblVal values(720,'20190407','20191212')
Insert into tblVal values(539,'20190708','20201212')
Insert into tblVal values(341,'20190221','20190712')
Table as this:
Id |Val |startdate |enddate
--- ----------------------------------------------
1 |500 |2018-09-07 |2019-12-12
2 |720 |2019-04-07 |2019-12-12
3 |539 |2019-07-08 |2020-12-12
4 |341 |2019-02-21 |2019-07-12
This is what I want:
Mon | Total
------------------
Jan | 500
Feb | 841
March | 841
April | 1561
May | 1561
June | 1561
July | 2100
........|.........
I want to sum Val column if it lies in that particular month. For ex. in case of April month it lies between two of the rows. I have to check both the condition start date and end date. and then sum the values.
This is what I have tried:
select *
into #ControlTable
from dbo.tblVal
DECLARE #cnt INT = 0;
while #cnt<12
begin
select sum(CASE
WHEN MONTH(startdate) BETWEEN #cnt and MONTH(enddate) THEN 0
ELSE 0
END)
from #ControlTable
SET #cnt = #cnt + 1;
end
drop table #ControlTable
but from above I was unable to achieve the result.
How do I solve this? Thanks.
I believe you want something like this:
with dates as (
select min(datefromparts(year(startdate), month(startdate), 1)) as dte,
max(datefromparts(year(enddate), month(enddate), 1)) as enddte
from tblVal
union all
select dateadd(month, 1, dte), enddte
from dates
where dte < enddte
)
select d.dte, sum(val)
from dates d left join
tblval t
on t.startdate <= eomonth(dte) and
t.enddate >= dte
group by d.dte
order by d.dte;
This does the calculation for all months in the data.
The results are a bit different from your sample results, but seem more consistent with the data provided.
Here is a db<>fiddle.
Hi if i understand your wall query i think this query can respond :
Create table #tblVal
(
Id int identity(1,1),
Val NVARCHAR(100),
startdate datetime,
enddate datetime
)
--Inserting Records--
Insert into #tblVal values(500,'20180907','20191212')
Insert into #tblVal values(720,'20190407','20191212')
Insert into #tblVal values(539,'20190708','20201212')
Insert into #tblVal values(341,'20190221','20190712')
Create table #tblMonth ( iMonth int)
Insert into #tblMonth values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12);
select * from #tblVal
select * from #tblMonth
SELECT *, SUM(case when Val is null then 0 else cast (Val as int) end) OVER(ORDER BY iMonth
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as 'Totaltime'
FROM #tblMonth
LEFT JOIN #tblVal ON MONTH(startdate) = iMonth
ORDER BY iMonth
drop table #tblVal
drop table #tblMonth
Not you have to use SQL Server version 2008 min for use OVER(ORDER BY iMonth
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
Link :
https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql?view=sql-server-2017
If you have older version you can use CTE or JOIN ON select .
DECLARE #outpuTable Table(
MOn INT,
Total nvarchar(MAX)
)
DECLARE #cnt INT = 1;
while (#cnt<=12)
begin
INSERT INTo #outpuTable VALUES(#cnt,
(select ISNULL(sum(CONVERT(INT,Val)),0)
from tblVal
WHERE #cnt BETWEEN MONTH(startdate) and MONTH(enddate) ))
SET #cnt = #cnt + 1;
end
Select * from #outpuTable

SQL Server 2008

Sorry for my English...
I Iave a table with column
project, month, year
abc 2 2017
xyz 5 2017
abc 3 2017
abc 5 2017
abc 1 2018
How can I search project abc with month = 2 year = 2017 until month = 1 year = 2018
As far as I know, SQL Server 2008 cannot use concat function
Use math comparison:
SELECT * FROM table1
WHERE (year * 12 + month) BETWEEN (2017 * 12 + 1) AND (2018 * 12 + 1)
Try this:
Select *
From YourTable
Where DATETIMEFROMPARTS(year, month, 1, 1, 1, 1, 1)
between '2017-02-01' And '2018-01-01'
I have edited the code to account for the leading zero in the month.
Declare #temp Table
(
project varchar(50),
month int,
year int
);
Insert Into #temp
(project, month, year)
Values ('abc', 2, 2017)
Insert Into #temp
(project, month, year)
Values ('xyz', 5, 2017)
Insert Into #temp
(project, month, year)
Values ('abc', 3, 2017)
Insert Into #temp
(project, month, year)
Values ('abc', 5, 2017)
Insert Into #temp
(project, month, year)
Values ('abc', 1, 2018)
Insert Into #temp
(project, month, year)
Values ('xxx', 5, 2010)
Insert Into #temp
(project, month, year)
Values ('xxx', 12, 2018)
Declare #FromYear int = 2010;
Declare #FromMonth int = 04;
Declare #ToYear int = 2018;
Declare #ToMonth int = 05;
Select *
From #temp
Where Convert(varchar, year) + right('00' + Convert(varchar, month), 2) Between '201004' and '201805'
How can I search project abc with month = 2 year = 2017 until month = 1 year = 2018
You can use
SELECT *
FROM T
WHERE (([Year] * 10) + [Month]) BETWEEN 20172 AND 20181
AND
project = 'abc';
Demo

Count active items/year without a temporary year table

With the following record structure:
<item> | <start> | <stop>
Item-A | 2013-04-05 | 2014-06-07
Item-B | 2012-06-07 | 2015-03-07
Is it possible to query using SQL (in MS-SQL >=2008 and Firebird >= 2.5) how many items are active per year? The result should be:
2012 | 1
2013 | 2
2014 | 2
2015 | 1
I've used a temorary table containing series (1900..2100) and join the origin table and temporary table using BETWEEN and extract(year..). But I'm searching for a better solution without using an extra table.
This meets your requirement.
select Years.[Year],
count(1) [Count]
from MyTable mt
join (select distinct(year(start)) as [Year]
from MyTable
union
select distinct(year(stop))
from MyTable
union
select year(getdate())
from MyTable
where exists
(select 1
from MyTable
where stop is null)) as Years
on Years.[Year] between Year(mt.start) and Year(isnull(mt.stop, getdate()))
group by Years.[Year]
order by Years.[Year]
This is how I would do it using a helper function.
declare #StartMin datetime
declare #StopMax datetime
select #StartMin = min(start),
#StopMax = max(isnull(stop, getdate()))
from MyTable
select Years.[Year],
count(1) [Count]
from MyTable mt
join [dbo].[YearsBetween](#StartMin, #StopMax) as Years
on Years.[Year] between Year(mt.start) and Year(isnull(mt.stop, getdate()))
group by Years.[Year]
order by Years.[Year]
This is the helper function, I have a lot of them, DatesBetween, MonthsBetween, NumbersBetween, etc.
create function [dbo].[YearsBetween](#Start datetime, #Stop datetime)
returns #Years TABLE
(
[Year] int
)
begin
declare #StartYear int
declare #StopYear int
set #StartYear = year(#Start)
set #StopYear = year(#Stop)
while(#StartYear <= #StopYear)
begin
insert into #Years
values(#StartYear)
set #StartYear = #StartYear + 1
end
return;
end

Count total students actively studying during date range

I am looking to get one output showing the total count of students actively studying in each year in a user defined range.
for eg DB Structure
StudentId Course StartDate EndDate
1 BSc Maths 2012-01-01 2015-01-01
2 BSc English 2014-01-01 2017-01-01
If the user defines actively studying between '2013' and '2016' the output i would like to get is this;
YEAR Student_Count
2013 1
2014 2
2015 2
2016 1
Thanks for your time :)
You need to have a years table to do this.
Here am generating years table through Recursive CTE. You can create a physical years table and use it instead of recursive CTE.
DECLARE #max_yr INT = (SELECT Max(Year(EndDate)) yr FROM yourtable);
WITH cte
AS (SELECT Min(Year(StartDate)) yr
FROM yourtable
UNION ALL
SELECT yr + 1
FROM cte
WHERE yr < #max_yr)
SELECT a.yr as [YEAR],
Count(1) as [Student_Count]
FROM cte a
JOIN yourtable b
ON a.yr BETWEEN Year(b.StartDate) AND Year(b.EndDate)
AND a.yr BETWEEN 2013 AND 2016
GROUP BY a.yr
You could try this, it certainly worked for me - obviously #startYear and #endYear would be replaced by your own variables but I've left them in so you understand how the code is working:
Create Table #Temp2 ([YEAR] int, Student_Count int)
Declare #startYear int = '2013',
#endYear int = '2016'
while (#startYear <= #endYear)
Begin
Insert into #Temp2
Select #startYear [YEAR], count(studentId)
from table
where Cast(Cast(#startYear as varchar) as date) between StartDate and EndDate
Set #startYear = #startYear + 1
End
Select * from #Temp2