ефективний спосіб впровадження пейджингу


118

Чи варто використовувати LINQ Skip()та Take()метод для підкачки, або реалізувати власний підказок із запитом SQL?

Що найефективніше? Чому я обрав би один над іншим?

Я використовую SQL Server 2008, ASP.NET MVC та LINQ.


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

Подивіться на цей відповідь , а також: stackoverflow.com/a/10639172/416996
Özbek

Погляньте на це також aspsnippets.com/Articles/…
Frank Myat Чт

Відповіді:


175

Намагаючись дати вам коротку відповідь на свої сумніви, якщо ви будете виконувати skip(n).take(m)методи на linq (з SQL 2005/2008 як сервером бази даних), ваш запит буде використовувати Select ROW_NUMBER() Over ...оператор, з якимось чином прямим пейджингом у двигуні SQL.

Надаючи вам приклад, у мене називається таблиця db, mtcityі я написав наступний запит (також працюйте з linq для сутностей):

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]

Що є віконним доступом до даних (досить круто, btw 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, оскільки він буде використовувати виключно індекс для створення вікна доступу до даних; це означає, що якщо вам потрібна деяка фільтрація, фільтрування має бути (або повинно бути) у списку Entity (де створений рядок), а також слід створити деякі індекси, щоб підтримувати хороші показники.

Тепер, що краще?

Якщо у вашій логіці є досить міцний робочий процес, реалізація правильного способу SQL буде складним. У цьому випадку рішенням буде LINQ.

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


2
Хороша відповідь - загальне вираження таблиці - це хороший спосіб здійснення пейджингу.
Jarrod Dixon

Ви можете перевірити моє запитання ( stackoverflow.com/questions/11100929/… )? Я зробив SP, який додав до свого EDMX і використав його у запиті на зв’язок між особами.
Місі

2
+1, хороша відповідь, я вдячний вам пояснити переваги ефективності другого прикладу
Cohen

@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-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 мільйонах записів. Цей підхід набагато кращий, ніж "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY", наданий 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   dbo.MtCity  t0
   Inner Join  CityEntities c on c.CodCity = t0.CodCity

Причина: тому що ви використовуєте клас у таблиці CityEntities, який усуне багато записів перед приєднанням до MtCity, тому на 100% впевнений, що це збільшить продуктивність у багато разів ...

У будь-якому випадку відповідь rodrigoelp справді корисна.

Дякую


Я сумніваюся, що за допомогою цієї поради буде впливати будь-яка ефективність. Неможливо знайти посилання на це, але внутрішній порядок приєднання в запиті може відрізнятися від фактичного замовлення на приєднання. Останнє визначається оптимізатором запитів, використовуючи статистичні дані таблиці та оцінки витрат на експлуатацію.
Імре Пювель

@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.