Select two entry from a joined table (1:m) - sql

I have a device table (dev) and device_date table (dev_data). Relationship 1:M
dev table:
| id |name |status |
|-----|-------|-------|
| 1 |a |111 |
|-----|-------|-------|
| 2 |b |123 |
|-----|-------|-------|
| ....|..... |.... |
dev_data table:
|id |dev_id |status |date |
|---|-------|--------|------------------------|
|1 |1 | 123 |2019-04-16T18:53:07.908Z|
|---|-------|--------|------------------------|
|2 |1 | 120 |2019-04-16T18:54:07.908Z|
|---|-------|--------|------------------------|
|3 |1 | 1207 |2019-04-16T18:55:07.908Z|
|---|-------|--------|------------------------|
|4 |2 | 123 |2019-04-16T18:53:08.908Z|
|---|-------|--------|------------------------|
|5 |2 | 121 |2019-04-16T18:54:08.908Z|
|---|-------|--------|------------------------|
|6 |2 | 127 |2019-04-16T18:55:08.908Z|
|...|.......|........|........................|
I need to select all dev and join dev_data, but add only 2 last records (by date)
the final response should look like this one:
status_calc_1 and status_calc_2 is diff between status in dev and dev_data
status_calc_1 => status difference of the last row from dev_data and dev
status_calc_2 => status difference of prelast row from dev_data and dev
|id |name |status_calc_1 | status_calc_2 |
|----|------|---------------|---------------|
|1 |a |1207-111 |120-111 |
|----|------|---------------|---------------|
|2 |b |127-123 |121-123 |
I tried this one:
select id, "name", status, max(dd.date) as last,
(select date from device_data p where p.dev_id = device.id and date < dd.date limit 1) as prelast
from device
inner join device_data dd on device.id = dd.dev_id
group by id, "name", status;
but get an error:
ERROR: subquery uses ungrouped column "device.id" from outer query
and this one:
select id, "name", status, max(dd.date) as last, max(dd2.date) as prelast,
from device
inner join device_data dd on device.id = dd.dev_id
inner join device_data dd2 on device.id = dd2.dev_id and dd2.date < dd.date
group by id, "name", status;
I get correct 2 last dev_data, but still, have no idea how to make 2 columns status_calc_1 and status_calc_2
status_calc_1 = last row dev_data.status - dev.status
status_calc_2 = prelast row dev_data.status - dev.status

You can use conditional aggregation:
select d.id, d.name, d.status,
max(dd.date) as last,
max(case when dd.seqnum = 2 then dd.date end) as prelast,
(max(case when dd.seqnum = 1 then dd.status end) - d.status) as status_calc_1,
(max(case when dd.seqnum = 2 then dd.status end) - d.status) as status_calc_2
from device d join
(select dd.*,
row_number() over (partition by dd.dev_id order by dd.date desc) as seqnum
from device_data dd
) dd
on d.id = dd.dev_id
where seqnum <= 2
group by d.id, d.name, d.status;

Related

Produce a per customer per month sales report via SQL eg group by two columns and sum the third

I rarely write SQL (Azure SQL) however I am trying to generate a per month sales total per customer.
Customer:
|Username |ID |
|user1 |1 |
|user2 |2 |
|user3 |3 |
Order:
|CustomerId |Month |Total |
|1 |1 |275 |
|1 |1 |10 |
|2 |1 |100 |
|1 |3 |150 |
|2 |2 |150 |
|2 |2 |65 |
|3 |2 |150 |
I want to produce
|Username |Month1Total |Month2Total | Month3Total |
|user1 |285 |275 | 150 |
|user2 |100 |215 | 0 |
|user3 |0 |150 | 0 |
I can do the following
SELECT customerTable.Username Username, SUM(orderTable.OrderTotal) TotalMay
FROM "Order" orderTable
JOIN Customer customerTable ON orderTable.CustomerId = customerTable.Id
WHERE DATENAME(Month, (orderTable.PaidDateUTC)) = 'May'
GROUP BY Username
Which will give me an output per month. However I don't know how to loop this, do it per month and then group by username.
IF you want to have a separate column for each month then try this
SELECT customerTable.Username Username
,SUM(iif(ordertable.[month] = 1,orderTable.OrderTotal,0)) TotalJan
,SUM(iif(ordertable.[month] = 2,orderTable.OrderTotal,0)) TotalFeb
,SUM(iif(ordertable.[month] = 3,orderTable.OrderTotal,0)) TotalMar
,SUM(iif(ordertable.[month] = 4,orderTable.OrderTotal,0)) TotalApr
,SUM(iif(ordertable.[month] = 5,orderTable.OrderTotal,0)) TotalMay
FROM "Order" orderTable
JOIN Customer customerTable ON orderTable.CustomerId = customerTable.Id
GROUP BY Username
should be easy to add the remaining months
you can use case when
with t1 as
( select o.CustomerId,o.Month, sum(Total) as total from
[Order]
group by o.CustomerId,o.Month
) select c.Username,
case when t1.month=1 then t1.total else 0 end month1,
case when t1.month=2 then t1.total else 0 end month2,
case when t1.month=3 then t1.total else 0 end month3
from t1 join Customer c on t1.CustomerId=c.ID
Or you can use PIVOT
select c.username, t.* from
(
select * from
(select * from ord
) src
pivot
( sum(Total) FOR Month IN ([1],[2],[3])
) pvt
) as t join Customer c on t.CustomerId=c.ID
Something like this:
SELECT DATENAME(Month, (orderTable.PaidDateUTC)) MonthName,
customerTable.Username Username,
SUM(orderTable.OrderTotal) TotalMay
FROM "Order" orderTable
JOIN Customer customerTable ON orderTable.CustomerId = customerTable.Id
GROUP BY DATENAME(Month, (orderTable.PaidDateUTC)), Username
You just need to move the month name from the WHERE clause to the GROUP BY.
I would simply do JOIN with conditional aggregation :
SELECT c.Username,
SUM(CASE WHEN o.Month = 1 THEN o.Total ELSE 0 END) AS [Month1Total],
SUM(CASE WHEN o.Month = 2 THEN o.Total ELSE 0 END) AS [Month2Total],
SUM(CASE WHEN o.Month = 3 THEN o.Total ELSE 0 END) AS [Month3Total],
. . .
FROM Customer C INNER JOIN
Order o
ON o.CustomerId = c.id
GROUP BY c.Username;

T-SQL get the last date time record

My table looks like this:
+---------+------------------------+-------+---------+---------+
|channel |date |code |comment |order_id |
+---------+------------------------+-------+---------+---------+
|1 |2017-10-27 12:04:45.397 |2 |comm1 |1 |
|1 |2017-10-27 12:14:20.997 |1 |comm2 |1 |
|2 |2017-10-27 12:20:59.407 |3 |comm3 |1 |
|2 |2017-10-27 13:14:20.997 |1 |comm4 |1 |
|3 |2017-10-27 12:20:59.407 |2 |comm5 |1 |
|3 |2017-10-27 14:20:59.407 |1 |comm6 |1 |
+---------+------------------------+-------+---------+---------+
And I expect result like this:
+---------+------------------------+-------+---------+
|channel |date |code |comment |
+---------+------------------------+-------+---------+
|1 |2017-10-27 12:14:20.997 |1 |comm2 |
|2 |2017-10-27 13:14:20.997 |1 |comm4 |
|3 |2017-10-27 14:20:59.407 |1 |comm6 |
+---------+------------------------+-------+---------+
Always 1 record with order_id = x and max date for each channel. Total number of channels is constant.
My query works but I'm worried about performance as the table grows. Doing three almost identical queries doesn't seem smart.
select
*
from
(select top(1)
channel,
date,
code,
comment
from
status
where
channel = 1 and
order_id = 1 and
cast(date as date) = '2017-10-27'
order by
date desc) channel1
union
select
*
from
(select top(1)
channel,
date,
code,
comment
from
status
where
channel = 2 and
order_id = 1 and
cast(date as date) = '2017-10-27'
order by
date desc) channel2
union
select
*
from
(select top(1)
channel,
date,
code,
comment
from
status
where
channel = 3 and
order_id = 1 and
cast(date as date) = '2017-10-27'
order by
date desc) channel3
How can I improve this?
Another option is using the WITH TIES clause. No sub-query or extra field.
Select top 1 with ties *
From YourTable
Order By Row_Number() over (Partition By channel order by date desc)
Try using the ROW_NUMBER() function and a derived table. It will save you a lot of headaches. Try:
select channel
,date
,code
,comment
from
(select *
,row_number() over(partition by channel order by code asc) rn --probably don't need asc since it is ascending by default
from mytable) t
where t.rn = 1
Assuming you want the latest row for each channel, this would work.
SELECT *
FROM (
SELECT
ROW_NUMBER() OVER (PARTITION BY s.channel ORDER BY [date] DESC) AS rn,
*
FROM [status] AS s
) AS t
WHERE t.rn = 1

On MS Access, group by then filter

I have a table on ms access which has 13 columns.I want to group by column Name then check the latest one by comparing column id and take the record if the latest row has value if not take the previous record. the comparison will be done for each columns.
+-----+-----+-------+-------+-------+
| id |Name |colum1 |colum2 |colum3 |
+-----+-----+-------+-------+-------+
| 1 |a |x | |x |
+-----+-----+-------+-------+-------+
| 2 |b | |y |y |
+-----+-----+-------+-------+-------+
| 3 |a |z |z | |
+-----+-----+-------+-------+-------+
| 4 |a |m | | |
+-----+-----+-------+-------+-------+
Expected output
+-----+-----+-------+-------+-------+
| id |Name |colum1 |colum2 |colum3 |
+-----+-----+-------+-------+-------+
| 2 |b | |y |y |
+-----+-----+-------+-------+-------+
| 4 |a |m |z |x |
+-----+-----+-------+-------+-------+
You can do Self Join.
SELECT T1.*
FROM
table_name T1
INNER JOIN
(SELECT `Name`,MAX(`id`) AS ID FROM table_name GROUP BY `Name` ) T2
ON T1.`id`= T2.`ID` AND T1.`Name` = T2.`Name`
Hope this helps.
I'm not sure if it will work in MS Access. It works in SQL Server. Even if it does, it will be very slow.
SELECT
Groups.Name
,(
SELECT TOP(1) T.colum1
FROM T
WHERE T.Name = Groups.Name AND T.colum1 <> ''
ORDER BY T.ID DESC
) AS C1
,(
SELECT TOP(1) T.colum2
FROM T
WHERE T.Name = Groups.Name AND T.colum2 <> ''
ORDER BY T.ID DESC
) AS C2
,(
SELECT TOP(1) T.colum3
FROM T
WHERE T.Name = Groups.Name AND T.colum3 <> ''
ORDER BY T.ID DESC
) AS C3
FROM
(
SELECT Name
FROM T
GROUP BY Name
) AS Groups

Many to one relation, select only rows which ancestor met all criteria

First I will show You architecture of tables.
Table "public.questionare"
Column | Type |
--------------------+-----------------------+
id | integer |
Table "public.questionareacceptance"
Column | Type |
-------------------------+-----------------------+
id | integer |
questionare_id | integer |
accept_id | integer |
impact_id | integer |
Table questionareacceptance contains:
id | questionare_id | accept_id| impact_id |
----+----------------+----------+------------------+
1 |1 |1 | |
2 |1 |1 | 1 |
3 |1 |1 | 1 |
4 |2 | | 1 |
5 |3 |1 | 1 |
6 |4 |1 | 1 |
7 |4 |1 | 1 |
What I am trying to get is a list of questionare ID where in each questionareacceptance fields accept_id and impact_id are not NULL
My query looks like:
SELECT q.id AS quest,
qa.id AS accepted
FROM questionare q,
questionareacceptance qa
WHERE q.id = qa.questionare_id
AND qa.accept_id IS NOT NULL
AND qa.impact_id IS NOT NULL;
But the result is as fallows:
quest | accepted |
--------------------+-----------------------+
1 |1 |
1 |2 |
1 |3 |
2 |4 |
3 |5 |
4 |6 |
4 |7 |
But the result that should be returned are only 3 and 4 others have impact_id or accept_id null.
Can anyone point me where I am doing the mistake?
your query could be written with not exists:
select
q.id as quest, qa.id as accepted
from questionare as q
inner join questionareacceptance as qa on qa.questionare_id = q.id
where
not exists (
select *
from questionareacceptance as tqa
where
tqa.questionare_id = q.id and
(tqa.accept_id is null or tqa.impact_id is null)
)
but I think faster one would using window functions:
with cte as (
select
q.id as quest, qa.id as accepted,
sum(case when qa.accept_id is not null and qa.impact_id is not null then 1 else 0 end) over(partition by q.id) as cnt1,
count(*) over(partition by q.id) as cnt2
from questionare as q
inner join questionareacceptance as qa on qa.questionare_id = q.id
)
select quest, accepted
from cte
where cnt1 = cnt2
actually looks like you don't need join at all:
with cte as (
select
qa.questionare_id as quest, qa.id as accepted,
sum(case when qa.accept_id is not null and qa.impact_id is not null then 1 else 0 end) over(partition by qa.questionare_id) as cnt1,
count(*) over(partition by qa.questionare_id) as cnt2
from questionareacceptance as qa
)
select quest, accepted
from cte
where cnt1 = cnt2;
sql fiddle demo

How to query multiple COUNT(*) with good performance

I have this table:
CREATE TABLE schedule (
schedule_id serial NOT NULL,
start_date date,
CONSTRAINT schedule_id PRIMARY KEY (schedule_element_id)
)
And this table:
CREATE TABLE schedule_user (
schedule_user_id serial NOT NULL,
schedule_id integer,
state int,
CONSTRAINT fk_schedule_id FOREIGN KEY (schedule_id)
REFERENCES schedule (schedule_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
schedule
-------------------------
|schedule_id |date |
|------------+------------|
|1 |'2013-10-10'|
|2 |'2013-10-20'|
|3 |'2013-08-13'|
-------------------------
schedule_user
-----------------------------------
|schedule_user_id|schedule_id |state|
|----------------+------------+-----|
|1 | 1 |0 |
|2 | 1 |1 |
|3 | 1 |2 |
|4 | 1 |0 |
|5 | 1 |1 |
|6 | 1 |1 |
|4 | 2 |0 |
|5 | 2 |1 |
|7 | 2 |0 |
|2 | 3 |1 |
-----------------------------------
And I want a table like this:
characteristic
---------------------------------------
|schedule_id |state0|state1|state2|total|
|------------+------+------+------+-----|
|1 |2 |3 |1 |6 |
|2 |2 |1 |0 |3 |
|3 |1 |1 |0 |2 |
---------------------------------------
I've made this query that looks as as horrible as it's performance.
SELECT
schedule.schedule_id AS id,
(( SELECT count(*) AS count
FROM schedule_user
WHERE schedule_user.schedule_id = schedule.schedule_id
AND state=0))::integer AS state0,
(( SELECT count(*) AS count
FROM schedule_user
WHERE schedule_user.schedule_id = schedule.schedule_id
AND state=1))::integer AS state1,
(( SELECT count(*) AS count
FROM schedule_user
WHERE schedule_user.schedule_id = schedule.schedule_id
AND state=2))::integer AS state2,
(( SELECT count(*) AS count
FROM schedule_user
WHERE schedule_user.schedule_id = schedule.schedule_id))::integer
AS total
FROM schedule
Is there a better way to perform such a query?
Should I create an Index to 'state' column? if so, how should it look like?
You want to make a pivot table. An easy way to make one in SQL if you know all of the possible values of state beforehand is using sum and case statements.
select schedule_id,
sum(case state when 0 then 1 else 0 end) as state0,
sum(case state when 1 then 1 else 0 end) as state1,
sum(case state when 2 then 1 else 0 end) as state2,
count(*) as total
from schedule_user
group by schedule_id;
Another way is to use the crosstab table function.
Neither of these will let you get away with not knowing the set of values of state (and hence the columns in the result set).
I would try
SELECT s.schedule_id,
COUNT(CASE WHEN su.state = 0 THEN 1 END) AS state0,
COUNT(CASE WHEN su.state = 1 THEN 1 END) AS state1,
COUNT(CASE WHEN su.state = 2 THEN 1 END) AS state2,
COUNT(su.state) AS total
FROM schedule s
LEFT
OUTER
JOIN schedule_user su
ON su.schedule_id = s.schedule_id
GROUP
BY s.schedule_id
;
Ths standard approach is to use SUM() with a CASE over a JOIN with a GROUP BY:
SELECT
schedule.schedule_id AS id,
SUM (case when state=0 then 1 else 0 end) AS state0,
SUM (case when state=1 then 1 else 0 end) AS state1,
SUM (case when state=2 then 1 else 0 end) AS state2,
count(*) AS total
FROM schedule
LEFT JOIN schedule_user
ON schedule_user.schedule_id = schedule.schedule_id
GROUP BY 1