I have the following user-defined function:
create function [dbo].[FullNameLastFirst]
(
#IsPerson bit,
#LastName nvarchar(100),
#FirstName nvarchar(100)
)
returns nvarchar(201)
as
begin
declare #Result nvarchar(201)
set #Result = (case when #IsPerson = 0 then #LastName else case when #FirstName = '' then #LastName else (#LastName + ' ' + #FirstName) end end)
return #Result
end
I can't create an Index on a computed column using this function cause it's not deterministic.
Someone could explain why is it not deterministic and eventually how to modify to make it deterministic?
Thanks
You just need to create it with schemabinding.
SQL Server will then verify whether or not it meets the criteria to be considered as deterministic (which it does as it doesn't access any external tables or use non deterministic functions such as getdate()).
You can verify that it worked with
SELECT OBJECTPROPERTY(OBJECT_ID('[dbo].[FullNameLastFirst]'), 'IsDeterministic')
Adding the schemabinding option to your original code works fine but a slightly simpler version would be.
CREATE FUNCTION [dbo].[FullNameLastFirst] (#IsPerson BIT,
#LastName NVARCHAR(100),
#FirstName NVARCHAR(100))
RETURNS NVARCHAR(201)
WITH SCHEMABINDING
AS
BEGIN
RETURN CASE
WHEN #IsPerson = 0
OR #FirstName = '' THEN #LastName
ELSE #LastName + ' ' + #FirstName
END
END
You need to declare the User Defined Function WITH SCHEMABINDING to appease the 'deterministic' requirement of an index on the computed column.
A function declared WITH SCHEMABINDING will retain additional knowledge about the object dependencies used in the function (e.g. columns in the table), and will prevent any changes to these columns, unless the function itself is dropped beforehand.
Deterministic functions can also assist Sql Server in optimizing its execution plans, most notably the Halloween Protection problem.
Here's an example of creating an index on a computed column using a schema bound function:
create function [dbo].[FullNameLastFirst]
(
#IsPerson bit,
#LastName nvarchar(100),
#FirstName nvarchar(100)
)
returns nvarchar(201)
with schemabinding
as
begin
declare #Result nvarchar(201)
set #Result = (case when #IsPerson = 0 then #LastName
else case when #FirstName = '' then #LastName
else (#LastName + ' ' + #FirstName) end end)
return #Result
end
create table Person
(
isperson bit,
lastname nvarchar(100),
firstname nvarchar(100),
fullname as [dbo].[FullNameLastFirst] (isperson, lastname, firstname)
)
go
insert into person(isperson, lastname, firstname) values (1,'Firstname', 'Surname')
go
create index ix1_person on person(fullname)
go
select fullname from Person with (index=ix1_person) where fullname = 'Firstname Surname'
go
Related
For the below query, SQL Server is creating a unique query plan depending on the parameter which is passed, Is there any way to optimize the below query to reduce the number of query plans and optimize the query.
CREATE PROCEDURE [dbo].[Foo_search]
#ItemID INT,
#LastName VARCHAR(50),
#MiddleName VARCHAR(40),
#FirstName VARCHAR(50)
AS
BEGIN
DECLARE #Sql NVARCHAR(max)
SELECT #Sql = N 'Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email, From Employees Where DeletedOn Is Null ' +
CASE WHEN #LastName IS NULL OR #LastName = '' THEN '' ELSE ' And FamilyName=''' + #LastName + ''' ' END +
CASE WHEN #MiddleName IS NULL OR #MiddleName = '' THEN '' ELSE ' And MiddleName=''' + #MiddleName + ''' ' END +
CASE WHEN #FirstName IS NULL OR #FirstName = '' THEN '' ELSE ' And FirstName=''' + #FirstName + ''' ' END +
CASE WHEN #ItemID IS NOT NULL AND #ItemID > 0 THEN ' And ItemID=' + CONVERT(VARCHAR(10), #ItemID) + ' ' ELSE ' ' END
EXEC Sp_executesql #sql
END
Yes there is a way you can improve it, use the parameters correctly, rather than using string concatenation. Your method will generate a different query plan for every different combination of values of parameter, rather than just every different combination of parameters, which will generate orders of magnitude more query plans.
DECLARE #Sql nvarchar(max) = N'
Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email
From Employees
Where DeletedOn Is Null
'
+ CASE WHEN #LastName <> '' THEN ' And FamilyName = #LastName' ELSE '' END
+ CASE WHEN #MiddleName <> '' THEN ' And MiddleName = #MiddleName' ELSE '' END
+ CASE WHEN #FirstName <> '' THEN ' And FirstName = #FirstName' ELSE '' END
+ CASE WHEN #ItemID > 0 THEN ' And ItemID = #ItemID' ELSE '' END;
EXEC sp_executesql
#Sql,
N'#LastName varchar(50), #MiddleName varchar(40), #FirstName varchar(50)',
#LastName = #LastName,
#MiddleName = #MiddleName,
#FirstName = #FirstName;
And as Aaron Bertrand points out, this also fixes the issue where any parameter value containing a single quote (') will fail.
Beyond this however, as Aaron also mentions, the performance is most likely down to other issues such as indexing.
I don't think you're blaming the right thing, but if you really think that multiple plans are causing your CPU spikes, it's easy to change this back to a single plan strategy:
ALTER PROCEDURE dbo.Foo_search
#ItemID INT,
#LastName VARCHAR(50),
#MiddleName VARCHAR(40),
#FirstName VARCHAR(50)
AS
BEGIN
Select ID, FirstName, FamilyName, MiddleName, MaidenName, Email
From dbo.Employees
Where DeletedOn Is Null
AND (FamilyName = #LastName OR #LastName IS NULL)
AND (MiddleName = #MiddleName OR #MiddleName IS NULL)
AND (FirstName = #FirstName OR #FirstName IS NULL)
AND (ItemID = #ItemID OR #ItemID IS NULL);
END
Let us know how that works out for you; I'm curious what index you're going to implement to make that perform well for all possible parameter combinations.
I have the below requirement. When FirstName present I should return the only condtionFirstName when LastName present I should return the only condtionlastName when First and email both are present I should return condtionFirstName + condtionEmail.
Basically whichever value is present I should return that condition, If 1 value 1 condition, if 1 and 2 then condition 1 and 2, if 1 and 3 then condition 1 and 3, if only 3 then condition 3.
Kindly help me to get this logic.
DECLARE #FirstName NVARCHAR(100)
DECLARE #LastName NVARCHAR(100)
DECLARE #Email NVARCHAR(200)
DECLARE #condtionFirstName NVARCHAR(200)
DECLARE #condtionlastName NVARCHAR(200)
DECLARE #condtionEmail NVARCHAR(200)
SET #FirstName = 'JOhn'
SET #LastName = 'David'
SET #Email = 'john.david#abc.com'
SET #condtionFirstName = ' AND FirstName = ' + '''' + #FirstName + ''''
SET #condtionlastName = ' AND LastName = ' + '''' + #LastName + ''''
SET #condtionEmail = ' AND Email = ' + '''' + #Email + ''''
This is what I've long called "the kitchen sink" - you want a single query that can support any combination of search criteria. Some thoughts:
Stop trying to concatenate user input with executable strings - this is dangerous and error-prone. These should always be passed in as explicitly-typed parameters.
You can build a single string containing all the conditions instead of trying to create a new condition string for every possible condition.
You can declare and pass parameters to sp_executesql even if they don't all end up in the dynamic SQL statement. This is just like declaring a local variable and not using it.
e-mail addresses can be 320 characters long. If you only support 200, that could bite you.
Sample:
DECLARE #FirstName nvarchar(100),
#LastName nvarchar(100),
#Email nvarchar(320),
#conditions nvarchar(max) = N'';
SET #FirstName = N'John';
SET #LastName = N'David';
SET #Email = N'john.david#abc.com';
SET #conditions += CASE WHEN #FirstName IS NOT NULL THEN
N' AND FirstName = #FirstName' ELSE N'' END
+ CASE WHEN #LastName IS NOT NULL THEN
N' AND LastName = #LastName' ELSE N'' END
+ CASE WHEN #Email IS NOT NULL THEN
N' AND Email = #Email' ELSE N'' END;
DECLARE #sql nvarchar(max) = N'SELECT ... FROM dbo.table
WHERE 1 = 1' + #conditions;
PRINT #sql; -- try this when populating or not populating each of
-- #FirstName, #LastName, #Email
EXEC sys.sp_executesql #sql,
N'#FirstName nvarchar(100), #LastName nvarchar(100), #Email nvarchar(320)',
#FirstName, #LastName, #Email;
More details:
#BackToBasics: An Updated Kitchen Sink Example
Protecting Yourself from SQL Injection in SQL Server - Part 1
Protecting Yourself from SQL Injection in SQL Server - Part 2
Let's say I have have the following parameters:
#firstName varchar(50), #middleName varchar(50), #LastName varchar(50)
I want the middle name to be optional, How do I return the value (#portalUserName) without middle name if the middle name is not passed as an input parameter?
PS: I need the middle names first character to be added in the user name if the user has a middle name (i.e. if middle name value isn't null)
CREATE PROCEDURE [dbo].[cst_sproc_UserName_Get]
(#firstName VARCHAR(50),
#middleName VARCHAR(50),
#LastName VARCHAR(50)
)
AS
BEGIN
DECLARE #portalUserName VARCHAR(50)
SET #portalUserName = SUBSTRING(UPPER(RTRIM(#firstname)), 1, 1) +
SUBSTRING(UPPER(RTRIM(#firstname)), 1, 1) +
LOWER(RTRIM(#LastName))
IF NOT EXISTS (SELECT 'TRUE' FROM wpUser WHERE UserCode = #portalUserName)
BEGIN
SELECT #portalUserName UserCode
RETURN
END
END
To make a parameter optional, you need to give it a default value, which could be NULL.
CREATE PROC [dbo].[cst_sproc_UserName_Get]
(
#firstName varchar(50),
#middleName varchar(50) = NULL,
#LastName varchar(50)
)
AS
BEGIN
Declare #portalUserName varchar(50)
SET #portalUserName = SUBSTRING(upper(RTRIM(#firstname)),1,1)+ SUBSTRING(upper(RTRIM(#firstname)),1,1) + lower(RTRIM(#LastName))
IF NOT EXISTS (SELECT 'TRUE' FROM wpUser WHERE UserCode = #portalUserName)
BEGIN
SELECT #portalUserName UserCode
Return
END
But to call it, you need to follow Best Practices and call your procedure with parameter naming.
EXEC [dbo].[cst_sproc_UserName_Get] #firstName = 'Luis', #LastName = 'Cazares';
The question is quite extensive, please bear with me. I have a single mapping table with the following structure:
This particular table is used in the process of generating a hierarchy. The order and position of the columns in the table indicate the order of hierarchy (Organization, Category, Continent, Country.. etc.) Each entity in this hierarchy has a related table with associated Id and Name. For example, there is a Country table with CountryId and CountryName. Note that since the MappingTable's values are all nullable there are no foreign key constraints.
I want to generate a procedure that will do the following:
Based on conditions provided, retrieve values of the next entity in the hierarchy. For example, if the OrganizationId and CategoryId are given, the values of ContinentId that satisfy said condition need to be retrieved.
Also, if the value of ContinentId is NULL, then the values of CountryId need to be retrieved. Here, given the condition OrganizationId = 1 and CategoryId = 1 the procedure should return the list of RegionId.
In addition to retrieving the RegionId, the corresponding RegionName should be retrieved from the Region Table.
So far, the procedure looks something like this - just a few things to explain here.
ALTER PROCEDURE [dbo].[GetHierarchy]
(
#MappingTableName VARCHAR(30),
#Position VARCHAR(5),
-- Given in the form of Key-value pairs 'OrganizationId:1,CategoryId:1'
#InputData VARCHAR(MAX),
#Separator CHAR(1),
#KeyValueSeperator CHAR(1)
)
AS
BEGIN
DECLARE #Sql NVARCHAR(MAX)
DECLARE #Result NVARCHAR(MAX)
DECLARE #Sql1 NVARCHAR(MAX);
DECLARE #TableName NVARCHAR(30)
DECLARE #Exists bit
SELECT #TableName = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #MappingTableName AND ORDINAL_POSITION = #position
SET #TableName = SUBSTRING(#TableName,0,LEN(#TableName) - 1)
-- Returns a dynamic query like "SELECT ContinentId from Continent WHERE OrganizationId = 1 and CategoryId = 1".
SELECT #Sql = [dbo].[KeyValuePairs](#TableName, #InputData, #Separator, #KeyValueSeperator)
SET #Sql1 = N'SET #Exists = CASE WHEN EXISTS(' + #Sql + N' AND ' + #TableName + N'Id IS NOT NULL) THEN 1 ELSE 0 END'
PRINT #Sql
EXEC sp_executesql #Sql1,
N'#Exists bit OUTPUT',
#Exists = #Exists OUTPUT
IF(#Exists = 1)
BEGIN
SET #Sql1 = 'SELECT ' + #TableName + 'Id, ' + #TableName + 'Name FROM '+ #TableName+' WHERE ' + #TableName +'Id IN (' + #Sql + ')';
PRINT #Sql1
EXECUTE sp_executesql #SQL1
END
ELSE
BEGIN
--PRINT 'NOT EXISTS'
DECLARE #nextPosition INT
SELECT #nextPosition = CAST(#position AS INT)
SET #nextPosition = #nextPosition + 1
SET #Position = CONVERT(VARCHAR(5), CAST(#position AS INT))
EXEC [dbo].[GetHierarchy] #MappingTableName, #Position, #InputData, #Separator, #KeyValueSeperator
END
END
The logic of this procedure is such that, I get the name of the column at a particular position (based on the conditions here, it is Continent) and generate the dynamic query to retrieve the next column's values based on the condition of the input condition (I am using a separate function to do this for me).
Once retrieved, I run the query to check if it returns any rows. If the query returns rows, then I retrieve the corresponding ContinentName from the Continent table. If no rows are returns, I recursively call the procedure again with the next position as the input.
On the business side of things, it seems like a two step process. But, as a procedure it is quite complex, extensive and - not to mention, recursive. Is there an easier way to do this? I am not familiar with CTEs - can the same logic be implemented using CTEs?
This is quite similar to what is asked here: Working with a dynamic hierarchy SQL Server
Might be the little lengthy approach. Try this
DECLARE #T TABLE
(
SeqNo INT IDENTITY(1,1),
CatId INT,
Country INT,
StateId INT,
DistId INT
)
DECLARE #State TABLE
(
StateId INT,
StateNm VARCHAR(20)
)
DECLARE #Country TABLE
(
CountryId INT,
CountryNm VARCHAR(20)
)
INSERT INTO #State
VALUES(3,'FL')
INSERT INTO #Country
VALUES(2,'USA')
INSERT INTO #T(CatId)
VALUES(1)
INSERT INTO #T(CatId,Country)
VALUES(1,2)
INSERT INTO #T(CatId,StateId)
VALUES(1,3)
;WITH CTE
AS
(
SELECT
*,
IdVal = COALESCE(Country,StateId,DistId),
IdCol = COALESCE('Country '+CAST(Country AS VARCHAR(50)),'StateId '+CAST(StateId AS VARCHAR(50)),'DistId '+CAST(DistId AS VARCHAR(50)))
FROM #T
WHERE CatId = 1
),C2
AS
(
SELECT
SeqNo,
CatId,
Country,
StateId,
DistId,
IdVal,
IdCol = LTRIM(RTRIM(SUBSTRING(IdCol,1,CHARINDEX(' ',IdCol))))
FROM CTE
)
SELECT
C2.SeqNo,
C2.CatId,
S.StateNm,
C.CountryNm
FROM C2
LEFT JOIN #State S
ON C2.IdCol ='StateId'
AND C2.IdVal = S.StateId
LEFT JOIN #Country C
ON C2.IdCol ='Country '
AND C2.IdVal = c.CountryId
I'm writing a general search Stored Procedure to search in a table based on many filters which user can select in the UI (using MS-SQL 2008).
Here is ther simplified version:
CREATE PROCEDURE SearchAll
#FirstName NVARCHAR(MAX) = NULL,
#LastName NVARCHAR(MAX) = NULL,
#Age INT = NULL
AS
SELECT *
FROM persons
WHERE
(#FirstName IS NULL OR FirstName = #firstname)
AND (#LastName IS NULL OR LastName = #LastName)
AND (#Age IS NULL OR Age = #Age)
It seems that if I pass NULL to #Age there'll be no performance cost. But, when I'm testing with huge amount of data, I have a great perfomance lost!
Here is the queries which are the same logicaly but VERY different practically:
DECLARE #FirstName NVARCHAR(MAX) = NULL
DECLARE #Age INT = 23
------------First slow------------
SELECT *
FROM persons
WHERE
(#FirstName IS NULL OR FirstName = #firstname)
AND (#Age IS NULL OR Age = #Age)
------------Very fast------------
SELECT *
FROM persons
WHERE
Age = #Age
Did is miss a point?
I know that SQL engine finds the best match for indexes, and ... (before running the query),but it's obvious that: #FirstName IS NULL and there's no need to analyse anything.
I've also tested ISNULL function in the query (the same result).
Queries that contain this construct of #variable is null or #variable = column are a performance disaster. This is because SQL plans are created so they work for any value of the variable. For a lengthy discussion of the topic, the problems and possible solutions see Dynamic Search Conditions in T-SQL
The problem, as has already been mentioned, is that the query plan will be built to work with any value of the variables. You can circumvent this by building the query with only the paremeter required, as follows:
CREATE PROCEDURE SearchAll
#FirstName NVARCHAR(MAX) = NULL,
#LastName NVARCHAR(MAX) = NULL,
#Age INT = NULL
AS
BEGIN
DECLARE #sql NVARCHAR(MAX), #has_where BIT
SELECT #has_where = 0, #sql = 'SELECT * FROM persons '
IF #FirstName IS NOT NULL
SELECT #sql = #sql + 'WHERE FirstName = ''' + #FirstName + '''', #has_where = 1
IF #LastName IS NOT NULL
SELECT #sql = #sql + CASE WHEN #has_where = 0 THEN 'WHERE ' ELSE 'AND ' END + 'LastName = ''' + #LastName + '''', #has_where = 1
IF #Age IS NOT NULL
SELECT #sql = #sql + CASE WHEN #has_where = 0 THEN 'WHERE ' ELSE 'AND ' END + 'Age = ' + CAST(#Age AS VARCHAR), #has_where = 1
EXEC sp_executesql #sql
END