cara yang efisien untuk mengimplementasikan paging

118

Haruskah saya menggunakan LINQ Skip()dan Take()metode untuk paging, atau menerapkan paging saya sendiri dengan kueri SQL?

Mana yang paling efisien? Mengapa saya harus memilih salah satu dari yang lain?

Saya menggunakan SQL Server 2008, ASP.NET MVC, dan LINQ.

StoneHeart
sumber
Saya pikir itu tergantung. Aplikasi apa yang sedang Anda kerjakan? jenis beban apa yang dimilikinya?
BuddyJoe
Lihatlah jawaban ini juga: stackoverflow.com/a/10639172/416996
Õzbek
Lihatlah ini juga aspsnippets.com/Articles/…
Frank Myat Kam

Jawaban:

175

Mencoba memberikan jawaban singkat atas keraguan Anda, jika Anda menjalankan skip(n).take(m)metode di linq (dengan SQL 2005/2008 sebagai server database) kueri Anda akan menggunakan Select ROW_NUMBER() Over ...pernyataan tersebut, dengan paging langsung di mesin SQL.

Memberi Anda contoh, saya memiliki tabel db yang dipanggil mtcitydan saya menulis kueri berikut (bekerja juga dengan linq ke entitas):

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

Kueri yang dihasilkan adalah:

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]

Yang merupakan akses data berjendela (cukup keren, btw cuz akan mengembalikan data sejak awal dan akan mengakses tabel selama kondisi terpenuhi). Ini akan sangat mirip dengan:

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

Dengan pengecualian, query kedua ini akan dieksekusi lebih cepat daripada hasil linq karena akan menggunakan indeks secara eksklusif untuk membuat jendela akses data; ini berarti, jika Anda memerlukan beberapa pemfilteran, pemfilteran harus (atau harus) dalam daftar Entitas (tempat baris dibuat) dan beberapa indeks harus dibuat juga untuk menjaga kinerja yang baik.

Sekarang, apa yang lebih baik?

Jika Anda memiliki cukup banyak alur kerja yang solid dalam logika Anda, menerapkan cara SQL yang tepat akan menjadi rumit. Dalam hal ini LINQ akan menjadi solusinya.

Jika Anda dapat menurunkan bagian logika itu langsung ke SQL (dalam prosedur tersimpan), itu akan menjadi lebih baik karena Anda dapat mengimplementasikan kueri kedua yang saya tunjukkan (menggunakan indeks) dan mengizinkan SQL untuk menghasilkan dan menyimpan Rencana Eksekusi dari query (meningkatkan kinerja).

rodrigoelp
sumber
2
Jawaban bagus - ekspresi tabel umum adalah cara yang baik untuk melakukan paging.
Jarrod Dixon
Bisakah Anda memeriksa pertanyaan saya ( stackoverflow.com/questions/11100929/… )? Saya membuat SP yang saya tambahkan ke EDMX saya dan menggunakannya dalam kueri linq-ke-entitas.
Misi
2
+1, jawaban yang bagus, saya menghargai Anda menjelaskan manfaat kinerja dari contoh kedua
Cohen
@ Johan: Ada alternatif yang disebut metode pencarian yang sangat mengungguli offset untuk nomor halaman besar.
Lukas Eder
50

Coba gunakan

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

untuk mendapatkan baris dari 501 hingga 600 di server SQL, tanpa memuatnya ke memori. Perhatikan bahwa sintaks ini telah tersedia dengan SQL Server 2012 hanya

d. popov
sumber
Saya pikir ini tidak benar. SQL yang ditampilkan menunjukkan baris dari 502-601 (kecuali Anda tidak mengindeks nol?)
Smudge202
Tidak, itu mendapatkan baris dari 501 hingga 600
Volkan Sen
12

Meskipun LINQ-to-SQL akan menghasilkan OFFSETklausa (mungkin ditiru menggunakan ROW_NUMBER() OVER() seperti yang disebutkan orang lain ), ada cara yang sama sekali berbeda dan jauh lebih cepat untuk melakukan paging dalam SQL. Ini sering disebut "metode pencarian" seperti yang dijelaskan dalam posting blog ini di sini .

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

Nilai @previousScoredan @previousPlayerIdadalah nilai masing-masing dari catatan terakhir dari halaman sebelumnya. Ini memungkinkan Anda untuk mengambil halaman "berikutnya". Jika ORDER BYarahnya adalah ASC, gunakan >saja.

Dengan metode di atas, Anda tidak bisa langsung melompat ke halaman 4 tanpa terlebih dahulu mengambil 40 record sebelumnya. Namun seringkali, Anda tidak ingin melompat sejauh itu. Sebaliknya, Anda mendapatkan kueri yang jauh lebih cepat yang mungkin dapat mengambil data dalam waktu yang konstan, bergantung pada pengindeksan Anda. Plus, halaman Anda tetap "stabil", tidak peduli apakah data dasarnya berubah (mis. Di halaman 1, sementara Anda di halaman 4).

Ini adalah cara terbaik untuk mengimplementasikan paging saat malas memuat lebih banyak data di aplikasi web, misalnya.

Catatan, "metode pencarian" juga disebut paging keyset .

Lukas Eder
sumber
5

LinqToSql secara otomatis akan mengubah .Skip (N1) .Take (N2) menjadi sintaks TSQL untuk Anda. Faktanya, setiap "kueri" yang Anda lakukan di Linq, sebenarnya hanya membuat kueri SQL untuk Anda di latar belakang. Untuk mengujinya, jalankan saja SQL Profiler saat aplikasi Anda sedang berjalan.

Metodologi lewati / ambil telah bekerja dengan sangat baik untuk saya, dan orang lain dari apa yang saya baca.

Karena penasaran, jenis query self-paging apa yang Anda miliki, yang menurut Anda lebih efisien daripada skip / take Linq?

mandreko.dll
sumber
4

Kami menggunakan CTE yang dibungkus dengan SQL Dinamis (karena aplikasi kami memerlukan pengurutan dinamis dari sisi server data) dalam prosedur yang tersimpan. Saya dapat memberikan contoh dasar jika Anda mau.

Saya belum sempat melihat T / SQL yang dihasilkan LINQ. Bisakah seseorang memposting sampel?

Kami tidak menggunakan LINQ atau akses langsung ke tabel karena kami memerlukan lapisan keamanan ekstra (diberikan SQL dinamis agak memecah ini).

Sesuatu seperti ini seharusnya berhasil. Anda dapat menambahkan nilai parameter untuk parameter, dll.

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'
mrdenny
sumber
2
@mrdenny - Satu petunjuk untuk contoh yang Anda berikan: Dengan sp_executesqlAnda memiliki kemungkinan untuk melewati parameter dengan cara yang aman, misalnya: EXECUTE sp_executesql 'WITH myCTE AS ... WHERE Col4=@p1) ...', '@p1 nvarchar(max)', @ValueForCol4. Aman dalam konteks ini berarti kuat terhadap injeksi SQL - Anda dapat meneruskan setiap kemungkinan nilai di dalam variabel @ValueForCol4- bahkan '--', dan kueri akan tetap berfungsi!
Matt
1
@mrdenny Hai, alih-alih menggabungkan kueri, kami menggunakan sesuatu seperti ini: SELECT ROW_NUMBER() OVER (ORDER BY CASE WHEN @CampoId = 1 THEN Id WHEN @CampoId = 2 THEN field2 END)
Ezequiel
Itu bisa menghasilkan beberapa rencana Eksekusi SQL yang buruk.
mrdenny
@mrdenny: Untuk nomor halaman besar, metode seek bisa jauh lebih cepat daripada ROW_NUMBER() OVER()emulasi offset. Lihat juga: 4guysfromrolla.com/webtech/042606-1.shtml
Lukas Eder
2

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

Di t0 semua record Di t1 hanya yang sesuai dengan halaman itu

ch2o
sumber
2

Pendekatan yang saya berikan adalah pagination tercepat yang dapat dicapai SQL server. Saya telah menguji ini pada 5 juta catatan. Pendekatan ini jauh lebih baik daripada "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY" yang disediakan oleh 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>>
srinivas vv
sumber
0

Anda dapat lebih meningkatkan kinerja, cek ini

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

jika Anda akan menggunakan dari dengan cara ini akan memberikan hasil yang lebih baik:

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

alasan: karena Anda menggunakan kelas where pada tabel CityEntities yang akan menghilangkan banyak rekaman sebelum bergabung dengan MtCity, jadi 100% yakin itu akan meningkatkan kinerja berkali-kali lipat ...

Pokoknya jawaban rodrigoelp sangat membantu.

Terima kasih

Ali Adravi
sumber
Saya ragu akan ada dampak kinerja dengan menggunakan saran ini. Tidak dapat menemukan referensi untuk ini tetapi urutan gabungan dalam kueri dapat berbeda dari urutan gabungan sebenarnya. Yang terakhir ditentukan oleh pengoptimal kueri menggunakan statistik tabel dan perkiraan biaya operasi.
Imre Pühvel
@ImreP: Ini mungkin sebenarnya agak sesuai dengan metode pencarian, yang telah saya jelaskan . Meskipun, saya tidak yakin dari mana @p0dan lebih tepatnya @p1berasal
Lukas Eder
0

Anda bisa mengimplementasikan paging dengan cara sederhana ini dengan meneruskan 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
Rae Lee
sumber
0

Pada tahun 2008 kami tidak dapat menggunakan Skip (). Take ()

Caranya adalah:

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();
Belen Martin
sumber