Optimisation Avancée des Requêtes et Gestion des Dates dans Entity Framework Core

Gestion des Champs Datetemps et Indexation

Pour les champs temporels, appliquer une fontcion de formatage directement dans la clause WHERE force généralement le moteur de base de données à effectuer un balayage complet de la table (Full Table Scan). Au lieu de cela, il est recommandé de définir des plages horaires précises permettant au moteur d'utiliser les index sur la colonne concernée.

-- Approche inefficace : utilisation de fonctions de formatage
SELECT COUNT(*) FROM commandes_ventes
WHERE DATE_FORMAT(timestamp_creation, '%Y-%m-%d') = '2023-12-01';

-- Approche recommandée : comparaison de plage horaire stricte
SELECT COUNT(*) FROM commandes_ventes
WHERE timestamp_creation >= '2023-12-01 00:00:00' 
AND timestamp_creation < '2023-12-02 00:00:00';

Si l'exécution planifie un accès de type "ALL" et signale aucun index utilisable, la performance se dégrade considérablement sur de grands volumes de données. La création d'un index dédié sur la colonne pertinente est indispensable.

CREATE INDEX idx_timestamp_creation ON commandes_ventes(timestamp_creation);

Prise en Charge des Formats de Date Flexibles

Dans certaines interfaces utilisateur, les utilisateurs peuvent saisir une année complète, un mois ou une date précise. Pour supporter ces entrées sans sacrifier les performances, il est nécessaire de développer une logique côté serveur capable d'identifier l'intervalle cible avant d'envoyer la requête SQL.

Voici une implémentation alternative pour analyser les entrées de date :

public static bool ParseurPlageDate(string parametre, out DateTime debut, out DateTime fin)
{
    début = DateTime.MinValue;
    fin = DateTime.MinValue;

    string[] elements = paramètre.Split('~');
    
    string[] formatsAcceptés = { "yyyy", "yyyy-MM", "yyyy-MM-dd" };

    // Cas : Date unique
    if (éléments.Length == 1)
    {
        if (!DateTime.TryParseExact(elements[0], formatsAcceptés, System.Globalization.CultureInfo.InvariantCulture, 
            System.Globalization.DateTimeStyles.None, out DateTime valueParsed))
        {
            return false;
        }

        int sepCount = elements[0].Count(c => c == '-');

        switch (sepCount)
        {
            case 0: // Année seule
                début = new DateTime(valueParsed.Year, 1, 1);
                fin = début.AddYears(1);
                break;
            case 1: // Mois
                début = new DateTime(valueParsed.Year, valueParsed.Month, 1);
                fin = début.AddMonths(1);
                break;
            default: // Jour
                début = valueParsed;
                fin = début.AddDays(1);
                break;
        }
        return true;
    }

    // Cas : Plange de dates
    if (éléments.Length == 2)
    {
        if (!DateTime.TryParseExact(elements[0], formatsAcceptés, System.Globalization.CultureInfo.InvariantCulture, 
            System.Globalization.DateTimeStyles.None, out DateTime valStart) ||
            !DateTime.TryParseExact(elements[1], formatsAcceptés, System.Globalization.CultureInfo.InvariantCulture, 
            System.Globalization.DateTimeStyles.None, out DateTime valEnd))
        {
            return false;
        }

        début = valStart.Date;
        fin = valEnd.Date;

        int sepEnd = éléments[1].Count(c => c == '-');
        if (sepEnd != 0)
        {
            if (sepEnd == 1)
                fin = fin.AddMonths(1);
            else
                fin = fin.AddDays(1);
        }
        else
        {
             fin = fin.AddYears(1);
        }
        return true;
    }
    
    return false;
}

Ce module permet ensuite de génier dynamiquement les expressions LINQ :

bool succès = ParseurPlageDate(termesRecherche, out DateTime start, out DateTime end);
var expression = (Expression<func bool="">>)(p => p.EtatPaiement.Contains(termesRecherche));

if (succès)
{
    expression = expression.And(p => p.DateCreation >= start && p.DateCreation < end);
}</func>

Gestion des Types de Données Spécifiques

Lorsque des colonnes utilisent des types de données anciens tels que TEXT, certaines opérations natifs comme DISTINCT ou LIKE peuvent échouer ou ne pas être optimisées par le SGBD. Par exemple, SQL Server refuse parfois l'utilisation de DISTINCT sur des types TEXT non comparables.

Pour contourner cette limitation sans modifier la schéma de base de données, il est préférable de récupérer les résultats bruts et d'effectuer la déduplication coté application C# en utilisant une collection HashSet.

public async Task<List<ResultatDto>> RécupérerArticles(string motCle)
{
    var contexte = new ContexteCMS();
    
    var itemsBruts = await (from article in contexte.Articles
                            join contenu in contexte.Contenus sur article.Id equals contenu.ArticleId into g1
                            from c1 in g1.DefaultIfEmpty()
                            join auteur in contexte Utilisateurs sur article.UserId equals auteur.Id into g2
                            from u2 in g2.DefaultIfEmpty()
                            where EF.Functions.Like(auteur.Nom, "%" + motCle + "%") 
                                || EF.Functions.Like(c1.ChampTexte, "%" + motCle + "%")
                            orderby article.DatePublication descending
                            select new { article, auteur, c1 })
                           .Skip((page-1)*limite).Take(limite).ToListAsync();

    // Déduplication côté client si nécessaire pour éviter DISTINCT SQL
    var ensembleUnique = itemsBruts.ToHashSet(item => item.article.Id);

    return ensembleUnique.Select(x => ConvertDto(x)).ToList();
}

Enfin, pour maintenir une architecture saine, il faut toujours surveiller les requêtes SQL générées par l'ORM. Les outils d'analyse comme EF.Profiler ou l'inspection directe via ToQueryString() permettent de détecter les conversions implicites coûteuses et de s'assurer que les indices pertinents sont exploités.

L'absence de stratégie de pagination appropriée (Skip/Take) peut également engorger la mémoire lors de la première lecture. Assurez-vous que les jointures sont bien limitées aux données nécessaires et privilégiez les projections sélectives plutôt que le chargement complet d'entités.

Étiquettes: EntityFrameworkCore SQLQueryOptimization DateTimeParsing CSharp DatabaseIndexing

Publié le 24 octobre à 17h30