SQL select max value with join - sql

I have two table:
USER (that can have multiple profiles)
id | name | profile | ...
BET
id | profile_id | date | amount | ...
I need to select name, and the date of the bet with maximum amount betted for every user from every profile.
So the output should be something like this:
name | max_amount | date_max_amount

USER (that can have multiple profiles)
It means that the user can have multiple ids and you would be identifying users on the basis of their names and have their max bids
SELECT u.name, MAX(b.amount)
FROM USER u
LEFT JOIN BET b ON (u.id = b.profile_id)
GROUP BY u.name
Then you can
SELECT u.name, d.max_amount, b.date AS date_max_amount
FROM USER u
LEFT JOIN (
SELECT u.name, MAX(b.amount) as max_amount
FROM USER u
LEFT JOIN BET b ON (u.id = b.profile_id)
GROUP BY u.name
)d ON (u.name = d.name)
LEFT JOIN BET b ON (u.id = b.profile_id AND b.amount = d.max_amount)

Related

SQL LIMIT by distinct column value without subqueries

user
id
name
age
1
anna
6
2
john
10
3
lord
50
cats
id
name
userID
1
miez
1
2
caty
1
3
random
2
4
idk
3
When using
SELECT U.id, C.name FROM user U
INNER JOIN cats C ON U.id = C.id
LIMIT 2
I get as a
result
UserID
CatName
1
miez
1
caty
What I want is to limit my rows by the distinct values of UserID, like this
SELECT U.id, C.name FROM user U
INNER JOIN cats C ON U.id = C.id
LIMIT 2 <distinct U.id rows>
UserID
CatName
1
miez
1
caty
2
random
People suggested using limit in subqueries and check if UserID is in the return
like
... WHERE UserID IN (SELECT id FROM User LIMIT 2)
but this only works well for small tables and is not an elegant solution for good performance.
My idea was using DENSE_RANK(), like:
SELECT U.id, C.name FROM user U
DENSE_RANK() OVER (ORDER BY U.id) as rows,
INNER JOIN cats C ON U.id = C.id
WHERE rows < 50
but it is not working either.
You can't use a column alias on the same level where you define it. You will have to wrap the query in a derived table. However if you want a specific number of row per user you need to use partition by, not order by
select id, name
from (
SELECT u.id,
c.name,
DENSE_RANK() OVER (PARTITION BY U.id ORDER BY c.name) as rnk
FROM user U
JOIN cats C ON U.id = C.userid
) t
WHERE t.rnk <= 2

SQL Join and count relations

Having a PostgreSQL query problem, and wondering if there's an efficient way to get this in a single query. Let's take the following simple table structure. Think of it as the traditional many to many relationship.
users <-> user_collections <-> collections
Given a users id, I'd like to first get all of their collections. This is the simple part for which I have a query:
SELECT c.id, c.name, c.description, c.created_at, c.updated_at
FROM collections c
JOIN user_collections uc ON c.id = uc.collection_id
WHERE uc.user_id = $1
ORDER BY created_at DESC
So for example:
users
id | email
1 | user1#example.com
2 | user2#example.com
3 | user3#example.com
user_collections
id | user_id | collection_id
1 | 1 | 1
2 | 2 | 1
3 | 3 | 1
collections
id | name | description
1 | Example | Demo collection
In the above case, querying for collections for user one would yield the first collection. However I'd also like to get a count of how many users are associated with each collection. In this case, a total count of 3, since all three members share this collection. A member count if you will. Is there a sensible way to do this in one query, or is two probably better?
An alternative approach to using correlated subquery is to pre-calculated the number of users in each collection, then join it to your existing query.
i.e.
with collection_counts as (
select
collection_id
, count(1) as collection_count
from user_collections
group by collection_id
)
SELECT
c.id
, c.name
, c.description
, c.created_at
, c.updated_at
, cc.collecion_count
FROM collections c
JOIN user_collections uc ON c.id = uc.collection_id
join collection_counts as cc on c.id = cc.collection_id
WHERE uc.user_id = $1
ORDER BY created_at DESC
You need to use here a correlated sub query to get the corresponding total user of that collection [Correlated sub query help https://www.geeksforgeeks.org/sql-correlated-subqueries/]
SELECT c.* , (select count(user_id) from user_collections sc where sc.collection_id=uc.collection_id) as GroupCount
FROM collections c
JOIN user_collections uc ON c.id = uc.collection_id
WHERE uc.user_id = $1
ORDER BY created_at DESC
One method is conditional aggregation:
SELECT c.id, c.name, c.description, c.created_at, c.updated_at,
COUNT(*) as num_users
FROM collections c JOIN
user_collections uc
ON c.id = uc.collection_id
GROUP BY c.id
HAVING COUNT(*) FILTER (WHERE uc.user_id = $1) > 0
ORDER BY created_at DESC;
That said, it might be faster to do:
SELECT c.id, c.name, c.description, c.created_at, c.updated_at,
(SELECT COUNT(*)
FROM user_collections uc
WHERE c.id = uc.collection_id
) as num_users
FROM collections c
WHERE EXISTS (SELECT 1
FROM user_collections uc
WHERE c.id = uc.collection_id AND
uc.user_id = $1
)
ORDER BY created_at DESC;
This would be faster for two reasons:
It avoids the outer aggregation. Aggregations on larger data are generally more expensive.
It calculates the count only for the collections that are in the result set.
This can also make use of indexes on the table -- which if you care about performance, you should have.

Selecting users who are in the same group with "name"

Users
id username
1 ryan
2 mike
3 annie
4 lisa
Groups
id name
1 football
2 hockey
Permissions
user_id group_id
1 1
1 2
2 1
4 2
So I'm looking for every username, who belong to at least one same group with username ryan. I'd also like to know everyone who is not in the same group.
SELECT Users.username
FROM Users
LEFT JOIN Permissions ON Users.id = Permissions.user_id
LEFT JOIN Groups.id = Permissions.group_id
WHERE Users.id;
So this is how I got it started, but have no idea how to continue.
Here is one method:
with ug as (
SELECT u.username, g.name
FROM Users u JOIN
Permissions p
ON u.id = p.user_id
)
SELECT DISTINCT ug.username
FROM ug
WHERE EXISTS (SELECT 1
FROM ug ug2
WHERE ug2.group_id = ug.group_id AND
ug2.username = 'Ryan'
);
The above assigns the group name to each user and then treats this as a single table. EXISTS is used to determine if the groups overlap.
Note that groups is not needed because you are not asking for the name of the group. The id is sufficient to answer the question.
I am not a big fan of using select distinct. It usually means that the database engine is doing more work than necessary -- creating duplicates and then removing them.
Here is an alternative solution that uses exists:
select u.*
from users u
where exists (select 1
from permissions p join
permissions pr
on pr.group_id = p.group_id join
users ur
on pr.user_id = ur.user_id and
ur.username = 'Ryan'
where p.user_id = u.id
);
You can do it with multiple joins of Users and Permissions:
select uu.username
from Users u
inner join Permissions p on p.user_id = u.id
inner join Permissions pp on pp.group_id = p.group_id
inner join Users uu on pp.user_id = uu.id and pp.user_id <> u.id
where u.username = 'ryan'
See the demo.
Results:
| username |
| -------- |
| mike |
| lisa |

Full outer join doesn't returns the results from right table

My application has a list of users who can be booked for a bookings. We need to pay the users for these bookings. I need to get the list of users who needs to be paid. A user can be paid if they have bookings or with payment created which has no bookings.
I tried the below:
SELECT
users.id as user_id,
user_bookings.id as user_booking_id,
user_bookings.payment_id as user_booking_payment_id,
payments.id as payment_id
FROM
users
LEFT OUTER JOIN user_bookings ON user_bookings.user_id = user.id
FULL JOIN payments ON payments.id = user_bookings.payment_id
Where payments.issued = false;
But it doesn't list the users with the payment with no bookings. It only lists the users with the bookings created. How can I get the users with payment which doesn't have any bookings?
EDIT
I tried the below query:
User with 37271 has the below payment records:
select payments.id, payments.issued from payments where payments.user_id=37271;
id | issued
--------+--------
133046 | f
133045 | t
(2 rows)
Below are the bookings I have:
select user_bookings.id, user_bookings.payment_issued, user_bookings.payment_id from user_bookings where user_id=37271;
id | payment_issued | payment_id
--------+-------------------+---------------
541136 | t | 133045
541137 | t | 133045
(2 rows)
While running the below query:
SELECT u.id as user_id, ub.id as user_booking_id, ub.payment_id as user_booking_payment_id,
p.id as payment_id, p.issued as payments_issued
FROM users u LEFT OUTER JOIN
user_bookings ub
ON ub.user_id = u.id FULL JOIN
(SELECT p.*
FROM payments p
WHERE NOT p.issued
) p
ON p.id = ub.payment_id
where users.id=37271;
It doesn't returns the payment with no bookings but it returns issued payments:
user_id | user_booking_id | user_booking_payment_id | payment_id | payments_issued
---------+-----------------+-------------------------+------------+-----------------
37271 | 541137 | 133045 | |
37271 | 541136 | 133045 | |
(2 rows)
SELECT
users.id as user_id,
user_bookings.id as user_booking_id,
user_bookings.payment_id as user_booking_payment_id,
payments.id as payment_id,
FROM
users
LEFT OUTER JOIN user_bookings ON user_bookings.user_id = user.id
FULL JOIN payments ON payments.id = linguist_bookings.payment_id
Where payments.issued = false
OR payments.issued IS NULL;
If you use an outer join, you turn it into an inner join if you use it in a WHERE clause; because NULL = [something] is never true.
You are turning the outer join into an inner join. Dealing with filtering with full outer join is tricky. I recommend a subquery:
SELECT u.id as user_id, ub.id as user_booking_id, ub.payment_id as user_booking_payment_id
p.id as payment_id,
FROM users u LEFT OUTER JOIN
user_bookings ub
ON ub.user_id = u.id FULL JOIN
(SELECT p.*
FROM payments p
WHERE NOT p.issued
) p
payments p
ON p.id = ub.payment_id;
I would, however, question why you want a FULL JOIN here. You could get payment ids with no other information from the other tables.
I would expect a LEFT JOIN to be sufficient:
SELECT u.id as user_id, ub.id as user_booking_id, ub.payment_id as user_booking_payment_id,
p.id as payment_id
FROM users u LEFT JOIN
user_bookings ub
ON ub.user_id = u.id LEFT JOIN
payments p
ON p.id = ub.payment_id AND NOT p.issued;

Find users not in an organization

Given this data:
users
id name
== ====
1 Alice
2 Bob
3 Carl
organizations
id name
== ====
1 Aaa
2 Bbb
memberships
id organization_id user_id
== =============== =======
1 1 1
2 2 1
3 1 2
I want to find users that do not belong to a particular organization X:
users with no membership records at all
AND
users with membership records, but not for organization X.
For example, I want users that are not in organization 2. I am expecting:
users.id
========
2
3
Attempting with this join isn't working:
SELECT *
FROM users left join memberships on memberships.user_id = users.id
where memberships.id is null or memberships.organization_id <> 1
It's returning users 1,2,3 since 1 matches on the second where condition.
Is there an efficient way to do this?
Restricting your JOIN to organization of 2 and then testing for null is the one way to do what you're looking for e.g.
SELECT *
FROM users
LEFT JOIN memberships
ON memberships.user_id = users.id
AND memberships.organization_id = 2
WHERE memberships.id IS NULL
You can also use NOT IN
SELECT *
FROM users
WHERE id NOT IN (SELECT user_id from memberships where organization_id = 2 )
You can also use the minus set operator:
select "name" from users
minus
select "name" from users u inner join memberships m on u.id = m.user_id inner join organizations o on m.organization_id = o.id and o."name" = 'X'
select Users.*
from users
inner join memberships
on Users.id = memberships.id
left join organizations
on memberships.organization_id = organizations.id
where memberships.id is null AND memberships.organization_id = 2
Here's another option using the exists clause:
select * from users
where not exists
(select memberships.id from memberships
inner join organizations on memberships.organization_id = organizations.id
and memberships.user_id = users.id
where organizations.id = 2)
select u.*
from users u
left join memberships m
on u.id = m.user_id
left join organizations o
on m.organization_id = o.id
where m.organization_id not in (2)
EDIT: modified to include all users