I’m working on a NestJS application with TypeORM and PostgreSQL, and I need to fetch a list of users sorted by the timestamp of the last message they sent.
I have two entities, User and Message, defined as follows:
user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Message } from './message.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Message, (message) => message.user)
messages: Message[];
}
message.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('messages')
export class Message {
@PrimaryGeneratedColumn()
id: number;
@Column()
userId: number;
@Column()
message: string;
@CreateDateColumn({ type: 'timestamp' })
created: Date;
@ManyToOne(() => User, (user) => user.messages)
user: User;
}
Here are some sample records in both tables:
Users Table:
+----+--------+
| id | name |
+----+--------+
| 1 | John |
| 2 | Mike |
| 3 | Norman |
+----+--------+
Messages Table:
+----+--------+-------------+---------------------+
| id | userId | message | created |
+----+--------+-------------+---------------------+
| 1 | 3 | Hello there | 2024-08-21 05:30:00 |
| 2 | 2 | Hi! | 2024-08-22 06:40:00 |
| 3 | 3 | Hello again | 2024-08-23 07:50:00 |
+----+--------+-------------+---------------------+
I want to write a query that fetches all users and sorts them based on the timestamp of their last message. The expected result should be something like this:
[
{
"id": 3,
"name": "Norman"
},
{
"id": 2,
"name": "Mike"
},
{
"id": 1,
"name": "John"
}
]
What I tried so far is:
this.userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.messages', 'message')
.groupBy('user.id')
.addGroupBy('user.name')
.orderBy('MAX(message.created)', 'DESC')
.getMany();
But I get this error:
"MAX(message" alias was not found. Maybe you forgot to join it?
2
Answers
What you want is a
.distinctOn
: demo at db-fiddleWhich should roughly translate to this:
From PostgreSQL doc:
In raw SQL you can order by aggregates directly but in your case
leftJoinAndSelect
would leave a danglingmessage
field you neither grouped by nor used in an aggregate function, so even if injecting theMAX(message.created)
worked, you’d get another error complaining about thatmessage
field: demo2 at db-fiddleYour simple query
What you were thinking of is basically valid in standard SQL (incl. Postgres):
① Do not list any columns from table
messages
, you don’t want to display them.The manual about expressions in the
ORDER BY
list:But your ORM seems to expect names only.
The widespread willingness to put up with half-baked ORMs instead of using universally applicable, unrestricted SQL is remarkable.
Plus, the sort order is not deterministic, yet. See explanation below. You rather want:
View on DB Fiddle
Better query
Depending on undisclosed details of your setup, other query styles are (much) faster.
DISTINCT ON
(like Zegarek provided) is nice and simple – and fast for only few messages per user.But
ORDER BY
cannot disagree withDISTINCT ON
, so you’d end up with users sorted byid
first (not what you want) and need a second query level to sort after distilling the latestcreated
per user. See:What’s more,
DISTINCT ON
only enters the picture to fix a situation created by the join. While fetching all users, it’s (much) faster and simpler to aggregate before joining:View on DB Fiddle
① We might use
DISTINCT ON
here, but a plain aggregate is just as well in the simple subquery.② Users with no messages end up with null for
last_msg
, which sorts first in descending order. You’d get users without messages first, which is certainly not what you want. AddNULLS LAST
. See:③ Without tiebreaker(s), users with the same
last_msg
are sorted arbitrarily. (Sort order may change between calls.)If there are many messages per user, emulate an index skip scan:
If you only want users with messages, a different query is slightly better.
If you only want the top N users, a different query is (much) better.