эффективный способ реализации пейджинга


118

Следует ли мне использовать LINQ Skip()иTake() метод для разбиения на страницы или реализовать собственное разбиение на страницы с помощью SQL-запроса?

Что наиболее эффективно? Почему я должен предпочесть одно другому?

Я использую SQL Server 2008, ASP.NET MVC и LINQ.


Я думаю, это зависит от обстоятельств. Над каким приложением вы работаете? какая на нем будет нагрузка?
BuddyJoe

Взгляните также на этот ответ: stackoverflow.com/a/10639172/416996
zbek

Взгляните также на aspsnippets.com/Articles/…
Фрэнк Миат Чт

Ответы:


175

Пытаясь дать вам краткий ответ на ваши сомнения, если вы выполните skip(n).take(m)методы на linq (с SQL 2005/2008 в качестве сервера базы данных), ваш запрос будет использоватьSelect ROW_NUMBER() Over ... оператор с каким-то прямым разбиением на страницы в механизме SQL.

Приведу пример: у меня есть таблица db с именем, mtcityи я написал следующий запрос (работает также с linq to entity):

using (DataClasses1DataContext c = new DataClasses1DataContext())
{
    var query = (from MtCity2 c1 in c.MtCity2s
                select c1).Skip(3).Take(3);
    //Doing something with the query.
}

В результате будет получен следующий запрос:

SELECT [t1].[CodCity], 
    [t1].[CodCountry], 
    [t1].[CodRegion], 
    [t1].[Name],  
    [t1].[Code]
FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]) AS [ROW_NUMBER], 
        [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]
    FROM [dbo].[MtCity] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]

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

With CityEntities As 
(
    Select ROW_NUMBER() Over (Order By CodCity) As Row,
        CodCity //here is only accessed by the Index as CodCity is the primary
    From dbo.mtcity
)
Select [t0].[CodCity], 
        [t0].[CodCountry], 
        [t0].[CodRegion], 
        [t0].[Name],
        [t0].[Code]
From CityEntities c
Inner Join dbo.MtCity t0 on c.CodCity = t0.CodCity
Where c.Row Between @p0 + 1 AND @p0 + @p1
Order By c.Row Asc

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

Что лучше?

Если в вашей логике есть достаточно надежный рабочий процесс, реализация правильного способа SQL будет сложной. В этом случае решением будет LINQ.

Если вы можете понизить эту часть логики непосредственно до SQL (в хранимой процедуре), это будет еще лучше, потому что вы сможете реализовать второй запрос, который я вам показал (с использованием индексов), и позволить SQL генерировать и сохранять план выполнения для запрос (повышение производительности).


2
Хороший ответ - обычное табличное выражение - хороший способ разбиения на страницы.
Джаррод Диксон

Не могли бы вы проверить мой вопрос ( stackoverflow.com/questions/11100929/… )? Я сделал SP, который добавил в свой EDMX, и использовал его в запросе linq-to-entity.
Misi

2
+1, хороший ответ, я ценю, что вы объяснили преимущества производительности второго примера
Коэн

@Johan: Существует альтернатива, называемая методом поиска, которая значительно превосходит смещения для больших номеров страниц.
Лукас Эдер

50

Попробуйте использовать

FROM [TableX]
ORDER BY [FieldX]
OFFSET 500 ROWS
FETCH NEXT 100 ROWS ONLY

чтобы получить строки от 501 до 600 на сервере SQL, не загружая их в память. Обратите внимание , что этот синтаксис стал доступен с SQL Server 2012 только


Я считаю это неверным. Отображаемый SQL показывает строки от 502-601 (если вы не индексируете нулевой уровень?)
Smudge202,

Нет, строки с 501 до 600
Volkan Sen

12

Хотя LINQ-to-SQL будет генерировать OFFSETпредложение (возможно, эмулируемое с использованием, ROW_NUMBER() OVER() как упоминалось другими ), существует совершенно другой, гораздо более быстрый способ выполнить разбиение на страницы в SQL. Это часто называют «методом поиска», как описано в этом сообщении в блоге здесь .

SELECT TOP 10 first_name, last_name, score
FROM players
WHERE (score < @previousScore)
   OR (score = @previousScore AND player_id < @previousPlayerId)
ORDER BY score DESC, player_id DESC

Значения @previousScoreи @previousPlayerIdявляются соответствующими значениями последней записи с предыдущей страницы. Это позволяет вам перейти на «следующую» страницу. Если ORDER BYнаправление есть ASC, просто используйте >вместо него.

С помощью описанного выше метода вы не можете сразу перейти к странице 4, не загрузив сначала предыдущие 40 записей. Но часто вы все равно не хотите прыгать так далеко. Вместо этого вы получаете гораздо более быстрый запрос, который может извлекать данные за постоянное время, в зависимости от вашей индексации. Кроме того, ваши страницы остаются «стабильными» независимо от того, изменяются ли базовые данные (например, на странице 1, пока вы находитесь на странице 4).

Это лучший способ реализовать разбиение на страницы, например, при отложенной загрузке большего количества данных в веб-приложения.

Обратите внимание, что «метод поиска» также называется пейджингом набора клавиш .


5

LinqToSql автоматически конвертирует .Skip (N1) .Take (N2) в синтаксис TSQL за вас. Фактически, каждый «запрос», который вы делаете в Linq, на самом деле просто создает для вас SQL-запрос в фоновом режиме. Чтобы проверить это, просто запустите SQL Profiler во время работы вашего приложения.

Из того, что я читал, методика пропуска / приема очень хорошо сработала для меня и других.

Из любопытства, какой у вас тип запроса на самостоятельную подкачку, который, по вашему мнению, более эффективен, чем пропуск / дубль Linq?


4

Мы используем CTE, завернутый в Dynamic SQL (поскольку наше приложение требует динамической сортировки на стороне сервера данных) внутри хранимой процедуры. Если хотите, я могу привести простой пример.

У меня не было возможности взглянуть на T / SQL, который производит LINQ. Может кто-нибудь выложить образец?

Мы не используем LINQ или прямой доступ к таблицам, поскольку нам требуется дополнительный уровень безопасности (при условии, что динамический SQL несколько нарушает это).

Что-то вроде этого должно помочь. Вы можете добавить параметризованные значения для параметров и т. Д.

exec sp_executesql 'WITH MyCTE AS (
    SELECT TOP (10) ROW_NUMBER () OVER ' + @SortingColumn + ' as RowID, Col1, Col2
    FROM MyTable
    WHERE Col4 = ''Something''
)
SELECT *
FROM MyCTE
WHERE RowID BETWEEN 10 and 20'

2
@mrdenny - Один намек на примере вы условии , что : С у sp_executesqlВас есть возможность передавать параметры в безопасном режиме, например: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Безопасность в этом контексте означает, что она устойчива к SQL-инъекциям - вы можете передать все возможные значения внутри переменной @ValueForCol4- даже '--', и запрос все равно будет работать!
Мэтт

1
@mrdenny Привет, вместо объединения запроса мы используем что-то вроде этого: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel

Это может привести к ужасным планам выполнения SQL.
mrdenny

@mrdenny: для больших номеров страниц метод поиска может быть намного быстрее, чем ROW_NUMBER() OVER()эмуляция смещения. См. Также: 4guysfromrolla.com/webtech/042606-1.shtml
Лукас Эдер

2

В SQL Server 2008:

DECLARE @PAGE INTEGER = 2
DECLARE @TAKE INTEGER = 50

SELECT [t1].*
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[COLUMNORDER] DESC) AS [ROW_NUMBER], [t0].*
    FROM [dbo].[TABLA] AS [t0]
    WHERE ([t0].[COLUMNS_CONDITIONS] = 1)
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN ((@PAGE*@TAKE) - (@TAKE-1)) AND (@PAGE*@TAKE)
ORDER BY [t1].[ROW_NUMBER]

В t0 находятся все записи, в t1 - только те, которые соответствуют этой странице


2

Подход, который я предлагаю, - это самая быстрая разбивка на страницы, которую может достичь SQL-сервер. Я проверил это на 5 миллионах записей. Этот подход намного лучше, чем "СМЕЩЕНИЕ 10 СТРОК ВЫБРАТЬ СЛЕДУЮЩИЕ 10 СТРОК ТОЛЬКО", предоставляемый SQL Server.

-- The below given code computes the page numbers and the max row of previous page
-- Replace <<>> with the correct table data.
-- Eg. <<IdentityColumn of Table>> can be EmployeeId and <<Table>> will be dbo.Employees

DECLARE @PageNumber int=1; --1st/2nd/nth page. In stored proc take this as input param.
DECLARE @NoOfRecordsPerPage int=1000;

 DECLARE @PageDetails TABLE
       (
        <<IdentityColumn of Table>> int,
        rownum int,
        [PageNumber] int
       )           
       INSERT INTO @PageDetails values(0, 0, 0)
       ;WITH CTE AS
       (
       SELECT <<IdentityColumn of Table>>, ROW_NUMBER() OVER(ORDER BY <<IdentityColumn of Table>>) rownum FROM <<Table>>
       )
       Insert into @PageDetails 
       SELECT <<IdentityColumn of Table>>, CTE.rownum, ROW_NUMBER() OVER (ORDER BY rownum) as [PageNumber] FROM CTE WHERE CTE.rownum%@NoOfRecordsPerPage=0


--SELECT * FROM @PageDetails 

-- Actual pagination
SELECT TOP (@NoOfRecordsPerPage)
FROM <<Table>> AS <<Table>>
WHERE <<IdentityColumn of Table>> > (SELECT <<IdentityColumn of Table>> FROM 
@PageDetails WHERE PageNumber=@PageNumber)
ORDER BY <<Identity Column of Table>>

0

вы можете еще больше улучшить производительность, проверьте это

From CityEntities c
Inner Join dbo.MtCity t0 on c.CodCity = t0.CodCity
Where c.Row Between @p0 + 1 AND @p0 + @p1
Order By c.Row Asc

если вы будете использовать from таким образом, это даст лучший результат:

From   dbo.MtCity  t0
   Inner Join  CityEntities c on c.CodCity = t0.CodCity

причина: потому что вы используете класс where в таблице CityEntities, который удалит многие записи перед присоединением к MtCity, поэтому на 100% уверен, что это многократно увеличит производительность ...

В любом случае ответ Родригоэлпа действительно полезен.

Спасибо


Я сомневаюсь, что использование этого совета повлияет на производительность. Не могу найти ссылку на это, но внутренний порядок соединения в запросе может отличаться от фактического порядка соединения. Последнее определяется оптимизатором запросов с использованием статистики таблицы и оценок стоимости операции.
Imre Pühvel,

@ImreP: На самом деле это может несколько соответствовать методу поиска, который я описал . Хотя, я не уверен, откуда @p0и конкретнее @p1взялся
Лукас Эдер

0

Вы можете реализовать разбиение на страницы таким простым способом, передав PageIndex

Declare @PageIndex INT = 1
Declare  @PageSize INT = 20

Select ROW_NUMBER() OVER ( ORDER BY Products.Name ASC )  AS RowNumber,
    Products.ID,
    Products.Name
into #Result 
From Products

SELECT @RecordCount = COUNT(*) FROM #Results 

SELECT * 
FROM #Results
WHERE RowNumber
BETWEEN
    (@PageIndex -1) * @PageSize + 1 
    AND
    (((@PageIndex -1) * @PageSize + 1) + @PageSize) - 1

0

В 2008 году мы не можем использовать Skip (). Take ()

Путь такой:

var MinPageRank = (PageNumber - 1) * NumInPage + 1
var MaxPageRank = PageNumber * NumInPage

var visit = Visita.FromSql($"SELECT * FROM (SELECT [RANK] = ROW_NUMBER() OVER (ORDER BY Hora DESC),* FROM Visita WHERE ) A WHERE A.[RANK] BETWEEN {MinPageRank} AND {MaxPageRank}").ToList();
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.