-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adding documentation for query null semantics
- Loading branch information
Showing
8 changed files
with
308 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
--- | ||
title: Query null semantics in EF Core | ||
description: Information on how Entity Framework Core handles null comparisons in queries | ||
author: maumar | ||
ms.date: 11/11/2020 | ||
uid: core/querying/null-semantics | ||
--- | ||
# Query null semantics | ||
|
||
## Introduction | ||
|
||
SQL databases operate on 3-valued logic (`true`, `false`, `null`) when performing comparisons, as opposed to boolean logic of C#. When translating LINQ queries to SQL, EF Core tries to compensate for the difference by introducing additional null checks for some elements of the query. | ||
To illustrate this lets define the following entity: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/NullSemanticsEntity.cs#Entity)] | ||
|
||
and issue several queries: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/Program.cs#BasicExamples)] | ||
|
||
First two queries produce simple comparison. In case of the first query both columns are non-nullable so null checks are not needed. In case of the second query `NullableInt` could contain null, however `Id` is non-nullable. Comparing `null` to non-null yields `null` as a result, which would be filtered out by `WHERE` operation. No additional terms need to be added either. | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [e].[Id] = [e].[Int] | ||
|
||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [e].[Id] = [e].[NullableInt] | ||
``` | ||
|
||
Third query introduces the null check. When `NullableInt` is `null` the comparison `Id <> NullableInt` yields `null`, which would be filtered out by `WHERE` operation. However from the boolean logic perspective this case should be returned as part of the result. Hence EF Core adds the necessary check to ensure that. | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE ([e].[Id] <> [e].[NullableInt]) OR [e].[NullableInt] IS NULL | ||
``` | ||
|
||
Queries four and five show the pattern when both columns are nullable. It's worth noting that `<>` operation produces significantly more complicated (and slower) query than equality operation. | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE ([e].[String1] = [e].[String2]) OR ([e].[String1] IS NULL AND [e].[String2] IS NULL) | ||
|
||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE (([e].[String1] <> [e].[String2]) OR ([e].[String1] IS NULL OR [e].[String2] IS NULL)) AND ([e].[String1] IS NOT NULL OR [e].[String2] IS NOT NULL) | ||
``` | ||
|
||
## Null semantics in functions | ||
|
||
Many functions in SQL can only return null result if some of their arguments are null. EF Core takes advantage of this to produce more efficient queries. | ||
The query below illustrates the optimization: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/Program.cs#Functions)] | ||
|
||
The generated SQL is as follows (we don't need to evaluate the `SUBSTRING` function): | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [e].[String1] IS NULL OR [e].[String2] IS NULL | ||
``` | ||
|
||
This optimization can also be used for user defined functions. This can be done by adding `PropagatesNullability()` call to relevant function parameters model configuration. | ||
To illustrate this, define two user functions inside the `DbContext`: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/NullSemanticsContext.cs#UdfBody)] | ||
|
||
The model configuration (inside `OnModelCreating` method) is as follows: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/NullSemanticsContext.cs#UdfModelConfiguration)] | ||
|
||
First function is configured in a standard way and the second function is configured to take advantage of the nullability propagation optimization. | ||
|
||
When issuing the following queries: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/Program.cs#UdfExamples)] | ||
|
||
We get this SQL: | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [dbo].[ConcatStrings]([e].[String1], [e].[String2]) IS NOT NULL | ||
|
||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [e].[String1] IS NOT NULL AND [e].[String2] IS NOT NULL | ||
``` | ||
|
||
Just like with built-in `Substring` function, the second query doesn't need to evaluate the function itself to test it's nullability. | ||
|
||
> [!NOTE] | ||
> This optimization should only be used if the function can only return `null` becuase it's parameters are `null`. | ||
## Optimizations and considerations | ||
|
||
- Comparing non-nullable columns is simpler and faster than comparing nullable columns. Consider marking columns as non-nullable wherever it is possible. | ||
|
||
- `Equal` comparison is simpler and faster than `NotEqual`, because query doesn't need to distinguish between `null` and `false` result. Consider using equality comparison whenever it is possible. | ||
|
||
> [!NOTE] | ||
> Wrapping `Equal` comparison around `Not` is effectively `NotEqual`. `Equal` comparison is only faster/simpler if it is not negated. | ||
- In some cases it is possible to simplify a complex comparison by filtering out `null` values from a column explicitly - e.g. when no `null` values are present or these values are not relevant in the result. Consider the following example: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/Program.cs#ManualOptimization)] | ||
|
||
These queries produce the following SQL: | ||
|
||
```sql | ||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE ((([e].[String1] <> [e].[String1]) OR [e].[String1] IS NULL) AND [e].[String1] IS NOT NULL) OR ((CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String1]) AS int)) OR [e].[String1] IS NULL) | ||
|
||
SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2] | ||
FROM [Entities] AS [e] | ||
WHERE [e].[String1] IS NOT NULL AND (CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String1]) AS int)) | ||
``` | ||
|
||
In the second query, `null` results are filtered out from `String1` column explicitly. EF Core can safely treat the `String1` column as non-nullable when performing comparison, which results is simpler query. | ||
|
||
## Using relational null semantics | ||
|
||
It is possible to disable the null comparison compensation and use relational null semantics directly. This can be done by calling `UseRelationalNulls(true)` method on the options builder inside `OnConfiguring` method: | ||
|
||
[!code-csharp[Main](../../../samples/core/Querying/NullSemantics/NullSemanticsContext.cs#UseRelationalNulls)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp3.1</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" /> | ||
</ItemGroup> | ||
|
||
</Project> |
56 changes: 56 additions & 0 deletions
56
samples/core/Querying/NullSemantics/NullSemanticsContext.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Infrastructure; | ||
|
||
namespace NullSemantics | ||
{ | ||
public class NullSemanticsContext : DbContext | ||
{ | ||
public DbSet<NullSemanticsEntity> Entities { get; set; } | ||
|
||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||
{ | ||
var relationalNulls = false; | ||
if (relationalNulls) | ||
{ | ||
#region UseRelationalNulls | ||
new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls(true); | ||
#endregion | ||
} | ||
|
||
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=NullSemanticsSample;Trusted_Connection=True;MultipleActiveResultSets=true"); | ||
} | ||
|
||
#region UdfBody | ||
public string ConcatStrings(string prm1, string prm2) | ||
=> throw new System.InvalidOperationException(); | ||
|
||
public string ConcatStringsOptimized(string prm1, string prm2) | ||
=> throw new System.InvalidOperationException(); | ||
#endregion | ||
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder) | ||
{ | ||
#region UdfModelConfiguration | ||
modelBuilder | ||
.HasDbFunction(typeof(NullSemanticsContext).GetMethod(nameof(ConcatStrings), new[] { typeof(string), typeof(string) })) | ||
.HasName("ConcatStrings"); | ||
|
||
modelBuilder.HasDbFunction( | ||
typeof(NullSemanticsContext).GetMethod(nameof(ConcatStringsOptimized), new[] { typeof(string), typeof(string) }), | ||
b => | ||
{ | ||
b.HasName("ConcatStrings"); | ||
b.HasParameter("prm1").PropagatesNullability(); | ||
b.HasParameter("prm2").PropagatesNullability(); | ||
}); | ||
#endregion | ||
|
||
modelBuilder.Entity<NullSemanticsEntity>().HasData( | ||
new NullSemanticsEntity { Id = 1, Int = 1, NullableInt = 1, String1 = "A", String2 = "A" }, | ||
new NullSemanticsEntity { Id = 2, Int = 2, NullableInt = 2, String1 = "A", String2 = "B" }, | ||
new NullSemanticsEntity { Id = 3, Int = 2, NullableInt = null, String1 = null, String2 = "A" }, | ||
new NullSemanticsEntity { Id = 4, Int = 2, NullableInt = null, String1 = "B", String2 = null }, | ||
new NullSemanticsEntity { Id = 5, Int = 1, NullableInt = 3, String1 = null, String2 = null }); | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
samples/core/Querying/NullSemantics/NullSemanticsEntity.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
namespace NullSemantics | ||
{ | ||
#region Entity | ||
public class NullSemanticsEntity | ||
{ | ||
public int Id { get; set; } | ||
public int Int { get; set; } | ||
public int? NullableInt { get; set; } | ||
public string String1 { get; set; } | ||
public string String2 { get; set; } | ||
} | ||
#endregion | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using Microsoft.EntityFrameworkCore; | ||
using System; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
|
||
namespace NullSemantics | ||
{ | ||
class Program | ||
{ | ||
static void Main(string[] args) | ||
{ | ||
using var context = new NullSemanticsContext(); | ||
context.Database.EnsureDeleted(); | ||
context.Database.EnsureCreated(); | ||
|
||
#region FunctionSqlRaw | ||
context.Database.ExecuteSqlRaw( | ||
@"create function [dbo].[ConcatStrings] (@prm1 nvarchar(max), @prm2 nvarchar(max)) | ||
returns nvarchar(max) | ||
as | ||
begin | ||
return @prm1 + @prm2; | ||
end"); | ||
#endregion | ||
|
||
BasicExamples(); | ||
Functions(); | ||
ManulaOptimization(); | ||
} | ||
|
||
static void BasicExamples() | ||
{ | ||
using var context = new NullSemanticsContext(); | ||
#region BasicExamples | ||
var query1 = context.Entities.Where(e => e.Id == e.Int); | ||
var query2 = context.Entities.Where(e => e.Id == e.NullableInt); | ||
var query3 = context.Entities.Where(e => e.Id != e.NullableInt); | ||
var query4 = context.Entities.Where(e => e.String1 == e.String2); | ||
var query5 = context.Entities.Where(e => e.String1 != e.String2); | ||
#endregion | ||
|
||
var result1 = query1.ToList(); | ||
var result2 = query2.ToList(); | ||
var result3 = query3.ToList(); | ||
var result4 = query4.ToList(); | ||
var result5 = query5.ToList(); | ||
} | ||
|
||
static void Functions() | ||
{ | ||
using var context = new NullSemanticsContext(); | ||
|
||
#region Functions | ||
var query = context.Entities.Where(e => e.String1.Substring(0, e.String2.Length) == null); | ||
#endregion | ||
|
||
#region UdfExamples | ||
var query1 = context.Entities.Where(e => context.ConcatStrings(e.String1, e.String2) != null); | ||
var query2 = context.Entities.Where(e => context.ConcatStringsOptimized(e.String1, e.String2) != null); | ||
#endregion | ||
|
||
var result = query.ToList(); | ||
var result1 = query1.ToList(); | ||
var result2 = query2.ToList(); | ||
} | ||
|
||
static void ManulaOptimization() | ||
{ | ||
using var context = new NullSemanticsContext(); | ||
|
||
#region ManualOptimization | ||
var query1 = context.Entities.Where(e => e.String1 != e.String1 || e.String1.Length == e.String1.Length); | ||
var query2 = context.Entities.Where(e => e.String1 != null && (e.String1 != e.String1 || e.String1.Length == e.String1.Length)); | ||
#endregion | ||
|
||
var result1 = query1.ToList(); | ||
var result2 = query2.ToList(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters