Deep Dive into an ORM:
The Entity Framework Core Query Pipeline
–
–
–
–
–
Func<int, bool> comparer = num => num < 5;
Expression<Func<int, bool>> comparerExpression
= num => num < 5;
Func<int, bool> compiled = comparerExpression.Compile();
var numParam = Expression.Parameter(typeof(int), "num");
Expression<Func<int, bool>> comparerExpression2 =
Expression.Lambda<Func<int, bool>>(
Expression.LessThan(
numParam,
Expression.Constant(5)),
numParam);
var compiled2 = comparerExpression2.Compile();
–
–
Roslyn - high-level source representation {.fragment data-fragment-index=1}
T4 - source representation with templates {.fragment data-fragment-index=2}
Expression trees {.fragment data-fragment-index=4}
Reflection.Emit - low-level IL {.fragment data-fragment-index=3}
–
–
Let’s translate this:
employees.Where(c => c.FirstName == c.LastName)
SELECT ... FROM Employees WHERE FirstName = LastName
Could be wrong, SQL has three-valued logic{style=color:red} {.fragment}
In SQL, FirstName = NULL is never true… (and it ain’t false either :))
–
Try to mimic C# behavior (best-effort only!)
WHERE FirstName = LastName OR
(FirstName IS NULL AND LastName IS NULL)
If one of the columns is non-nullable in the database schema, we can optimize {.fragment}
Counter-example: case-sensitivity {.fragment}
--
## Subqueries
Let's get complicated:
```csharp
employees.Where(e => e.Tasks.Count == 2)
```
SELECT ...
FROM Employees AS e
WHERE (
SELECT COUNT(*)
FROM Tasks AS t
WHERE t.EmployeeId = e.Id
) = 2;
WHERE e.Id = Boss.Id
employees.Where(e => e.Tasks.First() == x)
WHERE (SELECT ... FROM Tasks ...).Id1 = x.Id1
WHERE Id1 = Boss.Id1 AND Id2 = Boss.Id2
employees.Where(e => e.Tasks.First() == x)
WHERE (SELECT ...).Id1 = x.Id1 AND (SELECT ...).Id2 = x.Id2
employees.Include(b => b.Tasks)
SELECT ...
FROM Employees AS e
LEFT JOIN Tasks AS t ON e.Id = t.EmployeeId
ORDER BY e.Id, t.Id
class MyVisitor1 : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression b)
{
var visited = (BinaryExpression)base.VisitBinary(b);
if (b.NodeType == ExpressionType.NotEqual)
{
if (b.Right is ConstantExpression rightConstant &&
rightConstant.Value == null &&
!IsNullable(b.Left))
{
return Expression.Constant(true);
}
// TODO: Sides may be flipped!
}
return visited;
}
}
customers.Where(c => c.Age > 18 && true)
--
class MyVisitor2 : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression b)
{
var visited = (BinaryExpression)base.VisitBinary(b);
if (b.NodeType == ExpressionType.AndAlso)
{
if (b.Right is ConstantExpression rightConstant &&
rightConstant.Value is bool boolValue &&
boolValue)
{
return b.Left;
}
// TODO: Same for other side!
}
// TODO: Also take care of OrElse!
return visited;
}
}
customers.Where(c => c.Age > 18)
--
## Recap
From this:
customers.Where(c => c.Age > 18 && c.Name != null)</pre>
To this:customers.Where(c => c.Age > 18 && true)</pre>
</div>To this:customers.Where(c => c.Age > 18)</pre>
</div> -- ## EF Core Preprocessor-- ## SQL Translation * We've been talking about LINQ methods, but... at some point we want a model of an SQL query. * Instead of CLR types, methods and members, we want tables and columns. --public virtual Expression Process(Expression query) { query = new EnumerableToQueryableMethodConvertingExpressionVisitor().Visit(query); query = new QueryMetadataExtractingExpressionVisitor(_queryCompilationContext).Visit(query); query = new AllAnyToContainsRewritingExpressionVisitor().Visit(query); query = new GroupJoinFlatteningExpressionVisitor().Visit(query); query = new NullCheckRemovingExpressionVisitor().Visit(query); query = new EntityEqualityRewritingExpressionVisitor(_queryCompilationContext).Rewrite(query); query = new SubqueryMemberPushdownExpressionVisitor().Visit(query); query = new NavigationExpandingExpressionVisitor(...).Expand(query); query = new FunctionPreprocessingExpressionVisitor().Visit(query); new EnumerableVerifyingExpressionVisitor().Visit(query); return query; }
customers .Where(c => c.Name == "Joe") .OrderBy(c => c.Age) .Take(5) .Select(c => new { c.Name, c.Age });
-- ## SQL Translation 2 Still expressions, but now custom SQL ones! ```c# customers.Where(c => new[] { 1, 2, 3 }.Contains(c.Id)); // Becomes: WHERE c.ID IN (1, 2, 3) // This is an SqlInExpression ``` Expression trees can have custom expression types {.fragment} Non-relational providers will do something completely different {.fragment} -- ## Provider and User Translations ```csharp customers.Where(c => c.Name.Length > 5) // Becomes: WHERE LEN(c.Name) > 5 // On SqlServer WHERE LENGTH(c.Name) > 5 // On PostgreSQL ``` Users can define functions which render into arbitrary SQL expressions {.fragment} -- ## End of the Line * Some more postprocessing after SQL translation * QuerySqlGenerator is just another visitor, outputs an SQL string {.fragment} * Another visitor code-generates the shaper {.fragment} (...with... expression trees...) {.fragment} -- ## Query Pipeline Overview ![Query Pipeline](/talks/2019-10-08-dotnetos-efcore-query-internals/src/diagrams/pipeline.svg) --- ## Let's talk about Perffffff -- ## Parameterized Queries ```csharp var posts1 = ctx.Posts.Where(p => p.Title.Contains("dotnetos")); var title = "dotnetos"; // Read from somewhere var posts2 = ctx.Posts.Where(p => p.Title.Contains(title)); ```SELECT Name, Age FROM Customers WHERE Age > 18 ORDER BY Age LIMIT 10
* Can leverage cached plans and prepared queries {.fragment} * Closure variables are your way of asking for parameterization {.fragment} -- ## Compilation is Slow * No way we run all these visitors each time * The answer is always, always... caching. {.fragment} * **Input**: expression tree. **Outputs**: SQL+shaper. {.fragment} -- ## Structural Comparison We can just do recursive structural comparison: ```csharp var posts = ctx.Posts.Where(p => p.Title.Contains("dotnetos")); ```SELECT ... FROM Posts WHERE STRPOS('dotnetos') > 0;
SELECT ... FROM Posts WHERE STRPOS(@p) > 0;
-- ## Parameter Extraction ```csharp var title = "dotnetos"; // Read from somewhere var posts2 = ctx.Posts.Where(p => p.Title.Contains(title)); ``` * Before checking the cache, identify parameter references (to closure variables) * Extract them out and replace them with placeholders, "punching holes in the tree" {.fragment} -- ## Zoom out Again ![Full pipeline](/talks/2019-10-08-dotnetos-efcore-query-internals/src/diagrams/pipeline-full.svg)Cool! However, for each query, we still do: * Parameter extraction * Cache key hashcode calculation * Structural comparison with trees in the query cache-- ## Compiled Queries ```csharp using (var ctx = new MyContext()) { var id = 8; var posts = ctx.Posts.Single(p => p.Id == id); } ```-- ## Not always so easy Remember our InExpression from before? ```csharp customers.Where(c => new[] { 1, 2, 3 }.Contains(c.Id)); WHERE c.Id IN (1, 2, 3) ```// Create an explicitly compiled query private static Func<MyContext, int, Post> _postById = EF.CompileQuery((MyContext ctx, int id) => ctx.Posts.Single(p => p.Id == id)); // Use the compiled query by invoking it using (var ctx = new MyContext()) { var post = _postById(ctx, 8); }
How do we parameterize this?-- ## Solution :( Expand to constant **at execution time**: ```csharp var values = new[] { 1, 2, 3 }; ctx.Posts.Where(p => values.Contains(p.Foo)); WHERE p.Foo IN (1, 2, 3) ``` * Pollutes database query cache * PostgreSQL has `WHERE p.Foo = ANY (@p)` {.fragment} --- ## Closing Words * Writing an ORM is... complex :) * Being aware of what happens under the hood is always important. {.fragment} * Check the SQL being generated! {.fragment} --- ## Actual Heroes **Smit Patel** (new query pipeline architect) **Maurycy Markowski** (big query guy and also a Polish guy!) **And of course ... the rest of the EF team** --- ## Thank you! **Shay Rojansky** Blog & Presentation: <http://roji.org> Twitter: @shayrojanskyvar customerIds = new[] { 1, 2, 3 }; customers.Where(c => customerIds.Contains(c.Id));