กำลังดึงข้อมูลแถว n ต่อกลุ่ม


88

ฉันมักจะต้องเลือกจำนวนแถวจากแต่ละกลุ่มในชุดผลลัพธ์

ตัวอย่างเช่นฉันอาจต้องการแสดงมูลค่าการสั่งซื้อล่าสุด 'n' ที่สูงที่สุดหรือต่ำสุดต่อลูกค้า

ในกรณีที่ซับซ้อนมากขึ้นจำนวนแถวที่จะแสดงอาจแตกต่างกันไปในแต่ละกลุ่ม (กำหนดโดยแอตทริบิวต์ของการจัดกลุ่ม / ระเบียนหลัก) ส่วนนี้เป็นทางเลือก / สำหรับเครดิตเพิ่มเติมและไม่ได้มีเจตนาเพื่อห้ามไม่ให้คนตอบรับ

ตัวเลือกหลักสำหรับการแก้ไขปัญหาประเภทนี้ใน SQL Server 2005 และรุ่นที่ใหม่กว่าคืออะไร อะไรคือข้อดีและข้อเสียหลักของแต่ละวิธี?

ตัวอย่าง AdventureWorks (เพื่อความชัดเจนและเป็นตัวเลือก)

  1. แสดงวันที่ล่าสุดของการทำธุรกรรมห้าครั้งและรหัสจากTransactionHistoryตารางสำหรับแต่ละผลิตภัณฑ์ที่เริ่มต้นด้วยตัวอักษรจาก M ถึง R
  2. เหมือนกันอีกครั้ง แต่มีnบรรทัดประวัติต่อผลิตภัณฑ์โดยที่nห้าเท่าของDaysToManufactureแอตทริบิวต์ผลิตภัณฑ์
  3. เดียวกันสำหรับกรณีพิเศษที่ว่าเส้นหนึ่งของประวัติศาสตร์ที่ต่อผลิตภัณฑ์เป็นสิ่งจำเป็น (เดี่ยวรายการล่าสุดโดยผูกทำลายบนTransactionDateTransactionID

คำตอบ:


70

เริ่มจากสถานการณ์พื้นฐานกันก่อน

หากฉันต้องการได้จำนวนแถวออกมาจากตารางฉันมีสองตัวเลือกหลัก: ฟังก์ชันการจัดอันดับ TOPหรือ

ก่อนอื่นมาพิจารณาทั้งชุดProduction.TransactionHistoryสำหรับสิ่งใดสิ่งหนึ่งProductID:

SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;

สิ่งนี้จะส่งกลับ 418 แถวและแผนแสดงให้เห็นว่ามันตรวจสอบทุกแถวในตารางที่ค้นหาสิ่งนี้ - การสแกนดัชนีแบบไม่ จำกัด กลุ่มพร้อมเพรดิเคตเพื่อเตรียมตัวกรอง 797 อ่านที่นี่ซึ่งน่าเกลียด

สแกนราคาแพงด้วย 'ส่วนที่เหลือ' คำกริยา

ดังนั้นขอให้เป็นธรรมกับมันและสร้างดัชนีที่จะมีประโยชน์มากกว่า เงื่อนไขของเราเรียกร้องให้มีการแข่งขันที่เท่าเทียมกันในตามด้วยการค้นหาล่าสุดโดยProductID TransactionDateเราจำเป็นต้องกลับเกินไปดังนั้นขอไปด้วย:TransactionIDCREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);

เมื่อทำสิ่งนี้แล้วแผนของเราเปลี่ยนไปอย่างมีนัยสำคัญและลดจำนวนผู้อ่านลงเหลือเพียง 3 คนดังนั้นเราจึงกำลังปรับปรุงสิ่งต่าง ๆ มากกว่า 250x หรือมากกว่านั้น ...

ปรับปรุงแผน

ตอนนี้เราได้เล็งสนามเด็กเล่นให้ดูที่ตัวเลือกด้านบน - TOPฟังก์ชั่นการจัดอันดับและ

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
)
SELECT TransactionID, ProductID, TransactionDate
FROM Numbered
WHERE RowNum <= 5;

SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
ORDER BY TransactionDate DESC;

สองแผน - พื้นฐาน TOP \ RowNum

คุณจะสังเกตเห็นว่าTOPแบบสอบถามที่สอง ( ) นั้นง่ายกว่าแบบสอบถามแรกทั้งในแบบสอบถามและในแผน แต่สำคัญมากพวกเขาทั้งสองใช้TOPเพื่อ จำกัด จำนวนแถวที่ถูกดึงออกมาจากดัชนีจริง ๆ ค่าใช้จ่ายเป็นเพียงการประมาณการและไม่สนใจ แต่คุณสามารถเห็นความคล้ายคลึงกันจำนวนมากในสองแผนโดยROW_NUMBER()เวอร์ชันจะทำงานพิเศษเล็กน้อยเพื่อกำหนดตัวเลขและตัวกรองตามลำดับและทั้งสองแบบสอบถามจบลงด้วยการอ่านเพียง 2 ครั้ง การทำงานของพวกเขา. เครื่องมือเพิ่มประสิทธิภาพข้อความค้นหาตระหนักถึงแนวคิดของการกรองในROW_NUMBER()ฟิลด์อย่างแน่นอนโดยตระหนักว่าสามารถใช้ตัวดำเนินการด้านบนเพื่อข้ามแถวที่ไม่ต้องการได้ ข้อความค้นหาทั้งสองนี้ดีพอ - TOPไม่ใช่เรื่องที่ดีไปกว่านี้เพราะมันคุ้มค่าที่จะเปลี่ยนรหัส แต่มันง่ายกว่าและชัดเจนกว่าสำหรับผู้เริ่มต้น

ดังนั้นสิ่งนี้ใช้ได้กับผลิตภัณฑ์เดียว แต่เราต้องพิจารณาว่าจะเกิดอะไรขึ้นถ้าเราต้องทำสิ่งนี้กับผลิตภัณฑ์ที่หลากหลาย

โปรแกรมเมอร์ซ้ำเป็นไปที่จะต้องพิจารณาความคิดของการวนลูปผ่านผลิตภัณฑ์ที่น่าสนใจและเรียกใช้แบบสอบถามนี้หลายครั้งและเราจะได้รับจริงไปกับการเขียนแบบสอบถามในรูปแบบนี้ - ไม่ได้ใช้เม้าส์, APPLYแต่การใช้ ฉันกำลังใช้OUTER APPLYการหาว่าเราอาจต้องการคืนสินค้าด้วย NULL หากไม่มีธุรกรรมสำหรับมัน

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

แผนนี้เป็นวิธีการเขียนโปรแกรมซ้ำของ - วนซ้ำ, การดำเนินการด้านบนและแสวงหา (ทั้ง 2 ที่เราเคยอ่านมาก่อน) สำหรับแต่ละผลิตภัณฑ์ สิ่งนี้จะให้ 4 อ่านกับผลิตภัณฑ์และ 360 กับ TransactionHistory

นำไปใช้วางแผน

การใช้ROW_NUMBER()วิธีการนั้นจะใช้PARTITION BYในOVERข้อเพื่อให้เราเริ่มหมายเลขสำหรับแต่ละผลิตภัณฑ์ สามารถกรองได้เช่นนี้มาก่อน แผนแตกต่างกันมาก การอ่านแบบลอจิคัลต่ำกว่า TransactionHistory ประมาณ 15% โดยมีการสแกนดัชนีแบบเต็มเพื่อดำเนินการกับแถวต่างๆ

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

แผน ROW_NUMBER

แม้ว่าที่สำคัญแผนนี้มีตัวดำเนินการเรียงลำดับที่มีราคาแพง ดูเหมือนว่าการรวมเข้าร่วมไม่ได้รักษาลำดับของแถวใน TransactionHistory ข้อมูลจะต้องใช้เพื่อให้สามารถค้นหาจำนวนแถวได้ มันอ่านน้อยลง แต่การเรียงลำดับการบล็อกนี้อาจรู้สึกเจ็บปวด การใช้APPLYการวนซ้ำจะกลับแถวแรกอย่างรวดเร็วหลังจากอ่านเพียงไม่กี่ครั้ง แต่ด้วยการเรียงลำดับROW_NUMBER()จะส่งคืนแถวหลังจากการทำงานส่วนใหญ่เสร็จสิ้น

น่าสนใจหากROW_NUMBER()แบบสอบถามใช้INNER JOINแทนLEFT JOINแผนอื่นจะเกิดขึ้น

ROW_NUMBER () กับ INNER JOIN

APPLYแผนนี้ใช้ซ้อนห่วงเช่นเดียวกับ แต่ไม่มีโอเปอเรเตอร์สูงสุดจึงดึงธุรกรรมทั้งหมดสำหรับแต่ละผลิตภัณฑ์และใช้การอ่านมากขึ้นกว่า แต่ก่อน - 492 อ่านเทียบกับ TransactionHistory ไม่มีเหตุผลที่ดีที่จะไม่เลือกตัวเลือกผสานการเข้าร่วมที่นี่ดังนั้นฉันเดาว่าแผนนี้ถือว่าเป็น 'พอเพียง' ยังคง - มันจะไม่ปิดกั้นซึ่งเป็นสิ่งที่ดี - APPLYไม่ดีเท่า

PARTITION BYคอลัมน์ที่ผมใช้สำหรับROW_NUMBER()เป็นh.ProductIDในทั้งสองกรณีเพราะผมอยากจะให้ QO ตัวเลือกในการผลิตมูลค่า rownum ก่อนเข้าร่วมกับตารางสินค้า ถ้าฉันใช้p.ProductIDเราจะเห็นแผนรูปร่างเช่นเดียวกับการINNER JOINเปลี่ยนแปลง

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

แต่ผู้ดำเนินการเข้าร่วมพูดว่า 'Left Outer Join' แทนที่จะเป็น 'Inner Join' จำนวนการอ่านยังคงอยู่ภายใต้การอ่าน 500 ครั้งต่อตาราง TransactionHistory

พาร์ติชันโดยบน p.ProductID แทน h.ProductID

อย่างไรก็ตาม - กลับไปที่คำถามที่อยู่ในมือ ...

เราได้ตอบคำถามข้อที่ 1มีสองตัวเลือกที่คุณสามารถเลือกและเลือกได้ ส่วนตัวผมชอบAPPLYตัวเลือก

หากต้องการขยายให้ใช้หมายเลขตัวแปร ( คำถาม 2 ) 5จำเป็นต้องเปลี่ยนค่าjust โอ้และฉันได้เพิ่มดัชนีอีกอันเพื่อให้มีดัชนีProduction.Product.Nameอยู่ในDaysToManufactureคอลัมน์นั้น

WITH Numbered AS
(
SELECT p.Name, p.ProductID, p.DaysToManufacture, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5 * DaysToManufacture;

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5 * p.DaysToManufacture) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

และแผนทั้งสองนั้นแทบจะเหมือนกันกับที่เคยเป็นมาก่อน!

แถวตัวแปร

อีกครั้งให้เพิกเฉยต่อค่าใช้จ่ายโดยประมาณ - แต่ฉันยังคงชอบสถานการณ์ของ TOP เพราะมันง่ายกว่ามากและแผนไม่มีผู้ดำเนินการบล็อก การอ่านมีน้อยลงใน TransactionHistory เนื่องจากมีจำนวนศูนย์เป็นจำนวนมากDaysToManufactureแต่ในชีวิตจริงฉันสงสัยว่าเราต้องการเลือกคอลัมน์นั้น ;)

วิธีหนึ่งในการหลีกเลี่ยงการบล็อกคือการสร้างแผนที่จัดการROW_NUMBER()บิตไปทางขวา (ในแผน) ของการเข้าร่วม เราสามารถโน้มน้าวสิ่งนี้ให้เกิดขึ้นได้โดยทำการเข้าร่วมนอก CTE

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
)
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM Production.Product p
LEFT JOIN Numbered t ON t.ProductID = p.ProductID
    AND t.RowNum <= 5 * p.DaysToManufacture
WHERE p.Name >= 'M' AND p.Name < 'S';

แผนที่นี่ดูเรียบง่ายขึ้น - ไม่ได้ปิดกั้น แต่มีอันตรายซ่อนเร้น

เข้าร่วมนอก CTE

สังเกตว่า Compute Scalar ที่ดึงข้อมูลจากตารางผลิตภัณฑ์ นี่เป็นการหา5 * p.DaysToManufactureค่า ค่านี้ไม่ได้ถูกส่งผ่านไปยังสาขาที่ดึงข้อมูลจากตาราง TransactionHistory แต่จะถูกใช้ใน Merge Join เป็นสารตกค้าง

ที่เหลือส่อเสียด!

ดังนั้นการเข้าร่วมของ Merge จึงใช้เวลามากไม่เพียง แต่จำเป็นต้องมีหลายแถว แต่ยังต้องตรวจสอบสิ่งที่เหลือทั้งหมด สิ่งนี้เป็นอันตรายเมื่อจำนวนธุรกรรมเพิ่มขึ้น ฉันไม่ใช่แฟนของสถานการณ์นี้ - เพรดิเคตที่เหลือใน Merge Joins สามารถเพิ่มได้อย่างรวดเร็ว อีกเหตุผลหนึ่งที่ฉันชอบAPPLY/TOPฉากนี้

ในกรณีพิเศษที่มันเป็นหนึ่งแถวสำหรับคำถาม 3เราสามารถใช้ข้อความค้นหาเดียวกัน แต่1แทนที่จะ5ใช้ แต่จากนั้นเรามีตัวเลือกพิเศษซึ่งก็คือการใช้มวลรวมปกติ

SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;

แบบสอบถามเช่นนี้จะเป็นการเริ่มต้นที่มีประโยชน์และเราสามารถปรับเปลี่ยนได้อย่างง่ายดายเพื่อดึง TransactionID ออกมาเช่นกันสำหรับการผูกไทม์เบรก (ใช้การต่อข้อมูลซึ่งจะถูกแบ่งย่อย) แต่เรามองดัชนีทั้งหมดหรือ เราดำน้ำในผลิตภัณฑ์แต่ละผลิตภัณฑ์และเราไม่ได้รับการปรับปรุงอย่างมากกับสิ่งที่เรามีมาก่อนในสถานการณ์นี้

แต่ฉันควรชี้ให้เห็นว่าเรากำลังดูสถานการณ์เฉพาะที่นี่ ด้วยข้อมูลจริงและด้วยกลยุทธ์การจัดทำดัชนีที่อาจไม่เหมาะสมระยะทางอาจแตกต่างกันมาก แม้ว่าที่จริงแล้วเราเคยเห็นว่าAPPLYมีความแข็งแกร่งที่นี่ แต่มันอาจช้าลงในบางสถานการณ์ มันไม่ค่อยบล็อก แต่เนื่องจากมีแนวโน้มที่จะใช้ Nested Loops ซึ่งหลายคน (รวมตัวเอง) พบว่าน่าสนใจมาก

ฉันไม่ได้พยายามสำรวจความเท่าเทียมกันที่นี่หรือดำดิ่งลงไปในคำถามที่ 3 ซึ่งฉันเห็นว่าเป็นกรณีพิเศษที่ผู้คนไม่ค่อยต้องการตามความซับซ้อนของการต่อเชื่อมและแยกกัน สิ่งสำคัญที่ต้องพิจารณาที่นี่คือทั้งสองตัวเลือกมีความแข็งแกร่งมาก

APPLYฉันชอบ เห็นได้ชัดว่ามันใช้โอเปอเรเตอร์ยอดนิยมและไม่ค่อยทำให้เกิดการบล็อค


44

วิธีทั่วไปในการทำเช่นนี้ใน SQL Server 2005 และสูงกว่าคือการใช้ CTE และฟังก์ชั่นหน้าต่าง สำหรับ top n ต่อกลุ่มคุณสามารถใช้ROW_NUMBER()กับPARTITIONclause และกรองกับสิ่งนั้นในการสืบค้นภายนอก ตัวอย่างเช่นคำสั่งล่าสุด 5 อันดับแรกต่อลูกค้าสามารถแสดงได้ด้วยวิธีนี้:

DECLARE @top INT;
SET @top = 5;

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT CustomerID, OrderID, OrderDate
  FROM grp
  WHERE rn <= @top
  ORDER BY CustomerID, OrderDate DESC;

คุณสามารถทำได้ด้วยCROSS APPLY:

DECLARE @top INT;
SET @top = 5;

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (@top) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

ด้วยตัวเลือกเพิ่มเติมที่ระบุไว้ Paul บอกว่าตารางลูกค้ามีคอลัมน์ที่ระบุจำนวนแถวที่จะรวมต่อลูกค้า:

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT c.CustomerID, grp.OrderID, grp.OrderDate
  FROM grp 
  INNER JOIN dbo.Customers AS c
  ON grp.CustomerID = c.CustomerID
  AND grp.rn <= c.Number_of_Recent_Orders_to_Show
  ORDER BY c.CustomerID, grp.OrderDate DESC;

และอีกครั้งใช้CROSS APPLYและรวมตัวเลือกเพิ่มว่าจำนวนแถวสำหรับลูกค้าจะถูกกำหนดโดยคอลัมน์บางอย่างในตารางลูกค้า:

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (c.Number_of_Recent_Orders_to_Show) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

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

โดยส่วนตัวแล้วฉันชอบ CTE และวิธีแก้ปัญหาหน้าต่างมากกว่าCROSS APPLY/ TOPเพราะพวกเขาแยกลอจิกที่ดีขึ้นและใช้งานง่ายขึ้น (สำหรับฉัน) โดยทั่วไป (ทั้งในกรณีนี้และจากประสบการณ์ทั่วไปของฉัน) วิธีการ CTE สร้างแผนการที่มีประสิทธิภาพมากขึ้น (ตัวอย่างด้านล่าง) แต่สิ่งนี้ไม่ควรเป็นความจริงสากล - คุณควรทดสอบสถานการณ์ของคุณโดยเฉพาะหากดัชนีเปลี่ยนแปลงหรือ ข้อมูลเบ้อย่างมีนัยสำคัญ


ตัวอย่าง AdventureWorks - โดยไม่มีการเปลี่ยนแปลงใด ๆ

  1. แสดงวันที่ล่าสุดของการทำธุรกรรมห้าครั้งและรหัสจากTransactionHistoryตารางสำหรับแต่ละผลิตภัณฑ์ที่เริ่มต้นด้วยตัวอักษรจาก M ถึง R
-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= 5;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

การเปรียบเทียบสองสิ่งนี้ในเมทริกรันไทม์:

ป้อนคำอธิบายรูปภาพที่นี่

CTE / OVER()แผน:

ป้อนคำอธิบายรูปภาพที่นี่

CROSS APPLY วางแผน:

ป้อนคำอธิบายรูปภาพที่นี่

แผน CTE ดูซับซ้อนมากขึ้น แต่จริงๆแล้วมีประสิทธิภาพมากกว่า ให้ความสนใจเพียงเล็กน้อยกับตัวเลข% ของค่าใช้จ่ายโดยประมาณ แต่ให้ความสำคัญกับการสังเกตจริงที่สำคัญกว่าเช่นการอ่านน้อยลงและใช้เวลาน้อยลง ฉันวิ่งแข่งเหล่านี้โดยไม่ขนานกันและนี่ก็ไม่ต่างกัน ตัวชี้วัดรันไทม์และแผน CTE ( CROSS APPLYแผนยังคงเหมือนเดิม):

ป้อนคำอธิบายรูปภาพที่นี่

ป้อนคำอธิบายรูปภาพที่นี่

  1. เหมือนกันอีกครั้ง แต่มีnบรรทัดประวัติต่อผลิตภัณฑ์โดยที่nห้าเท่าของDaysToManufactureแอตทริบิวต์ผลิตภัณฑ์

ต้องการการเปลี่ยนแปลงเล็กน้อยมากที่นี่ สำหรับ CTE เราสามารถเพิ่มคอลัมน์ลงในเคียวรีด้านในและกรองในเคียวรีภายนอก สำหรับเราสามารถทำการคำนวณภายในมีลักษณะร่วมกันCROSS APPLY TOPคุณอาจคิดว่าสิ่งนี้จะช่วยเพิ่มประสิทธิภาพในการCROSS APPLYแก้ปัญหา แต่ก็ไม่ได้เกิดขึ้นในกรณีนี้ คำสั่ง:

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, p.DaysToManufacture, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= (5 * DaysToManufacture);

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5 * p.DaysToManufacture) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

ผลลัพธ์รันไทม์:

ป้อนคำอธิบายรูปภาพที่นี่

CTE แบบขนาน / OVER()แผน:

ป้อนคำอธิบายรูปภาพที่นี่

CTE แบบเธรดเดี่ยว / OVER()แผน:

ป้อนคำอธิบายรูปภาพที่นี่

CROSS APPLY วางแผน:

ป้อนคำอธิบายรูปภาพที่นี่

  1. เดียวกันสำหรับกรณีพิเศษที่ว่าเส้นหนึ่งของประวัติศาสตร์ที่ต่อผลิตภัณฑ์เป็นสิ่งจำเป็น (เดี่ยวรายการล่าสุดโดยผูกทำลายบนTransactionDateTransactionID

อีกครั้งการเปลี่ยนแปลงเล็กน้อยที่นี่ ในการแก้ปัญหา CTE เราเพิ่มTransactionIDไปยังข้อและเปลี่ยนกรองด้านนอกเพื่อOVER() rn = 1สำหรับCROSS APPLYเราเปลี่ยนTOPไปTOP (1)และเพิ่มการภายในTransactionIDORDER BY

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC, TransactionID DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn = 1;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (1) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC, TransactionID DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

ผลลัพธ์รันไทม์:

ป้อนคำอธิบายรูปภาพที่นี่

CTE แบบขนาน / OVER()แผน:

ป้อนคำอธิบายรูปภาพที่นี่

แผน CTE / OVER () แบบเธรดเดี่ยว:

ป้อนคำอธิบายรูปภาพที่นี่

CROSS APPLY วางแผน:

ป้อนคำอธิบายรูปภาพที่นี่

ฟังก์ชั่นหน้าต่างไม่ใช่ทางเลือกที่ดีที่สุดเสมอ (ไปได้COUNT(*) OVER()) และสิ่งเหล่านี้ไม่ใช่วิธีการเพียงสองวิธีในการแก้ปัญหา n แถวต่อปัญหากลุ่ม แต่ในกรณีเฉพาะนี้ - ให้สคีมาดัชนีที่มีอยู่และการกระจายข้อมูล - CTE อาการดีขึ้นโดยบัญชีที่มีความหมายทั้งหมด


ตัวอย่าง AdventureWorks - มีความยืดหยุ่นในการเพิ่มดัชนี

อย่างไรก็ตามถ้าคุณเพิ่มดัชนีสนับสนุนคล้ายกับที่ Paul พูดถึงในความคิดเห็นแต่มีคอลัมน์ที่ 2 และ 3 เรียงตามลำดับDESC:

CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory 
  (ProductID, TransactionDate DESC, TransactionID DESC);

คุณจะได้รับแผนที่เป็นที่นิยมมากขึ้นทั่ว ๆ ไปและตัวชี้วัดจะพลิกไปเป็นCROSS APPLYแนวทางในทั้งสามกรณี:

ป้อนคำอธิบายรูปภาพที่นี่

หากนี่คือสภาพแวดล้อมการผลิตของฉันฉันอาจจะพอใจกับระยะเวลาในกรณีนี้และจะไม่รบกวนการปรับแต่งเพิ่มเติม


ทั้งหมดนี้เป็นเรื่องน่าเกลียดมากใน SQL Server 2000 ซึ่งไม่สนับสนุนAPPLYหรือOVER()ข้อ


24

ใน DBMS เช่น MySQL ที่ไม่มีฟังก์ชั่นหน้าต่างหรือCROSS APPLYวิธีการทำเช่นนี้จะใช้ SQL มาตรฐาน (89) วิธีที่ช้าจะเป็นการรวมรูปสามเหลี่ยมข้ามรวม วิธีที่เร็วขึ้น ( แต่ยังคงอาจจะไม่เป็นที่มีประสิทธิภาพโดยใช้ข้ามใช้หรือฟังก์ชั่น row_number) จะเป็นสิ่งที่ผมเรียก"น่าสงสารของมนุษย์CROSS APPLY " มันจะน่าสนใจที่จะเปรียบเทียบแบบสอบถามนี้กับคนอื่น ๆ :

ข้อสันนิษฐาน: Orders (CustomerID, OrderDate)มีUNIQUEข้อ จำกัด :

DECLARE @top INT;
SET @top = 5;

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (@top) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

สำหรับปัญหาพิเศษของแถวบนสุดที่กำหนดเองต่อกลุ่ม:

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (c.Number_of_Recent_Orders_to_Show) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

หมายเหตุ: ใน MySQL แทนการอย่างใดอย่างหนึ่งจะใช้AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...) AND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1))SQL-Server เพิ่มFETCH / OFFSETไวยากรณ์ในเวอร์ชัน 2012 ข้อความค้นหาที่นี่ถูกปรับIN (TOP...)เพื่อให้ทำงานกับรุ่นก่อนหน้า


21

ฉันใช้วิธีที่แตกต่างกันเล็กน้อยโดยเฉพาะอย่างยิ่งเพื่อดูว่าเทคนิคนี้จะเปรียบเทียบกับคนอื่น ๆ อย่างไรเพราะการมีตัวเลือกดีใช่ไหม?

การทดสอบ

ทำไมเราไม่เริ่มด้วยการดูว่าวิธีการต่าง ๆ นั้นเรียงซ้อนกัน ฉันทำการทดสอบสามชุด:

  1. ชุดแรกรันโดยไม่มีการแก้ไข DB
  2. ชุดที่สองวิ่งตามหลังดัชนีถูกสร้างขึ้นเพื่อรองรับคำสั่งชั่นกับTransactionDateProduction.TransactionHistory
  3. ชุดที่สามทำให้สมมติฐานแตกต่างกันเล็กน้อย เนื่องจากการทดสอบทั้งสามนั้นใช้กับรายการผลิตภัณฑ์เดียวกันจะเกิดอะไรขึ้นถ้าเราแคชรายการนั้น วิธีการของฉันใช้แคชในหน่วยความจำในขณะที่วิธีอื่นใช้ตาราง temp ที่เทียบเท่า ดัชนีสนับสนุนที่สร้างขึ้นสำหรับการทดสอบชุดที่สองยังคงมีอยู่สำหรับการทดสอบชุดนี้

รายละเอียดการทดสอบเพิ่มเติม:

  • การทดสอบถูกเรียกใช้AdventureWorks2012บน SQL Server 2012, SP2 (Developer Edition)
  • สำหรับการทดสอบแต่ละครั้งฉันระบุว่าคำตอบของฉันได้นำแบบสอบถามมาจากไหนและเป็นแบบสอบถามแบบใด
  • ฉันใช้ตัวเลือก "ยกเลิกผลลัพธ์หลังจากดำเนินการ" ของ Query Options | ผล.
  • โปรดทราบว่าสำหรับการทดสอบสองชุดแรกRowCountsดูเหมือนว่าจะเป็น "ปิด" สำหรับวิธีการของฉัน เพราะนี่คือวิธีการของฉันเป็นคู่มือการดำเนินงานของสิ่งที่CROSS APPLYจะทำมันวิ่งแบบสอบถามเริ่มต้นกับProduction.Productและได้รับ 161 Production.TransactionHistoryแถวหลังที่มันแล้วใช้สำหรับการค้นหากับ ดังนั้นRowCountค่าสำหรับการเข้าร่วมของฉันจึงมากกว่า 161 รายการอื่น ๆ เสมอ ในการทดสอบชุดที่สาม (พร้อมแคช) การนับแถวจะเหมือนกันสำหรับวิธีการทั้งหมด
  • ฉันใช้ SQL Server Profiler เพื่อรวบรวมสถิติแทนการใช้แผนการดำเนินการ แอรอนและมิคาเอลทำหน้าที่ได้ยอดเยี่ยมมากในการแสดงแผนการสอบถามและไม่จำเป็นต้องทำซ้ำข้อมูลนั้น และจุดประสงค์ของวิธีการของฉันคือลดการสืบค้นให้อยู่ในรูปแบบง่าย ๆ ที่มันจะไม่สำคัญ มีเหตุผลเพิ่มเติมสำหรับการใช้ Profiler แต่จะกล่าวถึงในภายหลัง
  • แทนที่จะเลือกใช้การName >= N'M' AND Name < N'S'สร้างฉันเลือกที่จะใช้Name LIKE N'[M-R]%'และ SQL Server ถือว่าพวกเขาเหมือนกัน

ผลลัพธ์

ไม่มีดัชนีสนับสนุน

นี่คือ AdventureWorks2012 นอกกรอบเป็นหลัก ในทุกกรณีวิธีการของฉันดีกว่าวิธีอื่นอย่างชัดเจน แต่ไม่ดีเท่าวิธีที่ 1 หรือ 2

การทดสอบ 1 ทดสอบ 1 ผลลัพธ์โดยไม่มีดัชนี
CTE ของ Aaron เป็นผู้ชนะอย่างชัดเจนที่นี่

ทดสอบ 2 ทดสอบ 2 ผลลัพธ์โดยไม่มีดัชนี
Aaron's CTE (อีกครั้ง) และapply row_number()วิธีที่สองของ Mikael นั้นใกล้เคียงกัน

ทดสอบ 3 ทดสอบ 3 ผลลัพธ์โดยไม่มีดัชนี
ของ Aaron CTE (อีกครั้ง) เป็นผู้ชนะ

บทสรุป
เมื่อไม่มีดัชนีสนับสนุนTransactionDateวิธีการของฉันดีกว่าทำมาตรฐานCROSS APPLYแต่ถึงกระนั้นการใช้วิธี CTE เป็นวิธีที่ชัดเจน

พร้อมดัชนีสนับสนุน (ไม่มีแคช)

สำหรับชุดทดสอบนี้ฉันได้เพิ่มดัชนีที่ชัดเจนTransactionHistory.TransactionDateตั้งแต่แบบสอบถามทั้งหมดเรียงลำดับในฟิลด์นั้น ฉันพูดว่า "ชัดเจน" เนื่องจากคำตอบอื่น ๆ ส่วนใหญ่เห็นด้วยกับประเด็นนี้ และเนื่องจากคำค้นหาทั้งหมดต้องการวันที่ล่าสุดTransactionDateจึงควรสั่งซื้อฟิลด์DESCดังนั้นฉันเพิ่งคว้าCREATE INDEXคำแถลงที่ด้านล่างของคำตอบของมิคาเอลและเพิ่มความชัดเจนFILLFACTOR:

CREATE INDEX [IX_TransactionHistoryX]
    ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
    WITH (FILLFACTOR = 100);

เมื่อดัชนีนี้เข้าแทนที่ผลลัพธ์จะเปลี่ยนไปเล็กน้อย

การทดสอบ 1 ทดสอบ 1 ผลลัพธ์ - พร้อมดัชนีสนับสนุน
คราวนี้เป็นวิธีของฉันที่ออกมาข้างหน้าอย่างน้อยก็ในแง่ของการอ่านเชิงตรรกะ CROSS APPLYวิธีการก่อนหน้านี้นักแสดงที่เลวร้ายที่สุดสำหรับการทดสอบที่ 1, ชนะในระยะเวลาและแม้กระทั่งเต้นวิธี CTE บนตรรกะอ่าน

การทดสอบ 2 ทดสอบ 2 ผลลัพธ์ - พร้อมดัชนีสนับสนุน
คราวนี้เป็นapply row_number()วิธีแรกของ Mikael ที่เป็นผู้ชนะเมื่อดู Reads ในขณะที่ก่อนหน้านี้เป็นหนึ่งในนักแสดงที่แย่ที่สุด และตอนนี้วิธีการของฉันเข้ามาใกล้มากเป็นอันดับสองเมื่อมองไปที่การอ่าน ในความเป็นจริงแล้วนอกเหนือจากวิธีการ CTE ส่วนที่เหลือทั้งหมดก็ค่อนข้างใกล้ชิดในแง่ของการอ่าน

การทดสอบ 3 ที่ ทดสอบ 3 ผลลัพธ์ - พร้อมดัชนีสนับสนุน
นี่ CTE ยังคงเป็นผู้ชนะ แต่ตอนนี้ความแตกต่างระหว่างวิธีอื่น ๆ นั้นแทบจะไม่สังเกตเห็นได้ชัดเมื่อเทียบกับความแตกต่างอย่างมากที่มีอยู่ก่อนการสร้างดัชนี

บทสรุป
การบังคับใช้วิธีการของฉันชัดเจนยิ่งขึ้นในขณะนี้แม้ว่าจะมีความยืดหยุ่นน้อยลงหากไม่มีดัชนีที่เหมาะสม

ด้วยการสนับสนุนดัชนีและแคช

สำหรับชุดทดสอบนี้ฉันใช้แคชเพราะทำไมล่ะ วิธีการของฉันอนุญาตให้ใช้การแคชในหน่วยความจำที่วิธีอื่นไม่สามารถเข้าถึงได้ ดังนั้นเพื่อความเป็นธรรมฉันจึงสร้างตาราง temp ต่อไปนี้ซึ่งใช้แทนProduct.Productการอ้างอิงทั้งหมดในวิธีอื่นในการทดสอบทั้งสาม DaysToManufactureข้อมูลจะถูกใช้ในการทดสอบจำนวน 2 แต่มันก็ง่ายขึ้นเพื่อให้สอดคล้องข้ามสคริปต์ SQL ที่จะใช้ตารางเดียวกันและมันไม่ได้เจ็บที่จะมีมันมี

CREATE TABLE #Products
(
    ProductID INT NOT NULL PRIMARY KEY,
    Name NVARCHAR(50) NOT NULL,
    DaysToManufacture INT NOT NULL
);

INSERT INTO #Products (ProductID, Name, DaysToManufacture)
    SELECT  p.ProductID, p.Name, p.DaysToManufacture
    FROM    Production.Product p
    WHERE   p.Name >= N'M' AND p.Name < N'S'
    AND    EXISTS (
                    SELECT  *
                    FROM    Production.TransactionHistory th
                    WHERE   th.ProductID = p.ProductID
                );

ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);

การทดสอบ 1 ทดสอบ 1 ผลลัพธ์ - พร้อมดัชนีสนับสนุนและแคช
วิธีการทั้งหมดดูเหมือนจะได้รับประโยชน์อย่างเท่าเทียมกันจากการแคชและวิธีการของฉันยังคงออกมาก่อน

บททดสอบที่ 2 ทดสอบ 2 ผลลัพธ์ - พร้อมดัชนีสนับสนุนและแคช
ตอนนี้เราเห็นความแตกต่างในกลุ่มผู้เล่นตัวจริงเมื่อวิธีการของฉันออกมาข้างหน้าแทบจะ 2 อ่านดีกว่าapply row_number()วิธีแรกของมิคาเอล

ทดสอบ 3 ทดสอบ 3 ผลลัพธ์ - พร้อมดัชนีสนับสนุนและแคช
โปรดดูการปรับปรุงทางด้านล่าง (ด้านล่างบรรทัด) ที่นี่เราเห็นความแตกต่างอีกครั้ง รสชาติ "แปรปรวน" ของวิธีการของฉันตอนนี้แทบจะไม่ได้เป็นผู้นำโดย 2 อ่านเมื่อเทียบกับวิธีการ CROSS ของแอรอนใช้ (โดยไม่มีการแคชพวกเขาเท่ากัน) แต่สิ่งที่แปลกจริงๆคือเป็นครั้งแรกที่เราเห็นวิธีที่ได้รับผลกระทบจากการแคช: วิธี CTE ของแอรอน (ซึ่งก่อนหน้านี้ดีที่สุดสำหรับการทดสอบหมายเลข 3) แต่ฉันจะไม่รับเครดิตในกรณีที่ไม่ได้กำหนดและเนื่องจากไม่มีวิธีการแคช CTE ของแอรอนยังคงเร็วกว่าวิธีของฉันอยู่ที่นี่ด้วยการแคชวิธีที่ดีที่สุดสำหรับสถานการณ์นี้ดูเหมือนจะเป็นวิธี CTE ของแอรอน

บทสรุป โปรดดูการอัปเดตทางด้านล่าง (ใต้บรรทัด)
สถานการณ์ที่ใช้ผลการสืบค้นซ้ำครั้งที่สองบ่อยครั้งจะได้รับประโยชน์ (แต่ไม่เสมอไป) จากการแคชผลลัพธ์เหล่านั้น แต่เมื่อการแคชเป็นประโยชน์การใช้หน่วยความจำสำหรับการแคชดังกล่าวมีข้อดีกว่าการใช้ตารางชั่วคราว

วิธีการ

โดยทั่วไป

ฉันแยกแบบสอบถาม "ส่วนหัว" (เช่นได้รับProductIDs และในกรณีหนึ่งยังDaysToManufactureขึ้นอยู่กับการNameเริ่มต้นด้วยตัวอักษรบางตัว) จากแบบสอบถาม "รายละเอียด" (เช่นรับTransactionIDs และTransactionDates) แนวคิดคือการดำเนินการค้นหาที่ง่ายมากและไม่อนุญาตให้เครื่องมือเพิ่มประสิทธิภาพสับสนเมื่อเข้าร่วม เห็นได้ชัดว่านี่ไม่ใช่ข้อได้เปรียบเสมอไปเพราะมันไม่อนุญาตให้ตัวออปติไมซ์ทำงาน แต่อย่างที่เราเห็นในผลลัพธ์ขึ้นอยู่กับชนิดของแบบสอบถามวิธีนี้ไม่มีข้อดี

ความแตกต่างระหว่างรสชาติที่หลากหลายของวิธีนี้คือ:

  • ค่าคงที่:ส่งค่าที่เปลี่ยนได้ใด ๆ เป็นค่าคงที่แบบอินไลน์แทนที่จะเป็นพารามิเตอร์ สิ่งนี้จะอ้างถึงProductIDในการทดสอบทั้งสามครั้งและจำนวนแถวที่จะส่งคืนในการทดสอบ 2 เนื่องจากเป็นฟังก์ชันของ " DaysToManufactureแอตทริบิวต์ผลิตภัณฑ์ห้าเท่า" วิธีการย่อยนี้หมายความว่าแต่ละProductIDแผนจะได้รับแผนการปฏิบัติของตนเองซึ่งจะเป็นประโยชน์หากมีการกระจายข้อมูลที่ProductIDหลากหลาย แต่หากมีการเปลี่ยนแปลงเล็กน้อยในการกระจายข้อมูลค่าใช้จ่ายในการสร้างแผนเพิ่มเติมจะไม่คุ้มค่า

  • พารามิเตอร์:ส่งอย่างน้อยProductIDเป็น@ProductIDอนุญาตการแคชแผนปฏิบัติการและนำมาใช้ใหม่ มีตัวเลือกการทดสอบเพิ่มเติมเพื่อใช้กับจำนวนตัวแปรแถวเพื่อส่งคืนการทดสอบ 2 เป็นพารามิเตอร์

  • ไม่ทราบการเพิ่มประสิทธิภาพ:เมื่อมีการอ้างอิงProductIDว่า@ProductIDหากมีการกระจายข้อมูลที่หลากหลายมีความเป็นไปได้ที่จะแคชแผนที่มีผลกระทบเชิงลบกับProductIDค่าอื่น ๆดังนั้นจึงเป็นการดีที่จะทราบว่าการใช้ Query Hint นี้ช่วยอะไรบ้าง

  • ผลิตภัณฑ์แคช:แทนที่จะสืบค้นProduction.Productตารางในแต่ละครั้งเพียงเพื่อให้ได้รายการที่เหมือนกันเท่านั้นให้เรียกใช้แบบสอบถามหนึ่งครั้ง (และในขณะที่เราอยู่ที่นี่ให้กรองรายการProductIDที่ไม่ได้อยู่ในTransactionHistoryตารางออกไปดังนั้นเราจะไม่เสียอะไรเลย ทรัพยากรที่นั่น) และแคชรายการนั้น รายการควรมีDaysToManufactureเขตข้อมูล การใช้ตัวเลือกนี้จะมีการเข้าใช้ครั้งแรกที่สูงขึ้นเล็กน้อยในการอ่านแบบลอจิคัลสำหรับการดำเนินการครั้งแรก แต่หลังจากนั้นจะเป็นเพียงTransactionHistoryตารางที่มีการสอบถาม

เฉพาะ

ตกลง แต่เป็นเช่นนั้นเป็นไปได้อย่างไรที่จะออกแบบสอบถามย่อยทั้งหมดเป็นแบบสอบถามแยกต่างหากโดยไม่ใช้ CURSOR และทิ้งแต่ละผลลัพธ์ที่ตั้งค่าเป็นตารางชั่วคราวหรือตัวแปรตาราง การทำวิธี CURSOR / Temp Table อย่างชัดเจนจะสะท้อนให้เห็นอย่างชัดเจนในการอ่านและเขียน ดีโดยใช้ SQLCLR :) ด้วยการสร้าง SQLCLR ที่เก็บไว้ฉันสามารถเปิดชุดผลลัพธ์และสตรีมผลลัพธ์ของแบบสอบถามย่อยแต่ละรายการเป็นชุดผลลัพธ์ต่อเนื่องได้ (ไม่ใช่ชุดผลลัพธ์หลายชุด) ด้านนอกของข้อมูลผลิตภัณฑ์ (เช่นProductID, NameและDaysToManufacture) ไม่มีผลลัพธ์แบบสอบถามย่อยใด ๆ ที่จะถูกเก็บไว้ที่ใดก็ได้ (หน่วยความจำหรือดิสก์) และเพิ่งถูกส่งผ่านเป็นชุดผลลัพธ์หลักของกระบวนงานที่เก็บไว้ SQLCLR TransactionHistoryนี้ได้รับอนุญาตให้ผมทำแบบสอบถามง่ายๆในการได้รับข้อมูลสินค้าแล้ววงจรผ่านมันออกคำสั่งที่ง่ายมากกับ

และนี่คือเหตุผลที่ฉันต้องใช้ SQL Server Profiler เพื่อรวบรวมสถิติ SQLCLR ขั้นตอนการเก็บไม่ได้กลับแผนการดำเนินการอย่างใดอย่างหนึ่งโดยการตั้งค่า "รวมแผนการดำเนินการที่เกิดขึ้นจริง" SET STATISTICS XML ON;ตัวเลือกแบบสอบถามหรือโดยการออก

สำหรับการแคชข้อมูลผลิตภัณฑ์ฉันใช้readonly staticรายการทั่วไป (เช่น_GlobalProductsในรหัสด้านล่าง) ดูเหมือนว่าการเพิ่มไปยังคอลเลกชันไม่ได้ละเมิดreadonlyตัวเลือกดังนั้นรหัสนี้จะทำงานเมื่อแอสเซมบลีที่มีPERMISSON_SETของSAFE:) แม้ว่าที่เป็นเคาน์เตอร์ที่ใช้งานง่าย

แบบสอบถามที่สร้างขึ้น

แบบสอบถามที่สร้างโดย SQLCLR นี้กระบวนงานที่เก็บไว้มีดังนี้:

ข้อมูลสินค้า

หมายเลขทดสอบ 1 และ 3 (ไม่มีแคช)

SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM   Production.Product prod1
WHERE  prod1.Name LIKE N'[M-R]%';

หมายเลขทดสอบ 2 (ไม่มีการแคช)

;WITH cte AS
(
    SELECT prod1.ProductID
    FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
    WHERE  prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM   Production.Product prod2
INNER JOIN cte
        ON cte.ProductID = prod2.ProductID;

หมายเลขทดสอบ 1, 2 และ 3 (การแคช)

;WITH cte AS
(
    SELECT prod1.ProductID
    FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
    WHERE  prod1.Name LIKE N'[M-R]%'
    AND    EXISTS (
                SELECT *
                FROM Production.TransactionHistory th
                WHERE th.ProductID = prod1.ProductID
                  )
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM   Production.Product prod2
INNER JOIN cte
        ON cte.ProductID = prod2.ProductID;

ข้อมูลการทำธุรกรรม

หมายเลขทดสอบ 1 และ 2 (ค่าคงที่)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC;

หมายเลขทดสอบ 1 และ 2 (ปรับพารามิเตอร์)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

หมายเลขทดสอบ 1 และ 2 (Parameterized + OPTIMIZE UNKNOWN)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

หมายเลขทดสอบ 2 (Parameterized ทั้งสอง)

SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

หมายเลขทดสอบ 2 (ปรับพารามิเตอร์ทั้งสอง + เพิ่มประสิทธิภาพไม่รู้จัก)

SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

หมายเลขทดสอบ 3 (ค่าคงที่)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;

หมายเลขทดสอบ 3 (Parameterized)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;

หมายเลขทดสอบ 3 (ปรับพารามิเตอร์ + เพิ่มประสิทธิภาพไม่รู้จัก)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

รหัส

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class ObligatoryClassName
{
    private class ProductInfo
    {
        public int ProductID;
        public string Name;
        public int DaysToManufacture;

        public ProductInfo(int ProductID, string Name, int DaysToManufacture)
        {
            this.ProductID = ProductID;
            this.Name = Name;
            this.DaysToManufacture = DaysToManufacture;

            return;
        }
    }

    private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();

    private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
    {
        if (_GlobalProducts.Count > 0)
        {
            if (PrintQuery.IsTrue)
            {
                SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
                            " entries :)"));
            }

            return;
        }

        SqlConnection _Connection = new SqlConnection("Context Connection = true;");
        SqlCommand _Command = new SqlCommand();
        _Command.CommandType = CommandType.Text;
        _Command.Connection = _Connection;
        _Command.CommandText = @"
   ;WITH cte AS
   (
     SELECT prod1.ProductID
     FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
     WHERE  prod1.Name LIKE N'[M-R]%'
     AND    EXISTS (
                     SELECT *
                     FROM Production.TransactionHistory th
                     WHERE th.ProductID = prod1.ProductID
                   )
   )
   SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
   FROM   Production.Product prod2
   INNER JOIN cte
           ON cte.ProductID = prod2.ProductID;
";

        SqlDataReader _Reader = null;

        try
        {
            _Connection.Open();

            _Reader = _Command.ExecuteReader();

            while (_Reader.Read())
            {
                _GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
                                                    _Reader.GetInt32(2)));
            }
        }
        catch
        {
            throw;
        }
        finally
        {
            if (_Reader != null && !_Reader.IsClosed)
            {
                _Reader.Close();
            }

            if (_Connection != null && _Connection.State != ConnectionState.Closed)
            {
                _Connection.Close();
            }

            if (PrintQuery.IsTrue)
            {
                SqlContext.Pipe.Send(_Command.CommandText);
            }
        }

        return;
    }


    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void GetTopRowsPerGroup(SqlByte TestNumber,
        SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
        SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
    {
        SqlConnection _Connection = new SqlConnection("Context Connection = true;");
        SqlCommand _Command = new SqlCommand();
        _Command.CommandType = CommandType.Text;
        _Command.Connection = _Connection;

        List<ProductInfo> _Products = null;
        SqlDataReader _Reader = null;

        int _RowsToGet = 5; // default value is for Test Number 1
        string _OrderByTransactionID = "";
        string _OptimizeForUnknown = "";
        CommandBehavior _CmdBehavior = CommandBehavior.Default;

        if (OptimizeForUnknown.IsTrue)
        {
            _OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
        }

        if (UseSequentialAccess.IsTrue)
        {
            _CmdBehavior = CommandBehavior.SequentialAccess;
        }

        if (CacheProducts.IsTrue)
        {
            PopulateGlobalProducts(PrintQueries);
        }
        else
        {
            _Products = new List<ProductInfo>();
        }


        if (TestNumber.Value == 2)
        {
            _Command.CommandText = @"
   ;WITH cte AS
   (
     SELECT prod1.ProductID
     FROM   Production.Product prod1 WITH (INDEX(AK_Product_Name))
     WHERE  prod1.Name LIKE N'[M-R]%'
   )
   SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
   FROM   Production.Product prod2
   INNER JOIN cte
           ON cte.ProductID = prod2.ProductID;
";
        }
        else
        {
            _Command.CommandText = @"
     SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
     FROM   Production.Product prod1
     WHERE  prod1.Name LIKE N'[M-R]%';
";
            if (TestNumber.Value == 3)
            {
                _RowsToGet = 1;
                _OrderByTransactionID = ", th.TransactionID DESC";
            }
        }

        try
        {
            _Connection.Open();

            // Populate Product list for this run if not using the Product Cache
            if (!CacheProducts.IsTrue)
            {
                _Reader = _Command.ExecuteReader(_CmdBehavior);

                while (_Reader.Read())
                {
                    _Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
                                                  _Reader.GetInt32(2)));
                }

                _Reader.Close();

                if (PrintQueries.IsTrue)
                {
                    SqlContext.Pipe.Send(_Command.CommandText);
                }
            }
            else
            {
                _Products = _GlobalProducts;
            }

            SqlDataRecord _ResultRow = new SqlDataRecord(
                new SqlMetaData[]{
                    new SqlMetaData("ProductID", SqlDbType.Int),
                    new SqlMetaData("Name", SqlDbType.NVarChar, 50),
                    new SqlMetaData("TransactionID", SqlDbType.Int),
                    new SqlMetaData("TransactionDate", SqlDbType.DateTime)
                });

            SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
            _Command.Parameters.Add(_ProductID);
            SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
            _Command.Parameters.Add(_RowsToReturn);

            SqlContext.Pipe.SendResultsStart(_ResultRow);

            for (int _Row = 0; _Row < _Products.Count; _Row++)
            {
                // Tests 1 and 3 use previously set static values for _RowsToGet
                if (TestNumber.Value == 2)
                {
                    if (_Products[_Row].DaysToManufacture == 0)
                    {
                        continue; // no use in issuing SELECT TOP (0) query
                    }

                    _RowsToGet = (5 * _Products[_Row].DaysToManufacture);
                }

                _ResultRow.SetInt32(0, _Products[_Row].ProductID);
                _ResultRow.SetString(1, _Products[_Row].Name);

                switch (ParameterizeProductID.Value)
                {
                    case 0x01:
                        _Command.CommandText = String.Format(@"
   SELECT TOP ({0}) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = @ProductID
   ORDER BY th.TransactionDate DESC{2}
   {1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);

                        _ProductID.Value = _Products[_Row].ProductID;
                        break;
                    case 0x02:
                        _Command.CommandText = String.Format(@"
   SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = @ProductID
   ORDER BY th.TransactionDate DESC
   {0};
", _OptimizeForUnknown);

                        _ProductID.Value = _Products[_Row].ProductID;
                        _RowsToReturn.Value = _RowsToGet;
                        break;
                    default:
                        _Command.CommandText = String.Format(@"
   SELECT TOP ({0}) th.TransactionID, th.TransactionDate
   FROM   Production.TransactionHistory th
   WHERE  th.ProductID = {1}
   ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
                        break;
                }


                _Reader = _Command.ExecuteReader(_CmdBehavior);

                while (_Reader.Read())
                {
                    _ResultRow.SetInt32(2, _Reader.GetInt32(0));
                    _ResultRow.SetDateTime(3, _Reader.GetDateTime(1));

                    SqlContext.Pipe.SendResultsRow(_ResultRow);
                }
                _Reader.Close();
            }

        }
        catch
        {
            throw;
        }
        finally
        {
            if (SqlContext.Pipe.IsSendingResults)
            {
                SqlContext.Pipe.SendResultsEnd();
            }

            if (_Reader != null && !_Reader.IsClosed)
            {
                _Reader.Close();
            }

            if (_Connection != null && _Connection.State != ConnectionState.Closed)
            {
                _Connection.Close();
            }

            if (PrintQueries.IsTrue)
            {
                SqlContext.Pipe.Send(_Command.CommandText);
            }
        }


    }
}

แบบสอบถามการทดสอบ

ไม่มีที่ว่างพอที่จะโพสต์การทดสอบที่นี่ดังนั้นฉันจะหาที่ตั้งอื่น

บทสรุป

สำหรับบางสถานการณ์ SQLCLR สามารถใช้เพื่อจัดการลักษณะบางอย่างของแบบสอบถามที่ไม่สามารถทำได้ใน T-SQL และมีความสามารถในการใช้หน่วยความจำสำหรับแคชแทนตารางชั่วคราวแม้ว่าควรจะทำอย่างระมัดระวังและรอบคอบเนื่องจากหน่วยความจำไม่ได้รับการปล่อยตัวกลับไปที่ระบบโดยอัตโนมัติ วิธีนี้ไม่ใช่สิ่งที่จะช่วยให้การสืบค้นเฉพาะกิจแม้ว่าจะเป็นไปได้ที่จะทำให้มีความยืดหยุ่นมากกว่าที่ฉันได้แสดงไว้ที่นี่เพียงแค่เพิ่มพารามิเตอร์เพื่อปรับแต่งแง่มุมเพิ่มเติมของแบบสอบถามที่กำลังดำเนินการ


UPDATE

การทดสอบเพิ่มเติมการทดสอบ
ดั้งเดิมของฉันที่มีดัชนีสนับสนุนอยู่TransactionHistoryใช้คำจำกัดความต่อไปนี้:

ProductID ASC, TransactionDate DESC

ฉันตัดสินใจในเวลาที่จะสละรวมถึงTransactionId DESCในตอนท้ายการหาว่าในขณะที่มันอาจช่วยทดสอบหมายเลข 3 (ซึ่งระบุ tie- TransactionIdbreak ในล่าสุด- ดีสันนิษฐานว่า "ล่าสุด" เพราะไม่ได้ระบุไว้อย่างชัดเจน แต่ทุกคนดูเหมือน เพื่อยอมรับข้อสันนิษฐานนี้) มีความเป็นไปได้ไม่มากพอที่จะสร้างความแตกต่าง

แต่จากนั้นแอรอนสอบกลับด้วยดัชนีการสนับสนุนที่รวมอยู่TransactionId DESCและพบว่าCROSS APPLYวิธีการดังกล่าวเป็นผู้ชนะในการทดสอบทั้งสามครั้ง นี่แตกต่างจากการทดสอบของฉันซึ่งระบุว่าวิธี CTE นั้นดีที่สุดสำหรับหมายเลขทดสอบ 3 (เมื่อไม่ใช้แคชซึ่งสะท้อนการทดสอบของแอรอน) เห็นได้ชัดว่ามีรูปแบบเพิ่มเติมที่จำเป็นต้องทำการทดสอบ

ฉันลบดัชนีสนับสนุนปัจจุบันสร้างขึ้นใหม่ด้วยTransactionIdและล้างแคชแผน (เพื่อให้แน่ใจ):

DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;

CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
    ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
    WITH (FILLFACTOR = 100);

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

ฉันทำการทดสอบหมายเลข 1 อีกครั้งและผลลัพธ์ก็เหมือนกันตามที่คาดไว้ จากนั้นฉันก็ทำการทดสอบหมายเลข 3 อีกครั้งและผลลัพธ์ก็เปลี่ยนไปจริง ๆ :

ทดสอบ 3 ผลลัพธ์ - พร้อมดัชนีสนับสนุน (พร้อม TransactionId DESC)
ผลลัพธ์ข้างต้นใช้สำหรับการทดสอบที่ไม่ได้มาตรฐาน คราวนี้ไม่เพียงCROSS APPLYเอาชนะ CTE (เช่นเดียวกับการทดสอบของแอรอน) แต่ SQLCLR proc นำโดย 30 อ่าน (woo hoo)

ทดสอบ 3 ผลลัพธ์ - พร้อมดัชนีสนับสนุน (พร้อม TransactionId DESC) และแคช
ผลลัพธ์ข้างต้นสำหรับการทดสอบที่เปิดใช้งานแคช เวลานี้ประสิทธิภาพของ CTE จะไม่ลดลงแม้ว่าจะCROSS APPLYยังคงเต้นอยู่ อย่างไรก็ตามตอนนี้ pro SQLCLR จะเป็นผู้นำโดย 23 อ่าน (woo hoo อีกครั้ง)

ใช้สิ่งที่ได้

  1. มีตัวเลือกต่าง ๆ ให้ใช้ ทางที่ดีควรลองใช้หลาย ๆ วิธีเนื่องจากแต่ละคนมีจุดแข็ง การทดสอบที่ทำในที่นี้จะแสดงความแตกต่างกันเล็กน้อยในทั้งการอ่านและระยะเวลาระหว่างนักแสดงที่ดีที่สุดและแย่ที่สุดในการทดสอบทั้งหมด (พร้อมดัชนีสนับสนุน); รูปแบบในการอ่านคือประมาณ 350 และระยะเวลาคือ 55 มิลลิวินาที ในขณะที่ SQLCLR proc ชนะการทดสอบทั้งหมด 1 ครั้ง (ในแง่ของการอ่าน) การบันทึกเพียงไม่กี่การอ่านมักจะไม่คุ้มค่าใช้จ่ายในการบำรุงรักษาเส้นทาง SQLCLR แต่ใน AdventureWorks2012 Productตารางมีเพียง 504 แถวและTransactionHistoryมีเพียง 113,443 แถว ความแตกต่างด้านประสิทธิภาพของวิธีการเหล่านี้อาจเด่นชัดยิ่งขึ้นเมื่อจำนวนแถวเพิ่มขึ้น

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

  3. บทเรียนที่สำคัญที่สุดที่พบที่นี่ไม่เกี่ยวกับ CROSS ใช้กับ CTE กับ SQLCLR: มันเกี่ยวกับการทดสอบ อย่าทึกทัก รับแนวคิดจากหลาย ๆ คนและทดสอบสถานการณ์ให้มากที่สุดเท่าที่จะทำได้


2
ดูการแก้ไขคำตอบของ Mikael สำหรับเหตุผลของการอ่านเชิงตรรกะเพิ่มเติมที่เกี่ยวข้องกับการสมัคร
พอลไวท์

18

APPLY TOPหรือROW_NUMBER()? มีอะไรอีกที่จะพูดเกี่ยวกับเรื่องนี้ได้มากกว่านี้?

สรุปสั้น ๆ ของความแตกต่างและจริงๆให้มันสั้นผมจะแสดงเฉพาะแผนการสำหรับตัวเลือกที่ 2 Production.TransactionHistoryและฉันได้เพิ่มดัชนีบน

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate)

row_number()แบบสอบถาม :.

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         P.DaysToManufacture,
         row_number() over(partition by P.ProductID order by T.TransactionDate desc) as rn
  from Production.Product as P
    inner join Production.TransactionHistory as T
      on P.ProductID = T.ProductID
  where P.Name >= N'M' and
        P.Name < N'S'
)
select C.TransactionID,
       C.TransactionDate
from C
where C.rn <= 5 * C.DaysToManufacture;

ป้อนคำอธิบายรูปภาพที่นี่

apply topรุ่น:

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select top(cast(5 * P.DaysToManufacture as bigint))
                T.TransactionID,
                T.TransactionDate
              from Production.TransactionHistory as T
              where P.ProductID = T.ProductID
              order by T.TransactionDate desc
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

ป้อนคำอธิบายรูปภาพที่นี่

ความแตกต่างที่สำคัญระหว่างสิ่งเหล่านี้คือapply topตัวกรองที่นิพจน์ด้านบนด้านล่างของลูปซ้อนกันที่row_numberตัวกรองเวอร์ชันหลังจากการเข้าร่วม นั่นหมายความว่ามีคนอ่านProduction.TransactionHistoryมากกว่าที่จำเป็นจริงๆ

หากมีเพียงวิธีการที่จะผลักดันผู้ประกอบการที่รับผิดชอบในการระบุแถวลงไปที่สาขาที่ต่ำกว่าก่อนที่จะเข้าร่วมแล้วrow_numberรุ่นอาจทำได้ดีกว่า

ดังนั้นเข้าสู่apply row_number()รุ่น

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select T.TransactionID,
                     T.TransactionDate
              from (
                   select T.TransactionID,
                          T.TransactionDate,
                          row_number() over(order by T.TransactionDate desc) as rn
                   from Production.TransactionHistory as T
                   where P.ProductID = T.ProductID
                   ) as T
              where T.rn <= cast(5 * P.DaysToManufacture as bigint)
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

ป้อนคำอธิบายรูปภาพที่นี่

อย่างที่คุณเห็นapply row_number()มันค่อนข้างเหมือนกันกับapply topความซับซ้อนเพียงเล็กน้อยเท่านั้น เวลาดำเนินการก็เหมือนกันหรือช้าลงเล็กน้อย

ดังนั้นทำไมฉันรำคาญที่จะเกิดขึ้นกับคำตอบที่ไม่ดีกว่าสิ่งที่เรามีอยู่แล้ว? คุณมีอีกหนึ่งสิ่งที่ต้องลองในโลกแห่งความเป็นจริงและมีความแตกต่างในการอ่าน อันที่ฉันไม่มีคำอธิบายสำหรับ *

APPLY - ROW_NUMBER
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 230, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

APPLY - TOP
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 268, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

ในขณะที่ฉันอยู่ที่มันฉันก็อาจจะโยนในrow_number()รุ่นที่สองที่ในบางกรณีอาจเป็นวิธีที่จะไป บางกรณีผู้ที่จะเป็นตอนที่คุณคาดหวังที่คุณต้องการจริงมากที่สุดของแถวจากProduction.TransactionHistoryเพราะที่นี่คุณจะได้รับการผสานเข้าร่วมระหว่างและแจกแจงProduction.ProductProduction.TransactionHistory

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         T.ProductID,
         row_number() over(partition by T.ProductID order by T.TransactionDate desc) as rn
  from Production.TransactionHistory as T
)
select C.TransactionID,
       C.TransactionDate
from C
 inner join Production.Product as P
      on P.ProductID = C.ProductID
where P.Name >= N'M' and
      P.Name < N'S' and
      C.rn <= 5 * P.DaysToManufacture;

ป้อนคำอธิบายรูปภาพที่นี่

ในการรับรูปร่างด้านบนโดยไม่มีตัวดำเนินการเรียงคุณต้องเปลี่ยนดัชนีการสนับสนุนเพื่อเรียงลำดับจากTransactionDateมากไปน้อย

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate desc)

*แก้ไข: การอ่านแบบลอจิคัลพิเศษเกิดจากการดึงข้อมูลลูปซ้อนกันที่ใช้กับ Apply-top คุณสามารถปิดการใช้งานสิ่งนี้ด้วย TF 8744 (และ / หรือ 9115 ในรุ่นที่ใหม่กว่า) ที่ยังไม่ได้ทำการประมวลผลเพื่อรับจำนวนลอจิกการอ่านเท่ากัน การดึงข้อมูลล่วงหน้าอาจเป็นข้อได้เปรียบของทางเลือกที่ใช้สูงสุดในสถานการณ์ที่เหมาะสม - พอลไวท์


11

ฉันมักจะใช้การรวมกันของ CTE และฟังก์ชั่นหน้าต่าง คุณสามารถบรรลุคำตอบนี้โดยใช้สิ่งต่อไปนี้:

;WITH GiveMeCounts
AS (
    SELECT CustomerID
        ,OrderDate
        ,TotalAmt

        ,ROW_NUMBER() OVER (
            PARTITION BY CustomerID ORDER BY 
            --You can change the following field or sort order to whatever you'd like to order by.
            TotalAmt desc
            ) AS MySeqNum
    )
SELECT CustomerID, OrderDate, TotalAmt
FROM GiveMeCounts
--Set n per group here
where MySeqNum <= 10

สำหรับส่วนเครดิตพิเศษที่กลุ่มต่าง ๆ อาจต้องการส่งคืนจำนวนแถวที่แตกต่างกันคุณสามารถใช้ตารางแยกต่างหาก ให้พูดโดยใช้เกณฑ์ทางภูมิศาสตร์เช่นรัฐ:

+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK    |        10 |
| NY    |         5 |
| NC    |        23 |
+-------+-----------+

เพื่อให้บรรลุถึงสิ่งนี้ซึ่งค่าอาจแตกต่างกันคุณจะต้องเข้าร่วม CTE ของคุณไปยังตารางสถานะคล้ายกับสิ่งนี้:

SELECT [CustomerID]
    ,[OrderDate]
    ,[TotalAmt]
    ,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
    AND gmc.MySeqNum <= st.MaxSeqNum
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.