ฉันใช้วิธีที่แตกต่างกันเล็กน้อยโดยเฉพาะอย่างยิ่งเพื่อดูว่าเทคนิคนี้จะเปรียบเทียบกับคนอื่น ๆ อย่างไรเพราะการมีตัวเลือกดีใช่ไหม?
การทดสอบ
ทำไมเราไม่เริ่มด้วยการดูว่าวิธีการต่าง ๆ นั้นเรียงซ้อนกัน ฉันทำการทดสอบสามชุด:
- ชุดแรกรันโดยไม่มีการแก้ไข DB
- ชุดที่สองวิ่งตามหลังดัชนีถูกสร้างขึ้นเพื่อรองรับคำสั่งชั่นกับ
TransactionDate
Production.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 ของแอรอน
บทสรุป โปรดดูการอัปเดตทางด้านล่าง (ใต้บรรทัด)
สถานการณ์ที่ใช้ผลการสืบค้นซ้ำครั้งที่สองบ่อยครั้งจะได้รับประโยชน์ (แต่ไม่เสมอไป) จากการแคชผลลัพธ์เหล่านั้น แต่เมื่อการแคชเป็นประโยชน์การใช้หน่วยความจำสำหรับการแคชดังกล่าวมีข้อดีกว่าการใช้ตารางชั่วคราว
วิธีการ
โดยทั่วไป
ฉันแยกแบบสอบถาม "ส่วนหัว" (เช่นได้รับProductID
s และในกรณีหนึ่งยังDaysToManufacture
ขึ้นอยู่กับการName
เริ่มต้นด้วยตัวอักษรบางตัว) จากแบบสอบถาม "รายละเอียด" (เช่นรับTransactionID
s และTransactionDate
s) แนวคิดคือการดำเนินการค้นหาที่ง่ายมากและไม่อนุญาตให้เครื่องมือเพิ่มประสิทธิภาพสับสนเมื่อเข้าร่วม เห็นได้ชัดว่านี่ไม่ใช่ข้อได้เปรียบเสมอไปเพราะมันไม่อนุญาตให้ตัวออปติไมซ์ทำงาน แต่อย่างที่เราเห็นในผลลัพธ์ขึ้นอยู่กับชนิดของแบบสอบถามวิธีนี้ไม่มีข้อดี
ความแตกต่างระหว่างรสชาติที่หลากหลายของวิธีนี้คือ:
ค่าคงที่:ส่งค่าที่เปลี่ยนได้ใด ๆ เป็นค่าคงที่แบบอินไลน์แทนที่จะเป็นพารามิเตอร์ สิ่งนี้จะอ้างถึง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- TransactionId
break ในล่าสุด- ดีสันนิษฐานว่า "ล่าสุด" เพราะไม่ได้ระบุไว้อย่างชัดเจน แต่ทุกคนดูเหมือน เพื่อยอมรับข้อสันนิษฐานนี้) มีความเป็นไปได้ไม่มากพอที่จะสร้างความแตกต่าง
แต่จากนั้นแอรอนสอบกลับด้วยดัชนีการสนับสนุนที่รวมอยู่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: มันเกี่ยวกับการทดสอบ อย่าทึกทัก รับแนวคิดจากหลาย ๆ คนและทดสอบสถานการณ์ให้มากที่สุดเท่าที่จะทำได้