Пейджинг, сортировка и фильтрация в хранимой процедуре (SQL Server) - PullRequest
7 голосов
/ 14 марта 2012

Я искал разные способы написания хранимой процедуры для возврата «страницы» данных.Это было для использования с ASP ObjectDataSource, но это может считаться более общей проблемой.

Требуется вернуть подмножество данных на основе обычных параметров пейджинга;startPageIndex и maximumRows, но также параметр 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.Не хорошо.

Одним из решений является добавление дополнительного столбца в окончательный список выбора (ВЫБЕРИТЕ СЧЕТЧИК (*) ИЗ материала) AS TotalRows.Это дает нам сумму, но повторяет ее для каждой строки в наборе результатов, что не идеально.

[Метод 2]
Здесь приводится интересная альтернатива (http://www.4guysfromrolla.com/articles/032206-1.aspx) с использованием динамического SQLОн считает, что производительность лучше, потому что оператор CASE в первом решении затягивает. Достаточно справедливо, и это решение позволяет легко получить totalRows и добавить его в выходной параметр. Но я ненавижу кодировать динамический SQL. Все это'bit of SQL' + STR (@ parm1) + 'bit more 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 и поместить его в выходной параметр.

Я провел несколько тестов и с довольно простой версией oВ запросе (без sortBy и без фильтра) метод 1, кажется, идет впереди (почти в два раза быстрее, чем другие 2).Затем я решил проверить, вероятно, мне нужна была сложность, и мне нужно было, чтобы SQL был в хранимых процедурах.При этом я получаю метод 1, который занимает почти вдвое больше времени, чем другие 2 метода.Что кажется странным.

Есть ли веская причина, почему я не должен отвергать CTE и придерживаться метода 3?


ОБНОВЛЕНИЕ - 15 марта 2012

Я пыталсяадаптируя метод 1 для выгрузки страницы из CTE во временную таблицу, чтобы я мог извлечь TotalRows, а затем выбрать только соответствующие столбцы для набора результатов.Это, казалось, значительно прибавило времени (больше, чем я ожидал).Я должен добавить, что я запускаю это на ноутбуке с SQL Server Express 2008 (все, что у меня есть), но сравнение все равно должно быть верным.

Я снова посмотрел на метод динамического SQL.Оказывается, я на самом деле не делал это правильно (просто объединял строки).Я установил его как в документации для sp_executesql (со строкой описания параметра и списком параметров), и он стал намного более читаемым.Также этот метод работает быстрее всего в моей среде.Почему это должно все еще сбивать с толку меня, но я думаю, что ответ намекает на комментарий Хогана.

Ответы [ 4 ]

6 голосов
/ 14 марта 2012

Скорее всего, я бы разделил аргумент @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
…
2 голосов
/ 02 апреля 2015

Я бы определенно хотел сделать комбинацию временной таблицы и NTILE для такого подхода.

Временная таблица позволит вам выполнить сложный ряд условий только один раз.Поскольку вы храните только те фрагменты, которые вам интересны, это также означает, что когда вы начнете делать выборки против него в дальнейшем в этой процедуре, общий объем используемой памяти будет меньше, чем если бы вы выполняли условие несколько раз.

Мне нравится NTILE() за это лучше, чем ROW_NUMBER(), потому что он выполняет работу, которую вы пытаетесь выполнить для вас, вместо того, чтобы иметь дополнительные условия where, о которых нужно беспокоиться.

Пример ниже одинна основе аналогичного запроса, который я использую как часть исследовательского запроса;У меня есть удостоверение личности, которое я могу использовать и которое, я знаю, будет уникальным в результатах.Использование идентификатора, который был столбцом идентификаторов, также было бы уместным здесь.,(Ищите временную таблицу по сравнению с табличной переменной, и вы найдете множество примеров того, почему)

Динамический SQL был бы наиболее полезен для обработки метода сортировки.Используя мой пример, вы можете выполнить основной запрос в динамическом SQL и использовать только метод сортировки, который вы хотите использовать, в OVER().

В приведенном выше примере также вычисляется сумма в каждой строке возвращаемого набора, что, как вы упомянули, не было идеальным.Вместо этого вы можете иметь в своей процедуре выходную переменную @totalrows и получить ее, а также набор результатов.Это сэкономит вам CROSS APPLY, что я делаю выше в CTE подкачки.

0 голосов
/ 03 мая 2015

Я использую этот метод использования EXEC():

-- SP parameters:
-- @query: Your query as an input parameter
-- @maximumRows: As number of rows per page
-- @startPageIndex: As number of page to filter
-- @sortBy: As a field name or field names with supporting DESC keyword
DECLARE @query nvarchar(max) = 'SELECT * FROM sys.Objects', 
        @maximumRows int = 8,
        @startPageIndex int = 3,
        @sortBy as nvarchar(100) = 'name Desc'

SET @query = ';WITH CTE AS (' + @query + ')' +
            'SELECT *, (dt.pagingRowNo - 1) / ' + CAST(@maximumRows as nvarchar(10)) + ' + 1 As pagingPageNo' +
                ', pagingCountRow / ' + CAST(@maximumRows as nvarchar(10)) + ' As pagingCountPage ' +
                ', (dt.pagingRowNo - 1) % ' + CAST(@maximumRows as nvarchar(10)) + ' + 1 As pagingRowInPage ' +
            'FROM (  SELECT *, ROW_NUMBER() OVER (ORDER BY ' + @sortBy + ') As pagingRowNo, COUNT(*) OVER () AS pagingCountRow ' +
                    'FROM CTE) dt ' +
            'WHERE (dt.pagingRowNo - 1) / ' + CAST(@maximumRows as nvarchar(10)) + ' + 1 = ' + CAST(@startPageIndex as nvarchar(10))

EXEC(@query)

При наборе результатов после столбцов результатов запроса:

Примечание:
Я добавил несколько дополнительных столбцов, которые вы можете удалить их:

    pagingRowNo : The row number
 pagingCountRow : The total number of rows
   pagingPageNo : The current page number 
pagingCountPage : The total number of pages
pagingRowInPage : The row number that started with 1 in this page
0 голосов
/ 14 марта 2012

Я бы создал одну процедуру для постановки, сортировки и разбивки на страницы (используя 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...