Share via


Sémantique de requête null

Présentation

Les bases de données SQL fonctionnent sur une logique à 3 valeurs (true, false, null) lors de l’exécution de comparaisons, par opposition à la logique booléenne de C#. Lors de la traduction de requêtes LINQ en SQL, EF Core tente de compenser la différence en introduisant des vérifications null supplémentaires pour certains éléments de la requête. Pour illustrer cela, nous allons définir l’entité suivante :

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; }
}

et émettre plusieurs requêtes :

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);

Les deux premières requêtes produisent des comparaisons simples. Dans la première requête, les deux colonnes sont non nullables, de sorte que les vérifications null ne sont pas nécessaires. Dans la deuxième requête, NullableInt peut contenir null, mais Id n’est pas nullable ; avec des comparaisons de null avec des rendements non null null pour résultat, ce qui serait filtré par l’opération WHERE. Ainsi, aucun terme supplémentaire n’est nécessaire.

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]

La troisième requête introduit une vérification null. Quand NullableInt est null la comparaison Id <> NullableInt génère des rendements null, qui seraient filtrés par l’opération WHERE. Toutefois, du point de vue de la logique booléenne, ce cas doit être retourné comme faisant partie du résultat. EF Core ajoute donc la vérification nécessaire pour s’assurer de cela.

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

Les requêtes quatre et cinq affichent le modèle lorsque les deux colonnes sont nullables. Il est important de noter que l’opération <> produit une requête plus complexe (et potentiellement plus lente) que l’opération == .

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)

Traitement des valeurs nullables dans les fonctions

De nombreuses fonctions dans SQL ne peuvent retourner un résultat null que si certains de leurs arguments sont null. EF Core tire parti de cela pour produire des requêtes plus efficaces. La requête ci-dessous illustre l’optimisation :

var query = context.Entities.Where(e => e.String1.Substring(0, e.String2.Length) == null);

Le code SQL généré est le suivant (nous n’avons pas besoin d’évaluer la fonction SUBSTRING, car elle ne sera nulle que lorsque l’un de ses arguments est null.) :

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

L’optimisation peut également être utilisée pour les fonctions définies par l’utilisateur. Pour plus d’informations, consultez la page de mappage de fonction définie par l’utilisateur.

Écriture de requêtes performantes

  • La comparaison des colonnes non nullables est plus simple et plus rapide que la comparaison des colonnes nullables. Envisagez de marquer les colonnes comme non nullables dans la mesure du possible.

  • La vérification de l’égalité (==) est plus simple et plus rapide que la vérification de la non-égalité (!=), car la requête n’a pas besoin de faire la distinction entre le résultat null et le résultat false. Utilisez la comparaison d’égalité dans la mesure du possible. Toutefois, la simple négation de la comparaison == est effectivement la même que !=, de sorte qu’elle n’entraîne pas d’amélioration des performances.

  • Dans certains cas, il est possible de simplifier une comparaison complexe en filtrant explicitement les valeurs null d’une colonne , par exemple lorsqu’aucune valeur nulln’est présente ou que ces valeurs ne sont pas pertinentes dans le résultat. Prenons l’exemple suivant :

var query1 = context.Entities.Where(e => e.String1 != e.String2 || e.String1.Length == e.String2.Length);
var query2 = context.Entities.Where(
    e => e.String1 != null && e.String2 != null && (e.String1 != e.String2 || e.String1.Length == e.String2.Length));

Ces requêtes produisent le code SQL suivant :

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)) OR ((CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)) 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] IS NOT NULL AND [e].[String2] IS NOT NULL) AND (([e].[String1] <> [e].[String2]) OR (CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)))

Dans la deuxième requête, les résultats nullsont filtrés de manière explicite de la colonne String1. EF Core peut traiter en toute sécurité la colonne String1 comme non nullable pendant la comparaison, ce qui entraîne une requête plus simple.

Utilisation de la sémantique null relationnelle

Il est possible de désactiver directement la compensation de comparaison null et d’utiliser directement la sémantique null relationnelle. Pour ce faire, appelez la méthode UseRelationalNulls(true) sur le générateur d’options à l’intérieur de la méthode OnConfiguring :

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

Avertissement

Lorsque vous utilisez la sémantique null relationnelle, vos requêtes LINQ n’ont plus la même signification que dans C#, et peuvent produire des résultats différents que ceux prévus. Faites preuve de prudence lors de l’utilisation de ce mode.