Selecting specific Data per month - sql

So i have a table that would determine the Region Code of a Branch Depending on the Month,
Lets Say from January to february it's Region code would be 1, from February to March it would be 2 and for the month of april upto date would be 3
so here is the sample look of the table
i have a code that gets the data from a table, but what i want to achieve is that if the LoanDate of the selected Data is within the Dates above (between fld_Datefrom and fld_Dateto) it would use the fld_BranchRegion that is indicated like above above. (EG. the Loandate of the date is 2013-01-12 00:00:00 it would use the RegionCode 4A as indicated above and if the data is 2013-02-04 00:00:00 it would use the Region code 3
here is the code i use
SELECT
TE.LOAN
,bp.REGION
,BP.ID
,TE.AMOUNT
,te.ID
FROM #TrackExpired TE
inner join Transactions.TBLMAIN PM
on TE.ID = PM.ID
inner join #track BP
on BP.ID=cast(TE.ID/1000000000000 as decimal(38,0))
WHERE ((cast(TE.EXPIRATION as date) < cast(TE.newloandate as date))
OR(TE.NewLoanDate is null and (cast(TE.EXPIRATION as date) < cast(PM.REDEEMED as date))) or ((TE.NewLoanDate is null and PM.REDEEMED is null) and (PM.STATUS = 7 or PM.STATUS = 5)) )
The problem with this is that it generates duplicate values so i have 3 occurances of the dates in the #track table the number of the Data is also outputted 3 times with different Region Code!!
Instead of outputting them i would like to achive on selecting the Region Code from **#track
Based on the loan date of the Data.**
i just want to achieve that instead of outputting all of the region code, it would just use the Region code that is between the ranges based on the #track table provided..
Any Help? or other approach?? thank you!. sorry im new to SQL.
EDIT here is the code to create the temp tables.
#trackexpired
SELECT PH.ID
,PH.LOAN
,PH.EXPIRATION
,PH.AMOUNT
,(SELECT T3.LOAN FROM Transactions.HISTO T3 INNER JOIN
(
SELECT MIN(T2.ID) as pawnhisto
FROM Transactions.HISTO T2
WHERE T2.ID > PH.ID
AND PH.ID = T2.ID
) T4
ON T4.pawnhisto = T3.ID
)as 'NewLoanDate'
INTO #TrackExpired
FROM Transactions.HISTO PH
INNER JOIN Transactions.MAIN PM
ON PM.ID=PH.ID
WHERE YEAR(PH.LOAN) = #YEAR
#track
Select bt.CODE
,bp.ID
,AREA
,REGION
,NCODE
,FROM
,isnull(fld_Dateto,GETDATE()) as fld_Dateto
into #sort
from Transactions.tbl_BranchTracking bt
inner join Reference.tbl_BranchProfiles bp
on bt.CODE = bp.CODE
Select * into #track from #sort
where #YEAR >= year(FROM)
and
#YEAR <= year(fld_Dateto)

Test Data
create table #LoanTable (
ID int not null,
RegionCode nvarchar(50) not null,
LoanDate datetime not null
);
insert into #LoanTable values
(1,'5','10/01/2014'),
(2,'5','10/18/2014'),
(3,'5','10/02/2014'),
(4,'3','04/11/2014'),
(5,'3','04/05/2014'),
(6,'4A','01/09/2014'),
(7,'4A','01/05/2014')
create table #LoanDetailsTable (
ID int not null,
LoanAmount INT not null,
LoanDate datetime not null
);
insert into #LoanDetailsTable values
(1,5000,'10/15/2014'),
(2,1000,'10/11/2014'),
(3,2000,'10/09/2014'),
(4,1500,'04/13/2014'),
(5,5000,'04/17/2014'),
(6,500,'01/19/2014'),
(7,2500,'01/15/2014')
Query
;With RegCode
AS
(
SELECT RegionCode, MAX(MONTH(LoanDate)) [Month]
FROM #LoanTable
GROUP BY RegionCode
)
SELECT LDT.* , RC.RegionCode
FROM #LoanDetailsTable LDT INNER JOIN RegCode RC
ON MONTH(LDT.LoanDate) = RC.[Month]
Results
ID LoanAmount LoanDate RegionCode
1 5000 2014-10-15 00:00:00.000 5
2 1000 2014-10-11 00:00:00.000 5
3 2000 2014-10-09 00:00:00.000 5
4 1500 2014-04-13 00:00:00.000 3
5 5000 2014-04-17 00:00:00.000 3
6 500 2014-01-19 00:00:00.000 4A
7 2500 2014-01-15 00:00:00.000 4A
Using CTE extract the Month part of the date along with Region Code associated with it, then join it with you data table on Month of the loan date and extracted month in cte and get the Region code whatever it is at that time. happy days :)

Related

Count records using Date filter related tables SQL Server

I'm trying to calculates the number of reports (report_user table) per user between two date (Calendar table) and by day worked (agenda_user table).
Here is the diagram of my tables:
Calendar table :
DATE Year Month
---------------------------------
2020-01-01 2020 1
2020-01-02 2020 1
2020-01-03 2020 1
2020-01-04 2020 1
AGENDA_USER table :
ID_USER DATE Value
---------------------------------
1 2020-01-01 1
2 2020-01-01 1
1 2020-01-02 0
2 2020-01-02 1
User table :
ID_USER Name
-------------------------
1 Jack
2 Robert
Report_Result table :
ID_USER Date Result
-----------------------------------
1 2020-01-01 good
1 2020-01-01 good
2 2020-01-01 bad
2 2020-01-01 good
2 2020-01-02 good
2 2020-01-02 good
Result I'm trying to find with an SQL query
ID_USER Date Number of report Day work report/work
---------------------------------------------------------------------------
1 2020-01-01 2 1 2/1 = 2
2 2020-01-01 2 1 1
1 2020-01-02 0 0 0
2 2020-01-02 2 1 2
SELECT
REPORT_USER.ID_USER,
COUNT(ID_USER) AS result
FROM [DB].[dbo].REPORT_USER AS report,
JOIN [DB].[dbo].[USER] AS [USER]
ON [USER].ID_USER = report.ID_USER
JOIN [DB].[dbo].AGENDA_USER AS agenda
ON agenda.ID_USER = report.ID_USER
WHERE CAST(agenda.[Date] AS DATE) >= '2020-09-01'
AND CAST(agenda.[Date] AS DATE) <= '2021-07-28'
AND [USER].ID_user = 1167
GROUP BY
report.ID_VENDEUR;
I'm not entirely sure I understand your problem, but I think I'm reasonably close so here is a start, point out my invalid assumptions and we can refine. More data, particularly in Agenda and Reports would really help. An explanation is below (plus see the comment in the code).
The overall flow is to generate a list of days/people you want to report on (cteUserDays), generate a list of how many reports each user generated on each date (cteReps), generate a list of who worked on what days (cteWork), and then JOIN all 3 parts together using a LEFT OUTER JOIN so the report covers all workers on all days.
EDIT: Add cteRepRaw where DATETIME is converted to DATE and "bad" reports are filtered out. Grouping and counting still happens in cteReps, but joining to cteUserDays is not there because it was adding 1 to count if there was a NULL.
DECLARE #Cal TABLE (CalDate DATETIME, CalYear int, CalMonth int)
DECLARE #Agenda TABLE (UserID int, CalDate DATE, AgendaVal int)
DECLARE #User TABLE (UserID int, UserName nvarchar(50))
DECLARE #Reps TABLE (UserID int, CalDate DATETIME, RepResult nvarchar(50))
INSERT INTO #Cal(CalDate, CalYear, CalMonth)
VALUES ('2020-01-01', 2020, 1), ('2020-01-02', 2020, 1), ('2020-01-03', 2020, 1), ('2020-01-04', 2020, 1)
INSERT INTO #Agenda(UserID, CalDate, AgendaVal)
VALUES (1, '2020-01-01', 1), (2, '2020-01-01', 1), (1, '2020-01-02', 0), (2, '2020-01-02', 1)
INSERT INTO #User (UserID , UserName )
VALUES (1, 'Jack'), (2, 'Robert')
INSERT INTO #Reps (UserID , CalDate , RepResult )
VALUES (1, '2020-01-01', 'good'), (1, '2020-01-01', 'good')
, (2, '2020-01-01', 'bad'), (2, '2020-01-01', 'good')
, (2, '2020-01-02', 'good'), (2, '2020-01-02', 'good')
; with cteUserDays as (
--First, you want zeros in your table where no reports are, so build a table for that
SELECT CONVERT(DATE, D.CalDate) as CalDate --EDIT add CONVERT here
, U.UserID FROM #Cal as D CROSS JOIN #User as U
WHERE D.CalDate >= '2020-01-01' AND D.CalDate <= '2021-07-28'
--EDIT Watch the <= date here, a DATE is < DATETIME with hours of the same day
), cteRepRaw as (--EDIT Add this CTE to convert DATETIME to DATE so we can group on it
--Map the DateTime to a DATE type so we can group reports from any time of day
SELECT R.UserID
, CONVERT(DATE, R.CalDate) as CalDate --EDIT add CONVERT here
, R.RepResult
FROM #Reps as R
WHERE R.RepResult='good' --EDIT Add this test to only count good ones
), cteReps as (
--Get the sum of all reports for a given user on a given day, though some might be missing (fill 0)
SELECT R.UserID , R.CalDate , COUNT(*) as Reports --SUM(COALESCE(R.RepResult, 0)) as Reports
FROM cteRepRaw as R--cteUserDays as D
--Some days may have no reports for that worker, so use a LEFT OUTER JOIN
--LEFT OUTER JOIN cteRepRaw as R on D.CalDate = R.CalDate AND D.UserID = R.UserID
GROUP BY R.UserID , R.CalDate
) , cteWork as (
--Unclear what values "value" in Agenda can take, but assuming it's some kind of work
-- unit, like "hours worked" or "shifts" so add them up
SELECT A.UserID , A.CalDate, SUM(A.AgendaVal) as DayWork FROM #Agenda as A
WHERE A.CalDate >= '2020-01-01' AND A.CalDate <= '2021-07-28'
GROUP BY A.CalDate, A.UserID
)
SELECT D.UserID , D.CalDate, COALESCE(R.Reports, 0) as Reports, W.DayWork
--NOTE: While it's probably a mistake to credit a report to a day a worker had
--no shifts, it could happen and would throw an error so check
, CASE WHEN W.DayWork > 0 THEN R.Reports / W.DayWork ELSE 0 END as RepPerWork
FROM cteUserDays as D
LEFT OUTER JOIN cteReps as R on D.CalDate=R.CalDate AND R.UserID = D.UserID
LEFT OUTER JOIN cteWork as W on D.UserID = W.UserID AND D.CalDate = W.CalDate
ORDER BY CalDate , UserID
First, as per the comments in your OP "Agenda" represents when the user is working, you don't say how it's structured so I'll assume it can have multiple entries for a given person on a given day (i.e. a 4 hour shift and an 8 hour shift) so I'll add them up to get total work (cteWork). I also assume that if somebody didn't work, you can't have a report for them. I check for this, but normally I'd expect your data validator to pre-screen those out.
Second, I'll assume reports are 1 per record, and a given user can have multiple per day. You have that in your given, but it's important to this solution so I'm restating in case somebody else reads this later.
Third, I assume you want all days reported for all users, I assure this by generating a CROSS join between users and days (cteUserDays)

LEFT outer join returns returns duplicate rows even after adding distinct

Restricting the price change table to select a particular item
select * from price
where item = '13'
results of the query above
item Date_Changed New Old START_DATE end_DATE
13 01/11/2018 00:00 5.61 4.88 01/11/2018 00:00 30/11/2018 00:00
13 30/11/2018 00:00 2.84 5.61 01/11/2018 00:00 17/12/2018 00:00
13 17/12/2018 00:00 2.39 2.84 30/11/2018 00:00 17/12/2018 00:00
sales table
Date Item Qty Amount
05/07/2018 00:00 13 3 14.64
05/07/2018 00:00 13 3 14.64
04/07/2018 00:00 13 3 14.64
02/07/2018 00:00 13 1 4.88
02/07/2018 00:00 13 6 29.28
06/07/2018 00:00 13 7 34.16
03/07/2018 00:00 13 4 19.52
12/07/2018 00:00 13 2 9.76
10/08/2018 00:00 13 1 4.88
Sample code
SELECT distinct a.[Inv]
, (CASE
WHEN a.Date <= b.START_DATE THEN (b.Old * a.Qty)
WHEN a.Date between b.START_DATE and b.dt_end_DATE THEN (b.New * a.Qty)
ELSE 0
END) as calc_amount
,(a.[amount] - (CASE
WHEN a.Date <= b.START_DATE THEN (b.Old * a.Qty)
WHEN a.Date between b.START_DATE and b.end_DATE THEN (b.New * a.Qty)
ELSE 0
END)) as variance
[sales] a
left outer join price b
on a.[Item] = b.item
where b.item = '13'
The script then returns 27 rows instead of 9 rows. can someone assist on how i can improve my script to be more accurate
Use outer apply. I assume you want the most recent start date from price, so this looks like:
select s.*,
(case when s.date <= p.start_date
then p.Old * s.Qty
else p.New * s.Qty
end) as calc_amount
from sales s outer apply
(select top (1) p.*
from prices p
where p.item = s.item and
p.start_date <= s.date
order by p.date desc
) p
I am not sure if this is what you are looking for. but perhaps you can skip the join?
I created 2 sample data with the columns that you might need.
DECLARE #price table (item varchar(2),date_start date, new_price numeric(9,2))
Insert into #price (item , date_start,new_price)
values
( '13', '20190101', '1.00'),
( '13', '20190102', '1.01'),
( '13', '20190103', '1.02')
DECLARE #sales table (item varchar(2),date_sales date,qty int)
Insert into #sales (item , date_sales,qty)
values
( '13', '20190101', '5'),
( '13', '20190101', '2'),
( '13', '20190102', '5'),
( '13', '20190102', '2'),
( '13', '20190103', '5'),
( '13', '20190103', '2')
declare #item as varchar(2) = '13'
SELECT (select top (1) new_price from #price b where a.date_sales>=b.date_start and b.item = #item order by b.date_start desc ) * a.qty as 'new_price* qty'
from #sales a
where a.item = #item
I have not tested this in a table with a huge data set, so I also can't vouch for the speed of this query. I believe it would be better to have some kind of other ID to join the table
Your question is not clear... You added sample data, but I doubt this is correct...
Your price table is open to erronous data. It would be better to store just the price and a validFrom-date. In this case you can pick the price on a give date easily. Your format is open to overlapping periodes and there is no good reason to store the former price once again. That's why I ignore all fields you should not use...
Try this. I've changed the dates in a way to simulate validity periodes.
A mock-up scenario (please to this the next time for us):
CREATE TABLE priceMock(item INT, Date_Changed DATE, New DECIMAL(10,4), Old DECIMAL(10,4), [START_DATE] DATE, end_DATE DATE);
SET DATEFORMAT dmy;
INSERT INTO priceMock VALUES
(13,'01/11/2018 00:00',5.61,4.88,'01/07/2018 00:00','06/07/2018 00:00')
,(13,'30/11/2018 00:00',2.84,5.61,'07/07/2018 00:00','10/07/2018 00:00')
,(13,'17/12/2018 00:00',2.39,2.84,'11/07/2018 00:00','15/08/2018 00:00');
GO
CREATE TABLE salesMock ([Date] DATE, Item INT, Qty INT, Amount DECIMAL(10,4));
SET DATEFORMAT dmy;
INSERT INTO salesMock VALUES
('05/07/2018 00:00',13,3,14.64)
,('05/07/2018 00:00',13,3,14.64)
,('04/07/2018 00:00',13,3,14.64)
,('02/07/2018 00:00',13,1,4.88 )
,('02/07/2018 00:00',13,6,29.28)
,('06/07/2018 00:00',13,7,34.16)
,('03/07/2018 00:00',13,4,19.52)
,('10/07/2018 00:00',13,2,9.76 )
,('10/08/2018 00:00',13,1,4.88 );
GO
I'd add an inline-table-valued-function to get exactly one single line back.
CREATE FUNCTION dbo.GetPriceForItemOnDate(#item INT,#ValidOn DATE)
RETURNS TABLE
AS
RETURN
SELECT TOP 1 *
FROM priceMock
WHERE item=#item
AND [START_DATE] <= #ValidOn
ORDER BY [START_DATE] DESC
GO
--This query will combine your sales data with the price valid on the given date
SELECT s.[Date]
,s.Item
,s.Qty
,p.New AS CurrentPrice
,s.Qty * p.New AS ComputedAmount
FROM salesMock s
OUTER APPLY dbo.GetPriceForItemOnDate(s.item,s.[Date]) p
GO
--Clean up (carefull with real data)
DROP FUNCTION dbo.GetPriceForItemOnDate;
DROP TABLE priceMock;
DROP TABLE salesMock;
The idea in short:
The function will first filter to price lines for the given item. The second filter will cut the list and return just the prices for the given date and before the given date. As we sort this by the date in descending order we will get the latest price on top. By using TOP 1 we return just the one single line we want.
General remark: I use a validFrom-approach here. But you can turn this to the opposite and use a validTo-approach. The idea is the same.

SQL Server creating two tables and comparing them

I have a table with 3 columns (in SQL Server 2012). One of the columns is a date column. What I would like to do is split the table for two specified dates and merge them into one table with an extra field. Hopefully the example below will explain.
Example of what I currently have.
Company date no_employees
ABC 2014-05-30 35
DEF 2014-05-30 322
GHI 2014-05-30 65
JKL 2014-05-30 8
MNO 2014-05-30 30
ABC 2014-01-01 33
DEF 2014-01-01 301
GHI 2014-01-01 70
MNO 2014-01-01 30
What I would like a query to return for me (not sure if its possible),
Company start date no_employees end date no_employees diff
ABC 33 35 2
DEF 301 322 21
GHI 70 65 -5
JKL 0 8 8
MNO 30 30 0
PIVOT (and COALESCE to generate the 0s) seems to do it:
declare #t table (Company char(3),[date] date,no_employees int)
insert into #t(Company,[date],no_employees) values
('ABC','2014-05-30',35 ),
('DEF','2014-05-30',322 ),
('GHI','2014-05-30',65 ),
('JKL','2014-05-30',8 ),
('MNO','2014-05-30',30 ),
('ABC','2014-01-01',33 ),
('DEF','2014-01-01',301 ),
('GHI','2014-01-01',70 ),
('MNO','2014-01-01',30 )
select Company,
COALESCE(start,0) as start,
COALESCE([end],0) as [end],
COALESCE([end],0)-COALESCE(start,0) as diff
from
(select
Company,
CASE WHEN [date]='20140530' THEN 'end'
ELSE 'start' END as period,
no_employees
from #t
where [date] in ('20140101','20140530')
) t
pivot (MAX(no_employees) for period in ([start],[end])) u
Result:
Company start end diff
------- ----------- ----------- -----------
ABC 33 35 2
DEF 301 322 21
GHI 70 65 -5
JKL 0 8 8
MNO 30 30 0
This could easily be parameterized for the specific start and end dates to use.
Also, at the moment I'm using MAX because we have to have an aggregate in PIVOT, even though here the sample data contains a maximum of one row. If there's a possibility of multiple rows existing for the start or end date, we'd need to know how you want that handled.
declare #lowdate date = '2014-01-01'
declare #highdate date = '2014-05-30'
;with x as
(
select company, min(no_employees) no_employees
from #t records
where recorddate = #lowdate
group by company
), y as
(
select company, max(no_employees) no_employees
from #t records
where recorddate = #highdate
group by company
)
select coalesce(x.company, y.company) company,
coalesce(x.no_employees, 0) start_no_employees,
coalesce(y.no_employees, 0) end_no_employees,
coalesce(y.no_employees, 0) - coalesce(x.no_employees, 0) diff
from
x full outer join y
on
x.company = y.company
Create Table #temp(Company varchar(10), CDate date,emp int)
Select T1.Company,T1.emp,T2.emp,(T1.emp-T2.emp) Diff
from #temp T1
inner join #temp T2 On T1.Company=T2.Company and T1.CDate<T2.CDate
Order by T1.Company,T1.CDate
declare #t table (Company char(3),[date] date,no_employees int)
insert into #t(Company,[date],no_employees) values
('ABC','2014-05-30',35 ),
('DEF','2014-05-30',322 ),
('GHI','2014-05-30',65 ),
('JKL','2014-05-30',8 ),
('MNO','2014-05-30',30 ),
('ABC','2014-01-01',33 ),
('DEF','2014-01-01',301 ),
('GHI','2014-01-01',70 ),
('MNO','2014-01-01',30 )
select Company,MIN(no_employees),MAX(no_employees),CASE WHEN MIN(no_employees) = MAX(no_employees) then MAX(no_employees) else
MIN(no_employees) - MAX(no_employees) end as cNT from #t
GROUP BY Company
select the companies. Outer join the start date records. Outer join the end date records. Use coalesce to show 0 instead of null.
select
company,
coalesce(rec20140101.no_employees, 0) as empno_start,
coalesce(rec20140530.no_employees, 0) as empno_end
from
(
select distinct company
from records
) companies -- or use a company table if you have one
left join
(
select company, no_employees
from records
where recorddate = '2014-01-01'
) rec20140101
on rec20140101.company = companies.companyrec
left join
(
select company, no_employees
from records
where recorddate = '2014-05-30'
) rec20140530
on rec20140530.company = companies.company);
EDIT: And here is a way to scan the table just once. It's even a little shorter ;-)
select
company,
coalesce(min( case when recorddate = '2014-05-30' then no_employees end ), 0) as empno_start,
coalesce(min( case when recorddate = '2014-01-01' then no_employees end ), 0) as empno_end
from records
group by company;
Try this:
;with cte as
(select
COALESCE(src.company, tgt.company) company
isnull(tgt.no_employees,0) 'start date no_employees',
isnull(src.no_employees , 0) 'end date no_employees'
from
tbl src
full outer join tbl tgt on src.company = tgt.company and src.date <> tgt.date
where (src.date = (select max(date) from tbl) or src.date is null)
and (tgt.date = (select min(date) from tbl) or tgt.date is null)
)
select *, [end date no_employees] - [start date no_employees] diff
from cte

SQL Rollup last 4 weeks total

I have a table which I want to get the previous four weeks Order total in a query. But I want to return it with a SELECT (A total of the row's previous 4 weeks Order1 column - if they exist)
PurchasingID Order1 Date FourWeekTotal
------------ ------------------- ------- ---------------
1 1.00 2013-04-21 14.00
2 2.00 2013-04-14 12.00
3 3.00 2013-04-07 9.00
4 4.00 2013-03-31 5.00
5 5.00 2013-03-24 0.00
My understanding is for each record in your table, you want to see the sum of Order1 for itself and each record that has a Date value within four weeks prior to the primary record. Here you go:
create table MysteryTable
(
PurchasingId int not null primary key identity(1,1),
Order1 money not null,
[Date] date not null
)
insert MysteryTable( Order1, [Date] ) values ( 1.00, '2013-04-21' )
insert MysteryTable( Order1, [Date] ) values ( 2.00, '2013-04-14' )
insert MysteryTable( Order1, [Date] ) values ( 3.00, '2013-04-07' )
insert MysteryTable( Order1, [Date] ) values ( 4.00, '2013-03-31' )
insert MysteryTable( Order1, [Date] ) values ( 5.00, '2013-03-24' )
select
t1.PurchasingId
, t1.Order1
, t1.Date
, SUM( ISNULL( t2.Order1, 0 ) ) FourWeekTotal
from
MysteryTable t1
left outer join MysteryTable t2
on DATEADD( ww, -4, t1.Date ) <= t2.Date and t1.Date > t2.Date
group by
t1.PurchasingId
, t1.Order1
, t1.Date
order by
t1.Date desc
Explanation:
Join the table on itself, t1 representing the records to return, t2 to be the records to aggregate. Join based on t1's Date minus four weeks being less than or equal to t2's Date and t1's Date being greater than t2's Date. Then group the records by the t1 fields and sum t2.Order1. Left outer join is to account for the one record that will not have any preceding data.
Try this...
Declare #LBDate date
SET #LBDate = DATEADD(d,-28,getdate())
Now write ur select query...
Select * from Orders where Date between #LBDate and Getdate()
You can also use your required date instead to current date..

SQL calculate number of days of residency by month, by user, by location

I'm working on a query for a rehab organization where tenants (client/patients) live in a building when they first arrive, as they progress in their treatment they move to another building and as they near the end of treatment they are in a third building.
For funding purposes we need to know how many nights a tenant spent in each building in each month.
I can use DateDiff to get the total number of nights, but how do I get the total for each client in each month in each building?
For example, John Smith is in Building A 9/12-11/3; moves to Building B 11/3-15; moves to Building C on and is still there: 11/15 - today
What query returns a result that show the number of nights he spent in:
Building A in Septmeber, October and November.
Buidling B in November
Building C in November
Two tables hold the client's name, building name and move-in date and move-out date
CREATE TABLE [dbo].[clients](
[ID] [nvarchar](50) NULL,
[First_Name] [nvarchar](100) NULL,
[Last_Name] [nvarchar](100) NULL
) ON [PRIMARY]
--populate w/ two records
insert into clients (ID,First_name, Last_name)
values ('A2938', 'John', 'Smith')
insert into clients (ID,First_name, Last_name)
values ('A1398', 'Mary', 'Jones')
CREATE TABLE [dbo].[Buildings](
[ID_U] [nvarchar](50) NULL,
[Move_in_Date_Building_A] [datetime] NULL,
[Move_out_Date_Building_A] [datetime] NULL,
[Move_in_Date_Building_B] [datetime] NULL,
[Move_out_Date_Building_B] [datetime] NULL,
[Move_in_Date_Building_C] [datetime] NULL,
[Move_out_Date_Building_C] [datetime] NULL,
[Building_A] [nvarchar](50) NULL,
[Building_B] [nvarchar](50) NULL,
[Building_C] [nvarchar](50) NULL
) ON [PRIMARY]
-- Populate the tables with two records
insert into buildings (ID_U,Move_in_Date_Building_A,Move_out_Date_Building_A, Move_in_Date_Building_B,
Move_out_Date_Building_B, Move_in_Date_Building_C, Building_A, Building_B, Building_C)
VALUES ('A2938','2010-9-12', '2010-11-3','2010-11-3','2010-11-15', '2010-11-15', 'Kalgan', 'Rufus','Waylon')
insert into buildings (ID_U,Move_in_Date_Building_A,Building_A)
VALUES ('A1398','2010-10-6', 'Kalgan')
Thanks for your help.
I'd use a properly normalized database schema, your Buildings table is not useful like this. After splitting it up I believe that getting your answer will be pretty easy.
Edit (and updated): Here's a CTE which will take this strange table structure and split it into a more normalized form, displaying the user id, building name, move in and move out dates. By grouping on the ones you want (and using DATEPART() etc.) you should be able to get the data you need with that.
WITH User_Stays AS (
SELECT
ID_U,
Building_A Building,
Move_in_Date_Building_A Move_In,
COALESCE(Move_out_Date_Building_A, CASE WHEN ((Move_in_Date_Building_B IS NULL) OR (Move_in_Date_Building_C<Move_in_Date_Building_B)) AND (Move_in_Date_Building_C>Move_in_Date_Building_A) THEN Move_in_Date_Building_C WHEN Move_in_Date_Building_B>=Move_in_Date_Building_A THEN Move_in_Date_Building_B END, GETDATE()) Move_Out
FROM dbo.Buildings
WHERE Move_in_Date_Building_A IS NOT NULL
UNION ALL
SELECT
ID_U,
Building_B,
Move_in_Date_Building_B,
COALESCE(Move_out_Date_Building_B, CASE WHEN ((Move_in_Date_Building_A IS NULL) OR (Move_in_Date_Building_C<Move_in_Date_Building_A)) AND (Move_in_Date_Building_C>Move_in_Date_Building_B) THEN Move_in_Date_Building_C WHEN Move_in_Date_Building_A>=Move_in_Date_Building_B THEN Move_in_Date_Building_A END, GETDATE())
FROM dbo.Buildings
WHERE Move_in_Date_Building_B IS NOT NULL
UNION ALL
SELECT
ID_U,
Building_C,
Move_in_Date_Building_C,
COALESCE(Move_out_Date_Building_C, CASE WHEN ((Move_in_Date_Building_B IS NULL) OR (Move_in_Date_Building_A<Move_in_Date_Building_B)) AND (Move_in_Date_Building_A>Move_in_Date_Building_C) THEN Move_in_Date_Building_A WHEN Move_in_Date_Building_B>=Move_in_Date_Building_C THEN Move_in_Date_Building_B END, GETDATE())
FROM dbo.Buildings
WHERE Move_in_Date_Building_C IS NOT NULL
)
SELECT *
FROM User_Stays
ORDER BY ID_U, Move_In
This query run on your sample data produces he following output:
ID_U Building Move_In Move_Out
-------- ----------- ----------------------- -----------------------
A1398 Kalgan 2010-10-06 00:00:00.000 2010-11-23 18:35:59.050
A2938 Kalgan 2010-09-12 00:00:00.000 2010-11-03 00:00:00.000
A2938 Rufus 2010-11-03 00:00:00.000 2010-11-15 00:00:00.000
A2938 Waylon 2010-11-15 00:00:00.000 2010-11-23 18:35:59.050
(4 row(s) affected)
As you can see, from here on it will be much easier to isolate the days per patient or building, and also to find the records for specific months and calculate the correct stay duration in that case. Note that the CTE displays the current date for patients which are still in a building.
Edit (again): In order to get all months including their start and end dates for all relevant years, you can use a CTE like this:
WITH User_Stays AS (
[...see above...]
)
,
Months AS (
SELECT m.IX,
y.[Year], dateadd(month,(12*y.[Year])-22801+m.ix,0) StartDate, dateadd(second, -1, dateadd(month,(12*y.[Year])-22800+m.ix,0)) EndDate
FROM (
SELECT 1 IX UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12
)
m
CROSS JOIN (
SELECT Datepart(YEAR, us.Move_In) [Year]
FROM User_Stays us UNION
SELECT Datepart(YEAR, us.Move_Out)
FROM User_Stays us
)
y
)
SELECT *
FROM months;
So since we now have a tabular representation of all date ranges which can be of interest, we simply join this together:
WITH User_Stays AS ([...]),
Months AS ([...])
SELECT m.[Year],
DATENAME(MONTH, m.StartDate) [Month],
us.ID_U,
us.Building,
DATEDIFF(DAY, CASE WHEN us.Move_In>m.StartDate THEN us.Move_In ELSE m.StartDate END, CASE WHEN us.Move_Out<m.EndDate THEN us.Move_Out ELSE DATEADD(DAY, -1, m.EndDate) END) Days
FROM Months m
JOIN User_Stays us ON (us.Move_In < m.EndDate) AND (us.Move_Out >= m.StartDate)
ORDER BY m.[Year],
us.ID_U,
m.Ix,
us.Move_In
Which finally produces this output:
Year Month ID_U Building Days
----------- ------------ -------- ---------- -----------
2010 October A1398 Kalgan 25
2010 November A1398 Kalgan 22
2010 September A2938 Kalgan 18
2010 October A2938 Kalgan 30
2010 November A2938 Kalgan 2
2010 November A2938 Rufus 12
2010 November A2938 Waylon 8
-- set the dates for which month you want
Declare #startDate datetime
declare #endDate datetime
set #StartDate = '09/01/2010'
set #EndDate = '09/30/2010'
select
-- determine if the stay occurred during this month
Case When #StartDate <= Move_out_Date_Building_A and #EndDate >= Move_in_Date_Building_A
Then
(DateDiff(d, #StartDate , #enddate+1)
)
-- drop the days off the front
- (Case When #StartDate < Move_in_Date_Building_A
Then datediff(d, #StartDate, Move_in_Date_Building_A)
Else 0
End)
--drop the days of the end
- (Case When #EndDate > Move_out_Date_Building_A
Then datediff(d, #EndDate, Move_out_Date_Building_A)
Else 0
End)
Else 0
End AS Building_A_Days_Stayed
from Clients c
inner join Buildings b
on c.id = b.id_u
Try using a date table. For example, you could create one like so:
CREATE TABLE Dates
(
[date] datetime,
[year] smallint,
[month] tinyint,
[day] tinyint
)
INSERT INTO Dates(date)
SELECT dateadd(yy, 100, cast(row_number() over(order by s1.object_id) as datetime))
FROM sys.objects s1
CROSS JOIN sys.objects s2
UPDATE Dates
SET [year] = year(date),
[month] = month(date),
[day] = day(date)
Just modify the initial Dates population to meet your needs (on my test instance, the above yielded dates from 2000-01-02 to 2015-10-26). With a dates table, the query is pretty straight forward, something like this:
select c.First_name, c.Last_name,
b.Building_A BuildingName, dA.year, dA.month, count(distinct dA.day) daysInBuilding
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dA on dA.date between b.Move_in_Date_Building_A and isnull(b.Move_out_Date_Building_A, getDate())
group by c.First_name, c.Last_name,
b.Building_A, dA.year, dA.month
UNION
select c.First_name, c.Last_name,
b.Building_B, dB.year, dB.month, count(distinct dB.day)
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dB on dB.date between b.Move_in_Date_Building_B and isnull(b.Move_out_Date_Building_B, getDate())
group by c.First_name, c.Last_name,
b.Building_B, dB.year, dB.month
UNION
select c.First_name, c.Last_name,
b.Building_C, dC.year, dC.month, count(distinct dC.day)
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dC on dC.date between b.Move_in_Date_Building_C and isnull(b.Move_out_Date_Building_C, getDate())
group by c.First_name, c.Last_name,
b.Building_C, dC.year, dC.month
If you can't restructure the Building table you can create a query that will normalize it for you and allow for easier calculations:
SELECT "A" as Building, BuidlingA as Name, Move_in_Date_Building_A as MoveInDate,
Move_out_Date_Building_A As MoveOutDate
UNION
SELECT "B", BuidlingB, Move_in_Date_Building_B, Move_out_Date_Building_B
UNION
SELECT "C", BuidlingC, Move_in_Date_Building_C, Move_out_Date_Building_C