Sql 如何创建一个可以选择性地搜索列的存储过程?
我正在开发一个工作应用程序,它将查询我们的员工数据库。最终用户希望能够根据标准名称/部门标准进行搜索,但他们也希望能够灵活地查询在卫生部门工作的名为“James”的所有人员。我要避免的一件事是让存储过程获取参数列表并生成要执行的SQL语句,因为这将为内部级别的SQL注入打开大门Sql 如何创建一个可以选择性地搜索列的存储过程?,sql,sql-server,search,stored-procedures,parameters,Sql,Sql Server,Search,Stored Procedures,Parameters,我正在开发一个工作应用程序,它将查询我们的员工数据库。最终用户希望能够根据标准名称/部门标准进行搜索,但他们也希望能够灵活地查询在卫生部门工作的名为“James”的所有人员。我要避免的一件事是让存储过程获取参数列表并生成要执行的SQL语句,因为这将为内部级别的SQL注入打开大门 这可以做到吗?我的第一个想法是写一个类似这样的查询 SELECT EmpId, NameLast, NameMiddle, NameFirst, DepartmentName FROM dbo.Employee
这可以做到吗?我的第一个想法是写一个类似这样的查询
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代码,并进行相应的调整。实现这种搜索的最有效方法是使用存储过程。此处显示的语句创建一个接受所需参数的过程。如果未提供参数值,则将其设置为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)
摘自本页:
我以前做过。它工作得很好。这是可以做到的,但通常这些厨房水槽程序会导致一些糟糕的查询计划 话虽如此,以下是最常用于“可选”参数的策略。通常的方法是将NULL视为“ommitted” 编辑: 更好的方法是参数化查询。 以下是一篇博文,作者是该领域世界最重要的权威机构之一,来自LLBLGen Pro fame的Frans Bouma:
Erland Sommarskog的文章是如何做到这一点的很好参考。Erland提供了许多策略,介绍了如何在不使用动态SQL的情况下实现这一点(只是简单的IF块、OR、COALESCE等),甚至列出了每种技术的性能特征
如果您必须咬紧牙关,通过动态SQL路径,您还应该阅读Erland的书,他在书中给出了一些关于如何正确编写动态SQL的技巧虽然
合并技巧很巧妙,但我更喜欢的方法是:
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可以很容易地生成代码(并为表更改而重新生成)
您有一些处理数字的选项—这取决于您想要的是精确语义还是搜索语义。我会在临时查询上使用NULL/COALESCE方法,然后进行测试以确保没有性能问题
如果在搜索索引列时,查询运行缓慢,因为它正在执行表扫描,则始终可以使用允许搜索这些索引字段的其他特定存储过程来补充通用搜索存储过程。例如,您可能有一个特殊的SP,它可以按CustomerID或姓/名进行搜索。使用COALESCE方法存在一个问题,即如果列具有空值,则传入空搜索条件(意味着忽略搜索条件)将不会在许多数据库中返回该行
例如,请在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
您将只返回两行,因为在“我的字符串”列为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)
当然,你可以调整它以使用通配符或任何你想做的事情。从我的博客文章中复制以下内容:
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
编写一个过程来插入所有员工数据,这些数据的名称以表中的一个开头???我们可以使用Generic@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
我认为这些根本不管用。因为这些谓词不可搜索,所以表扫描的比例很大。当您说值设置为NULL时,是指在列名中搜索NULL还是忽略它。我看到的唯一问题是,如果我正在搜索姓氏为Schmoe的人,那么姓氏为“Joe”的人将被排除在外,因为该值不为NULL。Dillie-O,请查看COALESCE命令以了解其工作原理(或遵循我的帖子中提供的链接)。至于这类事情的表现。。。我在上面实现的系统有100万到200万行,并且运行良好。看起来一点也不慢。YMMV.在前面的回答中,你说这些没有那么好用,你认为我只是咬紧牙关,做大量的输入清理并创建一个临时查询,还是为所有不同的选项创建更专门的过程?这就是ORM(对象关系模型)系统非常流行的原因之一。它们中的大多数使用动态查询生成,但它们使用参数化,因此不会出现注入问题。看看参数化查询。在这种情况下,这可能会更好地为您服务。对于我的部门Id字段,这将如何工作?我可以使用“%”表示int还是指定不同的语法?您有几个处理数字的选项,这取决于您想要的是精确语义还是搜索语义。伙计,这正是我想要的。谢谢我想在这里提到的是,Cade Roux的解决方案对我最有效,因为我在目标表中有很多空数据值,但我可以看到,如果我在所有列中都有数据,那么合并会有多好,因此BoltBait的解决方案的向上投票肯定是值得的。Aaron Bertrand称之为“厨房-水槽过程”对于处理这类问题,我有一些很好的想法,这些想法可以在和中看到。我曾使用过CRM应用程序,其中搜索屏幕有30个字段。在那里,排列对你不利。ORM在这方面很有影响力。我不是建议你为每个排列创建不同的SP。(这将是2^30个存储过程)。你不同意吗
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