SQL - Reference Table has Information based on Date - sql-server

So I have a reference table which stores the primary key, description and update date columns. Something like this
SELECT * FROM tblReasonRef
ReasonCode Description UpdateDate
27 Lunch 2010-12-01
24 Meeting 2010-12-01
20 SpecialProj 2010-12-01
The other day, the code description was changed. So now the query returns the following...
ReasonCode Description UpdateDate
27 Lunch 2010-12-01
24 Meeting 2010-12-01
20 SpecialProj 2010-12-01
27 Training 2012-06-22
24 Meeting 2012-06-22
20 Lunch 2012-06-22
The source data tracks every 30 minutes what state a staff member might go into, so you would have the following query...
SELECT * FROM tblhActivity
MemberID Date Time ReasonCode ReasonDuration
10922 2012-06-21 1200 27 100
10922 2012-06-21 1500 24 1800
10922 2012-06-25 1230 27 100
So originally, the query I had was...
SELECT a.MemberID, a.Date, a.Time, r.Description, a.ReasonDuration
FROM tblhActivity a
INNER JOIN tblReasonRef r ON a.ReasonCode = r.ReasonCode
Which worked fine until the change on the 22nd. Now I have two definitions of each code. The question is, how can create a query that will pick the right code depending on the date.
For example, I know that when the date is the 21st, the description for code 27 should be lunch. On the 25th, the description returned should be Training.
Keep in mind also, that this will probably happen again where codes are added to the reference table. I am trying to think the join should also be on UpdateDate but I have to know the start and end date of each reference code. Is there a simple solution?

You really need the start and end dates for the period in which a particular reason is applicable. You can either modify your tblReasonRef to include these dates (best option) or you will need to calculate them.
The following query will calculate an end date for each reason as the day before a new entry for the ReasonCode is added.
SELECT ReasonCode
,Description
,UpdateDate StartDate
,DATEADD(d, -1, UpdateDate) PreviousEntryEndDate
,ROW_NUMBER() OVER(PARTITION BY ReasonCode ORDER BY UpdateDate) AS Row
INTO #reason
FROM tblReasonRef
SELECT a.MemberID
,a.Date
,a.Time
,reason.ReasonCode
,a.ReasonDuration
FROM tblhActivity a
INNER JOIN #reason reason
ON a.ReasonCode = reason.ReasonCode
LEFT JOIN #reason nextReason
ON reason.Row = nextReason.Row - 1
AND reason.ReasonCode = nextReason.ReasonCode
WHERE a.Date BETWEEN reason.StartDate AND ISNULL(nextReason.PreviousEntryEndDate, a.Date)
DROP TABLE #reason

If you modify your table tblReasonRef, like this:
ReasonCode, Description, StarDate, EndDate
you can do this SQL Query:
SELECT a.MemberID, a.Date, a.Time, r.Description, a.ReasonDuration
FROM tblhActivity a
INNER JOIN tblReasonRef r ON a.ReasonCode = r.ReasonCode
WHERE a.Date between r.StartDate and r.EndDate
Remember that you need your code and your model simple.

Related

Query Most Recent Records in MS Access Based on Date Provided in Form Field

Let me start by noting I have spent a few days searching through S.O. and have not been able to find a solution. I apologize in advance if the solution is very simple, but I am still learning and appreciate any help I can get.
I have a MS Access 2010 Database, and I am trying to create a set of queries to inform other forms and queries. There are two tables: Borrower Contact Info (BC_Info) and Basic Financial Indicators (BF_Indicators). Each month, I review and track key performance metrics of each borrower. I would like to create a query that supplies the most recent record based on a textbox input (Forms![Portfolio_Review Menu]!Text47).
Two considerations have separated this from other posts I have seen in the 'greatest-n-per-group' tag:
Not every borrower will have data for every month.
I need to be able to see back in time, i.e. if it is January 1, 2019 and I want to see the metrics as of July 31, 2017, I want to make
sure I am only seeing data from before July 31, 2017 but as close to
this date as possible.
Fields are as follows:
BC_Info
- BorrowerName
-PartnerID
BF_Indicators
-Fin_ID
-DateUpdated
The tables are connected by BorrowerName -- which is a unique naming convention used for the primary key of BC_Info.
What I currently have is:
SELECT BCI.BorrowerName, BCI.PartnerID, BFI.Fin_ID, BFI.DateUpdated
FROM ((BC_Info AS BCI
INNER JOIN BF_Indicators AS BFI
ON BFI.BorrowerName = BCI.BorrowerName)
INNER JOIN
(
SELECT Fin_ID, MAX(DateUpdated) AS MAX_DATE
FROM BF_Indicators
WHERE (DateUpdated <= Forms![Portfolio_Review Menu]!Text47 OR
Forms![Portfolio_Review Menu]!Text47 IS NULL)
GROUP BY Fin_ID
) AS Last_BF ON BFI.Fin_ID = Last_BF.Fin_ID AND
BFI.DateUpdated = Last_BF.MAX_DATE);
This gives me the fields I need, and will keep records out that are past the date given in the textbox, but will give all records from before the textbox input -- not just the most recent.
Results (Date Entered is 12/31/2018; MEHN-45543 is only Borrower with information later than 09/30/2018):
BorrowerName PartnerID Fin_ID DateUpdated
MEHN-45543 19 9 12/31/2018
ARYS-7940 5 10 9/30/2018
FINS-21032 12 11 9/30/2018
ELET-00934 9 12 9/30/2018
MEHN-45543 19 18 9/30/2018
Expected Results (Date Entered is 12/31/2018; MEHN-45543 is only Borrower with information later than 09/30/2018):
BorrowerName PartnerID Fin_ID DateUpdated
MEHN-45543 19 9 12/31/2018
ARYS-7940 5 10 9/30/2018
FINS-21032 12 11 9/30/2018
ELET-00934 9 12 9/30/2018
As mentioned, I am planning to use the results of this Query to generate further queries that use aggregated information from the Financial Indicators to determine portfolio quality at the time.
Please let me know if there is any other information I can provide. And again, thank you in advance.
Try joining BC_Info to a query that aggregates BF_Indicators on BorrowerName, not Fin_ID. Tested with literal date value:
SELECT BC_Info.*, MaxDate
FROM BC_Info
INNER JOIN
(SELECT BorrowerName, Max(DateUpdated) AS MaxDate
FROM BF_Indicators WHERE DateUpdated <=#12/31/2018# GROUP BY BorrowerName) AS Q1
ON BC_Info.BorrowerName=Q1.BorrowerName;
If you need to include Fin_ID in the results, then:
SELECT BC_Info.*, Fin_ID, DateUpdated FROM BC_Info
INNER JOIN
(SELECT * FROM BF_Indicators WHERE Fin_ID IN
(SELECT TOP 1 Fin_ID FROM BF_Indicators AS Dupe
WHERE Dupe.BorrowerName=BF_Indicators.BorrowerName AND DateUpdated<=#12/31/2018#
ORDER BY Dupe.DateUpdated DESC)
) AS Q1
ON BC_Info.BorrowerName = Q1.BorrowerName;
If you don't like TOP N, adjust your original query:
SELECT BCI.BorrowerName, BCI.PartnerID, BFI.Fin_ID, BFI.DateUpdated
FROM ((BC_Info AS BCI
INNER JOIN BF_Indicators AS BFI
ON BFI.BorrowerName = BCI.BorrowerName)
INNER JOIN
(
SELECT BorrowerName, MAX(DateUpdated) AS MAX_DATE
FROM BF_Indicators
WHERE (DateUpdated <= #12/31/2018#)
GROUP BY BorrowerName
) AS Last_BF ON BFI.BorrowerName = Last_BF.BorrowerName AND
BFI.DateUpdated = Last_BF.MAX_DATE);
And 1 more to think about:
SELECT BC_Info.PartnerID, BC_Info.BorrowerName, BF_Indicators.Fin_ID, BF_Indicators.DateUpdated
FROM BC_Info RIGHT JOIN BF_Indicators ON BC_Info.BorrowerName = BF_Indicators.BorrowerName
WHERE (((BF_Indicators.DateUpdated)=DMax("DateUpdated","BF_Indicators","BorrowerName='" & [BC_Info].[BorrowerName] & "' AND DateUpdated<=#12/31/2018#")));

Get count of latest consecutive daily logins

I have a SQL table containing a list of the daily logins of the subscribers to my site. The rows contain the user id and the date and time of the first login of each day, which means there is a maximum of one record per day for each member.
Is there a way to use SQL to get a count of the number if consecutive daily logins for each member, that is the latest login streak?
I could do this programmatically (C#) by going through each record for a user in reverse order and stop counting when a day is missing, but I was looking for a more elegant way to do this through a SQL function. Is this at all possible?
Thanks!
Answer from comment
You can use Lag function https://msdn.microsoft.com/en-IN/library/hh231256.aspx
If your database compatibility level is lower than 110 you cant use Lag function
The following code must get the latest streak of logins for you (only when there is record for 1st login of the day)
suppose if your table of dates for a single user is
pk_id dates
----------- -----------
27 2017-04-02
28 2017-04-03
29 2017-04-04
30 2017-04-05
31 2017-04-06
44 2017-04-09
45 2017-04-10
46 2017-04-11
47 2017-04-12
48 2017-04-13
then
SELECT ROW_NUMBER() OVER(ORDER BY dates desc) AS Row#,dates into #temp1 FROM
yourTable where userid = #userid
select top 1 a.Row# As LatestStreak from #temp1 a inner join #temp1 b on a.Row# = b.Row#-1
where a.dates <> DATEADD(DAY,1,b.dates) order by a.Row# asc
this gives you 5, I have used Inner Join so that it wont have server compatibility issue
or you can use this, you can get the dates in the last streak too if you use c.dates instead of count(*)
SELECT COUNT(*)
FROM
(SELECT MAX (a.dates) AS Latest
FROM #yourtable a
WHERE DATEADD(DAY,-1,dates)
NOT IN (SELECT dates FROM #yourtable)) AS B
JOIN #yourtable c
ON c.dates >= b.Latest
This solution is probably similar to what you want:
How do I group on continuous ranges
It has a link to the motivating explanation here:
Things SQL needs: SERIES()
The main idea is that if after you have grouped by individual id's, and ordered by dates, the difference between date and current row is an invariant within each series of consecutive dates. So you group by user and this invariant (and min date within this group). And then you can group by user and pick the count of the 2nd column and only pick the max count.

SQL Join Tables on Closest Date BEFORE Shipped Date

This may appear to be a repeat question but it is not because the other solutions in the forum don't work in this situation.
This is a query to our ERP database that is trying to get the final cost of goods sold total for parts. Basically the ERP makes it easy to get all the direct costs out but doesn't calculate scrap costs.
Where I'm stuck is in the sub query in the FROM section marked:
>>>>>>HELP NEEDED STARTING HERE
The sub query, as it is now written, pulls out all the shipments to our scrap vendor and gets a monthly average rate per pound, then joins to the other tables based on alloy type, month and year.
My Finance Department has told me the average is not a good solution, since some metal prices fluctuate too much or they don't sell the scrap metal in the same month the parts shipped so this won't work.
I need to get the rate we are paid for scrap metal from the closest date before the part was shipped from our facility.
I've found other examples on Stack Overflow that show ways to do this but the main tables and the sub query tables overlap so the other solutions I've seen have failed. I have commented in the code below to show and explain this.
I'm totally open to the idea I've approached this wrong. How can I make this work?
DECLARE #Date_From AS DATETIME;
DECLARE #Date_To AS DATETIME;
SET #Date_From = '2016-10-01 00:00:00.000';
SET #Date_To = GETDATE() ;
-- Start Main query
SELECT TOP 10
CCustomer.Customer_Type AS 'Industry'
,SShipper.Ship_Date AS 'Ship_Date'
,SSContainer.Serial_No AS 'Serial_No'
,PPart.Grade AS 'Alloy'
,tbl_ScrapValue.Scrap_Value_per_lb AS 'Scap_Value_per_lb'
FROM
Sales_v_Shipper_Line AS SSLine
JOIN Sales_v_Shipper AS SShipper
ON SShipper.Shipper_Key = SSLine.Shipper_Key
JOIN Part_v_Part AS PPart
ON SSLine.Part_Key = PPart.Part_Key
JOIN Common_v_Customer AS CCustomer
ON SShipper.Customer_No = CCustomer.Customer_No
-- >>>>>>HELP NEEDED STARTING HERE
-- Below is the sub query that pulls the scrap sales value per pound.
-- The key point is that both shipments to our customers of real parts,
-- and the 'shipments' of scrap metal sales come from the same tables,
-- mainly Part_v_Part and Sales_v_Shipper, because of that the other
--solutions for the 'join by closest date' in the forums don't work.
LEFT OUTER JOIN (SELECT
MONTH(SShipper.Ship_Date) AS 'Scrap_Ship_Month'
,YEAR(SShipper.SHip_Date) AS 'Scrap_Ship_Year'
,PPart.Grade AS 'Alloy'
,AVG(AARIDist.Unit_Price) AS 'Scrap_Value_per_lb'
FROM
Sales_v_Shipper AS SShipper
JOIN Sales_v_Shipper_Line AS SS_Line
ON SShipper.Shipper_Key = SS_Line.Shipper_Key
JOIN Part_v_Part AS PPart
ON SS_Line.Part_Key = PPart.Part_Key
JOIN Common_v_Customer AS CCustomer
ON SShipper.Customer_No = CCustomer.Customer_No
WHERE CCustomer.Customer_Code = 'Scrap_Vendor'
AND SSHipper.Ship_Date <= #Date_To
GROUP BY
MONTH(SShipper.Ship_Date)
,YEAR(SShipper.SHip_Date)
,PPart.Grade
) AS tbl_ScrapValue
ON PPart.Grade = tbl_ScrapValue.Alloy
AND
YEAR(SShipper.Ship_Date) = YEAR(tbl_ScrapValue.Scrap_Ship_Year)
AND
MONTH(SShipper.Ship_Date) =(tbl_ScrapValue.Scrap_Ship_Month)
--- >>>>HELP NEEDED ENDS HERE
WHERE
AND SShipper.Ship_Date >= #Date_From
AND SSHipper.Ship_Date <= #Date_To
GROUP BY
SShipper.Shipper_No
,SShipper.Ship_Date
,CCustomer.Customer_Type
,SSContainer.Quantity
,PPart.Grade
Here's a sample output from the query above, as you can see the 'Scrap_Value_per_lb' is failing:
[![Sample_Output][1]][1]
Industry Ship_Date Serial_No Alloy Scap_Value_per_lb
Material Processing 17-Oct-16 4:47:00 PM S472091 C182 NULL
Material Processing 17-Oct-16 4:47:00 PM S472210 C182 NULL
Material Processing 17-Oct-16 4:47:00 PM S472211 C182 NULL
Electronics 17-Oct-16 4:27:00 PM S436738 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463290 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463315 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463327 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463333 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463345 C180 NULL
Electronics 17-Oct-16 4:27:00 PM S463354 C180 NULL
Update
This was edited a second time #7am 10/19/2016 to simplify code further, added comments in code to clarify based on feedback from others.
So, if I understand it correctly, you want the sub query to return only ONE row (Same for every row in the outer query?)
SELECT Top 1
SShipper.Ship_Date
,PPart.Grade AS 'Alloy'
,AARIDist.Unit_Price AS 'Scrap_Value_per_lb'
FROM
Sales_v_Shipper AS SShipper
JOIN Sales_v_Shipper_Line AS SS_Line
ON SShipper.Shipper_Key = SS_Line.Shipper_Key
JOIN Part_v_Part AS PPart
ON SS_Line.Part_Key = PPart.Part_Key
JOIN Common_v_Customer AS CCustomer
ON SShipper.Customer_No = CCustomer.Customer_No
JOIN Accounting_v_AR_Invoice_Dist AS AARIDist
ON SS_Line.Shipper_Line_Key = AARIDist.Shipper_Line_Key
WHERE CCustomer.Customer_Code = 'Scrap_Value'
AND SSHipper.Ship_Date <= #Date_To
AND AARIDIst.Unit_Price < AARIDist.Quantity
AND AARIDist.Unit_Price > '0'
Order By SShipper.Ship_Date Desc
) AS tbl_SValue
If the value of the sub query should be different for each row of the outer query, then I need to know how each row in the sub query is joined to the row in the outer query
So when you sell a metal of a certain alloy, you sell everything that's available, i.e. no remainders?
Then you take the records from the buying table and join them with the next selling record for the same alloy. This can be achieved with a cross apply. Here is a query with simple tables to give you the idea what's needed:
select
year(matched_sold.sold_date),
month(matched_sold.sold_date),
sum(bought.amount * bought.price)
from bought
cross apply
(
select top 1 *
from sold
where sold.alloy = bought.alloy
and sold.sold_date > bought.bought_date
order by sold.sold_date desc
) matched_sold
group by
year(matched_sold.sold_date),
month(matched_sold.sold_date);
I don't know, whether I got your problem right. You want to join two result sets by using a JOIN on a date field, where there are no exact matches guaranteed. Possibly you could use the ROW_NUMBER function to generate partitioned row numbers including a sort and then join on the row numbers with e.g. ROW_NR = 1.
TABLE 1 TABLE 2
ROW_NR DATE ID ROW_NR DATE ID
------ ---------- -- ------ ---------- --
1 10/25/2016 1 -match- 1 10/27/2016 1
2 10/24/2016 1
3 10/20/2016 1
4 10/19/2016 1
1 10/23/2016 2 -match- 1 10/28/2016 2
2 10/15/2016 2
3 10/09/2016 2
4 10/08/2016 2
Row Numbering in Table 1:
Data for TABLE1 with TABLE1.DATE <= TABLE2.DATE
Partitioned by ID
Sorted by ID and DATE DESC
Row Numbering in Table 2:
ROW_NR is always 1
With that you can join implicitly on the data fields without an exact match. Sorry for not providing a SQL Statement.
I spent a couple of hours thinking about this question and boiled the sample code down even further and re-posted the question here.
I think it makes more sense and I thank all of you who responded and tried to help answer what I wrote. It helped but I still could not get it to work.

T-SQL find all records in a group with a max last modified date beyond a specific threshold

I have a Database table that has all the information I need arranged like so:
Inventory_ID | Dealer_ID | LastModifiedDate
Each Dealer_ID is attached to multiple Inventory_ID's. What I need is a query that calculates the Max Value LastModifiedDate for each dealer ID and then gives me a list of all the Dealer_ID's that have a last modified date beyond the last 30 days.
Getting The max last modified date for each Dealer_ID is simple, of course:
Select Dealer_ID, Max(LastModifiedDate)as MostRecentUpdate
from Inventory group by Dealer_ID order by MAX(LastModifiedDate)
The condition for records older than 30 day is also fairly simple:
LastModifiedDate < getdate() - 30
Somehow, I just can't figure out a way to combine the two that works properly.
Use HAVING:
Select Dealer_ID, Max(LastModifiedDate)as MostRecentUpdate
from Inventory
group by Dealer_ID
having LastModifiedDate < getdate() - 30
order by MAX(LastModifiedDate)
Check this query:
Select DT.DealerID, DT.MostRecentUpdate
(Select DealerID, Max(LastModifiedDate)as MostRecentUpdate
From YourTable
Group BY DealerID) DT
where DT.MostRecentUpdate < GETDATE() - 30

Advice on software / database design to avoid using cursors when updating database

I have a database that logs when an employee has attended a course and when they are next due to attend the course (courses tend to be annual).
As an example, the following employee attended course '1' on 1st Jan 2010 and, as the course is annual, is due to attend next on the 1st Jan 2011. As today is 20th May 2010 the course status reads as 'Complete' i.e. they have done the course and do not need to do it again until next year:
EmployeeID CourseID AttendanceDate DueDate Status
123456 1 01/01/2010 01/01/2011 Complete
In terms of the DueDate I calculate this in SQL when I update the employee's record e.g. DueDate = AttendanceDate + CourseFrequency (I pull course frequency this from a separate table).
In my web based app (asp.net mvc) I pull back this data for all employees and display it in a grid like format for HR managers to review. This allows HR to work out who needs to go on courses.
The issue I have is as follows.
Taking the example above, suppose today is 2nd Jan 2011. In this case, employee 123456 is now overdue for the course and I would like to set the Status to Incomplete so that the HR manager can see that they need to action this i.e. get employee on the course.
I could build a trigger in the database to run overnight to update the Status field for all employees based on the current date. From what I have read I would need to use cursors to loop over each row to amend the status and this is considered bad practice / inefficient or at least something to avoid if you can???
Alternatively, I could compute the Status in my C# code after I have pulled back the data from the database and before I display it on screen. The issue with this is that the Status in the database would not necessarily match what is shown on screen which just feels plain wrong to me.
Does anybody have any advice on the best practice approach to such an issue?
It helps, if I did use a cursor I doubt I would be looping over more than 1000 records at any given time. Maybe this is such small volume that using cursors is okay?
Unless I'm missing something from your explanation, there's no need for cursors at all:
UPDATE
dbo.YourTable
SET
Status = ‘Incomplete’
WHERE
DueDate < GETDATE()
It would be preferable not to maintain the DueDate or Status for these records at all. I woudl expect to see Employee, Course, EmployeeCourse and EmployeeCourseAttendance tables, with which you could use the following:
-- Employees that haven't attended a course
-- within dbo.Course.Frequency of current date
SELECT
ec.EmployeeID
, ec.CourseID
, eca.LastAttendanceDate
, DATEADD(day, c.Frequency, eca.LastAttendanceDate) AS DueDate
FROM
dbo.EmployeeCourse ec
INNER JOIN
dbo.Course c
LEFT OUTER JOIN
ebo.EmployeeCourseAttendance eca
ON eca.EmployeeID = ec.EmployeeId
AND eca.CourseID = ec.CourseID
WHERE
GETDATE() > DATEADD(day, c.Frequency, eca.LastAttendanceDate)
-- Show all employees and status for each course
SELECT
ec.EmployeeID
, ec.CourseID
, eca.LastAttendanceDate
, DATEADD(day, c.Frequency, eca.LastAttendanceDate) AS DueDate
, CASE
WHEN eca.LastAttendanceDate IS NULL THEN 'Has not attended'
WHEN (GETDATE() > DATEADD(day, c.Frequency, eca.LastAttendanceDate) THEN 'Incomplete'
WHEN (GETDATE() < DATEADD(day, c.Frequency, eca.LastAttendanceDate) THEN 'Complete'
END AS Status
FROM
dbo.EmployeeCourse ec
INNER JOIN
dbo.Course c
LEFT OUTER JOIN
ebo.EmployeeCourseAttendance eca
ON eca.EmployeeID = ec.EmployeeId
AND eca.CourseID = ec.CourseID
You could also use a computed column expression. This way you would never have to update the STATUS column/keep it in sync with dates
create table coureses
(
employeeid int not null,
courseid int not null,
attendancedate datetime null,
duedate datetime null,
[status] as case
when duedate is null and attendancedate is null then 'n/a'
when datediff(day,duedate, getdate()) > 0 then 'Incomplete'
when datediff(day,attendancedate, getdate()) > 0 then 'Complete'
else 'n/a'
end
)
I wouldn't use a trigger for this, but schedule a job, probably within SQL server. A cursor is not required here either, surely it's just:
UPDATE TABLE SET Status = 'Incomplete' WHERE DueDate < GetDate()

Resources