Optimizing a dynamic sorting SQL query - sql

This query (I've simulated it to show you. Otherwise, it is longer with many more lists of columns, some more tables joined; and finally to sort by every column as required by the user) takes too long, and can timeout from the UI sometimes. I was trying to optimize the stored procedure to reduce loading time without affecting the retrieved lists. I was considering to select * from the temporary table and then Order from there. That could help somehow. I also considered using a dynamic sql without specifying the #sortidr (DESC/ASC), but the user should have the flexibility to order either way.
I was wondering if anyone can suggest better ways of optimizing it.
CREATE PROCEDURE get_lists
#employeeid int
#ColumnName varchar(100) = NULL,
#sortidr varchar(4) = NULL,
#start_date datetime = NULL
AS
CREATE #tempemp
(emp_id int, first_name varchar(20), last_name varchar(20), SSN int)
INSERT INTO #tempemp
SELECT team_action, rec_id, total_records, cfirst_name, efirst_name, has_desc, category_name
FROM
(SELECT COALESCE(lt.description, lt.action) AS team_action
,cl.rec_id
,total_records = count(*) Over()
,c.cfirst_name
,e.efirst_name
, has_desc = cld.changelog_id IS NOT NULL THEN 1 ELSE 0 END
, ct.category_name
FROM claimll cll
JOIN itemtype lt ON cll.clLog_id = lt.clLog_id
LEFT JOIN categgory ct ON ct.id=clc.cat_id
LEFT JOIN clients c ON ct.client_id = c.client_id
LEFT JOIN employees e ON cll.emp_id = e.emp_id
LEFT JOIN detail d ON d.change_id = cll.change_id
WHERE cll.change_date > #start_date
AND cll.emp_id = #employeeid OR cll.by_employeeid = #employeeid
) t
ORDER BY
CASE WHEN #sortidr = 'asc' AND #ColumnName='team_action' THEN team_action END,
CASE WHEN #sortidr = 'desc' AND #ColumnName = 'team_action' THEN team_action END DESC,
CASE WHEN #sortidr = 'asc' AND #ColumnName='rec_id' THEN rec_id END,
CASE WHEN #sortidr = 'desc' AND #ColumnName = 'rec_id' THEN rec_id END DESC,
CASE WHEN #sortidr = 'asc' AND #ColumnName='total_records' THEN total_records END,
CASE WHEN #sortidr = 'desc' AND #ColumnName = 'total_records' THEN total_records END DESC,
CASE WHEN #sortidr = 'asc' AND #ColumnName='cfirst_name' THEN cfirst_name END,
CASE WHEN #sortidr = 'desc' AND #ColumnName = 'cfirst_name' THEN cfirst_name END DESC,
CASE WHEN #ColumnName IS NULL THEN first_name END DESC

I'm not at a PC so I won't be able to add code until later but you have provided enough information for me to provide some good insight.
First - the optimizer will never be able to leverage an index to prevent a sort when you order by a CASE statement. That said, there are many reasons your query could be slow; you should remove that logic then test performance to understand if that's the culprit.
Based on the limited info I have my suggestion would be update your proc to:
1. Use dynamic SQL to build your temp table, as you have done, but without the CASE statement.
Included in this ^^^ dynamic SQL should be code to build a clustered index with #columnName as the cluster key and using #sortDir to dictate the index sort order.
Now you have a temp table correctly indexed based on user supplied parameters.
Add a second dynamic SQL statement that does a SELECT * FROM #temp... You will again use #columnName and #sortOrder to build a SARGable ORDER BY (one which will leverage the index from Step #1.
Test with different parameters always looking at the Actual Execution plan to further tune your indexes.
Hope this helps. I'll add demo code a little later if you need.
Updated On 2/22/2019 to include examples:
Here's a stored proc that does what I'm describing (note the comments)--
SET NOCOUNT ON;
USE tempdb
GO
IF OBJECT_ID('dbo.DynamicSortProc','P') IS NOT NULL DROP PROC dbo.DynamicSortProc;
GO
CREATE PROC dbo.DynamicSortProc #sortby NVARCHAR(100), #sortorder NVARCHAR(4)
AS
BEGIN
-- 1. Populate the temp table (sample data)
IF OBJECT_ID('##tmp','U') IS NOT NULL DROP TABLE ##tmp;
SELECT SomeId = IDENTITY(INT,1,1), ColA, ColB
INTO ##tmp
FROM (VALUES(1,3),(6,5),(2,8),(5,5),(2,6),(5,1)) AS f(ColA,ColB);
-- NOTE: On SQL 2014+ SELECT INTO is often faster than the standard create+insert
;
-- 2. Setup #sortby
SET #sortby = QUOTENAME(#sortby)+' '+CASE #sortorder WHEN 'DESC' THEN 'DESC' ELSE '' END;
-- 3. Set up the Dynamic SQL
DECLARE #createIndex NVARCHAR(4000) = 'CREATE CLUSTERED INDEX x ON ##tmp ('+#sortby+') ';
DECLARE #executeQuery NVARCHAR(4000) = 'SELECT t.* FROM ##tmp AS t ORDER BY '+#sortby;
-- 4. Execute the queries
EXEC sys.sp_executesql #createIndex;
EXEC sys.sp_executesql #executeQuery;
END
GO
You can test like this:
EXEC dbo.DynamicSortProc 'ColA', 'ASC';
EXEC dbo.DynamicSortProc 'ColA', 'DESC';
EXEC dbo.DynamicSortProc 'ColB', 'ASC';
EXEC dbo.DynamicSortProc 'ColB', 'DESC';
In each case they sort as expected and there is no sort in the output query.

Related

How to set total number of rows before an OFFSET occurs in stored procedure

I've created a stored procedure that filters and paginates for a DataTable.
Problem: I need to set an OUTPUT variable for #TotalRecords found before an OFFSET occurs, otherwise it sets #TotalRecord to #RecordPerPage.
I've messed around with CTE's and also simply trying this:
SELECT *, #TotalRecord = COUNT(1)
FROM dbo
But that doesn't work either.
Here is my stored procedure, with most of the stuff pulled out:
ALTER PROCEDURE [dbo].[SearchErrorReports]
#FundNumber varchar(50) = null,
#ProfitSelected bit = 0,
#SortColumnName varchar(30) = null,
#SortDirection varchar(10) = null,
#StartIndex int = 0,
#RecordPerPage int = null,
#TotalRecord INT = 0 OUTPUT --NEED TO SET THIS BEFORE OFFSET!
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM
(SELECT *
FROM dbo.View
WHERE (#ProfitSelected = 1 AND Profit = 1)) AS ERP
WHERE
((#FundNumber IS NULL OR #FundNumber = '')
OR (ERP.FundNumber LIKE '%' + #FundNumber + '%'))
ORDER BY
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'asc'
THEN ERP.FundNumber
END ASC,
CASE
WHEN #SortColumnName = 'FundNumber' AND #SortDirection = 'desc'
THEN ERP.FundNumber
END DESC
OFFSET #StartIndex ROWS
FETCH NEXT #RecordPerPage ROWS ONLY
Thank you in advance!
You could try something like this:
create a CTE that gets the data you want to return
include a COUNT(*) OVER() in there to get the total count of rows
return just a subset (based on your OFFSET .. FETCH NEXT) from the CTE
So your code would look something along those lines:
-- CTE definition - call it whatever you like
WITH BaseData AS
(
SELECT
-- select all the relevant columns you need
p.ProductID,
p.ProductName,
-- using COUNT(*) OVER() returns the total count over all rows
TotalCount = COUNT(*) OVER()
FROM
dbo.Products p
)
-- now select from the CTE - using OFFSET/FETCH NEXT, get only those rows you
-- want - but the "TotalCount" column still contains the total count - before
-- the OFFSET/FETCH
SELECT *
FROM BaseData
ORDER BY ProductID
OFFSET 20 ROWS FETCH NEXT 15 ROWS ONLY
As a habit, I prefer non-null entries before possible null. I did not reference those in my response below, and limited a working example to just the two inputs you are most concerned with.
I believe there could be some more clean ways to apply your local variables to filter the query results without having to perform an offset. You could return to a temp table or a permanent usage table that cleans itself up and use IDs that aren't returned as a way to set pages. Smoother, with less fuss.
However, I understand that isn't always feasible, and I become frustrated myself with those attempting to solve your use case for you without attempting to answer the question. Quite often there are multiple ways to tackle any issue. Your job is to decide which one is best in your scenario. Our job is to help you figure out the script.
With that said, here's a potential solution using dynamic SQL.
I'm a huge believer in dynamic SQL, and use it extensively for user based table control and ease of ETL mapping control.
use TestCatalog;
set nocount on;
--Builds a temp table, just for test purposes
drop table if exists ##TestOffset;
create table ##TestOffset
(
Id int identity(1,1)
, RandomNumber decimal (10,7)
);
--Inserts 1000 random numbers between 0 and 100
while (select count(*) from ##TestOffset) < 1000
begin
insert into ##TestOffset
(RandomNumber)
values
(RAND()*100)
end;
set nocount off;
go
create procedure dbo.TestOffsetProc
#StartIndex int = null --I'll reference this like a page number below
, #RecordsPerPage int = null
as
begin
declare #MaxRows int = 30; --your front end will probably manage this, but don't trust it. I personally would store this on a table against each display so it can also be returned dynamically with less manual intrusion to this procedure.
declare #FirstRow int;
--Quick entry to ensure your record count returned doesn't excede max allowed.
if #RecordsPerPage is null or #RecordsPerPage > #MaxRows
begin
set #RecordsPerPage = #MaxRows
end;
--Same here, making sure not to return NULL to your dynamic statement. If null is returned from any variable, the entire statement will become null.
if #StartIndex is null
begin
set #StartIndex = 0
end;
set #FirstRow = #StartIndex * #RecordsPerPage
declare #Sql nvarchar(2000) = 'select
tos.*
from ##TestOffset as tos
order by tos.RandomNumber desc
offset ' + convert(nvarchar,#FirstRow) + ' rows
fetch next ' + convert(nvarchar,#RecordsPerPage) + ' rows only'
exec (#Sql);
end
go
exec dbo.TestOffsetProc;
drop table ##TestOffset;
drop procedure dbo.TestOffsetProc;

How to filter data based on different values of a column in sql server

I am stuck at a point.
I want to select based on the column entitytype if entitytype value is Booking or JOb then it will filter on its basis but if it is null or empty string('') then i want it to return all the rows containing jobs and bookings
create proc spproc
#entityType varchar(50)
as
begin
SELECT TOP 1000 [Id]
,[EntityId]
,[EntityType]
,[TenantId]
FROM [FutureTrakProd].[dbo].[Activities]
where TenantId=1 and EntityType= case #EntityType when 'BOOKING' then 'BOOKING'
when 'JOB' then 'JOB'
END
end
Any help would be appreciable
Thankyou
create proc spproc
#entityType varchar(50)
as
begin
SELECT TOP 1000 [Id]
,[EntityId]
,[EntityType]
,[TenantId]
FROM [FutureTrakProd].[dbo].[Activities]
where TenantId=1 and (#EntityType is null OR EntityType= #EntityType)
end
You don't need to use case expression you can do :
SELECT TOP 1000 [Id], [EntityId], [EntityType], [TenantId]
from [FutureTrakProd].[dbo].[Activities]
WHERE TenantId = 1 AND
(#EntityType IS NULL OR EntityType = #EntityType)
ORDER BY id; -- whatever order you want (asc/desc)
For your query procedure you need to state explicit ORDER BY clause otherwise TOP 1000 will give random Ids.
You don't need a CASE expression for this, you just need an OR. The following should put you on the right path:
WHERE TenantId=1
AND (EntityType = #EntityType OR #EntityType IS NULL)
Also, note it would also be wise to declare your parameter as NULLable:
CREATE PROC spproc #entityType varchar(50) = NULL
This means that someone can simply exclude the paramter, value than having to pass NULL (thus EXEc spproc; would work).
Finally, if you're going to have lots of NULLable parameters, then you're looking at a "catch-all" query; the solution would be different if that is the case. "Catch-all" queries can be notoriously slow.
You can execute a dynamic sql query.
Query
create proc spproc
#entityType varchar(50)
as
begin
declare #sql as nvarchar(max);
declare #condition as nvarchar(2000);
select = case when #entityType is not null then ' and [EntityType] = #entityType;' else ';' end;
select #sql = 'SELECT TOP 1000 [Id], [EntityId], [EntityType], [TenantId] FROM [FutureTrakProd].[dbo].[Activities] where TenantId = 1 ';
exec sp_executesql #sql,
N'#entityType nvarchar(1000)',
#entityType = #entityType
end

Dynamic Where Class Using 'CASE' will affect performance

I have a table called Employee.
Fields are
1.EmployeeId,
2.Name,
3.Salary,
4.Address
I need to write a procedure that will fetch all the employee details. The where clause of this procedure is dynamic. I can able to write a dynamic query do fetch the details. But i recently explore the below format query.
SELECT * FROM Employee
WHERE 1=1
AND EmployeeId = CASE WHEN #EmployeeId IS NULL OR LEN(#EmployeeId) =0 THEN EmployeeId ELSE #EmployeeId END
AND Name = CASE WHEN #Name IS NULL OR LEN(#Name) =0 THEN Name ELSE #Name END
...
...
ORDER BY Name ASC
#EmployeeId , #name are coming from application
Can any tell me this will make any performance impact over a dynamic query? or Which one is better performance?
The where clause condition is dynamic. This is not strict with 2 condition. Some times its 1 or 3 or 4 or 0
Sorry for any typo-errors
Thanks in advance.
Let's say this was the data you are working with:
-- sample data
USE tempdb;
GO
CREATE TABLE dbo.employee (EmployeeId int, Name varchar(100));
INSERT dbo.employee
SELECT TOP (100)
ABS(checksum(newid())%10)+1,
CASE abs(checksum(newid())%3)+1 WHEN 1 THEN 'Peter' WHEN 2 THEN 'Paul' ELSE 'Mary' END
+ ' ' +
CASE abs(checksum(newid())%3)+1 WHEN 1 THEN 'Smith' WHEN 2 THEN 'Jones' ELSE 'White' END
FROM sys.all_columns;
-- A good covering index
CREATE NONCLUSTERED INDEX nc_employee ON dbo.employee (EmployeeId, Name);
First note that you can get the exact same results and execution plan with this query:
SELECT *
FROM dbo.employee
WHERE (EmployeeId = #EmployeeId OR NULLIF(LEN(#EmployeeId),0) IS NULL)
AND (Name = #Name OR NULLIF(LEN(#Name),0) IS NULL)
Presuming there is a usable index present - your query (or the query above) will create a basic execution plan consisting of an index seek or index Scan, and a SELECT operator. A scan if you run the query as-is, a seek if you include OPTION (RECOMPILE) at the end of either query... An index seek is much better than a scan.
An alternative to using the aforementioned OPTION (RECOMPILE) hint to get an index seek would be to use Dynamic SQL as shown below.
DECLARE #EmployeeId int = 5,
#Name varchar(100) = 'Peter White';
DECLARE #sql nvarchar(2000) = N'SELECT * FROM dbo.employee';
DECLARE #ParmDefinition nvarchar(500);
SET #ParmDefinition = N'#EmployeeId int, #Name varchar(100)';
SET #sql +=
CASE
WHEN (NULLIF(LEN(#EmployeeId),0) IS NOT NULL AND NULLIF(LEN(#Name),0) IS NOT NULL)
THEN ' WHERE EmployeeId = #EmployeeId AND Name = #Name;'
WHEN (NULLIF(LEN(#EmployeeId),0) IS NULL AND NULLIF(LEN(#Name),0) IS NOT NULL)
THEN ' WHERE Name = #Name;'
WHEN (NULLIF(LEN(#EmployeeId),0) IS NOT NULL AND NULLIF(LEN(#Name),0) IS NULL)
THEN ' WHERE EmployeeId = #EmployeeId;'
ELSE ''
END;
EXECUTE sp_executesql #sql, #ParmDefinition, #EmployeeId = #EmployeeId, #Name = #Name;
Finally, it's worth noting that, for cases where #EmployeeID is NULL or blank but #Name has a value - you will get a scan. If you wanted a seek in those cases you would want another index that looks like this present:
CREATE NONCLUSTERED INDEX nc_employee2 ON dbo.employee (Name, EmployeeId);

Dynamic order by without using dynamic sql?

I have the following stored procedure which can be sorted ascending and descending on TemplateName,CreatedOn and UploadedBy. The following SP when runs doesnot sort records.if i replace 2,3,4 by columnname, i got an error message "Conversion failed when converting the nvarchar value 'Test Template' to data type int.".Please suggest how to achieve sorting.
CREATE PROCEDURE [dbo].[usp_SEL_GetRenderingTemplate]
(
#facilityID INT,
#sortOrder VARCHAR(5),
#sortExpression VARCHAR(100),
#errorCode INT OUTPUT
)
AS
BEGIN
SET NOCOUNT ON ;
BEGIN TRY
SET #sortOrder = CASE #sortOrder
WHEN 'Ascending' THEN 'ASC'
WHEN 'Descending' THEN 'DESC'
ELSE 'ASC'
END
SELECT TemplateID,
TemplateName,
CreatedOn,
( [user].LastName + ' ' + [user].FirstName ) AS UploadedBy
FROM Templates
INNER JOIN [user] ON [user].UserID = Templates.CreatedBy
WHERE facilityid = #facilityID
ORDER BY CASE WHEN #sortExpression = 'TemplateName'
AND #sortOrder = 'ASC' THEN 2
WHEN #sortExpression = 'CreatedOn'
AND #sortOrder = 'ASC' THEN 3
WHEN #sortExpression = 'UploadedBy'
AND #sortOrder = 'ASC' THEN 4
END ASC,
CASE WHEN #sortExpression = 'TemplateName'
AND #sortOrder = 'DESC' THEN 2
WHEN #sortExpression = 'CreatedOn'
AND #sortOrder = 'DESC' THEN 3
WHEN #sortExpression = 'UploadedBy'
AND #sortOrder = 'DESC' THEN 4
END DESC
SET #errorCode = 0
END TRY
BEGIN CATCH
SET #errorCode = -1
DECLARE #errorMsg AS VARCHAR(MAX)
DECLARE #utcDate AS DATETIME
SET #errorMsg = CAST(ERROR_MESSAGE() AS VARCHAR(MAX))
SET #utcDate = CAST(GETUTCDATE() AS DATETIME)
EXEC usp_INS_LogException 'usp_SEL_GetFacilityWorkTypeList',
#errorMsg, #utcDate
END CATCH
END
The dynamic ordering has to be of the same datatype. Here is an example of something that I use to case order three different datatypes - integer, date (ascending and descending), and string. This may not work for your situation, but at least you can see some techniques for casting to a common datatype.
...
ORDER BY
Case Parent.RankTypeID
When 0 Then dbo.Documents.Rank
When 1 Then Convert(int, dbo.Documents.DateStart, 112)
When 2 Then (1 - Convert(int, dbo.Documents.DateStart, 112))
When 3 Then Cast(dbo.Documents.Title as sql_variant)
End
Note:
112 is a date format of YYYYMMDD - useful for ordering.
We perform a similar sort of dynamic ordering in one of our products.
The only real different to your code is firstly, we don't use the live join.
We create a temporary table, so we can perform paging, and then apply the order.
We also use ints for ordering, cuts down on the overhead of string comparison.
It does make your SQL a bit longer, but this approach is definitely faster for the Query Optimiser. Also, and more importantly, I don't think you can mix types in Switch blocks, for ordering, so to follow your original code, you'd have to CONVERT all your data to the same time, which defeats the object :(
So you'd have
DECLARE #temp TABLE(ID int identity(1,1), TemplateID int, TemplateName nvarchar(100), CreatedOn datetime, UploadedBy nvarchar(100))
INSERT INTO #temp(TemplateID, TemplateName, CreatedOn, UploadedBy)
SELECT TemplateID,
TemplateName,
CreatedOn,
( [user].LastName + ' ' + [user].FirstName ) AS UploadedBy
FROM Templates
INNER JOIN [user] ON [user].UserID = Templates.CreatedBy
WHERE facilityid = #facilityID
Then:
IF #SortOrder = 1 --'ASC'
BEGIN
IF #sort = 2
Select *
From #Temp
Order by TemplateName ASC
ELSE IF #sort = 3
Select *
From #Temp
Order By CreatedBy ASC
-- and so on...
END
ELSE -- descending
BEGIN
-- Ad. Inf.
END
Delete
From #Temp
WHERE ID < #pageStart or ID > #pageStart + #pageSize
The following SP when runs doesnot
sort records.if i replace 2,3,4 by
columnname,
You shouldn't replace 2, 3, 4 in the ORDER BY clause by the column name. When you call the procedure just pass the column you want to sort by as the 3rd parameter.
EXEC [dbo].[usp_SEL_GetRenderingTemplate] 1,'Ascending','CreatedOn',#vErrorCode
The CASE will then evaluate the query to something like
...ORDER BY 3 DESC
which sorts the query by the 3rd column in the SELECT clause (i.e. CreatedOn)
The problem is that in CASE clauses the return values must have the same data type.
You can work around this problem by using multiple CASE statements http://www.extremeexperts.com/sql/articles/CASEinORDER.aspx
Or casting everything to the same data type.

How do I create a stored procedure that will optionally search columns?

I'm working on an application for work that is going to query our employee database. The end users want the ability to search based on the standard name/department criteria, but they also want the flexibility to query for all people with the first name of "James" that works in the Health Department. The one thing I want to avoid is to simply have the stored procedure take a list of parameters and generate a SQL statement to execute, since that would open doors to SQL injection at an internal level.
Can this be done?
While the COALESCE trick is neat, my preferred method is:
CREATE PROCEDURE ps_Customers_SELECT_NameCityCountry
#Cus_Name varchar(30) = NULL
,#Cus_City varchar(30) = NULL
,#Cus_Country varchar(30) = NULL
,#Dept_ID int = NULL
,#Dept_ID_partial varchar(10) = NULL
AS
SELECT Cus_Name
,Cus_City
,Cus_Country
,Dept_ID
FROM Customers
WHERE (#Cus_Name IS NULL OR Cus_Name LIKE '%' + #Cus_Name + '%')
AND (#Cus_City IS NULL OR Cus_City LIKE '%' + #Cus_City + '%')
AND (#Cus_Country IS NULL OR Cus_Country LIKE '%' + #Cus_Country + '%')
AND (#Dept_ID IS NULL OR Dept_ID = #DeptID)
AND (#Dept_ID_partial IS NULL OR CONVERT(varchar, Dept_ID) LIKE '%' + #Dept_ID_partial + '%')
These kind of SPs can easily be code generated (and re-generated for table-changes).
You have a few options for handling numbers - depending if you want exact semantics or search semantics.
The most efficient way to implement this type of search is with a stored procedure. The statement shown here creates a procedure that accepts the required parameters. When a parameter value is not supplied it is set to NULL.
CREATE PROCEDURE ps_Customers_SELECT_NameCityCountry
#Cus_Name varchar(30) = NULL,
#Cus_City varchar(30) = NULL,
#Cus_Country varchar(30) =NULL
AS
SELECT Cus_Name,
Cus_City,
Cus_Country
FROM Customers
WHERE Cus_Name = COALESCE(#Cus_Name,Cus_Name) AND
Cus_City = COALESCE(#Cus_City,Cus_City) AND
Cus_Country = COALESCE(#Cus_Country,Cus_Country)
Taken from this page: http://www.sqlteam.com/article/implementing-a-dynamic-where-clause
I've done it before. It works well.
Erland Sommarskog's article Dynamic Search Conditions in T-SQL is a good reference on how to do this. Erland presents a number of strategies on how to do this without using dynamic SQL (just plain IF blocks, OR, COALESCE, etc) and even lists out the performance characteristics of each technique.
In case you have to bite the bullet and go through the Dynamic SQL path, you should also read Erland's Curse and Blessings of Dynamic SQL where he gives out some tips on how to properly write dynamic SQLs
It can be done, but usually these kitchen-sink procedures result in some poor query plans.
Having said all that, here is the tactic most commonly used for "optional" parameters. The normal approach is to treat NULL as "ommitted".
SELECT
E.EmployeeID,
E.LastName,
E.FirstName
WHERE
E.FirstName = COALESCE(#FirstName, E.FirstName) AND
E.LastName = COALESCE(#LastName, E.LastName) AND
E.DepartmentID = COALESCE(#DepartmentID, E.DepartmentID)
EDIT:
A far better approach would be parameterized queries.
Here is a blog post from one of the world's foremost authorities in this domain, Frans Bouma from LLBLGen Pro fame:
Stored Procedures vs. Dynamic Queries
Using the COALESCE method has a problem in that if your column has a NULL value, passing in a NULL search condition (meaning ignore the search condition) will not return the row in many databases.
For example, try the following code on SQL Server 2000:
CREATE TABLE dbo.Test_Coalesce (
my_id INT NOT NULL IDENTITY,
my_string VARCHAR(20) NULL )
GO
INSERT INTO dbo.Test_Coalesce (my_string) VALUES (NULL)
INSERT INTO dbo.Test_Coalesce (my_string) VALUES ('t')
INSERT INTO dbo.Test_Coalesce (my_string) VALUES ('x')
INSERT INTO dbo.Test_Coalesce (my_string) VALUES (NULL)
GO
DECLARE #my_string VARCHAR(20)
SET #my_string = NULL
SELECT * FROM dbo.Test_Coalesce WHERE my_string = COALESCE(#my_string, my_string)
GO
You will only get back two rows because in the rows where the column my_string is NULL you are effective getting:
my_string = COALESCE(#my_string, my_string) =>
my_string = COALESCE(NULL, my_string) =>
my_string = my_string =>
NULL = NULL
But of course, NULL does not equal NULL.
I try to stick with:
SELECT
my_id,
my_string
FROM
dbo.Test_Coalesce
WHERE
(#my_string IS NULL OR my_string = #my_string)
Of course, you can adjust that to use wild cards or whatever else you want to do.
Copying this from my blog post:
USE [AdventureWorks]
GO
CREATE PROCEDURE USP_GET_Contacts_DynSearch
(
-- Optional Filters for Dynamic Search
#ContactID INT = NULL,
#FirstName NVARCHAR(50) = NULL,
#LastName NVARCHAR(50) = NULL,
#EmailAddress NVARCHAR(50) = NULL,
#EmailPromotion INT = NULL,
#Phone NVARCHAR(25) = NULL
)
AS
BEGIN
SET NOCOUNT ON
DECLARE
#lContactID INT,
#lFirstName NVARCHAR(50),
#lLastName NVARCHAR(50),
#lEmailAddress NVARCHAR(50),
#lEmailPromotion INT,
#lPhone NVARCHAR(25)
SET #lContactID = #ContactID
SET #lFirstName = LTRIM(RTRIM(#FirstName))
SET #lLastName = LTRIM(RTRIM(#LastName))
SET #lEmailAddress = LTRIM(RTRIM(#EmailAddress))
SET #lEmailPromotion = #EmailPromotion
SET #lPhone = LTRIM(RTRIM(#Phone))
SELECT
ContactID,
Title,
FirstName,
MiddleName,
LastName,
Suffix,
EmailAddress,
EmailPromotion,
Phone
FROM [Person].[Contact]
WHERE
(#lContactID IS NULL OR ContactID = #lContactID)
AND (#lFirstName IS NULL OR FirstName LIKE '%' + #lFirstName + '%')
AND (#lLastName IS NULL OR LastName LIKE '%' + #lLastName + '%')
AND (#lEmailAddress IS NULL OR EmailAddress LIKE '%' + #lEmailAddress + '%')
AND (#lEmailPromotion IS NULL OR EmailPromotion = #lEmailPromotion)
AND (#lPhone IS NULL OR Phone = #lPhone)
ORDER BY ContactID
END
GO
We can use Generic #Search Parameter and pass any value to it for searching.
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: --
-- Create date:
-- Description: --
-- =============================================
CREATE PROCEDURE [dbo].[usp_StudentList]
#PageNumber INT = 1, -- Paging parameter
#PageSize INT = 10,-- Paging parameter
#Search VARCHAR(MAX) = NULL, --Generic Search Parameter
#OrderBy VARCHAR(MAX) = 'FirstName', --Default Column Name 'FirstName' for records ordering
#SortDir VARCHAR(MAX) = 'asc' --Default ordering 'asc' for records ordering
AS
BEGIN
SET NOCOUNT ON;
--Query required for paging, this query used to show total records
SELECT COUNT(StudentId) AS RecordsTotal FROM Student
SELECT Student.*,
--Query required for paging, this query used to show total records filtered
COUNT(StudentId) OVER (PARTITION BY 1) AS RecordsFiltered
FROM Student
WHERE
--Generic Search
-- Below is the column list to add in Generic Serach
(#Search IS NULL OR Student.FirstName LIKE '%'+ #Search +'%')
OR (#Search IS NULL OR Student.LastName LIKE '%'+ #Search +'%')
--Order BY
-- Below is the column list to allow sorting
ORDER BY
CASE WHEN #SortDir = 'asc' AND #OrderBy = 'FirstName' THEN Student.FirstName END,
CASE WHEN #SortDir = 'desc' AND #OrderBy = 'FirstName' THEN Student.FirstName END DESC,
CASE WHEN #SortDir = 'asc' AND #OrderBy = 'LastName' THEN Student.LastName END,
CASE WHEN #SortDir = 'desc' AND #OrderBy = 'LastName' THEN Student.LastName END DESC,
OFFSET #PageSize * (#PageNumber - 1) ROWS FETCH NEXT #PageSize ROWS ONLY;
END
My first thought was to write a query something like this...
SELECT EmpId, NameLast, NameMiddle, NameFirst, DepartmentName
FROM dbo.Employee
INNER JOIN dbo.Department ON dbo.Employee.DeptId = dbo.Department.Id
WHERE IdCrq IS NOT NULL
AND
(
#bitSearchFirstName = 0
OR
Employee.NameFirst = #vchFirstName
)
AND
(
#bitSearchMiddleName = 0
OR
Employee.NameMiddle = #vchMiddleName
)
AND
(
#bitSearchFirstName = 0
OR
Employee.NameLast = #vchLastName
)
AND
(
#bitSearchDepartment = 0
OR
Department.Id = #intDeptID
)
...which would then have the caller provide a bit flag if they want to search a particular field and then supply the value if they are to search for it, but I don't know if this is creating a sloppy WHERE clause or if I can get away with a CASE statement in the WHERE clause.
As you can see this particular code is in T-SQL, but I'll gladly look at some PL-SQL / MySQL code as well and adapt accordingly.
I would stick with the NULL/COALESCE method over AdHoc Queries, and then test to make sure you don't have performance problems.
If it turns out that you have slow running queries because it's doing a table scan when you're searching on columns that are indexed, you could always supplement the generic search stored procedure with additional specific ones that allow searching on these indexed fields. For instance, you could have a special SP that does searches by CustomerID, or Last/First Name.
Write a procedure to insert all employee data whose name start with A in table??