วิธีที่มีประสิทธิภาพในการใช้เพจจิ้ง


118

ฉันควรใช้ LINQ Skip()และTake()เมธอดในการเพจหรือใช้เพจจิ้งของตัวเองกับคิวรี SQL?

ข้อใดมีประสิทธิภาพสูงสุด ทำไมฉันถึงเลือกอย่างใดอย่างหนึ่ง

ฉันใช้ SQL Server 2008, ASP.NET MVC และ LINQ


ฉันคิดว่ามันขึ้นอยู่กับ คุณกำลังทำงานกับแอพอะไรอยู่ จะมีโหลดแบบไหน?
BuddyJoe

ลองดูคำตอบนี้ด้วย: stackoverflow.com/a/10639172/416996
Õzbek

คำตอบ:


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 เพราะจะส่งคืนข้อมูลตั้งแต่เริ่มต้นและจะเข้าถึงตารางตราบเท่าที่ตรงตามเงื่อนไข) สิ่งนี้จะคล้ายกับ:

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
คำตอบที่ดี - นิพจน์ตารางทั่วไปเป็นวิธีที่ดีในการทำเพจ
Jarrod Dixon

คุณช่วยตรวจสอบคำถามของฉันได้ไหม ( stackoverflow.com/questions/11100929/… ) ฉันสร้าง SP ที่ฉันเพิ่มใน EDMX และใช้ในแบบสอบถาม linq-to-entities
Misi

2
+1 คำตอบที่ดีขอขอบคุณที่อธิบายถึงประโยชน์ด้านประสิทธิภาพของตัวอย่างที่สอง
โคเฮน

@ โจฮัน: มีอีกทางเลือกหนึ่งที่เรียกว่าวิธีการค้นหาที่มีประสิทธิภาพสูงกว่าการชดเชยสำหรับจำนวนหน้ามาก
Lukas Eder

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) โดยอัตโนมัติใช้ (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- แม้กระทั่ง'--'การสืบค้นจะยังคงใช้งานได้!
Matt

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

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 ในลักษณะนี้จะให้ผลลัพธ์ที่ดีกว่า:

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

เหตุผล: เนื่องจากคุณใช้คลาส where บนตาราง CityEntities ซึ่งจะกำจัดบันทึกจำนวนมากก่อนเข้าร่วม MtCity ดังนั้นมั่นใจได้ 100% ว่าจะเพิ่มประสิทธิภาพได้หลายเท่า ...

อย่างไรก็ตามคำตอบโดย rodrigoelp เป็นประโยชน์จริงๆ

ขอบคุณ


ฉันสงสัยว่าจะมีผลกระทบต่อประสิทธิภาพการทำงานโดยใช้คำแนะนำนี้ ไม่พบข้อมูลอ้างอิงสำหรับสิ่งนี้ แต่ลำดับการรวมภายในในแบบสอบถามอาจแตกต่างจากลำดับการเข้าร่วมจริง ตัวเลือกหลังนี้ถูกกำหนดโดยเครื่องมือเพิ่มประสิทธิภาพการสืบค้นโดยใช้สถิติของตารางและการประมาณต้นทุนการดำเนินการ
Imre Pühvel

@ImreP: อาจนี้จริงค่อนข้างสอดคล้องกับการแสวงหาวิธีการที่ผมได้อธิบาย แม้ว่าฉันไม่แน่ใจว่ามาจากไหน@p0และเฉพาะเจาะจงมากขึ้น@p1
Lukas Eder

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();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.