存储过程中的分页,排序和过滤(SQL Server)

时间:2012-03-13 21:38:45

标签: sql-server tsql stored-procedures

我正在寻找编写存储过程的不同方法来返回数据的“页面”。这适用于ASP ObjectDataSource,但它可以被认为是一个更普遍的问题。

要求是根据通常的寻呼参数返回数据的子集; startPageIndexmaximumRows,还有一个sortBy参数,可以对数据进行排序。还传递了一些参数来过滤各种条件下的数据。

这样做的一种常见方式似乎是这样的:

[方法1]

;WITH stuff AS (
    SELECT 
        CASE 
            WHEN @SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name)
            WHEN @SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC)
            WHEN @SortBy = ... 
            ELSE ROW_NUMBER() OVER (ORDER BY whatever)
        END AS Row,
        ., 
        ., 
        .,
    FROM Table1
    INNER JOIN Table2 ...
    LEFT JOIN Table3 ...
    WHERE ... (lots of things to check)
    ) 
SELECT *
FROM stuff 
WHERE (Row > @startRowIndex)
AND   (Row <= @startRowIndex + @maximumRows OR @maximumRows <= 0)
ORDER BY Row

这样做的一个问题是它没有给出总计数,通常我们需要另一个存储过程。第二个存储过程必须复制参数列表和复杂的WHERE子句。不太好。

一种解决方案是在最终选择列表中添加一个额外的列,(SELECT COUNT(*)FROM stuff)AS TotalRows。这给了我们总数,但是对于结果集中的每一行重复它,这是不理想的。

[方法2]
这里给出了一个有趣的替代方法(http://www.4guysfromrolla.com/articles/032206-1.aspx),使用动态SQL。他认为性能更好,因为第一个解决方案中的CASE语句拖延了事情。足够公平,这个解决方案可以很容易地获得totalRows并将其打成输出参数。但我讨厌编码动态SQL。所有这些'SQL'+ STR(@ parm1)+'更多SQL'gubbins。

[方法3]
我可以找到得到我想要的唯一方法,不重复必须同步的代码,并保持合理的可读性,这是回到使用表变量的“旧方式”:

DECLARE @stuff TABLE (Row INT, ...)

INSERT INTO @stuff
SELECT 
    CASE 
        WHEN @SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name)
        WHEN @SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC)
        WHEN @SortBy = ... 
        ELSE ROW_NUMBER() OVER (ORDER BY whatever)
    END AS Row,
    ., 
    ., 
    .,
FROM Table1
INNER JOIN Table2 ...
LEFT JOIN Table3 ...
WHERE ... (lots of things to check)

SELECT *
FROM stuff 
WHERE (Row > @startRowIndex)
AND   (Row <= @startRowIndex + @maximumRows OR @maximumRows <= 0)
ORDER BY Row

(或者在表变量上使用IDENTITY列的类似方法)。 在这里,我可以在表变量上添加SELECT COUNT以获取totalRows并将其放入输出参数。

我做了一些测试并且使用相当简单的查询版本(没有sortBy和没有过滤器),方法1似乎排在最前面(几乎是其他2的两倍)。然后我决定测试可能我需要复杂性,我需要SQL存储过程。有了这个,我得到方法1的时间几乎是其他两种方法的两倍。这看起来很奇怪。

为什么我不应该摒弃CTE并坚持使用方法3?


更新 - 2012年3月15日

我尝试调整方法1将页面从CTE转储到临时表中,以便我可以提取TotalRows,然后只选择结果集的相关列。这似乎显着增加了时间(超出我的预期)。我应该补充一点,我在使用SQL Server Express 2008的笔记本电脑上运行它(我只提供所有这些)但仍然比较有效。

我再次关注动态SQL方法。事实证明我并没有真正做到这一点(只是将字符串连接在一起)。我在sp_executesql的文档中设置了它(带有参数描述字符串和参数列表),并且它更具可读性。此方法在我的环境中运行速度最快。为什么这仍然让我感到困惑,但我想在Hogan的评论中暗示了答案。

3 个答案:

答案 0 :(得分:5)

我很可能将@SortBy参数拆分为两个,@SortColumn@SortDirection,然后像这样使用它们:

…
ROW_NUMBER() OVER (
  ORDER BY CASE @SortColumn
    WHEN 'Name'      THEN Name
    WHEN 'OtherName' THEN OtherName
    …
  END *
  CASE @SortDirection
    WHEN 'DESC' THEN -1
    ELSE 1
  END
) AS Row
…

这就是如何定义TotalRows列(在主要选择中):

…
COUNT(*) OVER () AS TotalRows
…

答案 1 :(得分:2)

对于这种方法,我肯定想要一个临时表和NTILE的组合。

临时表将允许您只执行一系列复杂的条件。因为您只存储您关心的部分,这也意味着当您开始在程序中进一步选择它时,它应该具有比您多次运行条件时更小的总内存使用量。

我比NTILE()更喜欢ROW_NUMBER(),因为它正在为您完成您正在尝试完成的工作,而不是拥有额外的where条件担心。

以下示例是基于类似查询的示例,我将其用作研究查询的一部分;我有一个我可以使用的ID,我知道它在结果中是独一无二的。但是,使用身份列的ID也是合适的。

--DECLARES here would be stored procedure parameters
declare @pagesize int, @sortby varchar(25), @page int = 1;

--Create temp with all relevant columns; ID here could be an identity PK to help with paging query below
create table #temp (id int not null primary key clustered, status varchar(50), lastname varchar(100), startdate datetime);

--Insert into #temp based off of your complex conditions, but with no attempt at paging
insert into #temp
(id, status, lastname, startdate)
select id, status, lastname, startdate
from Table1 ...etc.
where ...complicated conditions


SET @pagesize = 50;
SET @page = 5;--OR CAST(@startRowIndex/@pagesize as int)+1
SET @sortby = 'name';

--Only use the id and count to use NTILE
;with paging(id, pagenum, totalrows) as 
(
    select id,
    NTILE((SELECT COUNT(*) cnt FROM #temp)/@pagesize) OVER(ORDER BY CASE WHEN @sortby = 'NAME' THEN lastname ELSE convert(varchar(10), startdate, 112) END),
    cnt
    FROM #temp
    cross apply (SELECT COUNT(*) cnt FROM #temp) total
)
--Use the id to join back to main select
SELECT *
FROM paging
JOIN #temp ON paging.id = #temp.id
WHERE paging.pagenum = @page

--Don't need the drop in the procedure, included here for rerunnability
drop table #temp;

在这种情况下,我通常更喜欢临时表而不是表变量,这主要是因为您对结果集有明确的统计信息。 (搜索临时表和表变量,你会找到很多关于原因的例子)

动态SQL对于处理排序方法最有用。使用我的示例,您可以在动态SQL中执行主查询,并仅将要拉入的排序方法拉入OVER()

上面的示例也会在返回集的每一行中执行总计,如您所提到的那样不理想。相反,您可以在过程中使用@totalrows输出变量,并将其与结果集一起拉出。这样可以节省我在分页CTE中执行的CROSS APPLY

答案 2 :(得分:0)

我会创建一个过程来对一个临时表进行分级,排序和分页(使用NTILE());以及按页面检索的第二个过程。这样您就不必为每个页面运行整个主查询。

此示例查询AdventureWorks.HumanResources.Employee:

--------------------------------------------------------------------------
create procedure dbo.EmployeesByMartialStatus
@MaritalStatus nchar(1)
, @sort varchar(20)
as

-- Init staging table
if exists(
    select 1 from sys.objects o
    inner join sys.schemas s on s.schema_id=o.schema_id
    and s.name='Staging'
    and o.name='EmployeesByMartialStatus'
    where type='U'
)
drop table Staging.EmployeesByMartialStatus;

-- Populate staging table with sort value
with s as (
    select *
    , sr=ROW_NUMBER()over(order by case @sort
        when 'NationalIDNumber' then NationalIDNumber
        when 'ManagerID' then ManagerID
        -- plus any other sort conditions
        else EmployeeID end)
    from AdventureWorks.HumanResources.Employee
    where MaritalStatus=@MaritalStatus
)
select *
into #temp
from s;

-- And now pages
declare @RowCount int; select @rowCount=COUNT(*) from #temp;
declare @PageCount int=ceiling(@rowCount/20); --assuming 20 lines/page
select *
, Page=NTILE(@PageCount)over(order by sr)
into Staging.EmployeesByMartialStatus
from #temp;
go

--------------------------------------------------------------------------
-- procedure to retrieve selected pages
create procedure EmployeesByMartialStatus_GetPage
@page int
as
declare @MaxPage int;
select @MaxPage=MAX(Page) from Staging.EmployeesByMartialStatus;
set @page=case when @page not between 1 and @MaxPage then 1 else @page end;

select EmployeeID,NationalIDNumber,ContactID,LoginID,ManagerID
, Title,BirthDate,MaritalStatus,Gender,HireDate,SalariedFlag,VacationHours,SickLeaveHours
, CurrentFlag,rowguid,ModifiedDate
from Staging.EmployeesByMartialStatus
where Page=@page
GO

--------------------------------------------------------------------------
-- Usage

-- Load staging
exec dbo.EmployeesByMartialStatus 'M','NationalIDNumber';

-- Get pages 1 through n    
exec dbo.EmployeesByMartialStatus_GetPage 1;
exec dbo.EmployeesByMartialStatus_GetPage 2;
-- ...etc (this would actually be a foreach loop, but that detail is omitted for brevity)

GO