Dynamic filtering with SQL
The one of the most frequent asked question in database development is "How to filter the data returned by a query according to some user selected criteria?". To complete the solution with dynamic ordering see my other blog post.
For every method template I will add icons indicating corresponding advantages and drawbacks.
good query plan (uses indexes)
bad query plan for big tables (table scan)
query is recompiled every time
works only with NOT NULL columns
I use Microsoft demo database AdventureWorks for the tests. A test query should select persons by their first and last names.
USE AdventureWorks
GO
CREATE PROCEDURE Person.Person_GetByNames
@LastName nvarchar(50) = NULL,
@FirstName nvarchar(50) = NULL
AS
BEGIN
-- !! Add your code here !!
END
GO
To run the tests:
-- Filtered records only
EXEC Person.Person_GetByNames @LastName = 'Bacon', @FirstName = DEFAULT;
-- All records
EXEC Person.Person_GetByNames @LastName = DEFAULT, @FirstName = DEFAULT;
Now we'll replace "Add your code here" by different dynamic filtering methods.
Dynamic SQL
Create a WHERE condition dynamically according to specified parameters. Be aware that user should have the SELECT permission on the table.
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
Use IF..ELSE
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
Use OR
SELECT * FROM Person.Person
WHERE
(LastName = @LastName OR @LastName IS NULL) AND
(FirstName = @FirstName OR @FirstName IS NULL)
Use CASE expression
There is no big difference with previous "Use OR" method, just an another way to write SQL.
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
Use coalesce() function
SELECT * FROM Person.Person
WHERE
LastName = coalesce(@LastName, LastName) AND
FirstName = coalesce(@FirstName, FirstName)
Particular case solution: form the columns NOT NULL
of string type you may modify the expression.
SELECT * FROM Person.Person
WHERE
LastName LIKE coalesce(@LastName, '%') AND
FirstName LIKE coalesce(@FirstName, '%')
Use BETWEEN
You need to choose a good values for maximal and minimal constants according the data type.
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)
Use UNION ALL
The method is similar to "Use IF..ELSE" one but may be a more hard to optimize for SELECT *
case or if there is no index on filtered columns.
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: combine this method with "Use IF..ELSE" one.
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
blog comments powered by Disqus