ฉันใช้วิธีที่แตกต่างกันเล็กน้อยโดยเฉพาะอย่างยิ่งเพื่อดูว่าเทคนิคนี้จะเปรียบเทียบกับคนอื่น ๆ อย่างไรเพราะการมีตัวเลือกดีใช่ไหม?
การทดสอบ
ทำไมเราไม่เริ่มด้วยการดูว่าวิธีการต่าง ๆ นั้นเรียงซ้อนกัน ฉันทำการทดสอบสามชุด:
- ชุดแรกรันโดยไม่มีการแก้ไข DB
- ชุดที่สองวิ่งตามหลังดัชนีถูกสร้างขึ้นเพื่อรองรับคำสั่งชั่นกับ
TransactionDateProduction.TransactionHistory
- ชุดที่สามทำให้สมมติฐานแตกต่างกันเล็กน้อย เนื่องจากการทดสอบทั้งสามนั้นใช้กับรายการผลิตภัณฑ์เดียวกันจะเกิดอะไรขึ้นถ้าเราแคชรายการนั้น วิธีการของฉันใช้แคชในหน่วยความจำในขณะที่วิธีอื่นใช้ตาราง 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

CTE ของ Aaron เป็นผู้ชนะอย่างชัดเจนที่นี่
ทดสอบ 2

Aaron's CTE (อีกครั้ง) และapply row_number()วิธีที่สองของ Mikael นั้นใกล้เคียงกัน
ทดสอบ 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

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

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

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

ตอนนี้เราเห็นความแตกต่างในกลุ่มผู้เล่นตัวจริงเมื่อวิธีการของฉันออกมาข้างหน้าแทบจะ 2 อ่านดีกว่าapply row_number()วิธีแรกของมิคาเอล
ทดสอบ 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 อีกครั้งและผลลัพธ์ก็เปลี่ยนไปจริง ๆ :

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

ผลลัพธ์ข้างต้นสำหรับการทดสอบที่เปิดใช้งานแคช เวลานี้ประสิทธิภาพของ CTE จะไม่ลดลงแม้ว่าจะCROSS APPLYยังคงเต้นอยู่ อย่างไรก็ตามตอนนี้ pro SQLCLR จะเป็นผู้นำโดย 23 อ่าน (woo hoo อีกครั้ง)
ใช้สิ่งที่ได้
มีตัวเลือกต่าง ๆ ให้ใช้ ทางที่ดีควรลองใช้หลาย ๆ วิธีเนื่องจากแต่ละคนมีจุดแข็ง การทดสอบที่ทำในที่นี้จะแสดงความแตกต่างกันเล็กน้อยในทั้งการอ่านและระยะเวลาระหว่างนักแสดงที่ดีที่สุดและแย่ที่สุดในการทดสอบทั้งหมด (พร้อมดัชนีสนับสนุน); รูปแบบในการอ่านคือประมาณ 350 และระยะเวลาคือ 55 มิลลิวินาที ในขณะที่ SQLCLR proc ชนะการทดสอบทั้งหมด 1 ครั้ง (ในแง่ของการอ่าน) การบันทึกเพียงไม่กี่การอ่านมักจะไม่คุ้มค่าใช้จ่ายในการบำรุงรักษาเส้นทาง SQLCLR แต่ใน AdventureWorks2012 Productตารางมีเพียง 504 แถวและTransactionHistoryมีเพียง 113,443 แถว ความแตกต่างด้านประสิทธิภาพของวิธีการเหล่านี้อาจเด่นชัดยิ่งขึ้นเมื่อจำนวนแถวเพิ่มขึ้น
ในขณะที่คำถามนี้เป็นคำถามที่เฉพาะเจาะจงสำหรับการรับชุดของแถวที่เฉพาะเจาะจงก็ไม่ควรมองข้ามว่าปัจจัยที่ใหญ่ที่สุดในประสิทธิภาพเดียวคือการจัดทำดัชนีและไม่ใช่ SQL โดยเฉพาะ ดัชนีที่ดีจะต้องมีอยู่ก่อนที่จะกำหนดวิธีการที่ดีที่สุดอย่างแท้จริง
บทเรียนที่สำคัญที่สุดที่พบที่นี่ไม่เกี่ยวกับ CROSS ใช้กับ CTE กับ SQLCLR: มันเกี่ยวกับการทดสอบ อย่าทึกทัก รับแนวคิดจากหลาย ๆ คนและทดสอบสถานการณ์ให้มากที่สุดเท่าที่จะทำได้