In SQL I've got a table with students:
CREATE TABLE dbo.[Student]
(
[Id] bigint IDENTITY(1,1) NOT NULL CONSTRAINT [PK_Student] PRIMARY KEY NONCLUSTERED,
[ActiveFrom] [DATETIME] NOT NULL,
[ActiveUntil] [DATETIME] NULL,
) ON [PRIMARY]
Now I want to show a bar chart how many students have been active in each month of the year. A student is active in a month if [ActiveFrom] is before or in that month and [ActiveUntil] is null or later or in that month.
I guess I need some kind of group by, but since a student can be active for months or years I got no idea how to get those numbers in one SQL command.
Sample input
INSERT INTO Student (ActiveFrom, ActiveUntil) VALUES ('20181001', '20181231')
INSERT INTO Student (ActiveFrom, ActiveUntil) VALUES ('20181101', '20190131')
INSERT INTO Student (ActiveFrom, ActiveUntil) VALUES ('20181201', '20181231')
INSERT INTO Student (ActiveFrom, ActiveUntil) VALUES ('20190101', '20190430')
Expected output
Month, Activecount
2018-10, 1
2018-11, 2
2018-12, 3
2019-01, 2
2019-02, 1
2019-03, 1
2019-04, 1
DECLARE #ReportStartDate DATE = '20180101'
, #ReportEndDate DATE = '20191231'
; WITH MonthCounter AS
(
SELECT 1 i
UNION ALL
SELECT i+1 i
FROM MonthCounter
WHERE i < DATEDIFF(MONTH, #ReportStartDate, #ReportEndDate)
)
, Months AS
(
SELECT DATEADD(MONTH, i-1, #ReportStartDate) AS StartDate
, DATEADD(DAY, -1, DATEADD(MONTH, i, #ReportStartDate)) AS EndDate
FROM MonthCounter
)
SELECT mo.StartDate
, mo.EndDate
, COUNT(st.[Key]) AS ActiveStudents
FROM Months mo
LEFT JOIN Student st ON DATEDIFF(DAY, st.ActiveFrom, mo.enddate) >= 0
AND (st.ActiveUntil IS NULL OR DATEDIFF(DAY, mo.startdate, st.ActiveUntil) >= 0)
GROUP BY mo.startdate
, mo.enddate
ORDER BY mo.startdate
OPTION (MAXRECURSION 0)
Apologies for the convoluted month generating code, but I really tried to make it happen in a single SELECT query, and I couldn't find a much better method than the recursive CTE.
Pay attention to the comparison. To determine whether a student is active in a month, the record's ActiveFrom must start sometime BEFORE the END of the month, and it's ActiveTo must be some date on or AFTER the BEGINNING of the month.
Related
I have a table in which I am storing the Product Name and it's Renewal Date along with the payment plan (Monthly/Quarterly/Yearly). Now if the payment plan of the product is Yearly or Monthly it will display the get the month of Renewal and show the Rate/Amount against that month but if the payment plan is Monthly it should display the Rate/Amount in front of each month as shown below.
For example if a product named ABC has payment plan of Yearly, subscription rate of 276 and Renewal Date 2019-12-01 and there is another product XYZ with payment plan of Monthly, subscription rate of 17 and Renewal Date 2019-08-15 then the result set I want should something like this
ProductName RenewalMonth Rate
------------------------------------
ABC December 276
XYZ January 17
XYZ February 17
XYZ March 17
XYZ April 17
XYZ May 17
XYZ June 17
XYZ July 17
XYZ August 17
XYZ September 17
XYZ October 17
XYZ November 17
XYZ December 17
Here is the query which I have wrote which is returning data that's present in the database fine but not the months other than December for Product XYZ. Keeping in mind this should only display same rate for all other months provided in the desired dates where Payment Plan is 'Monthly', for other payment plans it should show rate in front of given month as present in the database.
Sample data is as follows:
CREATE TABLE ItemDefinition
(
ID INT NOT NULL,
ProductName VARCHAR(50),
PaymentPlan VARCHAR(50),
RenewalDate DATETIME,
UnitRate NUMERIC(18, 0)
);
INSERT INTO ItemDefinition
VALUES (1, 'ABC', 'Yearly', '2019-12-01', 276),
(1, 'XYZ', 'Monthly', '2019-08-15', 17);
And the query I am writing is
SELECT
ProductName, SUM(UnitRate) Rate,
DATENAME(MONTH , DATEADD(MONTH , MONTH(RenewalDate)-1 , '1900-01-01')) RenewalMonth
FROM
ItemDefinition
WHERE
MONTH(RenewalDate) IS NOT NULL
AND RenewalDate BETWEEN #dtStart AND #dtEnd
GROUP BY
ProductName, MONTH(RenewalDate)
ORDER BY
MONTH(RenewalDate)
It might be something like this:
DECLARE #DateBeg DATE = '2019-01-01'
,#DateEnd DAte = '2020-12-01';
WITH Ranges AS
(
SELECT *
,#DateBeg AS [DateBeg]
,#DateEnd AS [DateEnd]
FROM ItemDefinition DS
)
SELECT *
,DATENAME(MONTH ,ISNULL([GeneratedDate], [RenewalDate])) AS RenewalMonth
FROM Ranges
OUTER APPLY
(
SELECT DATEADD(MONTH, [number], [DateBeg])
FROM
(
select number
from master.dbo.spt_values
where [type] = 'P'
) numbers
WHERE DATEADD(MONTH, [number], [DateBeg]) < [DateEnd]
AND [PaymentPlan] = 'Monthly'
) AutoDates ([GeneratedDate]);
You can change the DateEnd parameter to something less and you will see how less months are generated.
The idea is to have start and end date for each row and depending on it to generate your months.
To get the records for the years use the following:
WITH Ranges AS
(
SELECT *
,#DateBeg AS [DateBeg]
,#DateEnd AS [DateEnd]
FROM ItemDefinition DS
)
SELECT *
,DATENAME(MONTH ,ISNULL([GeneratedDate], [RenewalDate])) AS RenewalMonth
,IIF([PaymentPlan] = 'Monthly', [UnitRate], IIF(CONVERT(VARCHAR(7), [RenewalDate], 121) = CONVERT(VARCHAR(7), [GeneratedDate], 121), [UnitRate], NULL))
FROM Ranges
OUTER APPLY
(
SELECT DATEADD(MONTH, [number], [DateBeg])
FROM
(
select number
from master.dbo.spt_values
where [type] = 'P'
) numbers
WHERE DATEADD(MONTH, [number], [DateBeg]) < [DateEnd]
) AutoDates ([GeneratedDate]);
or the following to get the year rate for the first record:
DECLARE #DateBeg DATE = '2019-01-01'
,#DateEnd DAte = '2020-12-01';
WITH Ranges AS
(
SELECT *
,#DateBeg AS [DateBeg]
,#DateEnd AS [DateEnd]
FROM ItemDefinition DS
)
SELECT *
,DATENAME(MONTH ,ISNULL([GeneratedDate], [RenewalDate])) AS RenewalMonth
,IIF([PaymentPlan] = 'Monthly', [UnitRate], IIF([number] = 0, [UnitRate], NULL))
FROM Ranges
OUTER APPLY
(
SELECT DATEADD(MONTH, [number], [DateBeg])
,[number]
FROM
(
select number
from master.dbo.spt_values
where [type] = 'P'
) numbers
WHERE DATEADD(MONTH, [number], [DateBeg]) < [DateEnd]
) AutoDates ([GeneratedDate], [number]);
My advice is to introduce an additional table that will have a single record for a Yearly plan and 12 records for Monthly plan. For example:
create table PaymentPlanInterval(
PaymentPlan VARCHAR(50),
Interval varchar(50)
)
And perhaps this table may contain 2 records for Semi-annual payment plan and 4 records for quartely plan.
In order to get your desired result you should be joining your ItemDefinition table with PaymentPlanInterval. Voila.
basically trying to create a query that will display employee anniversary dates for upcoming month or year of current date, also would like to display a column that shows the years of service
SELECT
Employee,
Hire_Date
CASE WHEN DATEADD(YY,DATEDIFF(yy,Hire_Date,GETDATE()),Hire_Date)<GETDATE() THEN DATEDIFF(yy,Hire_Date,GETDATE())
ELSE DATEDIFF(yy,Hire_Date,GETDATE())-1 END AS 'Years of service'
FROM
MyTable
looking to display employees with anniversary dates coming up in the coming month or in the next year
Here is the script validated (see pciture below) to display the employees with birth_date coming in the next month
Replace mytable by your own table
declare #mytable as table(employe varchar(100), birth_date datetime,hire_date datetime)
insert into #mytable values
('name1','01/01/1972','01/01/2000') ,
('name2','12/02/1985','01/02/2003') ,
('name3','04/12/1990','03/04/2005') ,
('name4','05/03/1969','12/12/2005') ,
('name5','04/02/1968','12/02/2010') ,
('name6','04/04/1968','12/11/2009') ,
('name7','12/03/1978','01/01/2019') ,
('name8','01/12/2000','03/02/2018') ,
('name9','12/12/1970','05/02/2019') ,
('name10','04/04/1980','04/04/2018')
select employe,birth_date,hire_date,
CASE WHEN DATEADD(YY,DATEDIFF(yy,Hire_Date,GETDATE()),Hire_Date)<GETDATE() THEN DATEDIFF(yy,Hire_Date,GETDATE())
ELSE DATEDIFF(yy,Hire_Date,GETDATE())-1 END AS 'Years of service'
from #mytable where (
month(getdate()) < 12
and
month(birth_date)=1+month(getdate()) )
or (month(getdate())=12 and month(birth_date)=1)
I don't understand well, but, if you need know who make anniversary in the next month, or next year, you should use DATEDIFF function for to filter the data.
Example:
SELECT Employee, Hire_Date, DATEDIFF(year, Hire_Date, getdate()) as YearsService
FROM MyTable
-- if you need fetch to next month, you should use <= 1
WHERE DATEDIFF(month, CONCAT(YEAR(GETDATE()), '-', MONTH(Hire_Date), '-' , DAY(Hire_Date)), GETDATE()) = 1
I have a table with job schedules :
job_id [unique ID]
pref_start [date]
spec_duration [time in seconds]
I can calculate the end date from the preferred start and duration. The pref_start is not fixed, and can be changed at whim by the engineers.
I need to report activity in any given week, so if I have data similar to:
jid start end
J1 01/01/yyyy 15/02/yyyy
J2 07/01/yyyy 08/02/yyyy
J3 09/02/yyyy 21/03/yyyy
How would I query "tell me the job id's that occur on each day of the week 07/02/yyyy to 12/02/yyyy"
First find the matching intervals between your jobs and your filtering interval, then the amount of days for the filter interval and the overlapping intervals must match:
DECLARE #Jobs TABLE (
ID INT IDENTITY,
StartDate DATE,
EndDate DATE)
INSERT INTO #Jobs (
StartDate,
EndDate)
VALUES
('2019-01-01', '2019-02-15'),
('2019-01-07', '2019-02-08'),
('2019-02-09', '2019-03-21')
DECLARE #FilterStartDate DATE = '2019-02-07'
DECLARE #FilterEndDate DATE = '2019-02-12'
;WITH AtLeast1DayOverlappingJobs AS
(
SELECT
J.ID,
J.StartDate,
J.EndDate,
OverlappingStartDate = CASE
WHEN J.StartDate > #FilterStartDate THEN J.StartDate ELSE #FilterStartDate END, -- Highest of 2
OverlappingEndDate = CASE
WHEN J.EndDate < #FilterEndDate THEN J.EndDate ELSE #FilterEndDate END -- Lowest of 2
FROM
#Jobs AS J
WHERE
-- They share at least 1 day
#FilterStartDate <= J.EndDate AND #FilterEndDate >= J.StartDate
)
SELECT
T.*
FROM
AtLeast1DayOverlappingJobs AS T
WHERE
-- Amount of days must match between filter and overlapping periods
DATEDIFF(DAY, #FilterStartDate, #FilterEndDate) = DATEDIFF(DAY, T.OverlappingStartDate, T.OverlappingEndDate)
Results:
ID StartDate EndDate OverlappingStartDate OverlappingEndDate
1 2019-01-01 2019-02-15 2019-02-07 2019-02-12
I am editing this to clarify my question.
Let's say I have a table that holds patient information. I need to find new patients for this year, and the date of their prescription first prescription when they were considered new. Anytime there is a six month gap they are considered a new patient.
How do I accomplish this using SQL. I can do this in Java and any other imperative language easily enough, but I am having problems doing this in SQL. I need this script to be run in Crystal by non-SQL users
Table:
Patient ID Prescription Date
-----------------------------------------
1 12/31/16
1 03/13/17
2 10/10/16
2 05/11/17
2 06/11/17
3 01/01/17
3 04/20/17
4 01/31/16
4 01/01/17
4 07/02/17
So Patients 2 and 4 are considered new patients. Patient 4 is considered a new patient twice, so I need dates for each time patient 4 was considered new 1/1/17 and 7/2/17. Patients 1 and 3 are not considered new this year.
So far I have the code below which tells me if they are new this year, but not if they had another six month gap this year.
SELECT DISTINCT
this_year.patient_id
,this_year.date
FROM (SELECT
patient_id
,MIN(prescription_date) as date
FROM table
WHERE prescription_date BETWEEN '2017-01-01 00:00:00.000' AND '2017-
12-31 00:00:00.000'
GROUP BY [patient_id]) AS this_year
LEFT JOIN (SELECT
patient_id
,MAX(prescription_date) as date
FROM table
WHERE prescription_date BETWEEN '2016-01-01 00:00:00.000' AND '2016-
12-31 00:00:00.000'
GROUP BY [patient_id]) AS last_year
WHERE DATEDIFF(month, last_year.date, this_year.date) > 6
OR last_year.date IS NULL
Patient 2 in your example does not meet the criteria you specified ... that being said ...
You can try something like this ... untested but should be similar (assuming you can put this in a stored procedure):
WITH ordered AS
(
SELECT *, ROW_NUMBER() OVER (ORDER BY [Prescription Date]) rn
FROM table1
)
SELECT o1.[PatientID], DATEDIFF(s, o1.[Prescription Date], o2.[Prescription Date]) diff
FROM ordered o1 JOIN ordered o2
ON o1.rn + 1 = o2.rn
WHERE DATEDIFF(m, o1.[Prescription Date], o2.[Prescription Date]) > 6
Replace table1 with the name of your table.
I assume that you mean the patient has not been prescribed in the last 6 months.
SELECT DISTINCT user_id
FROM table_name
WHERE prescribed_date >= DATEADD(month, -6, GETDATE())
This gives you the list of users that have been prescribed in the last 6 months. You want the list of users that are not in this list.
SELECT DISTINCT user_id
FROM table_name
WHERE user_id NOT IN (SELECT DISTINCT user_id
FROM table_name
WHERE prescribed_date >= DATEADD(month, -6, GETDATE()))
You'll need to amend the field and table names.
I have a table in MSSQL with the following structure:
PersonId
StartDate
EndDate
I need to be able to show the number of distinct people in the table within a date range or at a given date.
As an example i need to show on a daily basis the totals per day, e.g. if we have 2 entries on the 1st June, 3 on the 2nd June and 1 on the 3rd June the system should show the following result:
1st June: 2
2nd June: 5
3rd June: 6
If however e.g. on of the entries on the 2nd June also has an end date that is 2nd June then the 3rd June result would show just 5.
Would someone be able to assist with this.
Thanks
UPDATE
This is what i have so far which seems to work. Is there a better solution though as my solution only gets me employed figures. I also need unemployed on another column - unemployed would mean either no entry in the table or date not between and no other entry as employed.
CREATE TABLE #Temp(CountTotal int NOT NULL, CountDate datetime NOT NULL);
DECLARE #StartDT DATETIME
SET #StartDT = '2015-01-01 00:00:00'
WHILE #StartDT < '2015-08-31 00:00:00'
BEGIN
INSERT INTO #Temp(CountTotal, CountDate)
SELECT COUNT(DISTINCT PERSON.Id) AS CountTotal, #StartDT AS CountDate FROM PERSON
INNER JOIN DATA_INPUT_CHANGE_LOG ON PERSON.DataInputTypeId = DATA_INPUT_CHANGE_LOG.DataInputTypeId AND PERSON.Id = DATA_INPUT_CHANGE_LOG.DataItemId
LEFT OUTER JOIN PERSON_EMPLOYMENT ON PERSON.Id = PERSON_EMPLOYMENT.PersonId
WHERE PERSON.Id > 0 AND DATA_INPUT_CHANGE_LOG.Hidden = '0' AND DATA_INPUT_CHANGE_LOG.Approved = '1'
AND ((PERSON_EMPLOYMENT.StartDate <= DATEADD(MONTH,1,#StartDT) AND PERSON_EMPLOYMENT.EndDate IS NULL)
OR (#StartDT BETWEEN PERSON_EMPLOYMENT.StartDate AND PERSON_EMPLOYMENT.EndDate) AND PERSON_EMPLOYMENT.EndDate IS NOT NULL)
SET #StartDT = DATEADD(MONTH,1,#StartDT)
END
select * from #Temp
drop TABLE #Temp
You can use the following query. The cte part is to generate a set of serial dates between the start date and end date.
DECLARE #ViewStartDate DATETIME
DECLARE #ViewEndDate DATETIME
SET #ViewStartDate = '2015-01-01 00:00:00.000';
SET #ViewEndDate = '2015-02-25 00:00:00.000';
;WITH Dates([Date])
AS
(
SELECT #ViewStartDate
UNION ALL
SELECT DATEADD(DAY, 1,Date)
FROM Dates
WHERE DATEADD(DAY, 1,Date) <= #ViewEndDate
)
SELECT [Date], COUNT(*)
FROM Dates
LEFT JOIN PersonData ON Dates.Date >= PersonData.StartDate
AND Dates.Date <= PersonData.EndDate
GROUP By [Date]
Replace the PersonData with your table name
If startdate and enddate columns can be null, then you need to add
addditional conditions to the join
It assumes one person has only one record in the same date range
You could do this by creating data where every start date is a +1 event and end date is -1 and then calculate a running total on top of that.
For example if your data is something like this
PersonId StartDate EndDate
1 20150101 20150201
2 20150102 20150115
3 20150101
You first create a data set that looks like this:
EventDate ChangeValue
20150101 +2
20150102 +1
20150115 -1
20150201 -1
And if you use running total, you'll get this:
EventDate Total
2015-01-01 2
2015-01-02 3
2015-01-15 2
2015-02-01 1
You can get it with something like this:
select
p.eventdate,
sum(p.changevalue) over (order by p.eventdate asc) as total
from
(
select startdate as eventdate, sum(1) as changevalue from personnel group by startdate
union all
select enddate, sum(-1) from personnel where enddate is not null group by enddate
) p
order by p.eventdate asc
Having window function with sum() requires SQL Server 2012. If you're using older version, you can check other options for running totals.
My example in SQL Fiddle
If you have dates that don't have any events and you need to show those too, then the best option is probably to create a separate table of dates for the whole range you'll ever need, for example 1.1.2000 - 31.12.2099.
-- Edit --
To get count for a specific day, it's possible use the same logic, but just sum everything up to that day:
declare #eventdate date
set #eventdate = '20150117'
select
sum(p.changevalue)
from
(
select startdate as eventdate, 1 as changevalue from personnel
where startdate <= #eventdate
union all
select enddate, -1 from personnel
where enddate < #eventdate
) p
Hopefully this is ok, can't test since SQL Fiddle seems to be unavailable.