Saya mengambil pendekatan yang sedikit berbeda, terutama untuk melihat bagaimana teknik ini akan dibandingkan dengan yang lain, karena memiliki opsi yang baik, bukan?
Pengujian
Mengapa kita tidak mulai dengan hanya melihat bagaimana berbagai metode saling menumpuk. Saya melakukan tiga set tes:
- Set pertama berjalan tanpa modifikasi DB
- Set kedua berjalan setelah indeks dibuat untuk mendukung
TransactionDate
permintaan berbasiskan terhadap Production.TransactionHistory
.
- Set ketiga membuat asumsi yang sedikit berbeda. Karena ketiga tes berjalan terhadap daftar Produk yang sama, bagaimana jika kita men-cache daftar itu? Metode saya menggunakan cache di memori sementara metode lain menggunakan tabel temp yang setara. Indeks pendukung yang dibuat untuk set tes kedua masih ada untuk set tes ini.
Detail tes tambahan:
- Pengujian dijalankan terhadap
AdventureWorks2012
SQL Server 2012, SP2 (Edisi Pengembang).
- Untuk setiap tes yang saya beri label jawaban siapa saya mengambil kueri dari dan yang kueri tertentu
- Saya menggunakan opsi "Buang hasil setelah eksekusi" pada Opsi Kueri | Hasil.
- Harap dicatat bahwa untuk dua set tes pertama,
RowCounts
tampaknya "tidak aktif" untuk metode saya. Ini karena metode saya menjadi implementasi manual dari apa CROSS APPLY
yang dilakukan: ini menjalankan kueri awal terhadap Production.Product
dan mendapatkan 161 baris kembali, yang kemudian digunakan untuk kueri terhadap Production.TransactionHistory
. Oleh karena itu, RowCount
nilai untuk entri saya selalu 161 lebih dari entri lainnya. Pada set tes ketiga (dengan caching) jumlah baris sama untuk semua metode.
- Saya menggunakan SQL Server Profiler untuk menangkap statistik alih-alih mengandalkan rencana eksekusi. Aaron dan Mikael sudah melakukan pekerjaan yang bagus untuk menunjukkan rencana untuk pertanyaan mereka dan tidak perlu mereproduksi informasi itu. Dan maksud dari metode saya adalah untuk mengurangi pertanyaan menjadi bentuk yang sederhana sehingga tidak terlalu masalah. Ada alasan tambahan untuk menggunakan Profiler, tetapi itu akan disebutkan kemudian.
- Daripada menggunakan
Name >= N'M' AND Name < N'S'
konstruk, saya memilih untuk menggunakan Name LIKE N'[M-R]%'
, dan SQL Server memperlakukan mereka sama.
Hasil
Tidak Ada Indeks Pendukung
Ini pada dasarnya AdventureWorks2012 out-of-the-box. Dalam semua kasus, metode saya jelas lebih baik daripada yang lain, tetapi tidak pernah sebagus metode 1 atau 2 teratas.
Tes 1
CTE Harun jelas merupakan pemenang di sini.
Tes 2
CTE Harun (lagi) dan apply row_number()
metode kedua Mikael adalah yang kedua.
Tes 3
CTE Harun (lagi) adalah pemenangnya.
Kesimpulan
Ketika tidak ada indeks pendukung aktif TransactionDate
, metode saya lebih baik daripada melakukan standar CROSS APPLY
, tapi tetap saja, menggunakan metode CTE jelas merupakan cara yang harus dilakukan.
Dengan Indeks Pendukung (tanpa Caching)
Untuk rangkaian tes ini saya menambahkan indeks yang jelas TransactionHistory.TransactionDate
sejak semua pertanyaan mengurutkan pada bidang itu. Saya mengatakan "jelas" karena sebagian besar jawaban lain juga menyetujui hal ini. Dan karena semua kueri menginginkan tanggal terbaru, TransactionDate
bidang itu harus dipesan DESC
, jadi saya hanya mengambil CREATE INDEX
pernyataan di bagian bawah jawaban Mikael dan menambahkan secara eksplisit FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Setelah indeks ini berada di tempat, hasilnya berubah sedikit.
Tes 1
Kali ini metode saya yang keluar, paling tidak dalam hal Logical Reads. The CROSS APPLY
metode, sebelumnya pemain terburuk untuk Test 1, menang pada Durasi dan bahkan mengalahkan metode CTE pada Logical Membaca.
Tes 2
Kali ini adalah apply row_number()
metode pertama Mikael yang menjadi pemenang ketika melihat Reads, sedangkan sebelumnya itu adalah salah satu yang berkinerja terburuk. Dan sekarang metode saya masuk di tempat kedua yang sangat dekat ketika melihat Baca. Bahkan, di luar metode CTE, sisanya cukup dekat dalam hal Baca.
Tes 3
Di sini CTE masih menjadi pemenang, tetapi sekarang perbedaan antara metode lain hampir tidak terlihat dibandingkan dengan perbedaan drastis yang ada sebelum membuat indeks.
Kesimpulan
Penerapan metode saya lebih jelas sekarang, meskipun kurang tangguh untuk tidak memiliki indeks yang tepat di tempat.
Dengan Indeks Pendukung DAN Caching
Untuk serangkaian tes ini saya memanfaatkan caching karena, well, mengapa tidak? Metode saya memungkinkan untuk menggunakan cache dalam memori yang metode lain tidak dapat mengakses. Jadi agar adil, saya membuat tabel temp berikut yang digunakan Product.Product
untuk semua referensi dalam metode-metode lain di ketiga tes. The DaysToManufacture
lapangan hanya digunakan dalam Uji Nomor 2, tapi itu lebih mudah untuk konsisten di seluruh skrip SQL untuk menggunakan meja yang sama dan tidak ada salahnya untuk memilikinya di sana.
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);
Tes 1
Semua metode tampaknya mendapat manfaat yang sama dari caching, dan metode saya masih keluar di depan.
Tes 2
Di sini kita sekarang melihat perbedaan dalam formasi karena metode saya keluar sedikit di depan, hanya 2 Membaca lebih baik daripada apply row_number()
metode pertama Mikael , sedangkan tanpa caching metode saya di belakang oleh 4 Membaca.
Tes 3
Silakan lihat pembaruan di bagian bawah (di bawah garis) . Di sini kita kembali melihat beberapa perbedaan. Rasa "parameter" dari metode saya sekarang hampir tidak terbawa oleh 2 Baca dibandingkan dengan metode SALING SALIB Harun (tanpa caching mereka sama). Tetapi hal yang sangat aneh adalah bahwa untuk pertama kalinya kita melihat metode yang dipengaruhi secara negatif oleh caching: Metode CTE Harun (yang sebelumnya merupakan yang terbaik untuk Tes Nomor 3). Tapi, saya tidak akan mengambil kredit di mana tidak jatuh tempo, dan karena tanpa caching metode CTE Harun masih lebih cepat daripada metode saya di sini dengan caching, pendekatan terbaik untuk situasi khusus ini tampaknya adalah metode CTE Aaron.
Kesimpulan Silakan lihat pembaruan di bagian bawah (di bawah garis)
Situasi yang membuat penggunaan berulang hasil kueri sekunder sering dapat (tetapi tidak selalu) mendapat manfaat dari caching hasil tersebut. Tetapi ketika caching bermanfaat, menggunakan memori untuk kata caching memiliki beberapa keuntungan dibandingkan menggunakan tabel sementara.
Metode
Umumnya
Saya memisahkan kueri "header" (yaitu mendapatkan ProductID
s, dan dalam satu kasus juga DaysToManufacture
, berdasarkan Name
permulaan dengan huruf-huruf tertentu) dari kueri "detail" (yaitu mendapatkan TransactionID
s dan TransactionDate
s). Konsepnya adalah melakukan kueri yang sangat sederhana dan tidak membuat pengoptimal menjadi bingung ketika BERGABUNG dengannya. Jelas ini tidak selalu menguntungkan karena juga melarang pengoptimal dari, yah, mengoptimalkan. Tetapi seperti yang kita lihat di hasil, tergantung pada jenis permintaan, metode ini memang memiliki kelebihan.
Perbedaan antara berbagai rasa dari metode ini adalah:
Konstanta: Kirim nilai yang dapat diganti sebagai konstanta sebaris alih-alih sebagai parameter. Ini akan merujuk ProductID
pada ketiga tes dan juga jumlah baris untuk kembali dalam Tes 2 karena itu adalah fungsi "lima kali DaysToManufacture
atribut Produk". Sub-metode ini berarti bahwa masing-masing ProductID
akan mendapatkan rencana pelaksanaannya sendiri, yang dapat bermanfaat jika ada variasi luas dalam distribusi data untuk ProductID
. Tetapi jika ada sedikit variasi dalam distribusi data, biaya pembuatan rencana tambahan kemungkinan tidak akan sepadan.
Parameterisasi: Kirim setidaknya ProductID
sebagai @ProductID
, memungkinkan untuk caching rencana dan penggunaan kembali. Ada opsi tes tambahan untuk juga memperlakukan jumlah variabel baris untuk kembali untuk Uji 2 sebagai parameter.
Optimalkan Tidak Diketahui: Ketika merujuk ProductID
sebagai @ProductID
, jika ada variasi luas distribusi data, maka mungkin untuk men-cache rencana yang memiliki efek negatif pada ProductID
nilai - nilai lain sehingga akan baik untuk mengetahui apakah menggunakan Petunjuk Kueri ini membantu.
Cache Products: Daripada menanyakan Production.Product
tabel setiap kali, hanya untuk mendapatkan daftar yang sama persis, jalankan kueri sekali (dan sementara kami melakukannya, filter semua ProductID
yang bahkan tidak ada dalam TransactionHistory
tabel sehingga kami tidak membuang apa pun sumber daya di sana) dan cache daftar itu. Daftar harus menyertakan DaysToManufacture
bidang. Menggunakan opsi ini ada klik awal yang sedikit lebih tinggi pada Logical Reads untuk eksekusi pertama, tetapi setelah itu hanya TransactionHistory
tabel yang ditanyai.
Secara khusus
Ok, tapi begitu, um, bagaimana mungkin untuk mengeluarkan semua sub-kueri sebagai permintaan terpisah tanpa menggunakan CURSOR dan membuang setiap hasil yang disetel ke tabel sementara atau variabel tabel? Jelas melakukan metode CURSOR / Tabel Temp akan mencerminkan cukup jelas dalam Baca dan Tulis. Nah, dengan menggunakan SQLCLR :). Dengan membuat prosedur tersimpan SQLCLR, saya bisa membuka set hasil dan pada dasarnya mengalirkan hasil dari setiap sub-query ke sana, sebagai set hasil yang berkelanjutan (dan bukan beberapa set hasil). Di luar Informasi produk (yaitu ProductID
, Name
, danDaysToManufacture
), tidak ada hasil sub-query yang harus disimpan di mana saja (memori atau disk) dan baru saja dilewati sebagai set hasil utama dari prosedur tersimpan SQLCLR. Ini memungkinkan saya untuk melakukan permintaan sederhana untuk mendapatkan info Produk dan kemudian memutarnya, mengeluarkan pertanyaan yang sangat sederhana TransactionHistory
.
Dan, inilah mengapa saya harus menggunakan SQL Server Profiler untuk menangkap statistik. Prosedur tersimpan SQLCLR tidak mengembalikan rencana eksekusi, baik dengan menetapkan Opsi Permintaan "Sertakan Rencana Eksekusi Aktual", atau dengan mengeluarkan SET STATISTICS XML ON;
.
Untuk caching Info Produk, saya menggunakan readonly static
Daftar Generik (yaitu _GlobalProducts
dalam kode di bawah). Tampaknya menambahkan ke koleksi tidak melanggar readonly
opsi, maka kode ini berfungsi ketika majelis memiliki PERMISSON_SET
of SAFE
:), bahkan jika itu kontra-intuitif.
Kueri yang Dihasilkan
Pertanyaan yang dihasilkan oleh prosedur tersimpan SQLCLR ini adalah sebagai berikut:
Info produk
Nomor Tes 1 dan 3 (tanpa Caching)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Tes Nomor 2 (tanpa Caching)
;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;
Nomor Tes 1, 2, dan 3 (Caching)
;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;
Info Transaksi
Bilangan Tes 1 dan 2 (Konstanta)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Bilangan Tes 1 dan 2 (Parameterisasi)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Bilangan Tes 1 dan 2 (Parameter + MENGOPTIMALKAN TIDAK DIKENAL)
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));
Test Number 2 (Parameterisasi Keduanya)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Tes Nomor 2 (Parameterisasi Keduanya + OPTIMASI TIDAK DIKETAHUI)
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));
Tes Nomor 3 (Konstanta)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Tes Nomor 3 (Parameter)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Test Number 3 (Parameterized + MENGOPTIMALKAN TIDAK DIKETAHUI)
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));
Kode
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);
}
}
}
}
Pertanyaan Tes
Tidak ada cukup ruang untuk memposting tes di sini jadi saya akan menemukan lokasi lain.
Kesimpulannya
Untuk skenario tertentu, SQLCLR dapat digunakan untuk memanipulasi aspek kueri tertentu yang tidak dapat dilakukan dalam T-SQL. Dan ada kemampuan untuk menggunakan memori untuk caching daripada tabel temp, meskipun itu harus dilakukan dengan hemat dan hati-hati karena memori tidak secara otomatis dilepaskan kembali ke sistem. Metode ini juga bukan sesuatu yang akan membantu permintaan ad hoc, meskipun dimungkinkan untuk membuatnya lebih fleksibel daripada yang saya tunjukkan di sini hanya dengan menambahkan parameter untuk menyesuaikan lebih banyak aspek permintaan yang dieksekusi.
MEMPERBARUI
Tes Tambahan Tes
asli saya yang menyertakan indeks pendukung TransactionHistory
menggunakan definisi berikut:
ProductID ASC, TransactionDate DESC
Saya telah memutuskan pada saat itu untuk melupakan termasuk TransactionId DESC
pada akhirnya, memperkirakan bahwa sementara itu mungkin dapat membantu Tes Nomor 3 (yang menentukan pengikatan tie-on pada yang terbaru - TransactionId
yah, "paling baru" diasumsikan karena tidak dinyatakan secara eksplisit, tetapi semua orang tampaknya untuk menyetujui asumsi ini), kemungkinan tidak akan ada ikatan yang cukup untuk membuat perbedaan.
Tapi, kemudian Aaron menguji ulang dengan indeks pendukung yang memasukkan TransactionId DESC
dan menemukan bahwa CROSS APPLY
metode tersebut adalah pemenang di ketiga tes. Ini berbeda dari pengujian saya yang menunjukkan bahwa metode CTE terbaik untuk Tes Nomor 3 (ketika tidak ada caching yang digunakan, yang mencerminkan tes Aaron). Jelas bahwa ada variasi tambahan yang perlu diuji.
Saya menghapus indeks pendukung saat ini, membuat yang baru dengan TransactionId
, dan membersihkan cache rencana (hanya untuk memastikan):
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;
Saya menjalankan kembali Tes Nomor 1 dan hasilnya sama, seperti yang diharapkan. Saya kemudian menjalankan kembali Tes Nomor 3 dan hasilnya memang berubah:
Hasil di atas adalah untuk tes standar, non-caching. Kali ini, tidak hanya CROSS APPLY
mengalahkan CTE (seperti yang ditunjukkan oleh tes Harun), tetapi SQLCLR proc memimpin dengan 30 Reads (woo hoo).
Hasil di atas untuk pengujian dengan caching diaktifkan. Kali ini kinerja CTE tidak menurun, meskipun CROSS APPLY
masih mengalahkannya. Namun, sekarang SQLCLR proc memimpin dengan 23 Reads (woo hoo, lagi).
Ambil Aways
Ada berbagai opsi untuk digunakan. Yang terbaik adalah mencoba beberapa karena mereka masing-masing memiliki kekuatan. Tes yang dilakukan di sini menunjukkan varians yang agak kecil dalam Baca dan Durasi antara yang terbaik dan yang terburuk di semua tes (dengan indeks pendukung); variasi dalam Bacaan sekitar 350 dan Durasi adalah 55 ms. Sementara SQLCLR proc memang menang di semua kecuali 1 tes (dalam hal Baca), hanya menyimpan beberapa Baca biasanya tidak sebanding dengan biaya pemeliharaan untuk pergi rute SQLCLR. Namun dalam AdventureWorks2012, Product
tabel hanya memiliki 504 baris dan TransactionHistory
hanya memiliki 113.443 baris. Perbedaan kinerja di seluruh metode ini mungkin menjadi lebih jelas ketika jumlah baris meningkat.
Sementara pertanyaan ini khusus untuk mendapatkan satu set baris tertentu, tidak boleh diabaikan bahwa faktor tunggal terbesar dalam kinerja adalah pengindeksan dan bukan SQL tertentu. Indeks yang baik harus ada sebelum menentukan metode mana yang benar-benar terbaik.
Pelajaran paling penting yang ditemukan di sini bukan tentang CROSS APPLY vs CTE vs SQLCLR: ini tentang MENGUJI. Jangan berasumsi. Dapatkan ide dari beberapa orang dan uji skenario sebanyak mungkin.