Splitting datetime into date/year/month/dayname and count year - sql

I got question to split date, month year, dayname from datetime by using trigger, when I insert a datetime, then in next column will split date, time, month year dayname and count year (to know how old the man in my data) is that possible ?
For example, if I insert
INSERT INTO MAN VALUES ('04/06/1982')
then will be like this
DATETIME DATE MONTH YEAR DAYNAME AGE
04/06/1982 00:00:00 04 06 1982 friday 27

Try this :-
Declare #myDate datetime
set #myDate='19820604' --YYYYMMDD
Select #myDate as DateTime,
datename(day,#myDate) as Date,
month(#myDate) as Month,
datename(year,#myDate) as Year,
Datename(weekday,#myDate) as DayName,
DATEDIFF ( year , #myDate , getdate() ) as Age
Result
╔══════════════════════════════╦══════╦═══════╦══════╦═════════╦══════════╗
║ DateTime ║ DATE ║ MONTH ║ YEAR ║ DAYNAME ║ Age ║
╠══════════════════════════════╬══════╬═══════╬══════╬═════════╬══════════╣
║ April, 06 1982 00:00:00+0000 ║ 4 ║ 6 ║ 1982 ║ Friday ║ 31 ║
╚══════════════════════════════╩══════╩═══════╩══════╩═════════╩══════════╝
SQL Fiddle Demo

The code has been slightky altered to give the age correctly.
Select myDate,myDateDate,myDateMonth,myDateYear,myDateDayName, Convert(varchar(50),Age)+ ' Years and '+Convert(varchar(50),nodays) +'days' {Age] from
(
Select #myDate as myDate,
datename(day,#myDate) as myDateDate,
month(#myDate) as myDateMonth,
datename(year,#myDate) as myDateYear,
Datename(weekday,#myDate) as myDateDayName,
DATEDIFF ( year , #myDate , getdate() ) Age ,
DATEDIFF ( dd , #myDate , getdate() ) -365* DATEDIFF ( year , #myDate , getdate() ) as nodays
) As a
Thanks
Arun

Here there are two approaches for solving the issue:
- Approach 1:
you can add some computed columns in to your table, so when you retrieve the table contents the other remaining fields are computed in that time.
-- 1.1) Create the base of table 'MAN'
CREATE TABLE [dbo].[MAN](
[DATETIME] [datetime] NOT NULL
) ON [PRIMARY]
GO
-- 1.2) Insert a record in it
INSERT INTO MAN VALUES ('04/06/1982')
GO
-- 1.3) Add some computed columns
ALTER TABLE MAN
ADD
[DAY] AS DATENAME(DAY, [DATETIME]),
[MONTH] AS MONTH([DATETIME]),
[YEAR] AS DATENAME(YEAR, [DATETIME]),
[DAYNAME] AS DATENAME(WEEKDAY, [DATETIME]),
[AGE] AS DATEDIFF(YEAR, [DATETIME], GETDATE())
GO
-- 1.4) See the result
SELECT * FROM MAN
- Approach 2:
While you creating the table, you add the remaining fields that you need, so that in the next phase you add insert/update triggers in order to calculate the remaining fields and insert/update them.
-- 2.1) Create the table 'MAN' with all needed columns
CREATE TABLE [dbo].[MAN](
[DATETIME] [datetime] NOT NULL,
[DAY] [int] NULL,
[MONTH] [int] NULL,
[YEAR] [int] NULL,
[DAYNAME] [nvarchar](10) NULL,
[AGE] [int] NULL
) ON [PRIMARY]
GO
-- 2.2) Create Insert and update triggers in order to calculate the values of the rest fields while inserting/updating
CREATE TRIGGER [dbo].[trCalculateRemainingDateFields] ON MAN
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE MAN
SET
[DAY] = DATENAME(DAY, MAN.[DATETIME]),
[MONTH] = MONTH(MAN.[DATETIME]),
[YEAR] = DATENAME(YEAR, MAN.[DATETIME]),
[DAYNAME] = DATENAME(WEEKDAY, MAN.[DATETIME]),
[AGE] = DATEDIFF(YEAR, MAN.[DATETIME], GETDATE())
FROM Inserted i
WHERE i.[DATETIME] = MAN.[DATETIME]
END
GO
-- 2.3) Insert a record in it
INSERT INTO MAN ([DATETIME]) VALUES ('04/06/1985')
GO
-- 2.4) See the result
SELECT * FROM MAN
- Approach 3:
Creating a view of the main table which calculate the remaining fields can be a good option.
-- 3.1) Create the base of table 'MAN'
CREATE TABLE [dbo].[MAN](
[DATETIME] [datetime] NOT NULL
) ON [PRIMARY]
GO
-- 3.2) Insert a record in it
INSERT INTO MAN VALUES ('04/06/1982')
GO
-- 3.3) Create a view which is contained the remaining fields
CREATE VIEW dbo.vMAN
AS
SELECT [DATETIME],
DATENAME(DAY, [DATETIME]) AS [DAY],
MONTH([DATETIME]) AS [MONTH],
DATENAME(YEAR, [DATETIME]) AS [YEAR],
DATENAME(WEEKDAY, [DATETIME]) AS [DAYNAME],
DATEDIFF(YEAR, [DATETIME], GETDATE()) AS [AGE]
FROM MAN
GO
-- 3.4) See the result
SELECT * FROM vMAN

Related

What is the best way to get active employee count per month?

I have Employee like below:
DECLARE #Employees TABLE
(
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[HireDate] [datetime] NOT NULL,
[TerminationDate] [datetime] NULL
)
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/01/01','2016/01/02')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/02/01', '2017/01/30')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/03/01', '2016/05/05')
If I need to know the count of active employees for Feb 2016, I used below query:
SELECT * FROM #Employees
WHERE HireDate <= '2016-02-28' AND TerminationDate >= '2016-02-28'
However, I'm having difficulty on an easy method to find active employees for each month. For example, I want to know count of active employees from Jan 2016 to Jan 2017 every month.
Do I need to have separate table with each month and use some CTE to cross reference both tables and provide report for every month? Any directions will be grateful.
With the inputs so far, I have got to this. It seems to be working fine except for Jan 2016 where I have one employee active though only for 2 days, it is not reporting since I know I'm validating month-end. Any tweaks?
DECLARE #startDate DATETIME
DECLARE #endDate datetime
SET #startDate='2014-01-31'
SET #endDate='2017-05-31'
DECLARE #Employees TABLE
(
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[HireDate] [datetime] NOT NULL,
[TerminationDate] [datetime] NULL
)
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/01/01','2016/01/02')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/02/01', '2017/01/30')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/03/01', '2016/05/05')
;With MyListOfDates( MyCalendarMonthEnd )
AS
(
SELECT #startDate MyCalendarMonthEnd
UNION ALL
SELECT DATEADD(MONTH, 1, MyCalendarMonthEnd)
FROM MyListOfDates
WHERE MyCalendarMonthEnd < #endDate
)
SELECT YEAR(mld.MyCalendarMonthEnd) Year, MONTH(mld.MyCalendarMonthEnd) Month, COUNT(*) ActiveEmployeeCount
FROM MyListOfDates mld
JOIN #Employees e on 1 = 1
WHERE e.HireDate <= mld.MyCalendarMonthEnd and e.TerminationDate >= mld.MyCalendarMonthEnd
GROUP BY mld.MyCalendarMonthEnd
One option is to use an ad-hoc tally table. A tally/calendar table would do the trick as well
I opted for the DatePart DAY to capture any portion of the month
Example
Declare #Date1 date = '2016-01-01'
Declare #Date2 date = '2017-01-31'
Select Year = DatePart(YEAR,D)
,Month = DatePart(MONTH,D)
,EmpCnt = count(DISTINCT [EmployeeID])
From (Select Top (DateDiff(DAY,#Date1,#Date2)+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),#Date1) From master..spt_values n1,master..spt_values n2) A
Left Join #Employees B on D between [HireDate] and IsNull([TerminationDate],GetDate())
Group By DatePart(YEAR,D), DatePart(MONTH,D)
Order By 1,2
Returns
Year Month EmpCnt
2016 1 1
2016 2 1
2016 3 2
2016 4 2
2016 5 2
2016 6 1
2016 7 1
2016 8 1
2016 9 1
2016 10 1
2016 11 1
2016 12 1
2017 1 1
As Requested - Some Commentary
First we create a series of dates between X and Y. This is done via an ad-hoc tally table, Row_Number(), and DateAdd(). For example:
Declare #Date1 date = '2016-01-01'
Declare #Date2 date = '2017-01-31'
Select Top (DateDiff(DAY,#Date1,#Date2)+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),#Date1)
From master..spt_values n1,master..spt_values n2
Returns
D
2016-01-01
2016-01-02
2016-01-03
2016-01-04
...
2017-01-29
2017-01-30
2017-01-31
Notice that we are performing a cross join on spt_values (n1 and n2). This is because spt_values has only 2,523 records (or days). Considering that would equate to only 6 years, by using a cross join which expands the potential time span of 6.3 million days --- a ridiculous number, but you would never see that volume because we specify TOP ( nDays )
Once we have this dataset of target days, we then perform a LEFT JOIN to the EMPLOYEE table where D is between Hire and Term dates. This actually create a large temporal dataset. For example if an employee was active for only 10 days, we would see 10 records. 1 for for each day.
Then we perform a simple aggregation COUNT(DISTINCT EmployeeID) group by year and month.
In case anyone interested in the solution using CTEs. Preferred solution is provided by #JohnCappelleti
DECLARE #startDate DATETIME
DECLARE #endDate datetime
SET #startDate='2014-01-31'
SET #endDate='2017-05-31'
DECLARE #Employees TABLE
(
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[HireDate] [datetime] NOT NULL,
[TerminationDate] [datetime] NULL
)
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/01/01','2016/01/02')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/02/01', '2017/01/30')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/03/01', '2016/05/05')
;With MyListOfDates( MyCalendarMonthEnd )
AS
(
SELECT #startDate MyCalendarMonthEnd
UNION ALL
SELECT DATEADD(DAY, 1, MyCalendarMonthEnd)
FROM MyListOfDates
WHERE MyCalendarMonthEnd < #endDate
)
SELECT YEAR(mld.MyCalendarMonthEnd) Year, MONTH(mld.MyCalendarMonthEnd) Month, COUNT(DISTINCT EmployeeID) ActiveEmployeeCount
FROM MyListOfDates mld
JOIN #Employees e on 1 = 1
WHERE e.HireDate <= mld.MyCalendarMonthEnd and e.TerminationDate >= mld.MyCalendarMonthEnd
GROUP BY YEAR(mld.MyCalendarMonthEnd), MONTH(mld.MyCalendarMonthEnd)
ORDER BY 1,2
OPTION (MAXRECURSION 0)
I have already queried #Techspider to explain the output in tabular form.
I am not using ROW_Number or distinct.
I am not using CROSS Join because My output is Each Month,Each Year (not each day,each month,each year).
Also you have to find each month count
Also finding count for such long duration will slow down
Try this,
DECLARE #startDate DATETIME
DECLARE #endDate datetime
SET #startDate='2016-01-01'
SET #endDate='2017-01-31'
DECLARE #Employees TABLE
(
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[HireDate] [datetime] NOT NULL,
[TerminationDate] [datetime] NULL
)
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/01/01','2016/01/02')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/02/01', '2017/01/30')
INSERT INTO #Employees (HireDate, TerminationDate) VALUES ('2016/03/01', '2016/05/05')
SELECT datepart(year,EDT)[Year],datepart(month,edt)[Month]
,count( e.[EmployeeID]) EmpCount
FROM
(SELECT dateadd(month,number,#startDate)STDT
,dateadd(day,-1,dateadd(month,datediff(month,0,(dateadd(month,number,#startDate)))+1,0)) EDT
FROM MASTER.dbo.spt_values
WHERE name is null and number<=datediff(month,#startDate,#endDate)+1)n
left join #Employees E on
HireDate <= n.STDT
AND TerminationDate >= n.EDT
group by datepart(year,EDT),datepart(month,edt)
order by 1,2

How to select max of sum of columns per another column over datetime range?

CREATE TABLE [T]
(
CreatedOn DATETIME NOT NULL
,Name NVARCHAR(20) NOT NULL
,Code NVARCHAR(20) NOT NULL
,R FLOAT NULL
,C1 FLOAT NULL
,C2 FLOAT NULL
,C3 FLOAT NULL
);
INSERT INTO [T] VALUES
('2013-01-01', N'A', N'', 13, NULL, NULL, NULL)
,('2013-01-07', N'A', N'1', NULL, 5, NULL, NULL)
,('2013-01-31', N'A', N'2', NULL, 4, NULL, NULL)
,('2013-02-01', N'A', N'1', NULL, NULL, 6, NULL)
,('2013-02-15', N'A', N'2', NULL, NULL, NULL, 3)
,('2013-03-01', N'A', N'1', NULL, 1, NULL, NULL)
,('2013-03-05', N'A', N'', 8, NULL, NULL, NULL)
,('2013-03-22', N'A', N'2', NULL, NULL, NULL, 1)
,('2013-05-01', N'A', N'1', NULL, 2, NULL, NULL);
In [T]
1. One and only one non-null value per row for [R], [C1], [C2] and [C3]
2. [Code] contains a non-empty value if [C1], [C2] or [C3] contains a non-null value
3. There is an index on [Name]
4. Contains millions of rows
5. Few unique values of [Code], typically less than 100
6. Few unique values of [Name], typically less than 10000
7. Is actually a complex view containing several inner joins
How does one select from [T] ([DateMonth], [P]) where [CreatedOn] >= #Start AND [CreatedOn] <= #End AND [Name] = #Name AND [P] = Sum([R]) - (Sum(MaxOf(Sum([C1]), Sum([C2]), Sum([C3]), per unique [Code])))? (See the expected output below for a more accurate 'explanation'.) There should be a row in the resultset for each month #Start - #End regardless of whether there are rows for that month in [T]. Temporary table use is acceptable.
Expected Output
#Name = N'A'
#Start = '2012-12-01'
#End = '2013-07-01'
DateMonth P
'2012-12-01' 0
'2013-01-01' 4 -- 4 = SUM([R])=13 - (MaxForCode'1'(SUM(C1)=5, SUM(C2)=0, SUM(C3)=0)=5 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=0)=4)
'2013-02-01' 3 -- 3 = SUM([R])=13 - (MaxForCode'1'(SUM(C1)=5, SUM(C2)=6, SUM(C3)=0)=6 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3)=4)
'2013-03-01' 11 -- 11 = SUM([R])=13+8=21 - (MaxForCode'1'(SUM(C1)=5+1=6, SUM(C2)=6, SUM(C3)=0)=6 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3+1=4)=4)
'2013-04-01' 11
'2013-05-01' 9 -- 9 = SUM([R])=13+8=21 - (MaxForCode'1'(SUM(C1)=5+1=6, SUM(C2)=6+2=8, SUM(C3)=0)=8 + MaxForCode'2'(SUM(C1)=4, SUM(C2)=0, SUM(C3)=3+1=4)=4)
'2013-06-01' 9
'2013-07-01' 9
Here is one solution. There are certainly some performance improvements that could be made, but I'll leave that up to you and your specific situation. Note that the CTE usage is certainly not necessary and adding CreatedOn to the index would be very helpul. A temp table may also be better than a table variable, but you'll need to evaluate that.
Since I think what you're looking for are running totals, this article may be helpful in improving the performance of my suggested solution.
Personally, I would first consider not using the view, as working directly with the sql that creates the view may be more performant.
Here is the SQL and a SQLFiddle link.
DECLARE #Name NVARCHAR(1) = N'A',
#Start DATETIME = '2012-12-01',
#End DATETIME = '2013-07-01'
--get the date for the first of the start and end months
DECLARE #StartMonth DATETIME = DATEADD(month, DATEDIFF(month, 0, #Start), 0)
DECLARE #EndMonth DATETIME = DATEADD(month, DATEDIFF(month, 0, #End), 0)
DECLARE #tt TABLE
(
DateMonth DATETIME,
sum_r FLOAT,
code NVARCHAR(20),
max_c FLOAT
)
--CTE to create a simple table with an entry for each month (and nxt month)
;WITH Months
as
(
SELECT #StartMonth as 'dt', DATEADD(month, 1, #StartMonth) as 'nxt'
UNION ALL
SELECT DATEADD(month, 1, dt) as 'dt', DATEADD(month, 2, dt) as 'nxt'
FROM Months
WHERE dt < #EndMonth
)
--SELECT * FROM Months OPTION(MAXRECURSION 9965) --for the CTE, you could also select dates into a temp table/table var first
INSERT INTO #tt (DateMonth, sum_r, code, max_c)
SELECT M.dt DateMonth,
ISNULL(t.sum_r,0) sum_r,
ISNULL(t.code,'') code,
ISNULL(t.max_c,0) max_c
--sum_c1, sum_c2, sum_c3, cnt
FROM Months M
OUTER APPLY (
SELECT sum_r,
code,
CASE WHEN sum_c1 >= sum_c2 AND sum_c1 >= sum_c3 THEN sum_c1
WHEN sum_c2 >= sum_c1 AND sum_c2 >= sum_c3 THEN sum_c2
ELSE sum_c3
END max_c
--sum_c1, sum_c2, sum_c3, cnt
FROM ( --use a sub select here to improve case statement performance getting max_c
SELECT SUM(ISNULL(r,0)) sum_r,
code,
sum(ISNULL(c1,0)) sum_c1,
sum(ISNULL(c2,0)) sum_c2,
SUM(ISNULL(c3,0)) sum_c3
FROM T
WHERE CreatedOn >= #Start AND CreatedOn < M.nxt
AND CreatedOn <= #End
AND Name = #Name
GROUP BY code
) subselect
) t
OPTION (MAXRECURSION 999)
SELECT DateMonth, SUM(sum_r) - SUM(max_c) p
FROM #tt
GROUP BY DateMonth

How could I "auto-rotate" appended records in SQL (1 goes to 2, 2 goes 3, 3 goes to 4, 4 goes back to 1)?

I'm working on a system (ASP.NET/MSSQL/C#) for scheduling restaurant employees.
The problem I'm having is I need to "auto-rotate" the shift "InTimes" every week.
The user needs to be able to copy one day's schedule to the same day next week with all the employee shift times rotated one slot.
For example, in the table below, Monica has the 10:30am shift this Monday, so she would have the 11:00am next week, and Adam would go from 12:00pm to 10:30am.
The time between shifts is not constant, nor is the number of employees on each shift.
Any ideas on how to do this (ideally with SQL statements) would be greatly appreciated.
Please keep in mind I'm a relative novice.
RecordID EmpType Date Day Meal ShiftOrder InTime EmployeeID
1 Server 29-Aug-11 Monday Lunch 1 10:30:00 AM Monica
2 Server 29-Aug-11 Monday Lunch 2 11:00:00 AM Sofia
3 Server 29-Aug-11 Monday Lunch 3 11:30:00 AM Jenny
4 Server 29-Aug-11 Monday Lunch 4 12:00:00 PM Adam
5 Server 29-Aug-11 Monday Dinner 1 4:30:00 PM Adam
6 Server 29-Aug-11 Monday Dinner 2 4:45:00 PM Jenny
7 Server 29-Aug-11 Monday Dinner 3 5:00:00 PM Shauna
8 Server 29-Aug-11 Monday Dinner 4 5:15:00 PM Sofia
10 Server 29-Aug-11 Monday Dinner 5 5:30:00 PM Monica
Somehow an employee would need to get his last (few) shifts
SELECT TOP 3 * FROM shift WHERE EmployeeID LIKE 'monica' ORDER BY [date] DESC
Next he/she would need to enter the time and date offset he would like to work next week, relative to a schedule before.
INSERT INTO shift SELECT
recordID
,[date]
,CASE [Intime]
WHEN [Intime] BETWEEN 00:00 AND 10:00 THEN 'Breakfast'
WHEN [Intime] BETWEEN 10:01 AND 04:29 THEN 'Lunch'
WHEN [Intime] BETWEEN 04:30 AND 23:59 THEN 'Dinner'
END as Meal
,No_idea_how_to_generate_this AS ShiftOrder
,[Intime]
,EmployeeID
FROM (SELECT
NULL as recordID
,DATEADD(DAY, 7+#dateoffset, ls.[date]) as [date]
,CAST(DATEADD(MINUTE, #timeoffset, ls.[time] AS TIME) as [Intime]
,EmployeeId
FROM Shift WHERE recordID = #recordID ) AS subselect
Here:
- #recordID is the record the employee choose as the starting point for the new appointment.
- #dateoffset is the number of days to add the the starting record
- #timeoffset is the number of minutes to add to the starting record
All the rest is determined by the row the user used as the starting point.
Here's what I came up with:
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] DATE ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] TIME ,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,'4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,'4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,'5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,'5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,'5:30:00 PM','Monica');
WITH CountByShift AS (SELECT *, COUNT(1) OVER (PARTITION BY EmpType, [Day], [Meal]) AS [CountByShiftByDayByEmpType]
FROM [#tmp]
),
NewShiftOrder AS (
SELECT *, ([ShiftOrder] + 1) % [CountByShiftByDayByEmpType] AS [NewShiftOrder]
FROM [CountByShift]
)
SELECT [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
CASE WHEN [NewShiftOrder] = 0 THEN [CountByShiftByDayByEmpType] ELSE [NewShiftOrder] END AS [NewShiftOrder],
[InTime] ,
[EmployeeID]
FROM NewShiftOrder
ORDER BY [RecordID]
You need a table with all of the shifts in it:
create table dbo.Shifts (
[Day] varchar(9) not null,
Meal varchar(6) not null,
ShiftOrder integer not null,
InTime time not null,
constraint PK__dbo_Shifts primary key ([Day], Meal, ShiftOrder)
);
If that table is properly populated you can then run this to get a map of the current Day, Meal, ShiftOrder n-tuple to the next in that Day, Meal pair:
with numbers_per_shift as (
select [Day], Meal, max(ShiftOrder) as ShiftOrderCount
from dbo.Shifts s
group by [Day], Meal
)
select s.[Day], s.Meal, s.ShiftOrder,
s.ShiftOrder % n.ShiftOrderCount + 1 as NextShiftOrder
from dbo.Shifts as s
inner join numbers_per_shift as n
on s.[Day] = n.[Day]
and s.Meal = n.Meal;
For the table to be properly populated each of the shift orders would have to begin with one and increase by one with no skipping or repeating within a Day, Meal pair.
Borrowing most of the #tmp table definition from #Ben Thul, assuming you have an identity field, not assuming you are storing dates and times as dates and times...this should run well over and over, copying the latest date into the following week:
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] VARCHAR(9) ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] VARCHAR(11) ,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,' 4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,' 4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,' 5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,' 5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,' 5:30:00 PM','Monica');
with
Shifts as (
select EmpType, [Day], Meal, ShiftOrder, InTime
from #tmp
where [Date] = (select max(cast([Date] as datetime)) from #tmp)
),
MaxShifts as (
select EmpType, [Day], Meal, max(ShiftOrder) as MaxShiftOrder
from #tmp
where [Date] = (select max(cast([Date] as datetime)) from #tmp)
group by EmpType, [Day], Meal
)
insert into #tmp (EmpType, [Date], [Day], Meal, ShiftOrder, InTime, EmployeeID)
select s.EmpType
, replace(convert(varchar(11), dateadd(dd, 7, cast(a.[Date] as datetime)), 6), ' ', '-') as [Date]
, s.Day
, s.Meal
, s.ShiftOrder
, s.InTime
, a.EmployeeID
from #tmp as a
join MaxShifts as m on a.EmpType = m.EmpType
and a.[Day] = m.[Day]
and a.Meal = m.Meal
join Shifts as s on a.EmpType = s.EmpType
and a.[Day] = s.[Day]
and a.Meal = s.Meal
and 1 + a.ShiftOrder % m.MaxShiftOrder = s.ShiftOrder
where a.[Date] = (select max(cast([Date] as datetime)) from #tmp)
I'm assuming that the schedule is really tied to a meal and weekday in a below answer.
Also I would like to note that ShiftOrder and Day columns should not be columns. Day is obviously determined by Date so it is a total waste of space (computed column OR determine it on the UI side) and ShiftOrder is determined by Date and InTime columns (probably easy to calculate in a query with RANK() function or on the UI side). That said it will make this query a bit easier :)
declare #dt date = cast('29-Aug-11' as date)
/* note: the date above may be passed from UI or it maybe calculated based on getdate() and dateadd function or s.t. like that */
INSERT INTO [table] (EmpType,Date,Day,Meal,ShiftOrder,InTime,EmployeeID)
SELECT t1.EmpType, dateadd(day, 7, t1.date), t1.day, t1.meal, t2.ShiftOrder, t2.InTime, t1.EmployeeID
FROM [table] t1
INNER JOIN [table] t2
ON (t1.Date = t2.Date
and t1.Meal = t2.Meal
and (
t1.ShiftOrder = t2.ShiftOrder + 1
or
(
t1.ShiftOrder = (select max(shiftOrder) from [table] where meal = t1.meal and date =t1.date)
and
t2.ShiftOrder = (select min(shiftOrder) from [table] where meal = t1.meal and date =t1.date)
)
)
)
WHERE t1.Date = #dt
This is a pretty straight-forward set-oriented problem. Aggregations (count(*) and max()) and lookup tables are unnecessary. You can do it with one SQL statement.
The first step (set) is to identity those employees who simply slide down in the schedule.
The next step (set) is to identity those employees who need to "wrap around" to the head of the schedule.
Here's what I came up with:
/* Set up the temp table for demo purposes */
DROP TABLE #tmp
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] DATE ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] TIME,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,' 4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,' 4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,' 5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,' 5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,' 5:30:00 PM','Monica');
/* the "fills" CTE will find those employees who "wrap around" */
;WITH fills AS (
SELECT
[d2].[EmpType],
[d2].[Date],
[d2].[Day],
[d2].[Meal],
1 AS [ShiftOrder],
[d2].[InTime],
[d2].[EmployeeID]
FROM
[#tmp] d1
RIGHT OUTER JOIN
[#tmp] d2 ON
([d1].[Meal] = [d2].[Meal])
AND ([d1].[ShiftOrder] = [d2].[ShiftOrder] + 1)
WHERE
[d1].[EmployeeID] IS NULL
)
INSERT INTO [table] (EmpType,Date,Day,Meal,ShiftOrder,InTime,EmployeeID)
SELECT
[d1].[EmpType],
DATEADD(DAY, 7, [d1].[Date]) AS [Date],
DATENAME(dw,(DATEADD(DAY, 7, [d1].[Date]))) AS [Day],
[d1].[Meal],
[d1].[ShiftOrder],
[d1].[InTime],
ISNULL([d2].[EmployeeID], [f].[EmployeeID]) AS [EmployeeID]
FROM
[#tmp] d1
LEFT OUTER JOIN
[#tmp] d2 ON
([d1].[Meal] = [d2].[Meal]) AND ([d1].[ShiftOrder] = [d2].[ShiftOrder] + 1)
LEFT OUTER JOIN
[fills] f ON
([d1].[Meal] = [f].[Meal]) AND ([d1].[ShiftOrder] = [f].[ShiftOrder])
You can use a subquery (for a tutorial on subqueries, see http://www.databasejournal.com/features/mssql/article.php/3464481/Using-a-Subquery-in-a-T-SQL-Statement.htm) to get the last shift time.
After this, its trivial addition and modular division (in case you don't know what that is, have a look at this).
Hope this helped. I'm a bit tired right now, so I can't provide you with an example.
I'm a SQL programmer and DBA for 20 yrs now. With that said, business logic this complex should be in the C# part of the system. Then the TDD built application can handle the inevitable changes, and still be refactor-able and correct.
My recommendation is 'push-back'. Your response should be something along the lines of "This isn't just some look-up/fill-in the blank logic. This kind of complex business logic belongs in the App". It belongs in something that can be unit tested, and will be unit tested every time its changed.
The right answer sometimes is 'No', this is one of them.
How about using a Pivot Table for all employees and then adding shift timings as rows?? Order the names based on Shift for the initial Day.
Something like this..
Date_time Shift_Order Monica Sofia Jenny Adam Shauna
08/29/11 1 10:30AM 11:00AM 11:30AM 12:00PM NULL
08/29/11 2 5:30PM 5:15PM 4:45PM 4:30PM 5:00PM

SQL Select within a select

I'm creating a dataset that will be displayed in an SSRS report.
I have a query in a job that puts a count into a table [dbo].[CountMetersDue] on a rolling basis on the 1st of every month; the value changes throughout the month so need to take a snapshot at beginning.
I have the report set up which uses a custom expression to produce a cumulative trend graph. Basically takes one value, divides by another to work out a percentage. Therefore I have two queries that need combining... Took me ages to get my head round all this!
I just need help with the last bit.
SELECT (SELECT [Count]
FROM [MXPTransferDev].[dbo].[CountMetersDue]
WHERE [MXPTransferDev].[dbo].[CountMetersDue].[DateTime] =
[MXPTransferDev].[dbo].[Readings].[dateRead]) AS [MetersDue],
COUNT(readingid) AS [TotalReadings],
CONVERT(DATE, dateread) AS [dateRead]
FROM [MXPTransferDev].[dbo].[Readings]
WHERE ( [MXPTransferDev].[dbo].[Readings].[dateRead] BETWEEN
'01-may-11' AND '31-may-11' )
AND ( webcontactid IS NOT NULL )
AND ( meter = 1 )
GROUP BY CONVERT(DATE, [MXPTransferDev].[dbo].[Readings].[dateRead])
CREATE TABLE [dbo].[CountMetersDue](
[Count] [int] NULL,
[DateTime] [datetime] NULL
) ON [USER]
GO
ALTER TABLE [dbo].[CountMetersDue]
ADD CONSTRAINT [DF_CountMetersDue_DateTime] DEFAULT (getdate()) FOR [DateTime]
GO
CREATE TABLE [dbo].[Readings](
[readingId] [bigint] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
[dateRead] [datetime] NOT NULL,
[meter] [int] NOT NULL,
[webcontactid] [bigint] NULL,
Readings
readingId meter reading dateRead webcontactid
583089 4 3662 2011-05-25 15:00:33.040 479
583207 3 682 2011-05-25 15:00:33.027 479
583088 2 98064 2011-05-25 15:00:33.007 479
CountMetersDue
Count DateTime
2793 2011-12-01 00:00:00.000
1057 2011-05-01 14:08:20.437
610 2011-03-01 00:00:00.000
Second stab at answering your question (will probably need some clarification from yourself before the answer is correct):
/* DDL: 2 tables [CountMetersDue] & [Readings]
[CountMetersDue]
([DateTime] datetime,
[Count] int)
[Readings]
([ReadingId] bigint,
[dateRead] datetime,
[webcontactid] bigint,
[meter] int)
[CountMetersDue] - contains 1 record on the first of every month, with count of the number of readings at that date
[Readings] - contains all the individual readings
ie:
[CountMetersDue]
01-Jan-2011 1000
01-Feb-2011 2357
01-Mar-2011 3000
[Readings]
1 01-Jan-2011 11 1
2 02-Jan-2011 12 1
3 03-Jan-2011 13 1
...
*/
SELECT
CONVERT(DATE, [dbo].[Readings].[dateRead]) AS dateRead,
COUNT([dbo].[Readings].[readingId]) AS TotalReadings,
[dbo].[CountMetersDue].[Count] AS MetersDue
FROM
[CountMetersDue] /* get all count meters due */
left join [Readings] /* get any corresponding Reading records
where the dateRead in the same month as
the CountMetersDue */
on DATEPART(year, Readings.dateRead) = DATEPART(year, [CountMetersDue].[DateTime]) /* reading in same year as CountMetersDue */
and DATEPART(month, Readings.dateRead) = DATEPART(month, [CountMetersDue].[DateTime]) /* reading in same month as CountMetersDue */
WHERE ([MXPTransferDev].[dbo].[Readings].[dateRead]) BETWEEN
#StartDate AND #EndDate
AND ( webcontactid IS NOT NULL )
AND ( meter = 1 )
GROUP BY
[dbo].[CountMetersDue].[Count],CONVERT(DATE, [dbo].[Readings].[dateRead])
This would be the query you are looking for then?
Subqueries, as they are called, can be included by enclosing them in parentheses '()'.
SELECT (SELECT [Count] FROM [xxxxx].[dbo].[CountMetersDue] AS tabA WHERE tabA.[datefield] = tabB.dateRead) AS [MetersDue], COUNT(readingId) AS [TotalReadings], CONVERT(DATE, dateRead) AS [dateRead]
FROM [xxxxx] AS tabB
WHERE (dateRead BETWEEN #StartDate AND #EndDate) AND (webcontactid IS NOT NULL) AND (meter = 1)
GROUP BY CONVERT(DATE, dateRead)

SQL count exposure of life time by age

(Using SQL Server 2008)
I need some help visualizing a solution. Let's say I have the following simple table for members of a pension scheme:
[Date of Birth] [Date Joined] [Date Left]
1970/06/1 2003/01/01 2007/03/01
I need to calculate the number of lives in each age group from 2000 to 2009.
NOTE: "Age" is defined as "age last birthday" (or "ALB") on 1 January of each of those yeasrs. e.g. if you are exactly 41.35 or 41.77 etc. years old on 1/1/2009 then you would be ALB 41.
So if the record above were the only entry in the database, then the output would be something like:
[Year] [Age ] [Number of Lives]
2003 32 1
2004 33 1
2005 34 1
2006 35 1
2007 36 1
(For 2000, 2001, 2002, 2008 and 2009 there are no lives on file since the sole member only joined on 1/1/2003 and left on 1/3/2007)
I hope I am making myself clear enough.
Anyone have any suggestions?
Thanks, Karl
[EDIT]
Adding another layer to the problem:
What if I had:
[Date of Birth] [Date Joined] [Date Left] [Gender] [Pension Value]
1970/06/1 2003/01/01 2007/03/01 'M' 100,000
and I want the output to be:
[Year] [Age ] [Gender] sum([Pension Value]) [Number of Lives]
2003 32 M 100,000 1
2004 33 M 100,000 1
2005 34 M 100,000 1
2006 35 M 100,000 1
2007 36 M 100,000 1
Any ideas?
WITH years AS
(
SELECT 1900 AS y
UNION ALL
SELECT y + 1
FROM years
WHERE y < YEAR(GETDATE())
),
agg AS
(
SELECT YEAR(Dob) AS Yob, YEAR(DJoined) AS YJoined, YEAR(DLeft) AS YLeft
FROM mytable
)
SELECT y, y - Yob, COUNT(*)
FROM agg
JOIN years
ON y BETWEEN YJoined AND YLeft
GROUP BY
y, y - Yob
OPTION (MAXRECURSION 0)
People born on same year always have the same age in your model
That's why if they go at all, they always go into one group and you just need to generate one row per year for the period they stay in the program.
You can try something like this
DECLARE #Table TABLE(
[Date of Birth] DATETIME,
[Date Joined] DATETIME,
[Date Left] DATETIME
)
INSERT INTO #Table ([Date of Birth],[Date Joined],[Date Left]) SELECT '01 Jun 1970', '01 Jan 2003', '01 Mar 2007'
INSERT INTO #Table ([Date of Birth],[Date Joined],[Date Left]) SELECT '01 Jun 1979', '01 Jan 2002', '01 Mar 2008'
DECLARE #StartYear INT,
#EndYear INT
SELECT #StartYear = 2000,
#EndYear = 2009
;WITH sel AS(
SELECT #StartYear YearVal
UNION ALL
SELECT YearVal + 1
FROM sel
WHERE YearVal < #EndYear
)
SELECT YearVal AS [Year],
COUNT(Age) [Number of Lives]
FROM (
SELECT YearVal,
YearVal - DATEPART(yy, [Date of Birth]) - 1 Age
FROM sel LEFT JOIN
#Table ON DATEPART(yy, [Date Joined]) <= sel.YearVal
AND DATEPART(yy, [Date Left]) >= sel.YearVal
) Sub
GROUP BY YearVal
Try the following sample query
SET NOCOUNT ON
Declare #PersonTable as Table
(
PersonId Integer,
DateofBirth DateTime,
DateJoined DateTime,
DateLeft DateTime
)
INSERT INTO #PersonTable Values
(1, '1970/06/10', '2003/01/01', '2007/03/01'),
(1, '1970/07/11', '2003/01/01', '2007/03/01'),
(1, '1970/03/12', '2003/01/01', '2007/03/01'),
(1, '1973/07/13', '2003/01/01', '2007/03/01'),
(1, '1972/06/14', '2003/01/01', '2007/03/01')
Declare #YearTable as Table
(
YearId Integer,
StartOfYear DateTime
)
insert into #YearTable Values
(1, '1/1/2000'),
(1, '1/1/2001'),
(1, '1/1/2002'),
(1, '1/1/2003'),
(1, '1/1/2004'),
(1, '1/1/2005'),
(1, '1/1/2006'),
(1, '1/1/2007'),
(1, '1/1/2008'),
(1, '1/1/2009')
;WITH AgeTable AS
(
select StartOfYear, DATEDIFF (YYYY, DateOfBirth, StartOfYear) Age
from #PersonTable
Cross join #YearTable
)
SELECT StartOfYear, Age, COUNT (1) NumIndividuals
FROM AgeTable
GROUP BY StartOfYear, Age
ORDER BY StartOfYear, Age
First some preparation to have something to test with:
CREATE TABLE People (
ID int PRIMARY KEY
,[Name] varchar(50)
,DateOfBirth datetime
,DateJoined datetime
,DateLeft datetime
)
go
-- some data to test with
INSERT INTO dbo.People
VALUES
(1, 'Bob', '1961-04-02', '1999-01-01', '2007-05-07')
,(2, 'Sadra', '1960-07-11', '1999-01-01', '2008-05-07')
,(3, 'Joe', '1961-09-25', '1999-01-01', '2009-02-11')
go
-- helper table to hold years
CREATE TABLE dimYear (
CalendarYear int PRIMARY KEY
)
go
-- fill-in years for report
DECLARE
#yr int
,#StartYear int
,#EndYear int
SET #StartYear = 2000
SET #EndYear = 2009
SET #yr = #StartYear
WHILE #yr <= #EndYear
BEGIN
INSERT INTO dimYear (CalendarYear) values(#yr)
SET #yr =#yr+1
END
-- show test data and year tables
select * from dbo.People
select * from dbo.dimYear
go
Then a function to return person's age for each year, if the person is still an active member.
-- returns [CalendarYear], [Age] for a member, if still active member in that year
CREATE FUNCTION dbo.MemberAge(#DateOfBirth datetime, #DateLeft datetime)
RETURNS TABLE
AS
RETURN (
SELECT
CalendarYear,
CASE
WHEN DATEDIFF(dd, cast(CalendarYear AS varchar(4)) + '-01-01',#DateLeft) > 0
THEN DATEDIFF(yy, #DateOfBirth, cast(CalendarYear AS varchar(4)) + '-01-01')
ELSE -1
END AS Age
FROM dimYear
);
go
And the final query:
SELECT
a.CalendarYear AS "Year"
,a.Age AS "Age"
,count(*) AS "Number Of Lives"
FROM
dbo.People AS p
CROSS APPLY dbo.MemberAge(p.DateOfBirth, p.DateLeft) AS a
WHERE a.Age > 0
GROUP BY a.CalendarYear, a.Age
Deal with this in pieces (some random thoughts) - create views to test you dev steps if you can:
ALB - do a query that, for a given year, gives you your memeber's ALB
Member in year - another bit of query that tell you whether a member was a member in a given year
Put those two together and you should be able to create a query that says whether a person was a member in a given year and what their ALB was for that year.
Hmm, tricky - following this chain of thought what you'd then want to do is generate a table that has all the years the person was a member and their ALB in that year (and a unique id)
From 4. select year, alb, count(id) group by year, alb
I'm not sure I'm going in the right direction from about 3 though it should work.
You may find a (temporary) table of years helpful - joining things to a table of dates makes all kinds of things possible.
Not really an answer, but certainly some direction...