SQL Select Statement for Time and attendance for a month - sql

Anyone can help with this one please? Our attendance system generates the following data:
Empid Department Timestamp Read_ID
3221 IT 2017-01-29 11:12:00.000 1
5565 IT 2017-01-29 12:28:06.000 1
5565 IT 2017-01-29 12:28:07.000 1
3221 IT 2017-01-29 13:12:00.000 2
5565 IT 2017-01-29 13:28:06.000 2
3221 IT 2017-01-30 07:42:15.000 1
3221 IT 2017-01-30 16:16:15.000 2
3221 IT 2017-01-31 09:05:00.000 1
3221 IT 2017-01-31 11:05:00.000 2
3221 IT 2017-01-31 13:20:00.000 1
3221 IT 2017-01-31 16:10:00.000 2
Where Read_ID value are :
1 = Entry
2 = Exit
I'm looking for SQL query to run on MS SQL 2014 that summarize attendance time for each employee on monthly basis, for instance
Empid Department Year Month TotalHours
3221 IT 2017 1 15:24
5565 IT 2017 1 01:00

This query should give you the result you need. It works by selecting each entries, and joining it with the next exit of the same employee (entries without further exits are ignored) : this gives us the duration of this employee shift. Then results are aggregated and shift durations are sumed in each group.
SELECT
t1.empid,
t1.department,
YEAR(t1.timestamp) Year,
MONTH(t1.timestamp) Month,
CONVERT(
varchar(12),
DATEADD(minute, SUM(DATEDIFF(minute, t1.timestamp, t2.timestamp)), 0),
114
) TotalHours
FROM
mytable t1
INNER JOIN mytable t2
ON t1.empid = t2.empid
AND t2.read_id = 2
AND t2.timestamp = (
SELECT MIN(timestamp)
FROM mytable
WHERE
read_id = 2
AND empid = t2.empid
AND timestamp > t1.timestamp
)
WHERE
t1.read_id = 1
GROUP BY t1.empid, t1.department, YEAR(t1.timestamp), MONTH(t1.timestamp)
ORDER BY 1, 2, 3, 4
Returns :
empid | department | Year | Month | TotalHours
----: | :--------- | ---: | ----: | :-----------
3221 | IT | 2017 | 1 | 15:24:00:000
5565 | IT | 2017 | 1 | 02:00:00:000
DB Fiddle demo on SQL Server 2014
There is an edge case, however, where an employee enters twice and then exists (this happens in your data, where employee 5565 enters at 29/01/2017 12:28:06 and at 29/01/2017 12:28:07, and then exits at 29/01/2017 13:28:06. The above query will take in account the two overlaping entries and map them to the same exit, resulting in this hour of work being counted twice.
While this matches your expected results, is this what you really want ? Here is an alternative query that , if several consecutive of the same employee entries happen, only takes in account the latest one :
SELECT
t1.empid,
t1.department,
YEAR(t1.timestamp) Year,
MONTH(t1.timestamp) Month,
CONVERT(
varchar(12),
DATEADD(minute, SUM(DATEDIFF(minute, t1.timestamp, t2.timestamp)), 0),
114
) TotalHours
FROM
mytable t1
INNER JOIN mytable t2
ON t1.empid = t2.empid
AND t2.read_id = 2
AND t2.timestamp = (
SELECT MIN(timestamp)
FROM mytable
WHERE
read_id = 2
AND empid = t2.empid
AND timestamp > t1.timestamp
)
WHERE
t1.read_id = 1
AND NOT EXISTS (
SELECT 1
FROM mytable
WHERE
read_id = 1
AND empid = t1.empid
AND timestamp > t1.timestamp
AND timestamp < t2.timestamp
)
GROUP BY t1.empid, t1.department, YEAR(t1.timestamp), MONTH(t1.timestamp)
ORDER BY 1, 2, 3, 4
Returns :
empid | department | Year | Month | TotalHours
----: | :--------- | ---: | ----: | :-----------
3221 | IT | 2017 | 1 | 15:24:00:000
5565 | IT | 2017 | 1 | 01:00:00:000
DB fiddle

Try this. I was not sure what time format would satisfy your system, so I put both:
SELECT * INTO #Tbl3 FROM (VALUES
(3221,'IT','2017-01-29 11:12:00.000',1),
(5565,'IT','2017-01-29 12:28:06.000',1),
(5565,'IT','2017-01-29 12:28:07.000',1),
(3221,'IT','2017-01-29 13:12:00.000',2),
(5565,'IT','2017-01-29 13:28:06.000',2),
(3221,'IT','2017-01-30 07:42:15.000',1),
(3221,'IT','2017-01-30 16:16:15.000',2),
(3221,'IT','2017-01-31 09:05:00.000',1),
(3221,'IT','2017-01-31 11:05:00.000',2),
(3221,'IT','2017-01-31 13:20:00.000',1),
(3221,'IT','2017-01-31 16:10:00.000',2))
x (Empid,Department,Timestamp,Read_ID)
;With cte as (
SELECT t1.Empid, t1.Department
, [Year] = Year(t1.Timestamp)
, [Month] = Month(t1.Timestamp)
, Seconds = SUM(DATEDIFF(second, t1.Timestamp, t2.Timestamp))
FROM #Tbl3 as t1
OUTER APPLY (
SELECT Timestamp = MIN(t.Timestamp)
FROM #Tbl3 as t
WHERE t.Department = t1.Department and t.Empid = t1.Empid
and t.Timestamp > t1.Timestamp and t.Read_ID = 2
) as t2
WHERE t1.Read_ID = 1
GROUP BY t1.Empid, t1.Department, Year(t1.Timestamp), Month(t1.Timestamp))
SELECT *, TotalHours = Seconds / 3600., TotalTime =
RIGHT('0'+CAST(Seconds / 3600 as VARCHAR),2) + ':' +
RIGHT('0'+CAST((Seconds % 3600) / 60 as VARCHAR),2) + ':' +
RIGHT('0'+CAST(Seconds % 60 as VARCHAR),2)
FROM cte;

Related

how to know the changed name in table by date_key

i have a table with 3 value
Date_key | user_name | user_id
2022-07-12 | milkcotton | 1
2022-09-12 | cereal | 2
2022-06-12 | musicbox1 | 3
2022-12-31 | harrybel1 | 1
2022-12-25 | milkcotton1| 4
2023-01-01 | cereal | 2
i want to know the user who changed the user_name in 1 semester (01 july 2022 - 31 december 2022). Can i do this?
my expected value is:
previous_name| new_name | user_id
milkcotton | harrybel1 | 1
Thank you!
know the changed of the user_name from 1 table
Note: This is done in Postgres SQL. This should be similar in most of the SQL engines. Date functions could slightly different in other SQL engines.
Try this:
with BaseTbl as(
select *,
cast(to_char(Date_key, 'YYYYMM') as int) as year_month,
cast(to_char(Date_key, 'MM') as int) as month,
row_number() over(partition by user_id order by date_key desc) as rnk
from Table1
),
LatestTwoChanges as(
select *
from BaseTbl
where user_id in (select user_id from BaseTbl where rnk=2 )
and rnk <=2
)
select
t2.user_name as previous_name,
t1.user_name as new_name,
t1.user_id
from LatestTwoChanges t1
join LatestTwoChanges t2
on t1.user_id=t2.user_id
where t1.rnk=1
and t2.rnk=2
and t1.year_month-t2.year_month <6
and t1.user_name <> t2.user_name
and (t1.month + t2.month <= 12 or t1.month + t2.month >=14 )
-- this is to check whether the date falling in the same semester.
SQL fiddle demo Here
Here, the table t1 contains the latest changes and table t2 contains the previous changes for a user_id.
The last filter condition
and (t1.month + t2.month <= 12 or t1.month + t2.month >=14 )
is to make sure that the two dates are falling in the same semester or not . which means the two months should be either between 1 and 6 or 7 and 12

Group by month and name SQL

I need some help with SQL.
I have
Table1 with columns Id, Date1 and Date2
Table2 with columns Table1Id and Table2Id
Table3 with columns Id and Name
Here is my try:
with tmp_tab as (
select
v."Name" as name
, date_part('month', cv."OfferAcceptedDate") as MonthAcceptedName
, date_part('month', cv."OfferSentDate") as MonthSentName
, 1 as cntAcc
, 1 as cntSent
from hr_metrics."CvInfo" as cv
join hr_metrics."CvInfoVacancy" as civ
on civ."CvInfosId" = cv."Id"
join hr_metrics."Vacancy" as v
on civ."VacanciesId" = v."Id"
where cv."OfferSentDate" is not null
and date_part('year', cv."OfferSentDate") = date_part('year', CURRENT_DATE)
group by v."Name" , date_part('month', cv."OfferAcceptedDate"),
date_part('month', cv."OfferSentDate")
)
select distinct
tmp_tab."name" as name,
tmp_tab.MonthSentName as mSent,
tmp_tab.MonthAcceptedName as mAcc,
Sum(tmp_tab.cntSent) as sented,
Sum(tmp_tab.cntacc) as accepted
from tmp_tab as tmp_tab
group by tmp_tab.name, tmp_tab.MonthSentName, tmp_tab.MonthAcceptedName;
I need to take Count(date2)/Count(date1) grouped by monthes and name.
I have no idea how to do that, as there is no table with monthes.
DB - Postgres
sample data from comment:
t1
1 | 01/01/2021 | 31/03/2021
2 | 05/01/2021 | 18/01/2021
3 | 12/01/2021 | 31/01/2021
4 | 13/03/2021 | 22/03/2021
t2
1 | 1
2 | 1
3 | 2
4 | 1
t3
1 | SomeName1
2 | someName2
Desired result:
Name | month | value
SomeName1 | 1 | 1\2
SomeName1 | 3 | 2
SomeName2 | 1 | 1
Update: if count(date2) == 0, than count(date2) = -1
Source answer
Here code for my question thats work. And yeah, i've asked it on ru too.
select name, month, sum((SRC=1)::int) as AcceptedCount, sum((SRC=2)::int) as SentCount,
case when sum((SRC=1)::int) = 0 then -1
else sum((SRC=2)::int)::float / sum((SRC=1)::int) end as Result
from (
select v.name, SRC,
extract('month' from case SRC when 1 then OfferAcceptedDate else OfferSentDate end) as month
from (select (date_part('year', CURRENT_DATE)::char(4) || '-01-01')::timestamptz as from_date) x
cross join (select 1 as SRC union all select 2) s
join CvInfo as cv on (SRC=1 and cv.OfferAcceptedDate >= from_date and cv.OfferAcceptedDate < from_date + interval '1 year')
or (SRC=2 and cv.OfferSentDate >= from_date and cv.OfferSentDate < from_date + interval '1 year')
join CvInfoVacancy as civ on civ.CvInfosId = cv.Id
join Vacancy as v on civ.VacanciesId = v.Id
where case SRC when 1 then OfferAcceptedDate else OfferSentDate end is not null
) x
group by name, month

SQL query group by using day startdatetime and end datetime

I have the following table Jobs:
|Id | StartDateTime | EndDateTime
+----+---------------------+----------------------
|1 | 2020-10-20 23:00:00 | 2020-10-21 05:00:00
|2 | 2020-10-21 10:00:00 | 2020-10-21 11:00:00
Note job id 1 spans October 20 and 21.
I am using the following query
SELECT DAY(StartDateTime), COUNT(id)
FROM Job
GROUP BY DAY(StartDateTime)
To get the following output. But the problem I am facing is that day 21 is not including job id 1. Since the job spans two days I want to include it in both days 20 and 21.
Day | TotalJobs
----+----------
20 | 1
21 | 1
I am struggling to get the following expected output:
Day | TotalJobs
----+----------
20 | 1
21 | 2
One method is to generate the days that you want and then count overlaps:
with days as (
select convert(date, min(j.startdatetime)) as startd,
convert(date, max(j.enddatetime)) as endd
from jobs j
union all
select dateadd(day, 1, startd), endd
from days
where startd < endd
)
select days.startd, count(j.id)
from days left join
jobs j
on j.startdatetime < dateadd(day, 1, startd) and
j.enddatetime >= startd
group by days.startd;
Here is a db<>fiddle.
You can first group by with same start and end date and then group by for start and end date having different start and end date
SELECT a.date, SUM(counts) from (
SELECT DAY(StartDateTime) as date, COUNT(id) counts
FROM Table1
WHERE DAY(StartDateTime) = DAY(EndDateTime)
GROUP BY StartDateTime
UNION ALL
SELECT DAY(EndDateTime), COUNT(id)
FROM Table1
WHERE DAY(StartDateTime) != DAY(EndDateTime)
GROUP BY EndDateTime
UNION ALL
SELECT DAY(StartDateTime), COUNT(id)
FROM Table1
WHERE DAY(StartDateTime) != DAY(EndDateTime)
GROUP BY StartDateTime) a
GROUP BY a.date
Here is SQL Fiddle link
SQL Fiddle
Also replace Table1 with Jobs when running over your db context

Display data for all date ranges including missing dates

I'm having a issue with dates. I have a table with given from and to dates for an employee. For an evaluation, I'd like to display each date of the month with corresponding values from the second sql table.
SQL Table:
EmpNr | datefrom | dateto | hours
0815 | 01.01.2019 | 03.01.2019 | 15
0815 | 05.01.2019 | 15.01.2019 | 15
0815 | 20.01.2019 | 31.12.9999 | 40
The given employee (0815) worked during 01.01.-15.01. 15 hours, and during 20.01.-31.01. 40 hours
I'd like to have the following result:
0815 | 01.01.2019 | 15
0815 | 02.01.2019 | 15
0815 | 03.01.2019 | 15
0815 | 04.01.2019 | NULL
0815 | 05.01.2019 | 15
...
0815 | 15.01.2019 | 15
0815 | 16.01.2019 | NULL
0815 | 17.01.2019 | NULL
0815 | 18.01.2019 | NULL
0815 | 19.01.2019 | NULL
0815 | 20.01.2019 | 40
0815 | 21.01.2019 | 40
...
0815 | 31.01.2019 | 40
as for the dates, we have:
declare #year int = 2019, #month int = 1;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value + 1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year,#month,1)))
)
SELECT b.empnr, b.hours, datefromparts(#year,#month,numbers.value) Datum FROM numbers left outer join
emptbl b on b.empnr = '0815' and (datefromparts(#year,#month,numbers.value) >= b.datefrom and datefromparts(#year,#month,numbers.value) <= case b.dateto )
which is working quite well, yet I have the odd issue, that this code is only shoes the dates between 01.01.2019 and 03.01.2019
thank you very much in advance!
Did you check, if datefrom and dateto is in correct range?
Minimum value of DateTime field is 1753-01-01 and maximum value is 9999-12-31.
Look at your source table to check initial values.
The recursive CTE needs to begin with MIN(datefrom) and MAX(dateto):
DECLARE #t TABLE (empnr INT, datefrom DATE, dateto DATE, hours INT);
INSERT INTO #t VALUES
(815, '2019-01-01', '2019-01-03', 15),
(815, '2019-01-05', '2019-01-15', 15),
(815, '2019-01-20', '9999-01-01', 40),
-- another employee
(999, '2018-01-01', '2018-01-31', 15),
(999, '2018-03-01', '2018-03-31', 15),
(999, '2018-12-01', '9999-01-01', 40);
WITH rcte AS (
SELECT empnr
, MIN(datefrom) AS refdate
, ISNULL(NULLIF(MAX(dateto), '9999-01-01'), CURRENT_TIMESTAMP) AS maxdate -- clamp year 9999 to today
FROM #t
GROUP BY empnr
UNION ALL
SELECT empnr
, DATEADD(DAY, 1, refdate)
, maxdate
FROM rcte
WHERE refdate < maxdate
)
SELECT rcte.empnr
, rcte.refdate
, t.hours
FROM rcte
LEFT JOIN #t AS t ON rcte.empnr = t.empnr AND rcte.refdate BETWEEN t.datefrom AND t.dateto
ORDER BY rcte.empnr, rcte.refdate
OPTION (MAXRECURSION 1000) -- approx 3 years
Demo on db<>fiddle
It could be in your select, try:
SELECT b.empnr, b.hours, datefromparts(#year,#month,numbers.value) Datum
FROM numbers
LEFT OUTER JOIN emptbl b ON b.empnr = '0815' AND
datefromparts(#year,#month,numbers.value) BETWEEN b.datefrom AND b.dateto
Your CTE produces only 31 number and therefore it is showing only January dates.
declare #year int = 2019, #month int = 1;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value + 1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year,#month,1)))
)
SELECT *
FROM numbers
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=a24e58ef4ce522d3ec914f90907a0a9e
You can try below code,
with t0 (i) as (select 0 union all select 0 union all select 0),
t1 (i) as (select a.i from t0 a ,t0 b ),
t2 (i) as (select a.i from t1 a ,t1 b ),
t3 (srno) as (select row_number()over(order by a.i) from t2 a ,t2 b ),
tbldt(dt) as (select dateadd(day,t3.srno-1,'01/01/2019') from t3)
select tbldt.dt
from tbldt
where tbldt.dt <= b.dateto -- put your condition here
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=b16469908b323b8d1b98d77dd09bab3d

Selecting a single row in the same table/view if a query returns no results

I have the following view in my SQL database, which selects data from a Transaction table and a Customer table:
+-------+-----------+---------------------+--------+
| RowNo | Name | Date | Amount |
+-------+-----------+---------------------+--------+
| 1 | Customer1 | 2018-11-10 01:00:00 | 55.49 |
| 2 | Customer2 | 2018-11-10 02:00:00 | 58.15 |
| 3 | Customer3 | 2018-11-10 03:00:00 | 79.15 |
| 4 | Customer1 | 2018-11-11 04:00:00 | 41.89 |
| 5 | Customer2 | 2018-11-11 05:00:00 | 5.15 |
| 6 | Customer3 | 2018-11-11 06:00:00 | 35.17 |
| 7 | Customer1 | 2018-11-12 07:00:00 | 43.78 |
| 8 | Customer1 | 2018-11-12 08:00:00 | 93.78 |
| 9 | Customer2 | 2018-11-12 09:00:00 | 80.74 |
+-------+-----------+---------------------+--------+
I need an SQL query that will return all a customer's transactions for a given day (easy enough), but then if a customer had no transactions on the given day, the query must return the customer's most recent transaction.
Edit:
The view is as follows:
Create view vwReport as
Select c.Name, t.Date, t.Amount
from Transaction t
inner join Customer c on c.Id = t.CustomerId
And then to get the data I just do a select from the view:
Select * from
vwReport r
where r.Date between '2018-11-10 00:00:00' and '2018-11-11 00:00:00'
So, to clarify, I need one query that returns all the customer transactions for a day, and included in that results set is the last transaction of any customers who don't have a transaction on that day. So, in the table above, running the query for 2018-11-12, should return row 7, 8 and 9, as well as row 6 for Customer3 that did not have a transaction on the 12th.
Take your existing query and UNION ALL it with a "most recent transaction query" for everyone who doesn't have a transaction in that range.
with found as
(
select c.Id, c.Name, t.Date, t.Amount
from Transaction t
inner join Customer c on c.Id = t.CustomerId
where Date between '2018-11-10 00:00:00' and '2018-11-11 00:00:00'
)
with unfound as
(
select c.Id, c.Name, t.Date, t.Amount, RANK() OVER (PARTITION BY Name ORDER BY CAST(Date AS DATE) DESC) AS row
from Transaction t
inner join Customer c on c.Id = t.CustomerId
WHERE Date < '2018-11-10 00:00:00'
)
select Name, Date, Amount
from found
union all
select Name, Date, Amount
from unfound
where Id not in ( select Id from found ) and row = 1
You're interested in selecting multiple rows with ties, you could use the RANK() function to find all rows ranked by date descending:
SELECT * FROM (
SELECT *, RANK() OVER (PARTITION BY Name ORDER BY CAST(Date AS DATE) DESC) AS rn
FROM txntbl
WHERE CAST(Date AS DATE) <= '2018-11-12'
) AS x
WHERE rn = 1
Demo on DB Fiddle
You can use a correlated subquery:
select t.*
from transactions t
where t.date = (select max(t2.date)
from transactions t2
where t2.name = t.name and
t2.date <= #date
);
Note: This only returns customers who had a transaction on or before the date in question.
With the limited information available from the question, the following presents a solution using a join as opposed to a correlated subquery:
select t1.*
from
vwReport t1 inner join
(
select t2.name, max(t2.date) as mdate
from vwReport t2
group by t2.name
) t3
on t1.name = t3.name and t1.date = t3.mdate
where
t1.date <= #date
Use UNION for the last date transactions only if there are no transactions for the given dates (BETWEEN '2018-11-10 00:00:00' AND '2018-11-11 00:00:00'):
SELECT * FROM vwReport r
WHERE (r.Date BETWEEN '2018-11-10 00:00:00' AND '2018-11-11 00:00:00')
AND (r.Name = #name)
UNION
SELECT * FROM vwReport r
WHERE (r.Date = (SELECT MAX(r.Date) FROM vwReport r WHERE r.Name = #name))
AND (r.Name = #name)
AND ((SELECT COUNT(*) FROM vwReport r
WHERE (r.Date BETWEEN '2018-11-10 00:00:00' AND '2018-11-11 00:00:00')
AND (r.Name = #name)) = 0)