31
<?php

class Cat extends Eloquent {

    public function user() {
        return $this->belongsTo('User');
    }
}

class User extends Eloquent {

    public function cats() {
        return $this->hasMany('Cat');
    }
}

Now:

$cats = Cat::with('user')->get();

Performs 2 queries:

select * from `cats`
select * from `users` where `users`.`id` in ('1', '2', 'x')

Why can't it just do:

select * from cats inner join users on cats.user_id = users.id

For those saying that there are both id columns in the table, that could be easily avoided with aliases:

select 
    c.id as cats__id,
    c.name as cats__name,
    c.user_id as cats__user_id,
    b.id as users__id,
    b.name as users__name
from cats c
inner join users b on b.id = c.user_id

UPDATE

Someone pointed out that Eloquent doens't know the columns of the tables from the models, but I guess they could provide a way to define them in the model so then it could use aliases and do a proper join instead of an extra query.

empz
  • 11,509
  • 16
  • 65
  • 106
  • Is that literally all there is to your class definitions? Because if so, the ORM has no way of knowing what columns to expect from each table, which might make it fall back to a "dumb" strategy? (I can think of a trivial workaround it could use though: result columns in SQL have well-defined order, so `SELECT cats.*, '---' AS _join_separator, users.* ...` would allow the columns to be neatly partitioned.) – IMSoP May 28 '14 at 20:22
  • There is the `$fillable` var which states the automapped fields, but that's all in regards of field declaration. I'm new to Laravel and Eloquent but if that's the problem, they could easily ask us to define the models fields and be able to do a proper JOIN query for eager loading. – empz May 28 '14 at 20:24
  • Why do you think there's need for join? And for that matter, `inner join` is definitely not the way to go. – Jarek Tkaczyk May 28 '14 at 20:24
  • You can manually join using `join`. – The Alpha May 28 '14 at 20:26
  • 2
    @deczo Ahmmm... because it's 1 query instead of 2? The `user_id` on `Cat` is not nullable, every `Cat` belongs to an `User`... So why not `inner join`? – empz May 28 '14 at 20:26
  • @WereWolf-TheAlpha I know I can use the query builder and use joins manually. I'm just asking if there's something I'm missing on why Eloquent can do it that way. – empz May 28 '14 at 20:27
  • Inner join as a rule, not in this particular situation. You can use more than 2 tables. Performance difference wouldn't be that obvious then, or would be? – Jarek Tkaczyk May 28 '14 at 20:47
  • Probably it's not the efficient way to create/write an `inner join` programmatically depending on the context. – The Alpha May 28 '14 at 20:49
  • 2
    @deczo Again, this is not a major problem if the ORM designers *wanted* to tackle it. The definition of the objects could easily distinguish "always has" vs "sometimes has", or the retrieval "definitely with" vs "possibly with", and choose inner, left, or right join appropriately. I don't know Eloquent, so maybe they just decided to keep things simple. – IMSoP May 28 '14 at 21:09
  • 4
    @deczo I don't think I'm following you. The join clause exists for a reason, so you don't have to do extra queries to match rows from different tables. If you're talking between different type of joins, like left, right, inner... I guess you could tell Eloquent which one to use depending on the relationship in the model. `belongsTo` would imply that it always belongs to, so the foreign key can't be null. They could add another like `canBelongTo` which would allow foreign key to be null, and in that cause it would use left join. Don't know, just saying... – empz May 28 '14 at 21:10
  • 1
    The thing is ORM is just another layer to make life easier. It's main purpose is to be dev-friendly, and I think this one is as easy as it can be. It has flaws and limitations, there are performance issues, inconsistencies in the code as well, but on the other hand it is really eloquent and great for easy tasks. However, there is no ORM flexible enough to meet all the requirements, that's why you (I) don't use ORM for more complex jobs. – Jarek Tkaczyk May 28 '14 at 21:30
  • 1
    That being said, all the joins you talk about, are great. But not that great to work with in this context, that's probably the reason of eloquent not using joins for eager loading. – Jarek Tkaczyk May 28 '14 at 21:32
  • @deczo You keep saying the joins are not that great in this context and I still can't understand what you mean. If you have to get all the cats with their owners in plain SQL, with the greatest performance, wouldn't you do use a join clause? – empz May 28 '14 at 23:36
  • I keep saying it's much easier to hydrate and match all the models when you work with separate queries in the context of ORM. I don't know for sure why it works that way, Taylor Otwell could give the answer. – Jarek Tkaczyk May 29 '14 at 07:20
  • 2
    @emzero, I recently started checking out laravel and I am facing the exact same question, I cant believe how a developer can say that extra query is ok, where join would do the job and/or `select *` is ok – dav Feb 05 '17 at 21:28
  • I am coming from a different framework background and new to laravel. i have posted my answer as to why i think laravel is using exists instead of join for eager loading. – Azraar Azward Nov 20 '18 at 11:54

3 Answers3

12

My guess is that this allows for eager loading multiple one to many relationships. Say, for instance, we also had a dogs table:

class User extends Eloquent {

    public function cats() {
        return $this->hasMany('Cat');
    }

    public function dogs() {
        return $this->hasMany('Dog');
    }
}

Now we want to eager load them both with the User:

$users = User::with('cats','dogs')->get();

There is no join that would work to combine these into a single query. However, doing a seperate query for each "with" element does work:

select * from `users`
select * from `cats` where `user`.`id` in ('1', '2', 'x')
select * from `dogs` where `user`.`id` in ('1', '2', 'x') 

So, while this methodology may produce an extra query in some simple circumstances, it provides the ability to eager load more complex data where the join method will fail.

This is my guess as to why it is this way.

DBCrocky
  • 204
  • 1
  • 6
  • 1
    This is right for N<>M and 1<>N relations, but not for 1<>1 or N<>1. Even if you implement only this 2 - there is still a very big improvement, especially for large sets. (Also, 1<>N can also be solved with one extra query regardless of the number of records in the set) – Yaron U. Sep 11 '16 at 18:45
  • for `belongsTo` relations this does not apply – dav Feb 05 '17 at 21:13
  • 1
    but eloquent can use join for belongsTo and another query for hasMany – fico7489 Dec 03 '17 at 15:56
  • 1
    `There is no join that would work to combine these into a single query.` Well there is, its the left join – Adam Sep 18 '20 at 08:30
3

I think, join query approach has a fatal drawback when you want to use LIMIT and/or OFFSET.

$users = User::with('cats')->get() - this will output the below 2 queries.

select * from `users`
select * from `cats` where `user`.`id` in ('1', '2', 'x')

and its not one single query as

select * from users inner join cats on cats.user_id = users.id

but lets say, we need to paginate this record set.

User::with('cats')->paginate(10) - this will output the below 2 queries with limit.

select * from `users` limit 10
select * from `cats` where `user`.`id` in ('1', '2', 'x')

with a join, it will be like

select * from users inner join cats on cats.user_id = users.id limit 10

it will fetch 10 records but it does not mean 10 users, because every user can have multiple cats.

Also another reason i think is, a relation between relational db and NOSQL db can be easily implemented with the separated query approach

Also as previous answer, id is ambiguous, and you would have to prefix every statement with the table name which is not desired.

On the other hand, JOIN is expensive than EXISTS and EXISTS is faster because it doesn't order RDBMS to fetch any data, just check whether relevant rows exist. EXISTS is used to return a boolean value, JOIN returns a whole other table.

For Scalability purpose if following sharding architecture, will have to remove the JOIN's. This was practiced by pinterest during the scaling. http://highscalability.com/blog/2013/4/15/scaling-pinterest-from-0-to-10s-of-billions-of-page-views-a.html

Azraar Azward
  • 1,586
  • 2
  • 12
  • 16
1

cats and users likely both have a column named id, leaving your suggested query ambiguous. Laravel's eager loading uses an additional query, but avoids this potential mishap.

ceejayoz
  • 176,543
  • 40
  • 303
  • 368
  • You're right. It isn't a way to append the table name to returned columns? Like `cat.id`, `cat.name`, `user.id`, etc? – empz May 28 '14 at 20:14
  • 8
    That's trivial to handle with aliases in the query (e.g. `cat.id as cat__id`), and I'd expect an ORM to take that in its stride. – IMSoP May 28 '14 at 20:15
  • @IMSoP Exactly, I'd expect the same too. – empz May 28 '14 at 20:16
  • 1
    @IMSoP Unlike some ORMs, Eloquent doesn't know what columns are in the tables - most of the time it's doing `SELECT *`. It wouldn't know which columns to alias. – ceejayoz May 28 '14 at 20:28
  • @ceejayoz I wish there could be a way to tell which columns are in the tables, and if that's the case, Eloquent will perform this JOIN query instead of the second query and matching it later. – empz May 28 '14 at 20:30
  • 3
    @ceejayoz See my comment above regarding returning an extra "separator" column for each join (a trick I use frequently in debugging). It would certainly be possible to build an ORM which used joins without knowing the non-key columns in advance. This is either a design decision on Eloquent's part, or simply a limitation they're not interested in fixing. – IMSoP May 28 '14 at 20:44
  • @IMSoP You're welcome to file an issue with Laravel on Github. I'm just explaining how it is currently implemented. – ceejayoz May 28 '14 at 21:19
  • @ceejayoz Yeah, just pointing out that it's still only a matter of "it doesn't do that" rather than "it *couldn't* do that" - the implementation could be entirely invisible to the user, so it's a question of internal design. The most plausible explanation for **why** it works the way it does seems to be that simplicity (of the implementation itself) was seen as more important than the efficiencies which can be gained from generating more complex SQL under the hood. – IMSoP May 28 '14 at 21:42
  • I'm still not seeing a good reason to not have left joins in Eloquent - every query saved improves response time. This seems a "pattern over practicality" approach. In other projects I've had 8 left joins, plus aliased joins on the same table in one query. Seems an odd decision. – anoldermark Sep 29 '17 at 15:29