How to get latest DETAIL entry against the MASTER entry? - sql

I have 2 tables
1. User Master
user_id, user_full_name, user_dob...so on
2. Login Details
login_id, login_user_id, login_time, login_date, logout_time
Problem
2nd table has n number of rows against User Master table id
I need to make a join but the condition is that it should show only last login data of the user
example
user_full_name, user_login, user_logout so on...

If you want the result for a single user, you could use a simple INNER JOIN combined with an ORDER BY and TOP 1:
SELECT TOP 1 user_full_name, login_time, login_date, logout_time
FROM Users INNER JOIN Logins ON
Users.user_id = Logins.user_id
WHERE
Users.user_id = #user_id
ORDER BY login_date DESC, login_time DESC
(See SQLFiddle)
If you want the result for all users, you could use CROSS APPLY:
SELECT user_full_name, l.*
FROM Users u CROSS APPLY (
SELECT TOP 1 login_time, login_date, logout_time
FROM Logins
WHERE
u.user_id = Logins.user_id
ORDER BY login_date DESC, login_time DESC
) l
(See SQLFiddle)

A common solution for this problem is to use the row_number window function and filter for rows with row number 1 in each partition (by user, ordered by date/time):
WITH UserDetails AS (
SELECT
*
, ROW_NUMBER() OVER (PARTITION BY login_user_id
ORDER BY login_date DESC, login_time DESC) AS RN
FROM LoginDetails
)
SELECT *
FROM UserMaster M
JOIN UserDetails D ON M.user_id = D.login_user_id
WHERE D.RN = 1;

You could try using a TOP 1 inside the JOIN clause:
SELECT a.user_id, a.user_full_name, b.login_id...
FROM UserMaster a INNER JOIN Logins b ON b.login_date =
(
SELECT TOP 1 login_date
FROM Logins
WHERE login_user_id = a.user_id
ORDER BY login_date DESC
)

Related

Select only those users who have the most visits to provided district

I have a query that selects users with the districts which they visited and visits count.
select users.id, places.district, count(users.id) as counts from users
left join visits on users.id = visits.user_id
inner join places on visits.place_id = places.id
group by users.id, places.district
I need to select only those users who have visited provided district the most. For example, I have a user with id 1 who visited district A one time and district B three times. If I provide district B as parameter, user 1 will be in select. If I want to select users from district A, user 1 will not be in select.
I think that's ranking, then filtering:
select *
from (
select u.id, p.district, count(*) as cnt_visits,
rank() over(partition by u.id order by count(*) desc)
from users u
inner join visits v on u.id = v.user_id
inner join places p on p.id = v.place_id
group by u.id, p.district
) t
where rn = 1 and district = ?
Note that you don't actually need table users to get this result. We could simplify the query as:
select *
from (
select v.user_id, p.district, count(*) as cnt_visits,
rank() over(partition by u.id order by count(*) desc)
from visits v
inner join places p on p.id = v.place_id
group by v.user_id, p.district
) t
where rn = 1 and district = ?
This query handles top ties: if a user had the same, maximum number of visits in two different districts, both are taken into account. If you don't need that feature, then we can simplify the subquery with distinct on:
select *
from (
select distinct on (v.user_id) v.user_id, p.district, count(*) as cnt_visits
from visits v
inner join places p on p.id = v.place_id
group by v.user_id, p.district
order by v.user_id, cnt_visits desc
) t
where district = ?

Issue with getting the rank of a user based on combined columns in a join table

I have a users table and each user has flights in a flights table. Each flight has a departure and an arrival airport relationship within an airports table. What I need to do is count up the unique airports across both departure and arrival columns (flights.departure_airport_id and flights.arrival_airport_id) for each user, and then assign them a rank via dense_rank and then retrieve the rank for a given user id.
Basically, I need to order all users according to how many unique airports they have flown to or from and then get the rank for a certain user.
Here's what I have so far:
SELECT u.rank FROM (
SELECT
users.id,
dense_rank () OVER (ORDER BY count(DISTINCT (flights.departure_airport_id, flights.arrival_airport_id)) DESC) AS rank
FROM users
LEFT JOIN flights ON users.id = flights.user_id
GROUP BY users.id
) AS u WHERE u.id = 'uuid';
This works, but does not actually return the desired result as count(DISTINCT (flights.departure_airport_id, flights.arrival_airport_id)) counts the combined airport ids and not each unique airport id separately. That's how I understand it works, anyway... I'm guessing that I somehow need to use a UNION join on the airport id columns but can't figure out how to do that.
I'm on Postgres 13.0.
I would recommend a lateral join to unpivot, then aggregation and ranking:
select *
from (
select f.user_id,
dense_rank() over(order by count(distinct a.airport_id) desc) rn
from flights f
cross join lateral (values
(f.departure_airport_id), (f.arrival_airport_id)
) a(airport_id)
group by f.user_id
) t
where user_id = 'uuid'
You don't really need the users table for what you want, unless you do want to allow users without any flight (they would all have the same, highest rank). If so:
select *
from (
select u.id,
dense_rank() over(order by count(distinct a.airport_id) desc) rn
from users u
left join flights f on f.user_id = u.id
left join lateral (values
(f.departure_airport_id), (f.arrival_airport_id)
) a(airport_id) on true
group by u.id
) t
where id = 'uuid'
You're counting the distinct pairs of (departure_airport_id, arrival_airpot_id). As you suggested, you could use union to get a single column of airport IDs (regardless of whether they are departure or arrival airports), and then apply a count on them:
SELECT user_id, DENSE_RANK() OVER (ORDER BY cnt DESC) AS user_rank
FROM (SELECT u.id AS user_id, COALESCE(cnt, 0) AS cnt
FROM users u
LEFT JOIN (SELECT user_id, COUNT DISTINCT(airport_id) AS cnt
FROM (SELECT user_id, departure_airport_id AS airport_id
FROM flights
UNION
SELECT user_id, arrival_airport_id AS airport_id
FROM flights) x
GROUP BY u.id) f ON u.id = f.user_id) t

Using Top in SQL Server 2012

I have a table Contacts, parent to table Activity. I would like to select the latest activity for each contact, but getting more than one row.
This is my query:
select top 30
*
from
Contacts o, Activity d
where
o.ID = d.contact
and d.ID > 401061
and Last_Action is null
order by
d.activity_date desc
I think I need Top? but not sure how to implement here. Any help would be appreciated.
You can use row_number() to number each contact's activities. In an outer query, you can filter down to only the latest activity per contact:
select top 30 *
from (
select row_number() over (
partition by o.ID
order by d.activity_date desc) as rn
, *
from Contacts o
join Activity d
on o.ID = d.contact
where d.ID > 401061
and Last_Action is null
) as SubQueryAlias
where rn = 1 -- Only last activity per contact
order by
activity_date desc
Here's a way using not exists that will work on most dbs. You're basically selecting each activity per contact where a newer activity does not exist (therefore it's the latest activity).
select top 30 * from activity a
join contact c on c.id = a.contact
where not exists (
select 1 from activity b
where b.contact = a.contact
and b.activity_date > a.activity_date
) and last_action is null and a.id > 401061
order by a.activity_date desc
This is slight guesswork as I can't see what your tables look like in terms of columns, but perhaps this (or something like it) would work?
select top 30 * from Contacts o,
(SELECT contact, max(activity_date) FROM Activity GROUP BY contact) d
where o.ID = d.contact And d.ID > 401061
and Last_Action is null order by d.activity_date desc
I think you want to use a subquery:
SELECT TOP 30 *
FROM
Contacts AS o,
( SELECT
contact,
MAX( activity_date ) AS activity_date
FROM
Activity
WHERE
contact > 401061 AND
Last_Action IS NULL
GROUP BY
contact
) AS d
WHERE
o.ID = d.contact
ORDER BY
d.activity_date
Assuming you want the top 30 actions per contact, this is the exact kind of thing CROSS APPLY was invented for.
Something like the following - uncertainties are because I can't see an example of your data.
select
*
from
contacts
cross apply (
select top 30
*
from
activity
where
contacts.id = activity.contact
and 401061 < activity.id
) as _ca
where
last_action is null -- Perhaps you could move this into the CA - but we don't know which table it's from
order by
activity.activity_date desc;
edit
select top 30
*
from
contacts
cross apply (
select top 1
*
from
activity
where
contacts.id = activity.contact
and 401061 < activity.id
order by
activity.activity_date desc
) as _ca
where
last_action is null -- assuming this is in table contacts
order by
_ca.activity_date desc;
Using a cte
WITH cte AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY contact ORDER BY activity_date DESC) rn
FROM Activity
WHERE d.ID > 401061 AND Last_Action IS NULL
)
SELECT TOP 30 *
FROM Contacts o
JOIN cte d ON o.Id = d.contact
WHERE d.rn = 1
ORDER BY cte.activity_date DESC

Select all threads and order by the latest one

Now that I got the Select all forums and get latest post too.. how? question answered, I am trying to write a query to select all threads in one particular forum and order them by the date of the latest post (column "updated_at").
This is my structure again:
forums forum_threads forum_posts
---------- ------------- -----------
id id id
parent_forum (NULLABLE) forum_id content
name user_id thread_id
description title user_id
icon views updated_at
created_at created_at
updated_at
last_post_id (NULLABLE)
I tried writing this query, and it works.. but not as expected: It doesn't order the threads by their last post date:
SELECT DISTINCT ON(t.id) t.id, u.username, p.updated_at, t.title
FROM forum_threads t
LEFT JOIN forum_posts p ON p.thread_id = t.id
LEFT JOIN users u ON u.id = p.user_id
WHERE t.forum_id = 3
ORDER BY t.id, p.updated_at DESC;
How can I solve this one?
Assuming you want a single row per thread and not all rows for all posts.
DISTINCT ON is still the most convenient tool. But the leading ORDER BY items have to match the expressions of the DISTINCT ON clause. If you want to order the result some other way, you need to wrap it into a subquery and add another ORDER BY to the outer query:
SELECT *
FROM (
SELECT DISTINCT ON (t.id)
t.id, u.username, p.updated_at, t.title
FROM forum_threads t
LEFT JOIN forum_posts p ON p.thread_id = t.id
LEFT JOIN users u ON u.id = p.user_id
WHERE t.forum_id = 3
ORDER BY t.id, p.updated_at DESC
) sub
ORDER BY updated_at DESC;
If you are looking for a query without subquery for some unknown reason, this should work, too:
SELECT DISTINCT
t.id
, first_value(u.username) OVER w AS username
, first_value(p.updated_at) OVER w AS updated_at
, t.title
FROM forum_threads t
LEFT JOIN forum_posts p ON p.thread_id = t.id
LEFT JOIN users u ON u.id = p.user_id
WHERE t.forum_id = 3
WINDOW w AS (PARTITION BY t.id ORDER BY p.updated_at DESC)
ORDER BY updated_at DESC;
There is quite a bit going on here:
The tables are joined and rows are selected according to JOIN and WHERE clauses.
The two instances of the window function first_value() are run (on the same window definition) to retrieve username and updated_at from the latest post per thread. This results in as many identical rows as there are posts in the thread.
The DISTINCT step is executed after the window functions and reduces each set to a single instance.
ORDER BY is applied last and updated_at references the OUT column (SELECT list), not one of the two IN columns (FROM list) of the same name.
Yet another variant, a subquery with the window function row_number():
SELECT id, username, updated_at, title
FROM (
SELECT t.id
, u.username
, p.updated_at
, t.title
, row_number() OVER (PARTITION BY t.id
ORDER BY p.updated_at DESC) AS rn
FROM forum_threads t
LEFT JOIN forum_posts p ON p.thread_id = t.id
LEFT JOIN users u ON u.id = p.user_id
WHERE t.forum_id = 3
) sub
WHERE rn = 1
ORDER BY updated_at DESC;
Similar case:
Return records distinct on one column but order by another column
You'll have to test which is faster. Depends on a couple of circumstances.
Forget the distinct on:
SELECT t.id, u.username, p.updated_at, t.title
FROM forum_threads t
LEFT JOIN forum_posts p ON p.thread_id = t.id
LEFT JOIN users u ON u.id = p.user_id
WHERE t.forum_id = 3
ORDER BY p.updated_at DESC;

Joining a subquery with multiple associcated rows in subquery

I have a query that selects a set of Users. Each User can have a number
of Events associated with it. I want to join each User with the earliest Event
associated with that User (resulting in one row per User), and do so within a single query.
So, I kind of want to do this:
SELECT * FROM users
left join (
select * from events where events.user_id = users.id
order by start_time limit 1) as event
ON ("event"."user_id" = "users"."id")
but it is illegal to reference 'users' within the join's select.
You can use a subquery to get the min(start_time) for each user_id. Then you will use this result to join back to the events table to get the details of the min event:
SELECT *
FROM users u
LEFT JOIN
(
SELECT Min(start_time) Min_Start, user_id
FROM events
GROUP BY user_id
) e1
ON u.id = e1.user_id
LEFT JOIN events e2
ON e1.user_id = e2.user_id
AND e1.min_start = e2.start_time
If you are using a database that has the ability to apply a row_number(), then you could use the following:
select *
from
(
SELECT *,
row_number() over(partition by e.user_id order by start_time) rn
FROM users u
LEFT JOIN events e
ON u.id = e.user_id
) src
where rn = 1
In most databases, you can use row_number() for this:
SELECT *
FROM users u left join
(select e.*, row_number() over (partition by e.user_id order by start_time) seqnum
from events e
) e
on e.user_id = u.id
MySQL and MS Access do not support this function, but most other databases do (and you do not specify what database you are using).