typeorm: QueryBuilder produces wrong SQL with order by + nested fields, breaks pagination-related queries with skip() and take()

Issue type:

[ ] question [x] bug report [ ] feature request [ ] documentation issue

Database system/driver:

[ ] cordova [ ] mongodb [ ] mssql [ ] mysql / mariadb [ ] oracle [x] postgres [ ] cockroachdb [ ] sqlite [ ] sqljs [ ] react-native [ ] expo

TypeORM version:

[ ] latest [ ] @next [x] 0.2.25

Steps to reproduce or a small repository showing the problem:

I am encountering unexpected behaviour with pagination-related skip() and take() methods using the query builder. It occurs when I add an order by clause to a joined field.

Suppose I have a query that returns 3 items with a given :accountId:

const query = this.createQueryBuilder('invoice')
  .innerJoin('invoice.account', 'account', 'account.id = :accountId', { accountId: user.account.id })
  .leftJoinAndSelect('invoice.items', 'items')
  .leftJoinAndSelect('invoice.client', 'client')
  .where('invoice.active = :active', { active: true })
  .addOrderBy('invoice.dueDate', 'ASC')

If I add skip(0) and take(2) I get the expected 2 items. If I query with getManyAndCount(), the count is the expected value of 3.

If I modify my query to sort the invoice line items based on a sort field, then skip() and take() no longer work as expected. See modified query below:

const query = this.createQueryBuilder('invoice')
  .innerJoin('invoice.account', 'account', 'account.id = :accountId', { accountId: user.account.id })
  .leftJoinAndSelect('invoice.items', 'items')
  .leftJoinAndSelect('invoice.client', 'client')
  .where('invoice.active = :active', { active: true })
  .addOrderBy('invoice.dueDate', 'ASC')
  .addOrderBy('items.sort', 'ASC') // new offending line

Note that the above order-by clause produces the desired result: the invoice line items are correctly sorted within each invoice.

However, if I add skip(0) and take(2) I now get an unexpected 1 item in the result. If I query with getManyAndCount(), the count is the expected value of 3.

If I use take(3) and take(4) I still get the unexpected 1 item in the result, again with the count showing the expected value of 3.

If I try take(5) I will get 2 items in the result. If I try take(6) I will finally get all 3 items.

I’m not too sure what’s going on or why this particular order-by clause is causing this issue. I’m relatively inexperienced with typeorm so there is a definite chance that I’m missing something.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 14
  • Comments: 20 (2 by maintainers)

Commits related to this issue

Most upvoted comments

I just encountered this issue using findAndCount using typeorm@0.3.17. I wanted to sort joined entities by their primary key as they appear to be unsorted by default.

// example object graph
[
  {
    "id": 1,
    // nested is unsorted
    "nested": [{ "id": 5 }, { "id": 1 }, { "id": 3 }]
  }
]

When adding a nested sort order I no longer got the expected entries returned. In my test data set I got 7 entries whereas I was expecting 10.

// example call
findAndCount({
  order: { nested: { id: "ASC" } },
  skip: 0,
  take: 10,
  relations: {
    nested: true,
  },
})

If there is another way to sort nested relations when using findAndCount please let me know 😸

I inspected the generated SQL and believe I’ve spotted the issue.

-- 1st of 3 queries executed by typeorm
SELECT
    DISTINCT "distinctAlias"."Foo_id" AS "ids_Foo_id",
    "distinctAlias"."Foo**Foo_nested_id"
FROM
    (
        -- omitted for brevity
    ) "distinctAlias"
ORDER BY
    "distinctAlias"."Foo**Foo_nested_id" ASC,
    "Foo_id" ASC
LIMIT
    10

This is doing DISTINCT and LIMIT with the nested ids joined on. This is the problem. Here’s an example of the output. Notice that there are duplicates in Foo_id

Foo_id Foo**Foo_nested_id
10 null
16 2
4 15
4 18
4 22
15 17
6 null
21 7
21 23
14 null

This output is used in the 3rd and final query.

-- 3rd of 3 queries executed by typeorm
-- rest omitted for brevity
WHERE
    "Foo"."id" IN (10, 16, 4, 4, 4, 15, 6, 21, 21, 14) -- notice the duplicate ids

I don’t know how to fix this in typeorm though. But hope it can help fix the issue.

Adding my take to the discussion:

  1. The simplest reproduction case:
await this.mainEntityRepository
      .createQueryBuilder('mainEntity')
      .leftJoin('mainEntity.joinedEntity', 'joinedEntity')
      .orderBy('joinedEntity.order', 'ASC')
      .take(<N>)
      .getMany();

(with multiple joinedEntitys per mainEntity)

  1. Analysing current query plan:
SELECT DISTINCT
	"distinctAlias"."mainEntity_id" as "ids_mainEntity_id",
    "distinctAlias"."joinedEntity_order"
FROM (
	SELECT "mainEntity"."id" AS "mainEntity_id", "joinedEntity"."order" AS "joinedEntity_order"
    FROM "mainEntity"
    LEFT JOIN "joinedEntity" ON "joinedEntity"."mainEntityId"="mainEntity"."id"
) "distinctAlias"
ORDER BY "distinctAlias"."joinedEntity_order" ASC, "mainEntity_id" ASC
LIMIT <N>;

SELECT "mainEntity".*, "joinedEntity".*
FROM "mainEntity" LEFT JOIN "joinedEntity" ON "joinedEntity"."recipeId"="mainEntity"."id"
WHERE "mainEntity"."id" IN (<N ids from previous query>)
ORDER BY "joinedEntity"."order" ASC;
  1. Suggestion of query plan:
SELECT "distinctAlias"."mainEntity_id" as "ids_mainEntity_id"
FROM (
	SELECT "mainEntity"."id" AS "mainEntity_id", "joinedEntity"."order" AS "joinedEntity_order"
    FROM "mainEntity"
    LEFT JOIN "joinedEntity" ON "joinedEntity"."mainEntityId"="mainEntity"."id"
) "distinctAlias"
GROUP BY "distinctAlias"."mainEntity_id"
ORDER BY min("distinctAlias"."joinedEntity_order") ASC, "mainEntity_id" ASC
LIMIT <N>;

SELECT "mainEntity".*, "joinedEntity".*
FROM "mainEntity" LEFT JOIN "joinedEntity" ON "joinedEntity"."recipeId"="mainEntity"."id"
WHERE "mainEntity"."id" IN (<N ids from previous query>)
ORDER BY "joinedEntity"."order" ASC;

Basically, replacing SELECT DISTINCT id, <other orderBy fields> FROM everything ORDER BY <all orderBy fields> by SELECT id FROM everything GROUP BY id ORDER BY <min of all orderBy fields>

  1. Discussion

I would be happy to submit a PR, but it would be awesome having feedbacks from typeorm core contributor(s) beforehand!

Thanks for the reading 😉

@Nilegfx now that we have a better handle on the issue (thanks everyone), I will rename it from original Unexpected behaviour with skip() and take() re queries with a particular order-by clause to QueryBuilder produces wrong SQL with order by + nested fields, breaks pagination-related queries with skip() and take() in hopes it helps get the issue more attention.

I’ve just encountered this bug too. Thank you firxworx for pointing out that the issue is with the orderby on nested field! In my case, I have a parent entity (left side of one to many), and a child entity with composite primary key (right side of one to many). One column of 2-column composite primary key references parent entity. I make the query builder as such:

const sessions = await sessionsRepository
  .createQueryBuilder('sessions')
  .leftJoinAndSelect('sessions.packets', 'packets')
  .select([
    'sessions.id',
    'packets.sequence',
    //... something else
  ])
  .where({
    userId: user.id,
  })
  .take(limit)
  .skip(offset)
  .orderBy('sessions.id', 'DESC')
  .addOrderBy('packets.sequence', 'ASC') // !!! THE ISSUE IS ON THIS LINE
  .getManyAndCount();

If I have the noted line in place, I see TypeORM produces the query:

SELECT DISTINCT "distinctAlias"."sessions_id" as "ids_sessions_id", "distinctAlias"."sessions_id", "distinctAlias"."packets_sequence" FROM ...

Which is incorrect: it selects primary column of parent entity AND BOTH primary columns of child entity, thus the result will have more than one row for one parent entity.

For comparison, without the noted line, here’s what TypeORM generates:

SELECT DISTINCT "distinctAlias"."sessions_id" as "ids_sessions_id", "distinctAlias"."sessions_id" FROM ...

Which is correct: it will give only one row per one parent entity (session).

Still facing the issue as well with 0.3.11

The bug still persists

We are at 0.3.7 right now. I will check later and inform you.

@firxworx , thanks for your reply.

Unfortunately, I’m trying to add a sorting feature to my library https://github.com/benjamin658/typeorm-cursor-pagination, and it relies on the query builder, so it’s not possible to use the raw query.

@benjamin658 if you need an escape hatch you can always hand-bomb a raw SQL query yourself. Refer to EntityManager.query(): https://typeorm.io/#/entity-manager-api. Of course, be careful re potential for SQL Injection vulnerabilities, etc. as the onus is now on you to be safe.

A part of me is surprised that this issue from last year is still kicking about… how are a ton of devs not facing this exact issue given the overall popularity of TypeORM especially w/ postgres!?!?