Recherche avec critères dynamiques en SQL

Une fonctionnalité fréquente en développement SQL qui permet rechercher dans les données en utilisant plusieurs critères de manière libre. Afin d'avoir la solution complète y compris les méthodes de tri dynamique voir une autre article dédié à ce sujet "Le tri dynamique en SQL".

Pour chaque "patron" je vais ajouter les pictogrammes afin de vous donner les avantages et inconvénients intrinsèques.

bon plan de requête (utilise les index)
mauvais plan pour les table volumineuses (balayage d'une table)
ré-compilation permanente
ne fonctionne qu'avec les colonnes non nulles (NOT NULL)

Je prend la base de données AdventureWorks2008 pour nos tests. Notre requête doit sélectionner les personnes par leurs prénom et/ou nom.

USE AdventureWorks2008
GO
CREATE PROCEDURE Person.Person_GetByNames
   @LastName  nvarchar(50) = NULL,
   @FirstName nvarchar(50) = NULL
AS
BEGIN
  -- !! Add your code here !!
END
GO

Pour lancer les tests :

-- Filtered records only
EXEC Person.Person_GetByNames @LastName = 'Bacon', @FirstName = DEFAULT;
-- All records
EXEC Person.Person_GetByNames @LastName = DEFAULT, @FirstName = DEFAULT;

Maintenant nous allons remplacer "Add your code here" par les différentes méthodes qui implémentent la recherche avec les critères dynamiques.

SQL dynamique

Vous créez la contrainte "WHERE" dynamiquement selon des critères passés.

   DECLARE 
      @SQL nvarchar(max) = '', 
      @Params nvarchar(max) = '@LastName nvarchar(50), @FirstName nvarchar(50)';
   IF @LastName IS NOT NULL
      SET @SQL = @SQL + ' LastName = @LastName AND ';
   IF @FirstName IS NOT NULL
      SET @SQL = @SQL + ' FirstName = @FirstName AND ';
   IF @SQL <> ''
      SET @SQL = 'WHERE ' + substring(@SQL, 1, len(@SQL) - 4);
   SET @SQL = 'SELECT * FROM Person.Person ' + @SQL;
   EXEC sp_executesql @SQL, @Params, @LastName, @FirstName

Utilisation "IF ... ELSE IF ..."

   IF @LastName IS NULL AND @FirstName IS NULL
      SELECT * FROM Person.Person
   ELSE IF @LastName IS NOT NULL AND @FirstName IS NULL
      SELECT * FROM Person.Person
      WHERE LastName = @LastName
   ELSE IF @LastName IS NULL AND @FirstName IS NOT NULL
      SELECT * FROM Person.Person
      WHERE FirstName = @FirstName
   ELSE 
      SELECT * FROM Person.Person
      WHERE LastName = @LastName AND FirstName = @FirstName

Utilisation "OR"

   SELECT * FROM Person.Person
   WHERE
      (LastName = @LastName OR @LastName IS NULL) AND
      (FirstName = @FirstName OR @FirstName IS NULL)

Utilisation "CASE"

Il n'y a pas trop de différence par rapport de la méthode "OR".

   SELECT * FROM Person.Person
   WHERE 
      CASE 
         WHEN @LastName IS NULL THEN 1
         ELSE CASE WHEN LastName = @LastName THEN 1 ELSE 0 END 
      END = 1
      AND 
      CASE 
         WHEN @FirstName IS NULL THEN 1 
         ELSE CASE WHEN FirstName = @FirstName THEN 1 ELSE 0 END 
      END = 1

Utilisation coalesce()

   SELECT * FROM Person.Person
   WHERE
      LastName = coalesce(@LastName, LastName) AND
      FirstName = coalesce(@FirstName, FirstName)

Solution particulière. Pour les colonnes de type "chaine de caractères" non nulles il est possible modifier l'expression.

   SELECT * FROM Person.Person
   WHERE
      LastName LIKE coalesce(@LastName, '%') AND
      FirstName LIKE coalesce(@FirstName, '%')

Utilisation BETWEEN

Dans cette méthode il nous faudra bien choisir les valeurs constantes maximale et minimale selon le type de données.

   DECLARE 
      @MinValue nvarchar(1) = '',
      @MaxValue nvarchar(1024) = replicate(char(255), 1024);
   SELECT * FROM Person.Person
   WHERE
      LastName BETWEEN coalesce(@LastName, @MinValue) 
         AND coalesce(@LastName, @MaxValue)
      AND
      FirstName BETWEEN coalesce(@FirstName, @MinValue) 
         AND coalesce(@FirstName, @MaxValue)

Utilisation UNION ALL

Cette méthode est similaire de celle "Utilisation IF ELSE IF" mais peut être plus lourde pour l'optimisation avec la sélection de toutes les données à la fin ou en cas d'absence des index même si ce champ n'utilise pas logiquement.

   SELECT * FROM Person.Person
   WHERE
      LastName = @LastName AND @LastName IS NOT NULL
      AND
      FirstName = @FirstName AND @FirstName IS NOT NULL
   UNION ALL
   SELECT * FROM Person.Person
   WHERE
      LastName = @LastName 
      AND @LastName IS NOT NULL
      AND @FirstName IS NULL
   UNION ALL
   -- If no index on FirstName ==> may introduce a bad plan
   SELECT * FROM Person.Person
   WHERE
      FirstName = @FirstName 
      AND @FirstName IS NOT NULL 
      AND @LastName IS NULL
   -- Will always scan table
   UNION ALL
   SELECT * FROM Person.Person
   WHERE @FirstName IS NULL AND @LastName IS NULL

Solution. Pour éviter cette genre de problèmes, composez le méthode avec celle de "Utlisation IF ELSE IF"

   IF @FirstName IS NULL AND @LastName IS NULL
      SELECT * FROM Person.Person
   ELSE
      SELECT * FROM Person.Person
      WHERE
         LastName = @LastName AND @LastName IS NOT NULL
         AND
         FirstName = @FirstName AND @FirstName IS NOT NULL
      UNION ALL
      SELECT * FROM Person.Person
      WHERE
         LastName = @LastName 
         AND @LastName IS NOT NULL
         AND @FirstName IS NULL
      UNION ALL
      SELECT * FROM Person.Person
      WHERE
         FirstName = @FirstName 
         AND @FirstName IS NOT NULL 
         AND @LastName IS NULL