Skip to content

includes(:relation) causing a join when .offset() and .limit() are used #219

@sphogan

Description

@sphogan

Basically, the issue I'm having is that calling Person.includes(:emails).limit(10).offset(10) is causing a LEFT OUTER JOIN rather than using Rails' usual method of a second IN query to grab related results.

I've created a small project (https://github.com/sphogan/offsettest) to demonstrate the issue (in case you want a concrete example).

Person.includes(:emails)
Person Load (17.8ms)  EXEC sp_executesql N'SELECT [people].* FROM [people]'
Email Load (3.9ms)  EXEC sp_executesql N'SELECT [emails].* FROM [emails] WHERE [emails].[person_id] IN (N''1'', N''2'', N''3'', N''4'', N''5'', N''6'', N''7'', N''8'', N''9'', N''10'', N''11'', N''12'', N''13'', N''14'', N''15'', N''16'', N''17'', N''18'', N''19'', N''20'', N''21'')'

Perfect! It executes two queries as expected.

Person.limit(10).offset(10)
Person Load (2.7ms)  EXEC sp_executesql N'SELECT TOP (10) [__rnt].* FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [people].[id] ASC) AS [__rn], [people].* FROM [people] ) AS [__rnt] WHERE [__rnt].[__rn] > (10) ORDER BY [__rnt].[__rn] ASC'

Awesome!

Person.limit(10).offset(10).includes(:emails)
Person Load (3.1ms)  EXEC sp_executesql N'SELECT TOP (10) [__rnt].id FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [people].[id] ASC) AS [__rn], [people].id FROM [people] LEFT OUTER JOIN [emails] ON [emails].[person_id] = [people].[id] GROUP BY [people].id ) AS [__rnt] WHERE [__rnt].[__rn] > (10) ORDER BY [__rnt].[__rn] ASC'
SQL (5.6ms)  EXEC sp_executesql N'SELECT [people].[id] AS t0_r0, [people].[name] AS t0_r1, [people].[created_at] AS t0_r2, [people].[updated_at] AS t0_r3, [emails].[id] AS t1_r0, [emails].[person_id] AS t1_r1, [emails].[address] AS t1_r2, [emails].[created_at] AS t1_r3, [emails].[updated_at] AS t1_r4 FROM [people] LEFT OUTER JOIN [emails] ON [emails].[person_id] = [people].[id] WHERE [people].[id] IN (11, 12, 13, 14, 15, 16, 17, 18, 19, 20)'

This is where the problem is. The adapter works as I assume it would work for Person.includes(:emails) as well as for Person.limit(10).offset(10). However, when the two are combined, it decides to do a LEFT OUTER JOIN to get the emails and I'm not sure why. It isn't using fewer statements since it's still using two SQL statements to load the people and their email addresses, they're just more complex SQL statements. The interesting thing is that calling .to_sql shows me the behavior I expect:

Person.limit(10).offset(10).includes(:emails).to_sql
 => "SELECT TOP (10) [__rnt].* FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [people].[id] ASC) AS [__rn], [people].* FROM [people] ) AS [__rnt] WHERE [__rnt].[__rn] > (10) ORDER BY [__rnt].[__rn] ASC"

The statement provided by .to_sql runs perfectly against my database.

Just as a comparison (to make sure I wasn't off-base with my expected behavior), I tested it on a MySQL database:

Person.limit(10).offset(10).includes(:emails)
Person Load (0.2ms)  SELECT `people`.* FROM `people` LIMIT 10 OFFSET 10
Email Load (26.8ms)  SELECT `emails`.* FROM `emails` WHERE `emails`.`person_id` IN (11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

Looking at visit_Arel_Nodes_SelectStatementWithOutOffset, the statement it returns is:

SELECT TOP (10) [__rnt].* FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [people].[id] ASC) AS [__rn], [people].* FROM [people] ) AS [__rnt] WHERE [__rnt].[__rn] > (10) ORDER BY [__rnt].[__rn] ASC

So, at that point, it's the same SQL that .to_sql is showing and that I'm expecting. I'm not sure if this is getting altered by something in Arel::TreeManager or I'm just missing something.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions