SQL sum a particular row based on condition? - sql

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

Related

SQL - Selecting rows with dates before and after column value change

I have a table called test.
In test I have An ID, a value and a date.
The dates are ordered for each ID.
I want to select rows for an ID, before and after a change of value, so the following example table.
RowNum--------ID------- Value -------- Date
1------------------001 ---------1----------- 01/01/2015
2------------------001 ---------1----------- 02/01/2015
3------------------001 ---------1----------- 04/01/2015
4------------------001 ---------1----------- 05/01/2015
5------------------001 ---------1----------- 06/01/2015
6------------------001 ---------1----------- 08/01/2015
7------------------001 ---------0----------- 09/01/2015
8------------------001 ---------0----------- 10/01/2015
9------------------001 ---------0----------- 11/01/2015
10-----------------001 ---------1----------- 12/01/2015
11-----------------001 ---------1----------- 14/01/2015
12------------------002 ---------1----------- 01/01/2015
13------------------002 ---------1----------- 04/01/2015
14------------------002 ---------0----------- 05/01/2015
15------------------002 ---------0----------- 07/01/2015
The result would return rows 6, 7, 9, 10, 13, 14
You could use analytic functions LAG() and LEAD() to access value in preceding and following rows, then check that it does not match value in current row.
SELECT *
FROM (
SELECT RowNum,
ID,
Value,
Date,
LAG(VALUE, 1, VALUE) OVER(ORDER BY RowNum) PrevValue,
LEAD(VALUE, 1, VALUE) OVER(ORDER BY RowNum) NextValue
FROM test)
WHERE PrevValue <> Value
OR NextValue <> Value
Params passed to this functions are
some scalar expression (column name in this case);
offset (1 row before or after);
default value (LAG() will return NULL for first row and LEAD() will return NULL for last row, but they don't seem special in your question, so I used column value as default).
Refer the below one for without using LEAD and LAG:
DECLARE #i INT = 1,
#cnt INT,
#dstvalue INT,
#srcvalue INT
CREATE TABLE #result
(
id INT,
mydate DATE
)
CREATE TABLE #temp1
(
rn INT IDENTITY(1, 1),
id INT,
mydate DATE
)
INSERT INTO #temp1
(id,
mydate)
SELECT id,
mydate
FROM table
ORDER BY id,
mydate
SELECT #cnt = Count(*)
FROM #temp1
SELECT #srcvalue = value
FROM #temp1
WHERE rn = #i
WHILE ( #i <= #cnt )
BEGIN
SELECT #dstvalue = value
FROM #temp1
WHERE rn = #i
IF( #srcvalue = #dstvalue )
BEGIN
SET #i = #i + 1
CONTINUE;
END
ELSE
BEGIN
SET #srcvalue = #dstvalue
INSERT INTO #result
(id,
mydate)
SELECT id,
mydate
FROM #temp
WHERE rn = #i - 1
UNION ALL
SELECT id,
mydate
FROM #temp
WHERE rn = #i
END
SET #i = #i + 1
END
SELECT *
FROM #result
The answer using lag() and lead() is the right answer. If you are using a pre-SQL Server 2012 version, then you can do essentially the same thing using cross apply or a correlated subquery:
select t.*
from test t cross apply
(select top 1 tprev.*
from test tprev
where tprev.date < t.date
order by date desc
) tprev cross apply
(select top 1 tnext.*
from test tnext
where tnext.date > t.date
order by date asc
) tnext
where tprev.value <> tnext.value;

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

Convert Procedural Approach into Set Based Approach in Sql-Server

We are using procedural approach (while loop) for inserting records into a particular table. the insert syntax is like below,
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WHILE (#CNT <7)
BEGIN
SET #WEEK = DATEADD(D,#CNT,#WEEK )
IF EXISTS
(SELECT 1
FROM YEARRANGE
WHERE #WEEK BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
)
BEGIN
SET #FLAG =1
END
ELSE
BEGIN
SET #FLAG = 0
END
INSERT INTO #Tmpdata
(
officeId,id,weekDate,startsOn,endsOn,flag
)
VALUES
(
5134,#lvCounter,#week,'09:00 AM','05:00 PM',#flag
);
SET #cnt=#cnt+1;
end
(NOTE : TaxSeason is from january to august).
Is it possible to re-write the above logic in set based approach?
This is making a number of assumption because you didn't post ddl or any consumable sample data. Also, there is a variable #lvCounter not defined in your code. This is perfect opportunity to use a tally or numbers table instead of a loop.
declare #lvCounter int = 42;
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n))
, cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E1
)
select 5134 as officeId
, #lvCounter as Id
, DATEADD(DAY, N - 1, #WEEK) as weekDate
, '09:00 AM' as startsOn
, '05:00 PM' as EndOn
, Flag
from cteTally t
cross apply
(
select CAST(count(*) as bit) as Flag
from YearRange
where DATEADD(Day, t.N, #WEEK) > CONVERT(DATE,taxseasonbegin)
AND DATEADD(Day, t.N, #WEEK) <= CONVERT (DATE,taxSeasonEnd)
) y
where t.N <= 7;
Please can you provide sample data?
You can do something like:
SELECT DateIncrement = SUM(DATEADD(D,#CNT,#WEEK)) OVER (ORDER BY officeID)
FROM...
This gets an incremented date value for each record which you can then check against your start and end dates.
You could try some Kind of this one. This gives you the data I think you Need for your insert. I do not have a table named YEARRANGE so I couldn't test it completely
DECLARE #CNT INT = 0, #WEEK DATE = '2015-11-01', #FLAG INT;
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WITH CTE AS
(
SELECT num AS cnt,
DATEADD(D, SUM(num) OVER(ORDER BY num ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
, #WEEK) AS [week]
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY nl) -1 AS num
FROM
(SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
) AS ni
) AS no
)
INSERT INTO #Tmpdata (officeId,id,weekDate,startsOn,endsOn,flag)
SELECT 5134 AS officeID, cnt AS id, [week],'09:00 AM' AS startsOn,'05:00 PM' AS endsOn, COALESCE(A1.flag,0) AS flag
FROM CTE
OUTER APPLY (SELECT 1
FROM YEARRANGE
WHERE [week] BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
) AS A1(flag);

get best sales rep weekly SQL

I need a bit of help with a SQL Server issue.
I have 2 tables:
complete_sales_raw
(
Id int Identity(1,1) PK,
RepId int FK in sale_reps,
Revenue decimal(15,2),
Sale_date datetime2(7)
)
and
sale_reps
(
Id int Identity(1,1) PK,
RepName nvarchar(50)
)
What I need to do is get best sales rep based on the total revenue for each week, starting with 2014-06-01 and ending at current date.
Each week has 7 days and the first day is 2014-06-01.
So far I got to here:
SELECT TOP(1)
sr.RepName as RepName,
SUM(csr.Revenue) as Revenue
INTO #tmp1
FROM complete_sales_raw csr
JOIN sale_reps sr on csr.RepId = sr.Id
WHERE DATEDIFF( d,'2014-06-01', Sale_date ) BETWEEN 0 and 6
GROUP BY sr.RepName
ORDER BY 2 desc
But this only returns the best sale rep for the first week and I need it for each week.
All help is appreciated.
ok so, I created a week table like so
IF ( OBJECT_ID('dbo.tmp4') IS NOT NULL )
DROP TABLE dbo.tmp4
GO
Create Table tmp4(
StartDate datetime,Enddate datetime,WeekNo varchar(20)
)
DECLARE
#start_date DATETIME,
#end_date DATETIME,
#start_date1 DATETIME,
#end_date1 DATETIME
DECLARE #Table table(StartDate datetime,Enddate datetime,WeekNo varchar(20))
Declare #WeekDt as varchar(10)
SET #start_date = '2014-06-01'
SET #end_date = '2015-01-03'
Set #WeekDt = DATEPART(WEEK,#start_date)
SET #start_date1 = #start_date
While #start_date<=#end_date
Begin
--Select #start_date,#start_date+1
IF #WeekDt<>DATEPART(WEEK,#start_date)
BEGIN
Set #WeekDt = DATEPART(WEEK,#start_date)
SET #end_date1=#start_date-1
INSERT INTO tmp4 Values(#start_date1,#end_date1,DATEPART(WEEK,#start_date1))
SET #start_date1 = #start_date
END
set #start_date = #start_date+1
END
GO
and then I used Gordon's answer and made this:
SELECT t.StartDate as StartDate, sr.RepName as RepName, SUM(csr.Revenue) as Revenue,
RANK() OVER (PARTITION BY (t.StartDate) ORDER BY SUM(csr.Revenue) desc) as seqnum into tmp1
FROM tmp4 t,
complete_sales_raw csr
JOIN sale_reps sr on csr.RepId = sr.Id
WHERE DATEDIFF( d,t.StartDate, MAS_PostDate ) BETWEEN 0 and 6
GROUP BY sr.RepName, t.StartDate
SELECT * FROM tmp1
WHERE seqnum = 1
ORDER BY StartDate
which returns the best sales_rep for each week
You can do an aggregation to get the total sales by week. This requires some manipulation of the dates to calculate the number of weeks -- basically dividing the days by 7.
Then, use rank() (or row_number() if you only want one when there are ties) to get the top value:
SELECT s.*
FROM (SELECT tsr.RepName as RepName,
(DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7 as weeknum,
SUM(csr.Revenue) as Revenue,
RANK() OVER (PARTITION BY (DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7 ORDER BY SUM(csr.Revenue)) as seqnum
FROM complete_sales_raw csr JOIN
sale_reps sr
on csr.RepId = sr.Id
WHERE DATEDIFF(day, '2014-06-01', MAS_PostDate ) BETWEEN 0 and 6
GROUP BY sr.RepName, (DATEDIFF(day, '2014-06-01', MAS_PostDate ) - 1) / 7
) s
WHERE seqnum = 1;

Summing and grouping the number of records in a month

I have the following table which has employees' absence:
RecordId EmpID ActivityCode DateFrom DateTo
---------------------------------------------------------------
666542 1511 AB 29/01/2011 02/02/2011
666986 1511 AB 11/11/2011 11/11/2011
666996 1511 EL 13/11/2011 17/11/2011
755485 1787 SL 01/11/2011 14/11/2011
758545 1787 SL 15/11/2011 03/12/2011
796956 1954 AB 09/11/2011 09/11/2011
799656 1367 AB 09/11/2011 09/11/2011
808845 1527 EL 16/11/2011 16/11/2011
823323 1527 EL 17/11/2011 17/11/2011
823669 1527 EL 18/11/2011 18/11/2011
899555 1123 AB 09/11/2011 09/12/2011
990990 1511 AB 12/11/2011 12/11/2011
Now I want a report generated by a stored proc to sum all the absence days for a specific absence code for each month in a given year, for example If i want to know the totals of absence in 2011 from the previous table I will get something similar to:
Month TotalDays
---------------------------------
JAN 2011 201
FEB 2011 36
MAR 2011 67
APR 2011 91
....
The stored proc will have two params (#Year INT, #AbsCode NVARCHAR(3)).
Please Note, Sometimes a record overlaps another month (like the first row in the example table) and that should be counted separately for each month. I have tried using loops but with no luck. I am so weak in TSQL.
UPDATE
Right now I am using a scalar value user function and a stored procedure to do the job, Its ugly and hard to trace. Here it is any way:
The User function:
ALTER FUNCTION [dbo].[GetActivityTotalDaysInMonth]
(
#ActivityCode CHAR(3)
,#Year INT
,#Month INT
)
RETURNS INT
AS
BEGIN
DECLARE #FirstDayOfMonth DATETIME
DECLARE #LastDayOfMonth DATETIME
SET #FirstDayOfMonth = CAST(CAST(#Year AS varchar) + '-' + CAST(#Month AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
SET #LastDayOfMonth = DATEADD(s, -1, DATEADD(M, 1, #FirstDayOfMonth))
DECLARE #TotalDays INT
SELECT #TotalDays =
SUM(DATEDIFF(DAY,
(CASE WHEN ActivityDateFrom < #FirstDayOfMonth THEN #FirstDayOfMonth ELSE ActivityDateFrom END)
, (CASE WHEN ActivityDateTo > #LastDayOfMonth THEN #LastDayOfMonth ELSE ActivityDateTo END))+1)
FROM Activities
WHERE
ActivityCode=#ActivityCode
AND ((ActivityDateFrom < #FirstDayOfMonth AND ActivityDateTo >= #FirstDayOfMonth)
OR (ActivityDateFrom >= #FirstDayOfMonth AND ActivityDateTo <= #LastDayOfMonth)
OR (ActivityDateFrom <= #LastDayOfMonth AND ActivityDateTo > #LastDayOfMonth))
RETURN #TotalDays
END
Now, I call this function inside a loop in a stored procedure:
ALTER PROCEDURE GetAnnualActivityTotalDays
(
#ActivityCode CHAR(3)
,#Year INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Stats TABLE
([Month] NVARCHAR(50), TotalDays INT)
DECLARE #MonthNo INT
DECLARE #Month DATETIME
SET #MonthNo = 1
WHILE #MonthNo <= 12
BEGIN
SET #Month = CAST(CAST(#Year AS varchar) + '-' + CAST(#MonthNo AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
INSERT INTO #Stats ([Month], TotalDays)
SELECT UPPER(SUBSTRING(DATENAME(mm, #Month), 1, 3)) + ', ' + CAST(#Year AS NVARCHAR),
dbo.GetActivityTotalDaysInMonth(#ActivityCode
,#Year
,#MonthNo
,#Base)
SET #MonthNo = #MonthNo + 1
END
SELECT * FROM #Stats
END
As you can see, this is ugly code which I believe it can be done in an easier way.. Any suggestions?
You'd need to create a calendar table which will allow you to easily count the days for each month that the start and end dates encompass. For example recordid = 666542 has 3 days in January and 2 days in February. You'd be able to get that number by a query like
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
If you wrap that in a common table expression you can perform aggregation queries afterwards on the CTE.
with mycte as (
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
)
select calyear, calmonth, count(caldate)
from mycte
group by calyear, calmonth
order by calyear, calmonth
To generate the calendar table you can use code similar to
create table calendar (calyear, calmonth, caldate)
declare #numdays int --number of days to generate in the calendar
declare #datestart datetime --the date to begin from in the calendar
set #numdays = 365
set #datestart = 'jan 1 2011';
with num as (
select 0 number
union
select 1 number
union
select 2 number
union
select 3 number
union
select 4 number
union
select 5 number
union
select 6 number
union
select 7 number
union
select 8 number
union
select 9 number
),
numberlist as (
select ((hundreds.number * 100) + (tens.number * 10) + ones.number) n
from num hundreds
cross join num tens
cross join num ones
where ((hundreds.number * 100) + (tens.number * 10) + ones.number) < #numdays
)
insert into calendar (calyear, calmonth, caldate)
select
datepart(yy,dateadd(dd,n,#datestart)) calyear,
datepart(mm,dateadd(dd,n,#datestart)) calmonth,
dateadd(dd,n,#datestart)caldate
from numberlist