如何创建可以选择搜索列的存储过程?
Posted
技术标签:
【中文标题】如何创建可以选择搜索列的存储过程?【英文标题】:How do I create a stored procedure that will optionally search columns? 【发布时间】:2010-09-17 08:36:30 【问题描述】:我正在开发一个要查询我们员工数据库的工作应用程序。最终用户希望能够根据标准姓名/部门标准进行搜索,但他们还希望能够灵活地查询在卫生部门工作的所有名字为“James”的人。我要避免的一件事是简单地让存储过程获取参数列表并生成要执行的 SQL 语句,因为这将为内部级别的 SQL 注入打开大门。
这个可以吗?
【问题讨论】:
我想在这里提一下,Cade Roux 的解决方案对我来说效果最好,因为我在目标表中有很多 NULL 数据值,但我可以看到如果我有数据,COALESCE 的工作效果会如何在我的专栏中,因此对 BoltBait 解决方案的支持绝对值得。 Aaron Bertrand 将其称为“厨房水槽程序”,并且对处理此类问题有一些很好的想法,可以在 sqlsentry.tv/the-kitchen-sink-procedure 和 blogs.sqlsentry.com/aaronbertrand/… 看到。 【参考方案1】:虽然COALESCE
技巧很巧妙,但我更喜欢的方法是:
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 + '%')
这些类型的 SP 可以很容易地由代码生成(并为表更改重新生成)。
您有几个处理数字的选项 - 取决于您想要精确语义还是搜索语义。
【讨论】:
这对我的部门 ID 字段有什么作用?我可以使用 '%' 作为 int 还是指定不同的语法? 您有几个处理数字的选项 - 取决于您想要精确语义还是搜索语义。 伙计,这正是我想要的。谢谢!【参考方案2】:实现此类搜索的最有效方法是使用存储过程。此处显示的语句创建一个接受所需参数的过程。如果未提供参数值,则将其设置为 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)
取自此页面:http://www.sqlteam.com/article/implementing-a-dynamic-where-clause
我以前做过。效果很好。
【讨论】:
我认为这些都不是很好用。你会得到很大比例的表扫描,因为这些谓词不是 SARGable。 当你说它的值设置为 NULL 时,你的意思是在列名中搜索 NULL 还是被忽略。我看到的唯一问题是,如果我正在搜索姓氏为 Schmoe 的人,那么名字为“Joe”的人将被排除在外,因为该值不为 NULL。 Dillie-O,查看 COALESCE 命令以了解其工作原理(或点击我帖子中提供的链接)。至于这类东西的性能......我实现它的系统有 1 到 200 万行,并且运行良好。它看起来一点也不慢。 YMMV。【参考方案3】:Erland Sommarskog 的文章Dynamic Search Conditions in T-SQL 是有关如何执行此操作的一个很好的参考。 Erland 提出了一些关于如何在不使用动态 SQL 的情况下执行此操作的策略(只是简单的 IF 块、OR、COALESCE 等),甚至列出了每种技术的性能特征。
如果你不得不硬着头皮走动态 SQL 路径,你还应该阅读 Erland 的 Curse and Blessings of Dynamic SQL,他在其中给出了一些关于如何正确编写动态 SQL 的提示
【讨论】:
【参考方案4】:可以做到,但通常这些厨房水槽程序会导致一些糟糕的查询计划。
说了这么多,这里是最常用于“可选”参数的策略。正常的做法是将 NULL 视为“省略”。
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)
编辑: 更好的方法是参数化查询。 以下是该领域世界上最重要的权威之一,来自 LLBLGen Pro 的 Frans Bouma 的博客文章:
Stored Procedures vs. Dynamic Queries
【讨论】:
在前面的回答中,你说这些都不好用,你认为我只是硬着头皮,做大量的输入清理并创建一个 AdHoc 查询,或者创建更专业的程序所有不同的选择? 嗯,这就是 ORM(对象关系模型)系统真正流行起来的原因之一。它们中的大多数使用动态查询生成,但它们使用参数化,这样您就不会遇到注入问题。查看参数化查询。在这种情况下,这可能会更好地为您服务。【参考方案5】:使用 COALESCE 方法有一个问题,如果你的列有一个 NULL 值,传入一个 NULL 搜索条件(意味着忽略搜索条件)将不会返回许多数据库中的行。
例如,在 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
您只会返回两行,因为在 my_string 列为 NULL 的行中,您可以有效地获取:
my_string = COALESCE(@my_string, my_string) =>
my_string = COALESCE(NULL, my_string) =>
my_string = my_string =>
NULL = NULL
当然,NULL 不等于 NULL。
我尽量坚持:
SELECT
my_id,
my_string
FROM
dbo.Test_Coalesce
WHERE
(@my_string IS NULL OR my_string = @my_string)
当然,您可以调整它以使用通配符或其他任何您想做的事情。
【讨论】:
【参考方案6】:从我的博文中复制此内容:
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
【讨论】:
【参考方案7】:我们可以使用通用@Search 参数并将任何值传递给它进行搜索。
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
【讨论】:
【参考方案8】:我的第一个想法是写一个这样的查询......
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
)
...如果他们想要搜索特定字段,然后调用者会提供一个位标志,然后如果他们要搜索它则提供值,但我不知道这是否会造成草率 WHERE子句,或者我是否可以在 WHERE 子句中使用 CASE 语句。
正如您所见,这个特定的代码在 T-SQL 中,但我也很乐意查看一些 PL-SQL / mysql 代码并相应地进行调整。
【讨论】:
我只是想知道这个收到反对票的查询有什么问题? @Teomanshipahi 我的查询的问题是它需要两个变量,一个是搜索字段,然后是参数本身,这会导致一个非常大的存储过程变量绑定。通过使用合并,您可以为要查询的每个字段使用单个变量。我想我应该删除我自己的答案,考虑到这个线程有多老 8^D【参考方案9】:我会坚持使用 NULL/COALESCE 方法而不是 AdHoc 查询,然后进行测试以确保您没有性能问题。
如果事实证明您的查询运行速度很慢,因为在您搜索索引列时它正在执行表扫描,那么您始终可以使用其他特定存储过程来补充通用搜索存储过程,以允许在这些索引字段上进行搜索.例如,您可以有一个特殊的 SP,它按 CustomerID 或姓/名进行搜索。
【讨论】:
我使用过搜索屏幕有 30 个字段的 CRM 应用程序。排列在那里对你不利。 ORM 在这个领域大放异彩。 我并不是建议您为每个排列创建不同的 SP。 (那将是 2^30 个存储过程)。如果用户通过 CustomerID 搜索(我认为这是唯一的),您是否同意使用特殊情况 SP 是有意义的? 再一次,如果他发现如果存在 CustomerID,COALESCE 方法会产生次优查询计划,我只会建议这样做【参考方案10】:编写一个程序,将所有名称以A开头的员工数据插入表中??
【讨论】:
以上是关于如何创建可以选择搜索列的存储过程?的主要内容,如果未能解决你的问题,请参考以下文章