cách hiệu quả để triển khai phân trang


118

Tôi có nên sử dụng phương thức Skip()và của LINQ Take()để phân trang hoặc triển khai phân trang của riêng tôi với truy vấn SQL không?

Cái nào hiệu quả nhất? Tại sao tôi lại chọn cái này hơn cái kia?

Tôi đang sử dụng SQL Server 2008, ASP.NET MVC và LINQ.


Tôi nghĩ rằng nó phụ thuộc. Bạn đang làm việc trên ứng dụng nào? nó sẽ có loại tải nào?
BuddyJoe 14/02/09

Hãy xem cả câu trả lời này: stackoverflow.com/a/10639172/416996
Õzbek

Câu trả lời:


175

Cố gắng cung cấp cho bạn câu trả lời ngắn gọn cho sự nghi ngờ của bạn, nếu bạn thực thi các skip(n).take(m)phương thức trên linq (với SQL 2005/2008 làm máy chủ cơ sở dữ liệu) truy vấn của bạn sẽ sử dụng Select ROW_NUMBER() Over ...câu lệnh, bằng cách nào đó là phân trang trực tiếp trong SQL engine.

Cung cấp cho bạn một ví dụ, tôi có một bảng db được gọi mtcityvà tôi đã viết truy vấn sau (hoạt động tốt với linq cho các thực thể):

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

Truy vấn kết quả sẽ là:

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]

Đây là quyền truy cập dữ liệu dạng cửa sổ (khá tuyệt, btw cuz sẽ trả về dữ liệu kể từ đầu và sẽ truy cập bảng miễn là đáp ứng các điều kiện). Điều này sẽ rất giống với:

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

Với ngoại lệ, truy vấn thứ hai này sẽ được thực thi nhanh hơn kết quả linq vì nó sẽ sử dụng riêng chỉ mục để tạo cửa sổ truy cập dữ liệu; điều này có nghĩa là, nếu bạn cần một số bộ lọc, bộ lọc phải (hoặc phải có) trong danh sách Thực thể (nơi hàng được tạo) và một số chỉ mục cũng nên được tạo để duy trì hiệu suất tốt.

Bây giờ, có gì tốt hơn?

Nếu bạn có quy trình làm việc khá chắc chắn trong logic của mình, thì việc triển khai theo cách SQL thích hợp sẽ rất phức tạp. Trong trường hợp đó LINQ sẽ là giải pháp.

Nếu bạn có thể hạ thấp phần logic đó trực tiếp xuống SQL (trong một thủ tục được lưu trữ), nó sẽ thậm chí còn tốt hơn vì bạn có thể triển khai truy vấn thứ hai mà tôi đã chỉ cho bạn (sử dụng các chỉ mục) và cho phép SQL tạo và lưu trữ Kế hoạch thực thi của truy vấn (cải thiện hiệu suất).


2
Câu trả lời hay - biểu thức bảng thông thường là một cách tốt để phân trang.
Jarrod Dixon

Bạn có thể kiểm tra câu hỏi của tôi không ( stackoverflow.com/questions/11100929/… )? Tôi đã tạo SP mà tôi đã thêm vào EDMX của mình và sử dụng nó trong truy vấn linq-to-entity.
Misi

2
+1, câu trả lời tốt, tôi đánh giá cao bạn giải thích những lợi ích thực hiện các ví dụ thứ hai
Cohen

@Johan: Có một phương pháp thay thế được gọi là phương pháp tìm kiếm có hiệu quả vượt trội hơn nhiều so với hiệu số đối với số lượng trang lớn.
Lukas Eder

50

Hãy thử sử dụng

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

để lấy các hàng từ 501 đến 600 trong máy chủ SQL mà không cần tải chúng vào bộ nhớ. Lưu ý rằng cú pháp này đã trở thành có sẵn với SQL Server 2012 chỉ


Tôi nghĩ rằng điều này là không chính xác. SQL hiển thị các chương trình hàng 502-601 (trừ khi bạn zero-indexing?)
Smudge202

Không, nó nhận các hàng từ 501 đến 600
Volkan Sen

12

Mặc dù LINQ-to-SQL sẽ tạo ra một OFFSETmệnh đề (có thể được mô phỏng bằng cách sử dụng ROW_NUMBER() OVER() như những người khác đã đề cập ), có một cách hoàn toàn khác, nhanh hơn nhiều để thực hiện phân trang trong SQL. Đây thường được gọi là "phương pháp tìm kiếm" như được mô tả trong bài đăng trên blog này ở đây .

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

Giá trị @previousScore@previousPlayerIdgiá trị là các giá trị tương ứng của bản ghi cuối cùng từ trang trước. Điều này cho phép bạn tìm nạp trang "tiếp theo". Nếu ORDER BYhướng là ASC, chỉ cần sử dụng >thay thế.

Với phương pháp trên, bạn không thể ngay lập tức chuyển đến trang 4 mà không tìm nạp 40 bản ghi trước đó. Nhưng thường thì bạn cũng không muốn nhảy xa đến vậy. Thay vào đó, bạn nhận được một truy vấn nhanh hơn nhiều có thể tìm nạp dữ liệu trong thời gian không đổi, tùy thuộc vào việc lập chỉ mục của bạn. Ngoài ra, các trang của bạn vẫn "ổn định", cho dù dữ liệu cơ bản có thay đổi hay không (ví dụ: trên trang 1, trong khi bạn đang ở trang 4).

Đây là cách tốt nhất để thực hiện phân trang khi lười tải thêm dữ liệu trong các ứng dụng web.

Lưu ý, "phương thức tìm kiếm" còn được gọi là phân trang bộ phím .


5

LinqToSql sẽ tự động chuyển đổi a .Skip (N1) .Take (N2) thành cú pháp TSQL cho bạn. Trên thực tế, mọi "truy vấn" bạn thực hiện trong Linq, thực chất chỉ là tạo một truy vấn SQL cho bạn trong nền. Để kiểm tra điều này, chỉ cần chạy SQL Profiler trong khi ứng dụng của bạn đang chạy.

Phương pháp bỏ qua / bỏ qua đã hoạt động rất tốt đối với tôi và những người khác so với những gì tôi đọc được.

Vì tò mò, bạn có loại truy vấn tự phân trang nào mà bạn tin rằng hiệu quả hơn cách bỏ qua / lấy của Linq?


4

Chúng tôi sử dụng CTE được bao bọc trong SQL động (vì ứng dụng của chúng tôi yêu cầu sắp xếp động của phía máy chủ dữ liệu) trong một quy trình được lưu trữ. Tôi có thể cung cấp một ví dụ cơ bản nếu bạn muốn.

Tôi chưa có cơ hội xem xét T / SQL mà LINQ tạo ra. Ai đó có thể đăng một mẫu?

Chúng tôi không sử dụng LINQ hoặc quyền truy cập thẳng vào các bảng vì chúng tôi yêu cầu lớp bảo mật bổ sung (được cấp cho SQL động phá vỡ điều này phần nào).

Một cái gì đó như thế này nên làm thủ thuật. Bạn có thể thêm các giá trị được tham số hóa cho các tham số, v.v.

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 - Một gợi ý cho ví dụ bạn đã cung cấp: Với sp_executesqlbạn có khả năng để vượt qua các thông số một cách an toàn, ví dụ như: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Bảo mật trong ngữ cảnh này có nghĩa là nó chống lại SQL injection mạnh mẽ - bạn có thể chuyển mọi giá trị có thể vào bên trong biến @ValueForCol4- thậm chí '--'và truy vấn sẽ vẫn hoạt động!
Matt,

1
@mrdenny Hi, thay vì concatenating truy vấn chúng tôi sử dụng một cái gì đó như thế này: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel

Điều đó có thể tạo ra một số kế hoạch thực thi SQL khủng khiếp.
mrdenny

@mrdenny: Đối với số lượng trang lớn, phương pháp tìm kiếm có thể nhanh hơn nhiều so với ROW_NUMBER() OVER()mô phỏng bù đắp. Xem thêm: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder

2

Trong 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]

Trong t0 là tất cả các bản ghi Trong t1 chỉ là những bản ghi tương ứng với trang đó


2

Cách tiếp cận mà tôi đang đưa ra là phân trang nhanh nhất mà máy chủ SQL có thể đạt được. Tôi đã thử nghiệm điều này trên 5 triệu bản ghi. Cách tiếp cận này tốt hơn nhiều so với "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY" do SQL Server cung cấp.

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

bạn có thể cải thiện hơn nữa hiệu suất, chech này

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

nếu bạn sử dụng từ theo cách này, nó sẽ cho kết quả tốt hơn:

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

lý do: bởi vì bạn đang sử dụng lớp where trên bảng CityEntities sẽ loại bỏ nhiều kỷ lục trước khi tham gia MtCity, vì vậy chắc chắn 100% nó sẽ tăng hiệu suất gấp nhiều lần ...

Dù sao câu trả lời của rodrigoelp là thực sự hữu ích.

Cảm ơn


Tôi nghi ngờ sẽ có bất kỳ tác động đến hiệu suất khi sử dụng lời khuyên này. Không thể tìm thấy tham chiếu cho điều này nhưng thứ tự nối bên trong trong truy vấn có thể khác với thứ tự tham gia thực tế. Sau đó được quyết định bởi trình tối ưu hóa truy vấn bằng cách sử dụng thống kê của bảng và ước tính chi phí hoạt động.
Imre Pühvel

@ImreP: Điều này thực sự có thể tương ứng với phương pháp tìm kiếm mà tôi đã mô tả . Mặc dù, tôi không chắc chắn nơi @p0và đặc biệt hơn @p1đến từ
Lukas Eder

0

Bạn có thể triển khai phân trang theo cách đơn giản này bằng cách chuyển 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

Trong năm 2008, chúng tôi không thể sử dụng Skip (). Take ()

Cách là:

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();
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.