Constructing an SQL query for schema - sql

I have the following database schema for an attendance system:
How would I write an SQL query to generate a good report of entries on day X? I need it to generate a report that has
Employee Name | TimeIn | TimeOut
Bob | 10:00 | 11:00
Sam | 10:30 | 18:00
Bob | 11:30 | 15:00
but the row that defines if it was a time in or out is set by entryType (1 being in, 0 being out), so I would aliases TimeIn and TimeOut.
My attempt was
`SELECT firstName, time from log INNER JOIN users on log.employeeID = users.employeeID WHERE date = GETDATE()`
but this doesn't handle the fact that some times are entry, some are exit.
Note that there can be multiple sign ins per date.
Update:
Another attempt, but the subquery returns multiple rows
select firstName, (select time as timeIn from log where entryType = 1), (select time as timeOut from log where entryType = 0) inner join users on log.uID = users.uID from log group by uID

This works in Oracle (apologies for the non-ANSI style, but you should get the drift)..
SELECT FORENAME,SURNAME,L1.TIME IN_TIME,L2.TIME OUT_TIME
FROM EMPLOYEES EMP, LOG L1, LOG L2
WHERE EMP.EMPLOYEE_ID = L1.EMPLOYEE_ID
AND EMP.EMPLOYEE_ID = L2.EMPLOYEE_ID
AND L1.ENTRYTYPE = 1
AND L2.ENTRYTYPE = 0
AND L2.TIME = (SELECT MIN(TIME) FROM LOG WHERE EMPLOYEE_ID = L2.EMPLOYEE_ID AND L2.ENTRYTYPE = 0 AND TIME > L1.TIME)
Update:
Ah, yes, hadn't considered that. In this case you need an outer join. something like this (untested):
SELECT FORENAME,SURNAME,L1.TIME IN_TIME,L2.TIME OUT_TIME
FROM EMPLOYEES EMP
INNER JOIN LOG L1 ON EMP.EMPLOYEE_ID = L1.EMPLOYEE_ID AND L1.ENTRYTYPE = 1
LEFT OUTER JOIN LOG L2 ON EMP.EMPLOYEE_ID = L2.EMPLOYEE_ID AND L2.ENTRYTYPE = 0
AND L2.TIME = (SELECT MIN(TIME) FROM LOG WHERE EMPLOYEE_ID = L2.EMPLOYEE_ID AND L2.ENTRYTYPE = 0 AND TIME > L1.TIME)

Simply this will work. Try this
SELECT FORENAME,SURNAME,LG.IN_TIME,LG.OUT_TIME FROM EMPLOYEES EMP INNER JOIN
(SELECT MIN(TIME) IN_TIME,MAX(TIME) OUT_TIME,EMPLOYEE_ID FROM LOG
GROUP BY EMPLOYEE_ID) LG ON EMP.EMPLOYEE_ID=LG.EMPLOYEE_ID
Note : I didnt include the entry type because at any time min time will be swipe in and max time will be swipe out
Updated
To show no of sign ins and outs try something like this,
SELECT FORENAME,SURNAME,LG.IN_TIME,LG.OUT_TIME,LG.no_of_ins,
LG.no_of_outs FROM EMPLOYEES EMP INNER JOIN
(SELECT MIN(TIME) IN_TIME,MAX(TIME) OUT_TIME,EMPLOYEE_ID,
COUNT( CASE WHEN ENTRY_TYPE='I' THEN 1 ELSE O END noi) no_of_ins,
COUNT( CASE WHEN ENTRY_TYPE='O' THEN 1 ELSE O END nou) no_of_outs,
GROUP BY EMPLOYEE_ID) LG ON EMP.EMPLOYEE_ID=LG.EMPLOYEE_ID

This query will give you the earliest time in and latest time out of an employee.
SELECT E.FORENAME,
(SELECT MIN(TIME) FROM LOG WHERE EMPLOYEEID = E.EMPLOYEEID AND ENTRYTYPE = 1 AND DATE = <YOUR DAYE>) AS "TIME_IN",
(SELECT MAX(TIME) FROM LOG WHERE EMPLOYEEID = E.EMPLOYEEID AND ENTRYTYPE = 0 AND DATE = <YOUR DAYE>) AS "TIME_OUT"
FROM EMPLOYEE E WHERE E.EMPLOYEEID = <EMPLOYEE ID>

Related

Select on same table without subselect

I have payment, period and event tables. For each employee, month and year, I want to return payment.value (SALARY) and payment.value (ADDITIONAL, like a bonus) on same row, depending of event number. The event number 10015 represent the ADDITIONAL, and event number 4986 represent the SALARY.
I was able to reach my goal:
SELECT payment.employee_id EMPLOYEE_ID, payment.value SALARY,
(SELECT payment.value ADDITIONAL FROM payment
INNER JOIN period ON payment.period_id = period.id
INNER JOIN event ON payment.event_id = event.id
WHERE period.month = 7
AND period.year = 2021
AND payment.employee_id = 71
AND event.number = 10015
) ADDITIONAL
FROM payment
INNER JOIN period ON payment.period_id = period.id
INNER JOIN event ON payment.event_id = event.id
WHERE period.month = 7
AND period.year = 2021
AND payment.employee_id = 71
AND event.number = 4986
Result:
But now I'm trying to refactor my query so I don't have nested SELECTS. How can I do that?
You can use aggregation:
SELECT p.employee_id,
SUM(CASE WHEN e.number = 4986 THEN p.value END) as SALARY,
SUM(CASE WHEN e.number = 10015 THEN p.value END) as ADDITIONAL
FROM payment p JOIN
period pe
ON p.period_id = pe.id JOIN
event e
ON p.event_id = e.id
WHERE pe.month = 7 AND
pe.year = 2021
p.employee_id = 71 AND
e.number IN (4986, 10015)
GROUP BY p.employee_id;
Note: This is not 100% equivalent to you query, but I think it is what you want to do. This returns one row with salary and additional on one row. If there are multiple rows for the employee's salary in the period, then this returns one row whereas yours would return each row separately.

Group By Dynamic Ranges in SQL (cockroachdb/postgres)

I have a query that looks like
select s.session_id, array_agg(sp.value::int8 order by sp.value::int8) as timestamps
from sessions s join session_properties sp on sp.session_id = s.session_id
where s.user_id = '6f129b1c-43a6-4871-86f6-1749bfe1a5af' and sp.key in ('SleepTime', 'WakeupTime') and value != 'None' and value::int8 > 0
group by s.session_id
The result would look like
f321c813-7927-47aa-88c3-b3250af34afa | {1588499070,1588504354}
f38a8841-c402-433d-939d-194eca993bb6 | {1588187599,1588212803}
2befefaf-3b31-46c9-8416-263fa7b9309d | {1589912247,1589935771}
3da64787-65cd-4305-b1ac-1393e2fb11a9 | {1589741569,1589768453}
537e69aa-c39d-484d-9108-2f2cd956d4ee | {1588100398,1588129026}
5a9470ff-f930-491f-a57d-8c089e535d53 | {1589140368,1589165092}
The first column is a unique id and the second column is from and to timestamps.
Now I have a third table which has some timeseries data
records
------------------------
timestamp | name | value
Is it possible to find avg(value) from from records in group of session_ids over the from and to timestamps.
I could run a for loop in the application and do a union to get the desired result. But I was wondering if that is possible in postgres or cockroachdb
I wouldn't aggregate the two values but use two joins to find them. That way you can be sure which value belongs to which property.
Once you have that, you can join that result to your records table.
with ranges as (
select s.session_id, st.value as from_value, wt.value as to_value
from sessions s
join session_properties st on sp.session_id = s.session_id and st.key = 'SleepTime'
join session_properties wt on wt.session_id = s.session_id and wt.key = 'WakeupTime'
where s.user_id = '6f129b1c-43a6-4871-86f6-1749bfe1a5af'
and st.value != 'None' and wt.value::int8 > 0
and wt.value != 'None' and wt.value::int8 > 0
)
select ra.session_id, avg(rc.value)
from records rc
join ranges ra
on ra.from_value >= rc.timewstamp
and rc.timestamp < ra.to_value
group by ra.session_id;

How to minimize the query to reduce waiting time

i have update query but when i execute the quesry it take a long time to execute until it success. i dont know whats wrong. i run in dbeaver is there anyway to execute the quesry without waiting too long ?
update m_deposit_account_term_and_preclosure
set last_accrued_amount = (select amount from acc_gl_journal_entry where entry_date = (select max(entry_date) from acc_gl_journal_entry) and entity_id = sa.id and type_enum = 2 and description = 'Accrual Deposit Interest Expense End Of Month')
from m_savings_account sa
where sa.id = m_deposit_account_term_and_preclosure.savings_account_id;
As written, the update is executing the subquery on a row by row basis. That's going to be very slow, indeed.
Changing to a set-based operation by joining your tables first will improve overall performance, but if you have a lot of rows to update, it could still take a long time. Adding a WHERE clause will help, but it's entirely dependent on your tables.
MySQL:
UPDATE
m_deposit_account_term_and_preclosure as da
JOIN
m_savings_account as sa
ON sa.id = da.savings_account_id
JOIN
(
SELECT
entity_id,
amount
FROM acc_gl_journal_entry
WHERE entry_date = (
SELECT max(entry_date)
FROM acc_gl_journal_entry
)
AND entity_id = sa.id
AND type_enum = 2
AND description = 'Accrual Deposit Interest Expense End Of Month'
) as amt
SET da.last_accrued_amount = amt.amount
WHERE da.last_accrued_amount <> amt.amount;
SQL Server:
UPDATE da
SET last_accrued_amount = amt.amount
FROM
m_deposit_account_term_and_preclosure as da
JOIN
m_savings_account as sa
ON sa.id = da.savings_account_id
JOIN
(
SELECT
entity_id,
amount
FROM acc_gl_journal_entry
WHERE entry_date = (
SELECT max(entry_date)
FROM acc_gl_journal_entry
)
AND entity_id = sa.id
AND type_enum = 2
AND description = 'Accrual Deposit Interest Expense End Of Month'
) as amt
WHERE da.last_accrued_amount <> amt.amount;

Subquery returned more than 1 value.The subquery that contains SUM(dbo.SalarySettingsBreakup.Amount) AS AmountSSB

My sub-query returns more than one value and gives error.
(SELECT dbo.employee.id,
dbo.employee.employeecode,
dbo.employee.firstname,
dbo.employee.departmentid,
dbo.salarysettings.monthlyoffered,
dbo.salarysettings.id AS SalarySettingsID,
(SELECT Sum(amount) AS AmountVP
FROM voucherprocesses
WHERE vouchertypeid = 2
AND employee = dbo.employee.id
AND voucherdate BETWEEN '9/1/2017 12:00:00 AM' AND
'9/30/2017 12:00:00 AM'
GROUP BY employee) AS SalaryAdvance,
(SELECT Sum(dbo.salarysettingsbreakup.amount) AS AmountSSB
FROM dbo.employee
LEFT JOIN dbo.salarysettings
ON dbo.employee.id = dbo.salarysettings.employee
LEFT JOIN dbo.salarysettingsbreakup
ON dbo.salarysettings.id =
dbo.salarysettingsbreakup.salarysetting
WHERE dbo.salarysettingsbreakup.paymenttype = 2
AND dbo.salarysettingsbreakup.isactive = 1
GROUP BY dbo.employee.id) AS TotalDeduction,
(SELECT CASE
WHEN employee.joiningdate BETWEEN
'9/1/2017 12:00:00 AM' AND '9/30/2017 12:00:00 AM' THEN(
( salarysettings.monthlyoffered / 30 ) * ( 30 -
( Datepart(dd, joiningdate) - 1 ) ) )
ELSE 0
END) AS PayToBank
FROM dbo.employee
LEFT JOIN dbo.salarysettings
ON dbo.employee.id = dbo.salarysettings.employee
WHERE dbo.salarysettings.isactive = 1)
hope will work, try this :
(SELECT e.id,
e.employeecode,
e.firstname,
e.departmentid,
dbo.salarysettings.monthlyoffered,
dbo.salarysettings.id AS SalarySettingsID,
(SELECT Sum(amount) AS AmountVP
FROM voucherprocesses
WHERE vouchertypeid = 2
AND voucherprocesses.employee = e.id
AND voucherdate BETWEEN '9/1/2017 12:00:00 AM' AND
'9/30/2017 12:00:00 AM'
) AS SalaryAdvance,
(SELECT Sum(dbo.salarysettingsbreakup.amount) AS AmountSSB
FROM dbo.employee e2
LEFT JOIN dbo.salarysettings
ON e2.id = dbo.salarysettings.employee
LEFT JOIN dbo.salarysettingsbreakup
ON dbo.salarysettings.id =
dbo.salarysettingsbreakup.salarysetting
AND dbo.salarysettingsbreakup.paymenttype = 2
AND dbo.salarysettingsbreakup.isactive = 1
WHERE e2.id = e.id
) AS TotalDeduction,
(SELECT CASE
WHEN employee.joiningdate BETWEEN
'9/1/2017 12:00:00 AM' AND '9/30/2017 12:00:00 AM' THEN(
( salarysettings.monthlyoffered / 30 ) * ( 30 -
( Datepart(dd, joiningdate) - 1 ) ) )
ELSE 0
END) AS PayToBank
FROM dbo.employee e
LEFT JOIN dbo.salarysettings
ON e.id = dbo.salarysettings.employee
WHERE dbo.salarysettings.isactive = 1)
You have much to learn. You need to understand how subqueries work as well as outer joins. The following is wrong due to 2 issues.
(SELECT Sum(dbo.salarysettingsbreakup.amount) AS AmountSSB
FROM dbo.employee
LEFT JOIN dbo.salarysettings
ON dbo.employee.id = dbo.salarysettings.employee
LEFT JOIN dbo.salarysettingsbreakup
ON dbo.salarysettings.id =
dbo.salarysettingsbreakup.salarysetting
WHERE dbo.salarysettingsbreakup.paymenttype = 2
AND dbo.salarysettingsbreakup.isactive = 1
GROUP BY dbo.employee.id) AS TotalDeduction,
First is that you did not properly correlate the subquery. As Rahmat posted (but did not explain), you need to associate the employee ID from the outer query with the subquery. Because you did not correlate the subquery, it produces multiple rows for each row in the outer query - producing your error.
In addition, your lack of understanding about the correlation causes you to add complexity and a logical mistake (which gets covered up when correlated correctly). There is no need to include the employee table in your subquery. Since you correlate it to the employee table in the main query, it is redundant. In addition, you don't need to group by anything in the subquery since it is intended to generate a single scalar value per row in the outer query. And lastly, there is no purpose to outer joining in the subquery. Either you have matching rows in salarysettingsbreakup or you don't. An inner and outer join will achieve the same result - NULL if no matches. I also question whether you need to sum at all given the table and column names involved. You should search for explanations about how outer joins work and what happens when you reference columns from the unpreserved table (e.g. salarysettingsbreakup) in the where clause.
So a better subquery is:
(SELECT Sum(bkp.amount)
FROM dbo.salarysettings as sset
INNER JOIN dbo.salarysettingsbreakup as bkp
ON sset.id = bkp.salarysetting
AND bkp.paymenttype = 2
AND bkp.isactive = 1
WHERE sset.employee = dbo.employee.id) as TotalDeduction,
Note the inclusion of some best practices. Give a readable alias to your tables and use it with all of the columns referenced. I also despise the practice of using a table name as a column name - that adds to the confusion of reading your queries IMO.

SQL: Get latest record

this is my relational model:
Request
------------------------------
RequestId
------------------------------
1
2
RequestState
------------------------------
RequestStateId | Name
------------------------------
10 | Received
20 | Processing
30 | Finsihed
Request_RequestState
-------------------------------------------------------------------
Request_RequestStateId | RequestId | RequestStateId | CreatedOn
-------------------------------------------------------------------
1 | 1 | 10 | 2010-01-01
2 | 1 | 20 | 2010-01-02
3 | 2 | 10 | 2010-01-15
Each time a request state changes, this change is stored.
Now I need to list requests by its current state.
Like "Get all requests with current state = Received".
So far I only managed to created a query that return requests of a given state, but it doesn't matter if it is the current state or an older one... So I somehow need to use CreatedOn to get the latest/current state.
Any help? Thanks in advance!
You change your model...
With the current scheme, as more and more data changes take place, it will take longer and longer to determine the current state using the queries suggested above...
You need a "Current_Request_State" attribute on your request.
This query should also give you what you want but I also agree with Martin Milan that you should consider caching the most recent status value on the Request table.
SELECT r.RequestId, rrs.RequestStateId, rs.RequestStateName, rrs.StateChangedDate
FROM Request r
INNER JOIN (
SELECT ROW_NUMBER() OVER (PARTITION BY RequestId ORDER BY CreatedOn DESC) AS ROWNUM,
RequestId,
RequestStateId
CreatedOn
FROM Request_RequestState
) rrs
ON r.RequestId = rrs.RequestId
AND ROWNUM = 1
INNER JOIN RequestState rs
ON rrs.RequestStateId = rs.REquestStateId
This should do it for you:
SELECT r.RequestId,
s.Name AS RequestStateName
FROM Request r
INNER JOIN Request_RequestState rs
ON rs.Request_RequestStateId = (
SELECT TOP 1 x.Request_RequestStateId
FROM Request_RequestState x
WHERE x.RequestId = r.RequestId
--// you could add filter to get "current" status at some DATE
--//AND x.CreatedOn < '2010-01-15'
ORDER BY x.CreatedOn DESC
)
INNER JOIN RequestState s
ON s.RequestStateId = rs.RequestStateId
WHERE s.Name = 'Received'
You can also get all "current" request as of some other date, if you use filter as commented in the code.
I would probably just create a view from the SQL query above, and use it:
SELECT * FROM MyRequestStateView WHERE RequestStateName = 'Received'
Assuming that Request_RequestStateId increments up with time (i.e. the records with the greatest ID has the latest CreatedOn date)....
SELECT rrs.RequestId, rrs.RequestStateId, rs.Name
FROM Request_RequestState rrs
JOIN (
SELECT MAX(Request_RequestStateId) AS LatestRequestStateId
FROM Request_RequestState
GROUP BY RequestId
) rrs2 ON rrs.Request_RequestStateId = rrs2.LatestRequestStateId
JOIN RequestState rs ON rrs.RequestStateId = rs.RequestStateId
WHERE rs.Name = 'Received'
A possible query could look like that:
select r.requestId,
rs.RequestStateId,
rs.Name,
rrs.CreatedOn
from (select r2.* from request_requeststate where r2.createdon = (select max(createdon) from request_requeststate r3 where r3.request_requeststateId = r2.request_requeststateId)) rrs
inner join requeststate rs on rs.requeststateId = rrs.reqeststateid
inner join request r on r.requestid = rrs.requestid
You could use this query as a view or add a where clause where you filter for a specific request-state.