I have a table in MS Access which holds staff details (tblStaff):
| Employee Number | Employee Name | Dept |
------------------------------------------------
| 205147 | Joe Bloggs | IT |
| 205442 | John Doe | Accounts |
I refresh this table with new data weekly and if any records have changed (e.g. changed dept) then they are archived in another table (tblArchiveStaff) along with the dates that that record was valid from and to.
| Employee Number | Employee Name | Dept | DateFrom | DateTo |
----------------------------------------------------------------------
| 205147 | Joe Bloggs | HR | 03/01/16 | 01/06/17 |
I am trying to write a query that will select records from either of these tables based on which ones were valid for a given date
We can assume that records in tblStaff are valid from the dateTo + 1 of the last entry for that employee in tblArchiveStaff, or from 03 Jan 16 if they have no archived records.
So far I have come up with the below query into which I have hardcoded the date condition of #07/01/2017#:
SELECT [Employee Number], [Dept], DateFrom, DateTo
FROM
(
SELECT ts.[Employee Number], ts.[Dept], nz(ta2.DateTo,#01/02/16#)+1 AS DateFrom, date() AS DateTo
FROM tblStaff ts LEFT JOIN tblArchiveStaff ta2 ON ts.[Employee Number] = ta2.[Employee Number]
UNION ALL
SELECT ta.[Employee Number], ta.[Dept], ta.DateFrom, ta.DateTo
FROM tblArchiveStaff ta
) AS tblUnion
WHERE #07/01/2017# BETWEEN DateFrom AND DateTo;
As I understand it the above query should return records valid on July 1st 2017 which would be both records in tblStaff, it is however returning the record in tblArchiveStaff. It is almost like it is treating the date condition as 7 January which would mean it is formatted dd/mm/yyyy which I thought was impossible.
Can anyone explain this please?
That is because you don't play by the rules. Always handle dates as date values, not strings, not numbers, no exceptions.
So, when you use Nz and even add 1, the union query cannot figure out the datatype, thus it falls back to return the result as text. Then DateFrom becomes Text while DateTo is Date which makes filtering a wild guess.
*Edit - I have also amended the first part of the union query to ensure the current record follows on from the most recent archived record)
Correct as this:
SELECT
tblUnion.[Employee Number],
tblUnion.[Dept],
tblUnion.DateFrom,
tblUnion.DateTo
FROM
(SELECT
ts.[Employee Number],
ts.[Dept],
DateAdd("d", 1, Nz(ta2.MaxDateTo, #01/02/16#)) AS DateFrom,
Date() AS DateTo
FROM
tblStaff ts
LEFT JOIN
(SELECT
[Employee Number],
max(DateTo) AS MaxDateTo
FROM
tblArchiveStaff
GROUP BY
[Employee Number]) ta2
ON ts.[Employee Number] = ta2.[Employee Number]
UNION ALL
SELECT
ta.[Employee Number],
ta.[Dept],
ta.DateFrom,
ta.DateTo
FROM
tblArchiveStaff ta) AS tblUnion
WHERE
#7/1/2017# Between [DateFrom] And [DateTo];
and you will get the desired output:
Employee Number Dept DateFrom DateTo
205147 IT 2017-01-07 2017-07-05
205442 Accounts 2016-01-03 2017-07-05
Standard date flipping errors. Try to always use neutral date formats in your code like
d MMM yyyy
1 Jul 2017 (viable but not recommended because of language differences)
or
yyyy-MM-dd
2017-07-01 (recommended, will work everywhere)
Related
I have an internal Department that will be switching account numbers in the future. I am trying to build a stored procedure to return only one of two rows based upon an effective date.
Here's the data makeup
|ID | EffectiveDate | AccountNum |
|-- | ------------- | ---------- |
| 1 | 2021-01-01 | 350000 |
| 2 | 2021-09-01 | 950000 |
I know this returns all the data
SELECT Id, EffectiveDate, AccountNum
FROM Account
This returns only the first row and never the 2nd:
SELECT Id, EffectiveDate, DeptNum
FROM Account
WHERE EffectiveDate <= GETDATE()
How would I dynamically return only 1 row based upon today's date?
So if the Effective date is less than today's date get row one. If the Effective Date is equal to or greater than todays date get row two.
I figured out the solution. Select only the top 1 deptnum from one of two possible answers given that the EffectiveDate is less than or equal to the current date and use an order by desc too. This ensures that the original EffectiveDate is obtained because the future EffectiveDate has not passed current date. When the future date is equal to or is earlier than the current date then it's value will be returned.
select TOP 1 deptnum
from Account
where EffectiveDate <= GETDATE()
order by EffectiveDate desc
I have two tables. One contains a list of employees and their information
EmployeeID | Name | Start Date |HoursCF | HoursTaken
------------+-------+-------------+--------+------------
1 | Conor | 15/10/2018 | 0 |0
2 | Joe | 01/05/2018 | 0 |0
3 | Tom | 01/01/2019 | 0 |0
The other Contains Holiday Request put in by Employees
EmployeeID | HoursTaken |
------------+----------------+
1 | 8 |
2 | 16 |
3 | 8 |
2 | 8 |
1 | 16 |
I want it so when a new Holiday request is created,deleted,or updated on my holiday request table it updates on my employee table such as
EmployeeID | Name | Start Date |HoursCF | HoursTaken
-----------+-------+-------------+--------+------------
1 | Conor | 15/10/2018 | 0 |24
2 | Joe | 01/05/2018 | 0 |24
3 | Tom |01/01/2019 | 0 |8
I have tried creating a view
CREATE VIEW vw_HoursTakenPerEmployee AS
SELECT e.[EmployeeID],
COALESCE(SUM(hr.[HoursTaken]), 0) AS HoursTaken
FROM [dbo].[Employees] e LEFT JOIN
[dbo].[HolidayRequests] hr
ON e.[EmployeeID] = hr.[EmployeeID]
GROUP BY e.[EmployeeID];
Ans then using a trigger to insert new data entered into the holiday request table into the employee table
ALTER trigger Inserttrigger on [dbo].[HolidayRequestForm]
after INSERT, UPDATE, DELETE
as
begin
TRUNCATE TABLE [dbo].[HoursTakenPerEmployee]
INSERT INTO [dbo].[HoursTakenPerEmployee] ([EmployeeID],[HoursTaken])
SELECT * FROM vw_HoursTakenPerEmployee;
end
I know the problem is the truncate statement as it works fine if all the employees already have an entry in the holiday request table. If they don't, they get truncated from the employees table any time a new holiday request is made that does not belong to them.
Any Thoughts?
You've already done all the work:
CREATE VIEW vw_HoursTakenPerEmployee AS
SELECT e.*,
COALESCE(SUM(hr.[HoursTaken]), 0) AS HoursTaken
FROM [dbo].[Employees] e LEFT JOIN
[dbo].[HolidayRequests] hr
ON e.[EmployeeID] = hr.[EmployeeID]
GROUP BY e.[EmployeeID];
Drop the hours taken column from the EmployeeTable. Any time you want to know how many hours an employee took for holiday, query the view. The view query will be re-run every time the view is queried, so it will be up to date
Bear in mind your system will only work for a year. I recommend that you add a year indicator to your HolidayRequests table so you can give employees a new allowance every new fiscal/holiday year they're at the company.
Also, if an employee starts part way through a year, you can have the view calculate how many hours they're entitled to:
CREATE VIEW vw_HoursTakenPerEmployee AS
SELECT
e.*,
hr.*,
DATEDIFF(HOUR, e.HolEntitleFrom, e.HolEntitleTo) / e.HolidayEntitlementHours as HoursEarnedSoFar,
COALESCE(hr.HoursTaken), 0) AS HoursTaken
FROM
(
SELECT *,
--change "Start Date" column name so it doesn't have a space in it!
CASE WHEN [Start Date] < d.HolidayYearStart THEN d.HolidayYearStart ELSE [Start Date] END as HolEntitleFrom,
CASE WHEN TerminationDate IS NULL THEN GetDate() ELSE TerminationDate as HolEntitleTo,
FROM
[dbo].[Employees]
--useful constants can go here, like when holiday year starts from
CROSS JOIN
(SELECT DATEFROMPARTS(YEAR(GETDATE()), 1, 1) as HolidayYearStart) d
) e
LEFT JOIN
(
SELECT employeeid, HolidayYear, SUM(HoursTaken) AS HoursTaken
FROM [dbo].[HolidayRequests]
GROUP BY EmployeeID, holidayyear
) hr
ON hr.EmployeeID = e.EmployeeID AND
hr.holidayYear = YEAR(e.HolEntitleFrom) --this year's holiday requests
;
as an example..
Put a HolidayYear column in HolidayRequests, to track which holiday year the request was made in. INT, data like 2018, 2019..
Put a HolidayEntitlementHours in Employee to track how many holiday hours the emp gets this year (most places have a system where the amount of holiday increases for each full year of service so year on year). Float/decimal type.. Or make it something the view calculates from a basic allowance plus a time-in-service calculation
Put a column for the terminationdate of an emp, so you can get an indication of whether they overspent their holidays (if your place allows to take more hours than have been earned so far). Imagine the emp took 20 days in jan, and then has to work a year to earn it. They hand in 1 months notice on 1 june. Setting a termination date of 1 july (about 6 months after the holiday entitlement start) would show them as having maximally earned 10 by the time they leave, so they need to pay back 10
I have a report that is grouped on week number but for presentation reasons want it to be week commencing.
Select
datepart(wk,[rhStartTime]) as [week number]
...
group by datepart(wk,[rhStartTime]),[rhOperatorName])
where
[week number] >= #StartWeek
and [week number] <= #EndWeek
My report parameters use week number to filter the data with #StartWeek and #EndWeek being integers that plug into the SQL. My question is one of presentation. It is tough for users to understand what Week 15 means in context so I would like to alter my output to show Week Commencing rather than week number but for the backend to still use weeknumber. I also don't want users to be able to pick any date because they will invariably pick dates that span multiple weeks without a full weeks data.
I look at similar questions and one here
SO question
recommended SQL of the format
DATEADD(dd, -(DATEPART(dw, WeddingDate)-1), WeddingDate) [WeekStart]
But plugging my columns into that format gave me a bit of a mess. It didn't group how I was expecting.
SELECT
DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])) as [week commencing]
,datepart(wk,[rhStartTime])) as [week number]
...
group by datepart(wk,[rhStartTime])),DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])),[rhoperatorname]
I got this output
where I was looking for all those week 15s to be grouped together with just one week commencing date.
Try This will work.This retrieves the dates eliminating time part of it
SELECT
Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time])) as [week number]
...
from Table X
group by Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time]))
,[Agent Name]
I think your problem is in how you are using the examples you have seen elsewhere and not with the examples themselves, as I have just tested the logic and it seems to be working for me without issue, as you can see in the script below.
I think your main problem is that you are not removing the time portion of your StartTime values, which you will need to do if you want to group all values that occur on the same day. The easiest way to do this is to simply cast or convert the values to date data types:
select cast(StartTime as date) as CastToDate
,convert(date, StartTime, 103) as ConvertToDate -- You may need to use 101 depending on your server setting for dd/mm/yyyy or mm/dd/yyyy
Script:
declare #StartDate date = '20170325'
,#EndDate date = '20170403';
-- Tally table to create dates to use in functions:
with n(n) as(select n from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n(n))
,d(d) as(select top(datediff(d,#StartDate,#EndDate)+1) dateadd(d,row_number() over (order by (select null))-1,#StartDate) from n n1,n n2,n n3,n n4,n n5,n n6)
select d
,datepart(week,d) as WeekNumber
,DATEADD(dd, -(DATEPART(dw, d)-1), d) as WeekCommencing
from d
order by d;
Output:
+------------+------------+----------------+
| d | WeekNumber | WeekCommencing |
+------------+------------+----------------+
| 2017-03-25 | 12 | 2017-03-19 |
| 2017-03-26 | 13 | 2017-03-26 |
| 2017-03-27 | 13 | 2017-03-26 |
| 2017-03-28 | 13 | 2017-03-26 |
| 2017-03-29 | 13 | 2017-03-26 |
| 2017-03-30 | 13 | 2017-03-26 |
| 2017-03-31 | 13 | 2017-03-26 |
| 2017-04-01 | 13 | 2017-03-26 |
| 2017-04-02 | 14 | 2017-04-02 |
| 2017-04-03 | 14 | 2017-04-02 |
+------------+------------+----------------+
Replace the field value in your SQL code with the expression below to remove time
DATEADD(dd, -(DATEPART(dw,[rhStartTime]) -1), DATEDIFF(dd, 0, [rhStartTime]) )
You can also achieve the same result by using the expression below in SSRS (change it to match your date field)
= DATEADD("d", - DATEPART(DateInterval.Weekday,Fields!rhStartTime.Value) +1,Fields!rhStartTime.Value)
Thanks for the answers. I'm sure they probably would have worked if I were more competent. In the end I created a simple table on my server with year,weeknumber,commencedate as the column headings and manually created them in excel. Then I linked my results as a cte to that table where year = 2017 and cte.weeknumber = commencedate.weeknumber It seems to have worked.
Now in my SSRS report parameter I am using weeknumber as the value and commence date as the label. So I don't have to change any of the other configuration.
I recently posted a question about a SQL Where Statement/Grouping here:
SQL statement using WHERE from a GROUP or RANK
Now I've got somewhat of a follow-up.
So similar to the previous question, let's assume I have a table of say 35,000 rows with these columns:
Sales Rep | Parent Account ID| Account ID | Total Contract Value | Date
Each row is individual by account id but multiple account IDs can fall under a parent account ID.
Similar to the responses on the first question, this is probably going to be a table w/i a table. So first, everything has to be grouped by Sales Rep. From that, everything needs to be grouped by Parent Account ID where the grouped total contract value of all the accounts is >= 10,000. Then everything will be displayed and ranked by the total TCV of the Parent account ID and I need the top 35 Parent account IDs by agent.
So the first couple of lines of data may look like this:
Sales Rep | Parent Account ID| Account ID | Total Contract Value | Date | Rank
John Doe | ParentABC12345 | ABC425 | 5,000 | 1/2/2013 |1
John Doe | ParentABC12345 | ABC426 | 10,000 | 1/2/2013 |1
John Doe | ParentDJE12345 | DJE523 | 11,000 | 1/2/2013 |2
John Doe | ParentFBC12345 | FBC6723 | 4,000 | 1/2/2013 |3
John Doe | ParentFBC12345 | FBC6727 | 4,000 | 1/2/2013 |3
Notice how the ranking works based off of the parent Account ID. The account ID DJE523 has the single greatest TCV but it's ranked second b/c the grouped value of parent account ID ParentABC12345 is greater. So there would be a ranking of 35 parent account IDs but in that ranking their could be say 100+ lines of actual data.
Any thoughts?
Always nice to follow up. The "parent rank" is added as an INNER JOIN.
Edit: As correctly mentioned by Dan Bracuk, my first answer was not correct. I altered the query to meet the correct conditions. I also applied the timespan to the Parent Account's.
DECLARE #minimumValue decimal(20,2) = 10000
DECLARE #numberOfAccounts int = 35
DECLARE #from datetime = '1/1/2013'
DECLARE #till datetime = DATEADD(MONTH, 1, #from)
SELECT
[sub].[Sales Rep],
[sub].[Rank],
[sub].[Account ID],
[sub].[Total Contract Value],
[sub].[Parent Account ID],
[sub].[Total],
[sub].[ParentRank]
FROM
(
SELECT
[s].[Sales Rep],
[s].[Account ID],
[s].[Total Contract Value],
DENSE_RANK() OVER (PARTITION BY [s].[Sales Rep] ORDER BY [s].[Total Contract Value] DESC) AS [Rank],
[p].[Parent Account ID],
[p].[Total],
[p].[ParentRank]
FROM [Sales] [s]
INNER JOIN
(
SELECT
[Parent Account ID],
SUM([Total Contract Value]) AS [Total],
RANK() OVER(ORDER BY SUM([Total Contract Value]) DESC) AS [ParentRank]
FROM [Sales]
WHERE[Date] > #from AND [Date] < #till
GROUP BY [Parent Account ID]
HAVING SUM([Total Contract Value]) > #minimumValue
) AS [p] ON [s].[Parent Account ID] = [p].[Parent Account ID]
WHERE [Date] > #from AND [Date] < #till
) AS [sub]
WHERE [sub].[Rank] <= #numberOfAccounts
ORDER BY
[Sales Rep] ASC,
[ParentRank] ASC,
[Rank] ASC
And here is a new Fiddle.
I think this will do it for you, if you're using SQL Server:
Select top 35
SalesRep,
ParentAccountId,
sum(TotalContractValue) from Table
group by SalesRep, ParentAccountId
order by sum(TotalContractValue) desc
I've a BillDate as date, and a Mark as bit column in First table.
(Mark=0 by default)
In Second table I've FromDate as date, and ToDate as date Column.
I want to set Mark=1 if BillDate is exists between FromDate & ToDate
Let Say In First Table the data is
----------------------------
BillDate | Mark
----------------------------
2012-11-10 11:15:30 | 0
2012-12-12 09:00:00 | 0
In Second Table the data is
---------------------------------------------
FromDate | ToDate
---------------------------------------------
2012-11-01 07:00:00 | 2012-11-09 23:59:59
2012-12-08 07:00:00 | 2012-12-15 23:59:59
So in the above scenario only the second row from First table
which is having, BillDate->2012-12-12 09:00:00 will be Mark as 1
because it comes between second row of second table
I hope I've explained my scenario,
You haven't said what flavor of SQL you are using and if there are any other fields which link your tables. Therefore assuming a cross join how about this:
Update ft
SET mark = 1
FROM FirstTable ft, SecondTable st
WHERE BillDate BETWEEN StartDate AND EndDate
I think this will work in most/all DMBS (as none specified)
UPDATE FirstTable
SET Mark = 1
WHERE EXISTS
( SELECT 1
FROM SecondTable
WHERE FirstTable.BillDate BETWEEN SecondTable.Fromdate AND SecondTable.ToDate
);