Earliest timestamp with join before group - sql

I have 3 tables users, levels, attempts in PostgreSQL. I need to select the earliest attempts by attempts.created_at for each user for each level and get sum of attempts.rate for each user.
CREATE TABLE IF NOT EXISTS users
(
id BIGSERIAL PRIMARY KEY,
nickname VARCHAR(255) UNIQUE
);
CREATE TABLE IF NOT EXISTS levels
(
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
);
CREATE TABLE IF NOT EXISTS attempts
(
id BIGSERIAL PRIMARY KEY,
rate INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
level_id BIGINT REFERENCES levels (id),
user_id BIGINT REFERENCES users (id)
);
For example attempts content
id | rate | created_at | level_id | user_id
------------------------------------------------------------
1 | 10 | 2022-10-21 16:53:13.818000 | 1 | 1
2 | 20 | 2022-10-21 11:53:13.818000 | 1 | 1
3 | 30 | 2022-10-21 14:53:13.818000 | 1 | 1
4 | 40 | 2022-10-21 10:53:13.818000 | 2 | 1 -- (nickname = 'Joe')
5 | 100 | 2022-11-21 10:53:13.818000 | 1 | 2 -- (nickname = 'Max')
For level 1 and user 1 earliest row with id = 2 for level 2 with id = 4, I need select
nickname | sum
-----------------
Max | 100
Joe | 60
As a result for user Max (user with id = 1) sum of the earliest attempts of all levels = 100. And order by sum descending.
Something like this but how to select only one earliest attempt for each level before summing:
select u.nickname, sum(a.rate) as sum
from attempts a
inner join users u on a.user_id = u.id
inner join levels l on l.id = a.level_id
-- on a.created_at is the earliest for level and user
group by u.id
order by sum desc

select user_id
,sum(rate)
from
(
select distinct on (level_id, user_id) *
from t
order by level_id, user_id, created_at
) t
group by user_id
user_id
sum
2
100
1
60
Fiddle

Related

How can I get last 2 records from another table as columns

I have a table called products with this schema:
CREATE TABLE products (
id INT PRIMARY KEY,
sku TEXT NOT NULL,
fee REAL
);
And another table with fee change log with this schema:
CREATE TABLE fee_change(
id SERIAL PRIMARY KEY,
sku_id INT NOT NULL,
old_fee REAL NOT NULL,
new_fee REAL NOT NULL,
FOREIGN KEY (sku_id) REFERENCES products(id)
);
Is there anyway to get last 2 fee changes for each sku in one sql and not 2 rows for each sku, I want to have 2 new columns with old_fee_1, new _fee_1, old_fee_2, new_fee_2:
Desired result:
id | sku | old_fee_1 | new_fee_1 | old_fee_2 | new_fee_2
1 | ASC | 4 | 2.5 | 3 | 4
2 | CF2 | 4 | 1 | 3 | 4
3 | RTG | 0.5 | 1 | 2 | 0.5
4 | VHN5 | null | null | null | null
dbfiddle
As starting point I took your query from the fiddle you linked:
SELECT *
FROM products AS p
LEFT JOIN LATERAL (
SELECT *
FROM fee_change
WHERE sku_id = p.id
ORDER BY id DESC
LIMIT 2
) AS oo
ON true
demo: db<>fiddle
You can use the FILTER clause (alternatively it works with a CASE WHEN construct as well) to pivot your joined table. To get the pivot value, you can add a row count (using the row_number() window function):
SELECT
p.id, p.sku, p.fee,
MAX(old_fee) FILTER (WHERE row_number = 1) AS old_fee_1, -- 2
MAX(new_fee) FILTER (WHERE row_number = 1) AS new_fee_1,
MAX(old_fee) FILTER (WHERE row_number = 2) AS old_fee_2,
MAX(new_fee) FILTER (WHERE row_number = 2) AS new_fee_2
FROM products AS p
LEFT JOIN LATERAL (
SELECT
*,
row_number() OVER (PARTITION BY sku_id) -- 1
FROM fee_change
WHERE sku_id = p.id
ORDER BY id DESC
LIMIT 2
) AS oo ON true
GROUP BY p.id, p.sku, p.fee -- 2
Create pivot value
Do the filtered aggregation to create the pivoted table.
Something like this should do the trick :
SELECT p.id,
p.sku,
old.old_fee_1,
old.new_fee_1,
new.old_fee_2,
new.new_fee_2
FROM products p
LEFT JOIN (SELECT fee.sku_id id, fee.old_fee old_fee_1, fee.new_fee new_fee_1
FROM fee_change ORDER BY fee.id DESC LIMIT 1 OFFSET 1) old ON old.id = p.id
LEFT JOIN (SELECT fee.sku_id id, fee.old_fee old_fee_2, fee.new_fee new_fee_2
FROM fee_change ORDER BY fee.id DESC LIMIT 1 OFFSET 0) new ON new.id = p.id

SQL: Multiple select statements in one query

I want to select information from three SQL tables within one query.
An example could be the following setup.
tblFriends
id | idmother | dayBirth
--------------------------
1 | 1 | 09/09/21
2 | 2 | 09/09/21
3 | 3 | 11/09/21
4 | 3 | 11/09/21
5 | 4 | 07/09/21
... | ... | ...
tblMothers
id | name
---------------
1 | Alice
2 | Samantha
3 | Veronica
4 | Maria
... | ...
tblIsAssignedParty
idMother | codeParty | price
------------------------------
1 | 231 | 15
2 | 645 | 28
3 | 164 | 33
... | ... | ...
I want to have a query that gives me the following:
dayBirth | weekDay | totalFriendsForParty | totalFriendsForPartyPercent | totalFriendsNoParty | totalFriendsNoPartyPercent
-----------------------------------------------------------------------------------------------------------------------------
07/09/21 | Tuesday | 0 | 0 | 1 | 0.??
09/09/21 | Thursday | 2 | 0.?? | 0 | 0
11/09/21 | Saturday | 2 | 0.?? | 0 | 0
Note:
dayBirth = simply the day of birth; I need the friends grouped by this date
weekDay = dayBirth name
totalFriendsForParty = friends who will be attending the party; we know if the mother has a party assigned
totalFriendsForPartyPercent = Percentatge of friends, of the total number of friends who will attend the parties
totalFriendsNoParty = friends who will not attend the party; we know if the mother does not have a party assigned
totalFriendsNoPartyPercent = Percentatge of friends, of the total number of friends who will not attend the parties
I need the number of friends based on whether their mothers are at a party or not. I tried to multiple select statements in Single query but the following code didn't work:
SELECT
(SELECT distinct dayBirth, TO_CHAR(dayBirth, 'DAY') from tblFriends) as firstSecondColumn,
(SELECT dayBirth, count(*) from tblFriends
where idMother IN (
SELECT f.idMother
from tblFriends f
left join tblIsAssignedParty iap
on f.idMother = iap.idMother
where iap.codeParty is not null)
group by dayBirth) as thirdColumn,
(SELECT TRUNC(count(*) / count(thirdColumn.id) , 2) from tblFriends) as quarterColumn,
(SELECT dayBirth, count(*) from tblFriends
where idMother IN (
SELECT f.idMother
from tblFriends f
left join tblIsAssignedParty iap
on f.idMother = iap.idMother
where iap.codeParty is not null)
group by dayBirth) as fifthColumn,
(SELECT TRUNC(count(*) / count(fifthColumn.id) , 2) from tblFriends) as sixthColumn,
order by dayBirth
Any advice on this one? I try to learn, I do what I can :-(
Edit: I can't add inserts because it's a file upload, but I can add an approximation of table creation.
Create tables:
CREATE TABLE tblFriends
(
id NUMBER(*,0),
idMother CHAR(10 CHAR),
CONSTRAINT PK_FRIEND PRIMARY KEY (id, idMother),
CONSTRAINT FK_IDMOTHER FOREIGN KEY (idMother)
REFERENCES tblMothers (id),
dayBirth DATE CONSTRAINT NN_DAY NOT NULL
)
CREATE TABLE tblMothers
(
id CHAR(10 CHAR) CONSTRAINT PK_MOTHER PRIMARY KEY (id),
name VARCHAR2(20 CHAR) CONSTRAINT NN_MNAME NOT NULL
)
CREATE TABLE tblIsAssignedParty
(
idMother CHAR(10 CHAR),
codeParty CHAR(10 CHAR),
CONSTRAINT PK_ASSIGNED PRIMARY KEY (idMother, codeParty),
CONSTRAINT FK_ASSIGNEDMOTHER FOREIGN KEY (idMother)
REFERENCES tblMothers (id),
CONSTRAINT FK_ASSIGNEDPARTY FOREIGN KEY (codeParty)
REFERENCES tblParties (codeParty),
price DECIMAL(10,2)
)
You appear to want to LEFT JOIN the firends and party tables and then use conditional aggregation:
SELECT dayBirth,
TO_CHAR(dayBirth, 'FMDAY', 'NLS_DATE_LANGUAGE=English') AS day,
COUNT(p.idmother)
AS totalFriendsForParty,
COUNT(p.idmother) / COUNT(*) * 100
AS totalFriendsForPartyPercent,
COUNT(CASE WHEN p.idmother IS NULL THEN 1 END) AS totalFriendsNoParty,
COUNT(CASE WHEN p.idmother IS NULL THEN 1 END) / COUNT(*) * 100
AS totalFriendsNoPartyPercent
FROM tblFriends f
LEFT OUTER JOIN tblIsAssignedParty p
ON (f.idmother = p.idmother)
GROUP BY dayBirth
Which, for the sample data:
CREATE TABLE tblFriends (id, idmother, dayBirth) AS
SELECT 1, 1, DATE '2021-09-09' FROM DUAL UNION ALL
SELECT 2, 2, DATE '2021-09-09' FROM DUAL UNION ALL
SELECT 3, 3, DATE '2021-09-11' FROM DUAL UNION ALL
SELECT 4, 3, DATE '2021-09-11' FROM DUAL UNION ALL
SELECT 5, 4, DATE '2021-09-07' FROM DUAL;
CREATE TABLE tblIsAssignedParty (idMother, codeParty, price) AS
SELECT 1, 231, 15 FROM DUAL UNION ALL
SELECT 2, 645, 28 FROM DUAL UNION ALL
SELECT 3, 164, 33 FROM DUAL;
Outputs:
DAYBIRTH
DAY
TOTALFRIENDSFORPARTY
TOTALFRIENDSFORPARTYPERCENT
TOTALFRIENDSNOPARTY
TOTALFRIENDSNOPARTYPERCENT
09-SEP-21
THURSDAY
2
100
0
0
11-SEP-21
SATURDAY
2
100
0
0
07-SEP-21
TUESDAY
0
0
1
100
db<>fiddle here

Check records that have gone through the same status more than once in a row

I have a status history table and I need to know which id_user pass by the same status sequentially.
Table structure
create table user (
id_user number,
user_name number,
status_name char(1),
created_at timestamp,
primary key (id_user)
);
create table user_status_hist (
id_user_status_hist number,
id_user number,
status_name char(1),
updated_at timestamp,
primary key (id_user),
constraint fk foreign key (id_user) references user(id_user)
);
imagine that in the example below, for user 123 it has passed 2 times in a row for status B.
How can i find all cases like this in my table?
select id_user, status_name, updated_at
from user_status_history
where id_user = 123;
--------+-------------+------------+
id_user | status_name | updated_at |
--------+-------------+------------+
123 | A | 2020-11-01 |
--------+-------------+------------+
123 | B | 2020-11-02 |
--------+-------------+------------+
123 | B | 2020-11-05 |
--------+-------------+------------+
With this query i find cases where i have a user that pass more than one time for the same status, but i cannot see if is sequential considering the updated_at column.
select count(*), idt_card
from user_status_hist
group by id_user, status_name
having count(*) > 1;
How can i get a output like this below? (the "count" column would be the number of times he went through these status sequentialy)
--------+-------------+------------+
id_user | status_name | count |
--------+-------------+------------+
123 | A | 3 |
--------+-------------+------------+
456 | B | 2 |
--------+-------------+------------+
789 | B | 6 |
--------+-------------+------------+
Use the LAG() analytic function. Since you must use it in a comparison, and analytic functions can only be computed in the SELECT clause (which comes after all the filters were applied), you must compute the analytic function in a subquery and reference it in an outer query.
select id_user, status_name, updated_at
from (
select id_user, status_name, updated_at,
lag(status_name) over (partition by id_user order by updated_at)
as prev_status
from user_status_hist
)
where status_name = prev_status
;
This will give you the full details of all occurrences. If you then want to group by id_user and status_name and count, you already know how to do that. (You can do it directly in the outer query of the solution shown above.)
You just need to include the columns you want in the select:
select idt_card, status_name, count(*)
from user_status_hist
group by id_user, status_name
having count(*) > 1;

A better way to aggregate into a default value

For this example I have three tables (individual, business, and ind_to_business). Individual has information on people. Business has information on businesses. And ind_to_business has information on which people are linked to which business. Here are their DDL:
CREATE TABLE individual
(
ID INTEGER PRIMARY KEY,
NAME VARCHAR2(100) NOT NULL,
ENTERPRISE_ID VARCHAR2(25) NOT NULL UNIQUE
);
CREATE TABLE business
(
ID INTEGER PRIMARY KEY,
NAME VARCHAR2(100) NOT NULL,
ENTERPRISE_ID VARCHAR2(25) NOT NULL UNIQUE
);
CREATE TABLE ind_to_business
(
ID INTEGER PRIMARY KEY,
IND_ID REFERENCES individual(id),
BUS_ID REFERENCES business(id),
START_DT DATE NOT NULL,
END_DT DATE
);
I'm looking for the best way to display one row for each person. If they are linked to one business, I want to display the the business's ENTERPRISE_ID. If they are linked to more than one business, I want to display the default value 'Multiple'. They will always be linked to a business, so there is no LEFT JOIN necessary. They can also be linked to a business more than once (Leaving and coming back). Multiple records for the same business would be aggregated.
So for the following sample data:
Individual:
+----+------------+---------------+
| ID | NAME | ENTERPRISE_ID |
+----+------------+---------------+
| 1 | John Smith | 53a23B7 |
| 2 | Jane Doe | 63f2a35 |
+----+------------+---------------+
Business:
+----+----------+---------------+
| ID | NAME | ENTERPRISE_ID |
+----+----------+---------------+
| 3 | ABC Corp | 2a34d9b |
| 4 | XYZ Inc | 34bf21e |
+----+----------+---------------+
ind_to_business
+----+--------+--------+-------------+-------------+
| ID | IND_ID | BUS_ID | START_DT | END_DT |
+----+--------+--------+-------------+-------------+
| 5 | 1 | 3 | 01-JAN-2000 | 31-DEC-2002 |
| 6 | 1 | 3 | 01-JAN-2015 | |
| 7 | 2 | 3 | 01-JAN-2000 | |
| 8 | 2 | 4 | 01-MAR-2006 | 05-JUN-2010 |
| 9 | 2 | 4 | 15-DEC-2019 | |
+----+--------+--------+-------------+-------------+
I would expect the following output:
+---------+------------+------------+
| IND_ID | NAME | LINKED_BUS |
+---------+------------+------------+
| 53a23B7 | John Smith | 2a34d9b |
| 63f2a35 | Jane Doe | Multiple |
+---------+------------+------------+
Here is my current query:
SELECT DISTINCT
sub.ind_id,
sub.name,
DECODE(sub.bus_count, 1, sub.bus_id, 'Multiple') AS LINKED_BUS
FROM (SELECT i.enterprise_id AS IND_ID,
i.name,
b.enterprise_id AS BUS_ID,
COUNT(DISTINCT b.enterprise_id) OVER (PARTITION BY i.id) AS BUS_COUNT
FROM individual i
INNER JOIN ind_to_business i2b ON i.id = i2b.ind_id
INNER JOIN business b ON i2b.bus_id = b.id) sub;
My query works, but this is running on a large dataset and taking a long time to run. I'm wondering if anyone has any ideas on how improve this so that there isn't so much wasted processing (i.e Needing to do a DISTINCT on the final result or doing COUNT(DISTINCT) in the inline view only to use that value in the DECODE above).
I've also created a DBFiddle for this question. (Link)
Thanks in advance for any input.
You could try and use a correlated subquery. This removes the need for outer distinct:
SELECT
i.enterprise_id ind_id,
i.name,
(
SELECT DECODE(COUNT(DISTINCT b.enterprise_id), 1, MIN(bus_id), 'Multiple')
FROM ind_to_business i2b
INNER JOIN business b ON i2b.bus_id = b.id
WHERE i2b.ind_id = i.id
) linked_bus
FROM individual i
You can join with the aggregated ind_to_business per individual. One way to do this:
select i.id, i.name, coalesce(b.enterprise_id, 'Multiple')
from individual i
join
(
select
ind_id,
case when min(bus_id) = max(bus_id) then min(bus_id) else null end as bus_id
from ind_to_business
group by ind_id
) ib on ib.ind_id = i.id
left join business b on b.id = ib.bus_id
order by i.id;
First you should sub-query to get all needed dimensions and then do all your final aggregation using CASE statement.
select
ind_id,
name,
case
when count(*) > 1 then 'Multiple'
else ind_id
end as linked_bus
from
(
select
distinct i.enterprise_id as ind_id,
i.name,
b.enterprise_id as bus_id
from individual i
join ind_to_business i2b
on i.id = i2b.ind_id
join business b
on i2b.bus_id = b.id
) vals
group by
ind_id,
name
order by
ind_id
No need of using DISTINCT twice. You could use subquery factoring and put the in-line view in WITH clause, and make the data set DISTINCT in the subquery itself.
WITH data AS
(
SELECT distinct
i.enterprise_id AS IND_ID,
i.name,
b.enterprise_id AS BUS_ID
FROM individual i
JOIN ind_to_business i2b ON i.id = i2b.ind_id
JOIN business b ON i2b.bus_id = b.id
)
SELECT ind_id,
name,
case
when count(*) = 1 then MIN(bus_id)
else 'Multiple'
end AS LINKED_BUS
FROM data
GROUP BY ind_id, name;
IND_ID NAME LINKED_BUS
---------- ---------- -------------------------
53a23B7 John Smith 2a34d9b
63f2a35 Jane Doe Multiple

Postgresql select the two top towns

Have row like:
user | town 1 | town 2 | town 3 | town 4 | town 5 | town 6|
The towns all have integer values where town 3 and town 4 have the largest number
I want to select the two top towns for the user so the end result should be:
user | town 3 | town 4 |
This is the properly normalized model:
create table users (
user_id serial primary key,
user_name varchar(100)
);
create table town (
town_id serial primary key,
town_int int
);
create table user_town (
town_id int references town (town_id),
user_id int references users (user_id),
primary key (user_id, town_id)
);
insert into users (user_name) values ('John');
insert into town (town_int) values (1),(2),(3),(4),(5),(6);
insert into user_town (user_id, town_id) values (1,1),(1,2),(1,3),(1,4),(1,5),(1,6);
How to query it:
select user_id, user_name, town_id, town_int
from
user_town
inner join
users using (user_id)
inner join
town using (town_id)
where user_id = 1
order by town_int desc
limit 2
;
user_id | user_name | town_id | town_int
---------+-----------+---------+----------
1 | John | 6 | 6
1 | John | 5 | 5
Somtimes we have just what we have, poor designed legacy DB for example. Then
WITH t AS (
SELECT 100 as userid, 11 as town1, 23 as town2, 77 as town3, 14 as town4, 15 as town5, 16 as town6
UNION ALL
SELECT 101 as userid, 21 as town1, 235 as town2, 177 as town3, 24 as town4, 25 as town5, 26 as town6
)
SELECT userid, max(r.town) as top1, min(r.town) as top2
FROM t
CROSS JOIN LATERAL(
SELECT town1 as town FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town2 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town3 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town4 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town5 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town6 FROM t t2 WHERE t2.userid=t.userid
ORDER BY town DESC LIMIT 2) AS r
GROUP BY userid;