TypeORM sort users by their last sent message timestamp

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

The issue you’re facing with the query is that the alias message is not properly recognized in the MAX(message.created) expression. In TypeORM, you need to ensure that the join and the usage of the alias in the MAX function are correctly structured.

Here’s a corrected approach to achieve the desired result:

this.userRepository
  .createQueryBuilder('user')
  .leftJoin('user.messages', 'message')
  .groupBy('user.id')
  .addGroupBy('user.name')
  .orderBy('MAX(message.created)', 'DESC')
  .getMany();

Key Points:

You are using leftJoin instead of leftJoinAndSelect because you don’t need to select all the messages. You just need the created timestamp for sorting.
The groupBy and orderBy are correctly used for aggregating by user and sorting by the maximum message.created.
If you also want to avoid joining messages if no messages exist for a user, consider filtering those users by using a HAVING clause. However, the above approach should meet your requirements for fetching users sorted by their last message timestamp.

What you want is a .distinctOn: demo at db-fiddle

this.userRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.messages', 'message')
  .distinctOn('user.id')
  .orderBy({"user.id": "ASC",
            "message.created": "DESC"})
  .getMany();

Which should roughly translate to this:

select distinct on("user".id) "user".*, message.message
from "user"
left join message 
on "user".id=message.user_id
order by "user".id asc, 
         message.created desc;

From PostgreSQL doc:

SELECT DISTINCT ON ( expression [, ...] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. The DISTINCT ON expressions are interpreted using the same rules as for ORDER BY.

In raw SQL you can order by aggregates directly but in your case leftJoinAndSelect would leave a dangling message field you neither grouped by nor used in an aggregate function, so even if injecting the MAX(message.created) worked, you’d get another error complaining about that message field: demo2 at db-fiddle

select "user".*,message.message
from "user"
left join message 
on "user".id=message.user_id
group by "user".id
order by max(message.created) desc;
error: column "message.message" must appear in the GROUP BY clause or be used in an aggregate function

Your simple query

What you were thinking of is basically valid in Postgres – and standard SQL:1999 (id being the primary key):

SELECT u.id, u.name  -- ①
FROM   users u
LEFT   JOIN messages m ON m."userId" = u.id
GROUP  BY u.id
ORDER  BY max(m.created) DESC;

① Do not list any columns from table messages, you don’t want to display them.

The manual about expressions in the ORDER BY list:

Each expression can be the name or ordinal number of an output column
(SELECT list item), or it can be an arbitrary expression formed from
input-column values.

But your ORM seems to expect names only. (As in the outdated SQL:92 standard.) See:

  • max(), group by and order by

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:

SELECT u.id, u.name
FROM   users u
LEFT   JOIN messages m ON m."userId" = u.id
GROUP  BY u.id
ORDER  BY max(m.created) DESC NULLS LAST, u.name, u.id;

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 with DISTINCT ON, so you’d end up with users sorted by id first (not what you want) and need a second query level to sort after distilling the latest created per user. See:

  • Select first row in each GROUP BY group?
  • PostgreSQL DISTINCT ON with different ORDER BY

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:

SELECT u.id, u.name
FROM   users u
LEFT   JOIN (
   SELECT "userId", max(created) AS last_msg
   FROM   messages
   GROUP  BY "userId"  -- ①
   ) m ON m."userId" = u.id
ORDER  BY m.last_msg DESC NULLS LAST  -- ②
        , u.name, u.id;  -- ③

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. Add NULLS LAST. See:

  • Sort by column ASC, but NULL values first?

③ 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:

  • SELECT DISTINCT is slower than expected on my table in PostgreSQL
  • Optimize GROUP BY query to retrieve latest row per user

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.

Your query will be this:

SELECT u.id, u.name
FROM users u
LEFT JOIN messages m ON m.user_id = u.id
GROUP BY u.id
ORDER BY max(m.created_at) DESC;

In typeORM it will be:

this.userRepository
  .createQueryBuilder('user')
  .select('user.id')
  .addSelect('user.name')
  .leftJoin(Message, 'msg', 'msg."userId" = user.id') 
  .groupBy('user.id')
  .orderBy('MAX(msg.created)', 'DESC')
  .getMany();

make sure that the alias for the aggregated column is correctly referenced in both the addSelect and orderBy methods. Moreover, you should ensure that the join is correctly set up and that the query builder is aware of the message alias

import { getRepository } from 'typeorm';
import { User } from './User';
import { Message } from './Message';

async function getUsersSortedByLastMessageTimestamp() {
  const userRepository = getRepository(User);

  const users = await userRepository
    .createQueryBuilder('user')
    .leftJoin('user.messages', 'message')
    .addSelect('MAX(message.timestamp)', 'lastMessageTimestamp')
    .groupBy('user.id')
    .orderBy('lastMessageTimestamp', 'DESC')
    .getMany();

  return users;
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật