Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse complex projection in operators afterwards #7776

Open
msmolka opened this issue Mar 3, 2017 · 9 comments
Open

Reuse complex projection in operators afterwards #7776

msmolka opened this issue Mar 3, 2017 · 9 comments

Comments

@msmolka
Copy link

msmolka commented Mar 3, 2017

The code generated for SQL is far from optimal comparing to EF6

Steps to reproduce

I'm getting model from database in following way:

from user in context.Users
                   let info= context.LastInfo.OrderByDescending(t => t.CreatedDate).FirstOrDefault(t => t.UserId== user.Id)
                   let extra = context.Extras.OrderByDescending(t => t.CreatedDate).FirstOrDefault(s => s.UserId == user.Id)
                   select new UserExtraInfo
                   {
                       Active = user.Active,                    
                       Id = user.Id,
                       LastInfoDate = info.CreatedDate,                    
                       LastTest1 = extra.Test1,
                       LastTest2 = extra.Test2,
                       LastTest3 = extra.Test3,                   
                       InfoTest1= info.Test1,
                       InfoTest1= info.Test2                   
                   };

The code produced by EF is

SELECT
    [user].[Active],
    [user].[Id],
(
    SELECT TOP (1)
        [t].[CreatedDate]
    FROM [Info] AS [t]
    WHERE
        [t].[UserId] = [user].[Id]
    ORDER BY
        [t].[CreatedDate] DESC
),
 
(
    SELECT TOP (1)
        [t0].[Test1]
    FROM [Extra] AS [t0]
    WHERE
        [t0].[UserId] = [user].[Id]
    ORDER BY
        [t0].[CreatedDate] DESC
),
(
    SELECT TOP (1)
        [t1].[Test2]
    FROM [Extra] AS [t1]
    WHERE
        [t1].[UserId] = [user].[Id]
    ORDER BY
        [t1].[CreatedDate] DESC
),
(
    SELECT TOP (1)
        [t2].[Test3]
    FROM [Extra] AS [t2]
    WHERE
        [t2].[UserId] = [user].[Id]
    ORDER BY
        [t2].[CreatedDate] DESC
),
(
    SELECT TOP (1)
        [t4].[Test1]
    FROM [Info] AS [t4]
    WHERE
        [t4].[UserId] = [user].[Id]
    ORDER BY
        [t4].[CreatedDate] DESC
),
(
    SELECT TOP (1)
        [t5].[Test2]
    FROM [Info] AS [t4]
    WHERE
        [t5].[UserId] = [user].[Id]
    ORDER BY
        [t5].[CreatedDate] DESC
)
FROM [User] AS [user]

As far as I remember EF6 generated following query with CROSS APPLY/OUTER APPLY which is about 10x quicker.
Instead of select multiple times from the same row projection. Select each row once using OUTER APPLY.

Further technical details

EF Core version: 1.1.0
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Window 10
IDE: Visual Studio 2015

@tuespetre
Copy link
Contributor

tuespetre commented Mar 3, 2017

I had this implemented in a HUGE branch that did way too much at one time. I've been piecemealing out everything I can as I have time -- I hope to get back to this one soon (note: I am not an EF team member, just doing this out of ❤️, don't lean too heavily on my words)

@tuespetre
Copy link
Contributor

Alright, I've put together #7857 for implementing OUTER APPLY (although that is mostly a sugar for what can already be done with CROSS APPLY.) Other than that, the inefficiency here is that the range variables which are subqueries are expanded into each place they are used in the projection. An optimization can be applied to the query model to create a single lateral join instead (via an AdditionalFromClause.) I will see if I can tackle that.

tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 13, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 13, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 13, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 13, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 15, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 15, 2017
tuespetre added a commit to tuespetre/EntityFramework that referenced this issue Mar 19, 2017
@evantrimboli
Copy link

I wanted to be able to do something like this:

context.Products
    .Select(p => new {
        Product = p,
        MostRecentReview = p.Reviews.OrderByDescending(r => r.Date).FirstOrDefault(),
        ReviewCount = p.Reviews.Count
});

While the query works, it does 2 queries per product to get the most recent review and the count.

Ultimately I'd like to be able to mirror a SQL query something like:

SELECT * FROM Products p
	OUTER APPLY
	(SELECT TOP 1 *, COUNT(*) OVER() AS ReviewCount
		FROM Review r
		WHERE r.ProductId = p.Id ORDER BY r.Date DESC
       ) reviewinfo

To do this I had to write a custom query. Would be nice to see something like this added, or another way to do it more efficiently.

Tasteful pushed a commit to Tasteful/EntityFramework that referenced this issue Oct 30, 2017
Tasteful pushed a commit to Tasteful/EntityFramework that referenced this issue Oct 30, 2017
Tasteful pushed a commit to Tasteful/EntityFramework that referenced this issue Oct 30, 2017
@ajcvickers ajcvickers added the verify-fixed This issue is likely fixed in new query pipeline. label Sep 4, 2019
@ajcvickers ajcvickers modified the milestones: Backlog, 3.1.0 Sep 4, 2019
@ajcvickers ajcvickers modified the milestones: 3.1.0, Backlog Oct 11, 2019
@ajcvickers ajcvickers added this to the 5.0.0 milestone Nov 13, 2019
@maumar maumar removed the verify-fixed This issue is likely fixed in new query pipeline. label Jan 31, 2020
@maumar
Copy link
Contributor

maumar commented Jan 31, 2020

We still generate sub optimal SQL:

query:

            return AssertQuery(
                async,
                ss => from root in ss.Set<Level1>()
                      let first = ss.Set<Level2>().OrderByDescending(f => f.Name).FirstOrDefault(f => f.Id == root.Id)
                      let second = ss.Set<Level2>().OrderByDescending(s => s.Name).FirstOrDefault(s => s.Name != root.Name)
                      select new
                      {
                          rootId = root.Id,
                          rootName = root.Name,
                          firstId = first.Id,
                          firstName = first.Name,
                          secondId = second.Id,
                          secondName = second.Name,
                      });

generates sql:

SELECT [l3].[Id] AS [rootId], [l3].[Name] AS [rootName], (
    SELECT TOP(1) [l].[Id]
    FROM [LevelTwo] AS [l]
    WHERE [l].[Id] = [l3].[Id]
    ORDER BY [l].[Name] DESC) AS [firstId], (
    SELECT TOP(1) [l0].[Name]
    FROM [LevelTwo] AS [l0]
    WHERE [l0].[Id] = [l3].[Id]
    ORDER BY [l0].[Name] DESC) AS [firstName], (
    SELECT TOP(1) [l1].[Id]
    FROM [LevelTwo] AS [l1]
    WHERE (([l1].[Name] <> [l3].[Name]) OR ([l1].[Name] IS NULL OR [l3].[Name] IS NULL)) AND ([l1].[Name] IS NOT NULL OR [l3].[Name] IS NOT NULL)
    ORDER BY [l1].[Name] DESC) AS [secondId], (
    SELECT TOP(1) [l2].[Name]
    FROM [LevelTwo] AS [l2]
    WHERE (([l2].[Name] <> [l3].[Name]) OR ([l2].[Name] IS NULL OR [l3].[Name] IS NULL)) AND ([l2].[Name] IS NOT NULL OR [l3].[Name] IS NOT NULL)
    ORDER BY [l2].[Name] DESC) AS [secondName]
FROM [LevelOne] AS [l3]

What we could generate:

SELECT [l3].[Id], [l3].[Name], [first].[Id], [first].[Name], [second].[Id], [second].[Name]
FROM [LevelOne] AS [l3]
OUTER APPLY (SELECT TOP(1) [f].[Id], [f].[Name] 
    FROM [LevelTwo] AS [f]
    WHERE [f].[Id] = [l3].[Id]
    ORDER BY [f].[Name] DESC) AS [first]
OUTER APPLY (SELECT TOP(1) [s].[Id], [s].[Name] 
    FROM [LevelThree] AS [s]
    WHERE [s].[Name] <> [l3].[Name]
    ORDER BY [s].[Name] DESC) AS [second]

@maumar
Copy link
Contributor

maumar commented Jan 31, 2020

Note that we do the "right thing" when the projections are not scalars but entities:

            return AssertQuery(
                async,
                ss => from root in ss.Set<Level1>()
                      let first = ss.Set<Level2>().OrderByDescending(f => f.Name).FirstOrDefault(f => f.Id == root.Id)
                      let second = ss.Set<Level2>().OrderByDescending(s => s.Name).FirstOrDefault(s => s.Name != root.Name)
                      select new
                      {
                          rootId = root.Id,
                          rootName = root.Name,
                          first,
                          second,
                      });

sql:

SELECT [l].[Id], [l].[Name], [t0].[Id], [t0].[Date], [t0].[Level1_Optional_Id], [t0].[Level1_Required_Id], [t0].[Name], [t0].[OneToMany_Optional_Inverse2Id], [t0].[OneToMany_Optional_Self_Inverse2Id], [t0].[OneToMany_Required_Inverse2Id], [t0].[OneToMany_Required_Self_Inverse2Id], [t0].[OneToOne_Optional_PK_Inverse2Id], [t0].[OneToOne_Optional_Self2Id], [t1].[Id], [t1].[Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id]
FROM [LevelOne] AS [l]
LEFT JOIN (
    SELECT [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id]
    FROM (
        SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Id] ORDER BY [l0].[Name] DESC) AS [row]
        FROM [LevelTwo] AS [l0]
    ) AS [t]
    WHERE [t].[row] <= 1
) AS [t0] ON [l].[Id] = [t0].[Id]
OUTER APPLY (
    SELECT TOP(1) [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id]
    FROM [LevelTwo] AS [l1]
    WHERE (([l1].[Name] <> [l].[Name]) OR ([l1].[Name] IS NULL OR [l].[Name] IS NULL)) AND ([l1].[Name] IS NOT NULL OR [l].[Name] IS NOT NULL)
    ORDER BY [l1].[Name] DESC
) AS [t1]

@roji
Copy link
Member

roji commented Jan 27, 2022

Perf research into scalar subquery in projection vs. in CROSS APPLY:

SQL Server

-- In projection:
SELECT c.*, (SELECT TOP(1) o.OrderDate FROM orders AS o WHERE o.CustomerID = c.CustomerID)
FROM customers AS c

-- In CROSS APPLY:
SELECT c.*, t.OrderDate
FROM customers AS c
CROSS APPLY (SELECT TOP (1) o.* FROM orders AS o WHERE c.CustomerID = o.CustomerID) t

TotalSubtreeCost is 16.285336 (projection) vs. 16.280334 (CROSS APPLY)

PostgreSQL

-- In projection:
SELECT c.*, (SELECT "Orders"."Amount" FROM "Orders" WHERE "Orders"."CustomerID" = c."CustomerID" LIMIT 1)
FROM "Customers" as c;

-- In LATERAL JOIN:
SELECT c.*, t."Amount"
From "Customers" as c,
LATERAL (SELECT * from "Orders" AS o WHERE o."CustomerID" = c."CustomerID" LIMIT 1) t;

Total cost is 0.00..2228952 (projection) vs. 0.42..2238952 (LATERAL JOIN)

Summary

So there's a difference, but it seems really negligible (and probably constant, not increasing with number of rows).

Full details

SQL Server

Setup

DROP TABLE IF EXISTS Orders;
DROP TABLE IF EXISTS Customers;

CREATE TABLE Customers (
    "CustomerID" INT PRIMARY KEY IDENTITY,
    "Name" NVARCHAR(MAX));

CREATE TABLE Orders (
    "OrderId" INT PRIMARY KEY IDENTITY,
    "Amount" INT,
    "CustomerID" INT,
    FOREIGN KEY ("CustomerID") REFERENCES "Customers" ("CustomerID"));

CREATE INDEX ix_foo ON "Orders"("CustomerID");

Projection plan

+-------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+---------------------------------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+
|StmtText                                                                                                           |StmtId|NodeId|Parent|PhysicalOp          |LogicalOp           |Argument                                                                         |DefinedValues                                            |EstimateRows|EstimateIO|EstimateCPU|AvgRowSize|TotalSubtreeCost|OutputList                                |Warnings|Type    |Parallel|EstimateExecutions|
+-------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+---------------------------------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+
|SELECT c.*, (SELECT TOP(1) o.Amount FROM orders AS o WHERE o.CustomerID = c.CustomerID)                            |1     |1     |0     |NULL                |NULL                |1                                                                                |NULL                                                     |49999       |NULL      |NULL       |NULL      |16.285336       |NULL                                      |NULL    |SELECT  |false   |NULL              |
|FROM customers AS c                                                                                                |      |      |      |                    |                    |                                                                                 |                                                         |            |          |           |          |                |                                          |        |        |        |                  |
|  |--Compute Scalar(DEFINE:([Expr1003]=[test].[dbo].[Orders].[Amount] as [o].[Amount]))                            |1     |2     |1     |Compute Scalar      |Compute Scalar      |DEFINE:([Expr1003]=[test].[dbo].[Orders].[Amount] as [o].[Amount])               |[Expr1003]=[test].[dbo].[Orders].[Amount] as [o].[Amount]|49999       |0         |0.0049999  |4043      |16.285336       |[c].[CustomerID], [c].[Name], [Expr1003]  |NULL    |PLAN_ROW|false   |1                 |
|       |--Nested Loops(Left Outer Join, OUTER REFERENCES:([c].[CustomerID]))                                       |1     |3     |2     |Nested Loops        |Left Outer Join     |OUTER REFERENCES:([c].[CustomerID])                                              |NULL                                                     |49999       |0         |0.20899582 |4043      |16.280334       |[c].[CustomerID], [c].[Name], [o].[Amount]|NULL    |PLAN_ROW|false   |1                 |
|            |--Clustered Index Scan(OBJECT:([test].[dbo].[Customers].[PK__Customer__A4AE64B8508B43B3] AS [c]))     |1     |4     |3     |Clustered Index Scan|Clustered Index Scan|OBJECT:([test].[dbo].[Customers].[PK__Customer__A4AE64B8508B43B3] AS [c])        |[c].[CustomerID], [c].[Name]                             |49999       |0.18905093|0.0551559  |4039      |0.24420683      |[c].[CustomerID], [c].[Name]              |NULL    |PLAN_ROW|false   |1                 |
|            |--Top(TOP EXPRESSION:((1)))                                                                           |1     |5     |3     |Top                 |Top                 |TOP EXPRESSION:((1))                                                             |NULL                                                     |1           |0         |0.0000001  |11        |15.827133       |[o].[Amount]                              |NULL    |PLAN_ROW|false   |49999             |
|                 |--Index Spool(SEEK:([o].[CustomerID]=[test].[dbo].[Customers].[CustomerID] as [c].[CustomerID])) |1     |6     |5     |Index Spool         |Eager Spool         |SEEK:([o].[CustomerID]=[test].[dbo].[Customers].[CustomerID] as [c].[CustomerID])|NULL                                                     |1           |1.9388007 |0.1002572  |11        |15.822133       |[o].[Amount]                              |NULL    |PLAN_ROW|false   |49999             |
|                      |--Clustered Index Scan(OBJECT:([test].[dbo].[Orders].[PK__Orders__C3905BCFE2C0BDB0] AS [o]))|1     |7     |6     |Clustered Index Scan|Clustered Index Scan|OBJECT:([test].[dbo].[Orders].[PK__Orders__C3905BCFE2C0BDB0] AS [o])             |[o].[Amount], [o].[CustomerID]                           |99998       |0.323125  |0.1101548  |15        |0.4332798       |[o].[Amount], [o].[CustomerID]            |NULL    |PLAN_ROW|false   |1                 |
+-------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+---------------------------------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+

CROSS APPLY plan

+------------------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+
|StmtText                                                                                                                      |StmtId|NodeId|Parent|PhysicalOp          |LogicalOp           |Argument                                                                         |DefinedValues                 |EstimateRows|EstimateIO|EstimateCPU|AvgRowSize|TotalSubtreeCost|OutputList                                |Warnings|Type    |Parallel|EstimateExecutions|
+------------------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+
|SELECT c.*, t.Amount                                                                                                          |1     |1     |0     |NULL                |NULL                |1                                                                                |NULL                          |49999       |NULL      |NULL       |NULL      |16.280334       |NULL                                      |NULL    |SELECT  |false   |NULL              |
|FROM customers AS c                                                                                                           |      |      |      |                    |                    |                                                                                 |                              |            |          |           |          |                |                                          |        |        |        |                  |
|CROSS APPLY (SELECT TOP (1) o.* FROM orders AS o WHERE c.CustomerID = o.CustomerID) t                                         |      |      |      |                    |                    |                                                                                 |                              |            |          |           |          |                |                                          |        |        |        |                  |
|  |--Nested Loops(Inner Join, OUTER REFERENCES:([c].[CustomerID]))                                                            |1     |2     |1     |Nested Loops        |Inner Join          |OUTER REFERENCES:([c].[CustomerID])                                              |NULL                          |49999       |0         |0.20899582 |4043      |16.280334       |[c].[CustomerID], [c].[Name], [o].[Amount]|NULL    |PLAN_ROW|false   |1                 |
|       |--Clustered Index Scan(OBJECT:([test].[dbo].[Customers].[PK__Customer__A4AE64B8508B43B3] AS [c]))                     |1     |3     |2     |Clustered Index Scan|Clustered Index Scan|OBJECT:([test].[dbo].[Customers].[PK__Customer__A4AE64B8508B43B3] AS [c])        |[c].[CustomerID], [c].[Name]  |49999       |0.18905093|0.0551559  |4039      |0.24420683      |[c].[CustomerID], [c].[Name]              |NULL    |PLAN_ROW|false   |1                 |
|       |--Top(TOP EXPRESSION:((1)))                                                                                           |1     |4     |2     |Top                 |Top                 |TOP EXPRESSION:((1))                                                             |NULL                          |1           |0         |0.0000001  |11        |15.827133       |[o].[Amount]                              |NULL    |PLAN_ROW|false   |49999             |
|            |--Index Spool(SEEK:([o].[CustomerID]=[test].[dbo].[Customers].[CustomerID] as [c].[CustomerID]))                 |1     |5     |4     |Index Spool         |Eager Spool         |SEEK:([o].[CustomerID]=[test].[dbo].[Customers].[CustomerID] as [c].[CustomerID])|NULL                          |1           |1.9388007 |0.1002572  |11        |15.822133       |[o].[Amount]                              |NULL    |PLAN_ROW|false   |49999             |
|                 |--Clustered Index Scan(OBJECT:([test].[dbo].[Orders].[PK__Orders__C3905BCFE2C0BDB0] AS [o]))                |1     |6     |5     |Clustered Index Scan|Clustered Index Scan|OBJECT:([test].[dbo].[Orders].[PK__Orders__C3905BCFE2C0BDB0] AS [o])             |[o].[Amount], [o].[CustomerID]|99998       |0.323125  |0.1101548  |15        |0.4332798       |[o].[Amount], [o].[CustomerID]            |NULL    |PLAN_ROW|false   |1                 |
+------------------------------------------------------------------------------------------------------------------------------+------+------+------+--------------------+--------------------+---------------------------------------------------------------------------------+------------------------------+------------+----------+-----------+----------+----------------+------------------------------------------+--------+--------+--------+------------------+
PostgreSQL

Setup

DROP TABLE IF EXISTS "Customers" CASCADE;
DROP TABLE IF EXISTS "Orders" CASCADE;

CREATE TABLE "Customers" (
    "CustomerID" INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "Name" TEXT);
CREATE TABLE "Orders" (
    "OrderId" INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "Amount" INT,
    "CustomerID" INT,
    FOREIGN KEY ("CustomerID") REFERENCES "Customers" ("CustomerID"));
CREATE INDEX ix_foo ON "Orders"("CustomerID");

DO $$BEGIN
FOR i IN 1..500000 LOOP
    INSERT INTO "Customers" ("Name") VALUES (i::TEXT);
    INSERT INTO "Orders" ("Amount", "CustomerID") VALUES (i, i);
    INSERT INTO "Orders" ("Amount", "CustomerID") VALUES (i, i);
END LOOP;
END$$;

Projection plan

+-----------------------------------------------------------------------------------+
|QUERY PLAN                                                                         |
+-----------------------------------------------------------------------------------+
|Seq Scan on "Customers" c  (cost=0.00..2228952.00 rows=500000 width=14)            |
|  SubPlan 1                                                                        |
|    ->  Limit  (cost=0.42..4.44 rows=1 width=4)                                    |
|          ->  Index Scan using ix_foo on "Orders"  (cost=0.42..8.46 rows=2 width=4)|
|                Index Cond: ("CustomerID" = c."CustomerID")                        |
|JIT:                                                                               |
|  Functions: 9                                                                     |
|  Options: Inlining true, Optimization true, Expressions true, Deforming true      |
+-----------------------------------------------------------------------------------+

LATERAL JOIN plan

+------------------------------------------------------------------------------------+
|QUERY PLAN                                                                          |
+------------------------------------------------------------------------------------+
|Nested Loop  (cost=0.42..2238952.00 rows=500000 width=14)                           |
|  ->  Seq Scan on "Customers" c  (cost=0.00..7702.00 rows=500000 width=10)          |
|  ->  Limit  (cost=0.42..4.44 rows=1 width=12)                                      |
|        ->  Index Scan using ix_foo on "Orders" o  (cost=0.42..8.46 rows=2 width=12)|
|              Index Cond: ("CustomerID" = c."CustomerID")                           |
|JIT:                                                                                |
|  Functions: 7                                                                      |
|  Options: Inlining true, Optimization true, Expressions true, Deforming true       |
+------------------------------------------------------------------------------------+

@msmolka
Copy link
Author

msmolka commented Jan 27, 2022

In this example you are selecting only 1 column once in sub query. Problem exists when you are selecting multiple times from 1 projection as in my initial example.

@roji
Copy link
Member

roji commented Jan 27, 2022

@msmolka yeah, I'm aware - this is a related perf investigation for @smitpatel for a solution to the original problem.

@stevendarby
Copy link
Contributor

Does this comment on the closed duplicate imply a fix for this issue may be on the cards for 7.0? Fantastic news if so.

Is anyone able to help me understand whether the following expression, produced by AutoMapper, which leads to multiple scalar sub-queries in the SQL, is also one that could be addressed by a fix for the original issue?

A map configuration and query such as this

var config = new MapperConfiguration(x =>
{
    x.CreateMap<Person, PersonDto>()
        .ForMember(d => d.FirstAddress, o => o.MapFrom(s => s.Addresses.OrderBy(a => a.Id).FirstOrDefault()));
     x.CreateMap<Address, AddressDto>();
});
var query1 = context.People.ProjectTo<PersonDto>(config);

Expression - uses a proxy object for an intermediate Select, which I think roughly correlates with the use of 'let' in the original issue?

.Call System.Linq.Queryable.Select(
    .Call System.Linq.Queryable.Select(
        .Extension<Microsoft.EntityFrameworkCore.Query.QueryRootExpression>,
        '(.Lambda #Lambda1<System.Func`2[Person,Proxy_System.Object_-1957032940___FirstAddress_Name]>)),
    '(.Lambda #Lambda2<System.Func`2[Proxy_System.Object_-1957032940___FirstAddress_Name,PersonDto]>))

.Lambda #Lambda1<System.Func`2[Person,Proxy_System.Object_-1957032940___FirstAddress_Name]>(Person $dtoPerson) {
    .New Proxy_System.Object_-1957032940___FirstAddress_Name(){
        __FirstAddress = .Call System.Linq.Enumerable.FirstOrDefault(.Call System.Linq.Enumerable.OrderBy(
                $dtoPerson.Addresses,
                .Lambda #Lambda3<System.Func`2[Address,System.Int32]>)),
        Name = $dtoPerson.Name
    }
}

.Lambda #Lambda2<System.Func`2[Proxy_System.Object_-1957032940___FirstAddress_Name,PersonDto]>(Proxy_System.Object_-1957032940___FirstAddress_Name $dtoLet)
{
    .New PersonDto(){
        FirstAddress = .If ($dtoLet.__FirstAddress == null) {
            null
        } .Else {
            .New AddressDto(){
                Line1 = ($dtoLet.__FirstAddress).Line1,
                Line2 = ($dtoLet.__FirstAddress).Line2
            }
        },
        Name = $dtoLet.Name
    }
}

.Lambda #Lambda3<System.Func`2[Address,System.Int32]>(Address $a) {
    $a.Id
}
Full code
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;

{
    using var context = new PersonContext();
    
    var config = new MapperConfiguration(x =>
    {
        x.CreateMap<Person, PersonDto>()
            .ForMember(d => d.FirstAddress, o => o.MapFrom(s => s.Addresses.OrderBy(a => a.Id).FirstOrDefault()));
        x.CreateMap<Address, AddressDto>();
    });
    var query1 = context.People.ProjectTo<PersonDto>(config);
    Console.WriteLine(query1.Expression);
    Console.WriteLine(query1.ToQueryString());
}

public class PersonContext : DbContext
{
    public DbSet<Person> People { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer("Server=.;Database=Test;Trusted_Connection=True;");
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Address> Addresses { get; set; }
}

public class PersonDto
{
    public string Name { get; set; }
    public AddressDto FirstAddress { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public Person Person { get; set; }
}

public class AddressDto
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants