ebean-orm / ebean-orm/ebean

ManyToMany generates non optimal query when getting only ids

Open
#1,884 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.5k
Forks
267
Avg merge
3d 20h
Merged PRs (30d)
5

Description

Description

I've noticed that ebean generates query which includes left join when trying to get only ids from ManyToMany relation.
Database - PostgreSQL.

Expected behavior

We expect execution of two queries:

  1. firstly we select people:
select t0.id, t0.first_name, t0.last_name, t0.address, t0.city, t0.country, t0.birth_date, t0.created_at, t0.modified_at from person t0 order by t0.id desc limit 100;
  1. after that we use subquery to fetch related ids:
select t0.person_id, t0.tag_id from person_tag t0 where (t0.person_id) = any(?);

Actual behavior

Actual behavior differs in second query, ebean generates it using left join

select int_.person_id, t0.id from tag t0 left join person_tag int_ on int_.tag_id = t0.id  where (int_.person_id) = any(?);

And this query can drastically affect performance on big tables.

Steps to reproduce

Consider we havethe following entities:

@Entity
class PersonEntity {
  @Id
  var id: Long? = null
  var firstName: String? = null
  var lastName: String? = null
  var address: String? = null
  var city: String? = null
  var country: String? = null
  var birthDate: LocalDate? = null
 
  @CreatedTimestamp
  @Column
  lateinit var createdAt: Timestamp
    protected set

  @UpdatedTimestamp
  @Version
  lateinit var modifiedAt: Timestamp
    protected set

  @ManyToMany
  var tags: List<TagEntity> = emptyList()
}

and

@Entity
class TagEntity {
  @Id
  var id: Long? = null

  @Column(nullable = false, updatable = false)
  @WhoCreated
  val createdBy: Long? = 0L

  @CreatedTimestamp
  lateinit var createdAt: Timestamp
    protected set

  @UpdatedTimestamp
  @Version
  lateinit var modifiedAt: Timestamp
    protected set

  @WhoModified
  val modifiedBy: Long? = 0L

  @ManyToMany(mappedBy = "tags")
  var persons: List<PersonEntity>? = null
}

And we're trying to fetch all fields from PersonEntity and only id field from TagEntity. For this we can use following query:

server.find(PersonEntity::class.java)
  .order("id DESC")
  .setMaxRows(100)
  .fetch("tags", "id", FetchConfig().query(100))
  .findPagedList()

Execution of this query will produce following log

DEBUG io.ebean.SQL - txn[1009] select t0.id, t0.first_name, t0.last_name, t0.address, t0.city, t0.country, t0.birth_date, t0.created_at, t0.modified_at from person t0 order by t0.id desc limit 100; --bind()
DEBUG io.ebean.SUM - txn[1009] FindMany type[person] origin[DtBvNV.A.A] exeMicros[9797] rows[4] predicates[] bind[]

DEBUG io.ebean.SQL - txn[1009] select int_.person_id, t0.id from tag t0 left join person_tag int_ on int_.tag_id = t0.id  where (int_.person_id) = any(?); --bind(Array[4]={5,3,2,1})
DEBUG io.ebean.SUM - txn[1009] FindMany mode[+query] type[tag] origin[DtBvNV.A.A] load[path:tags size:4] exeMicros[13778] rows[1] predicates[(int_.person_id) = any(?)] bind[Array[4]={5,3,2,1}]

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the ManyToMany fetch path exercised by server.find(...).fetch("tags", "id", FetchConfig().query(100)) and compare the logged second SQL query with the expected person_tag query. Trace where the left join is introduced, then add or update coverage for this reproduction and verify the generated SQL no longer joins tag.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin, postgresql
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.