Salah satu masalah yang lebih membingungkan untuk dipecahkan di SQL Server adalah masalah yang terkait dengan pemberian memori. Beberapa kueri membutuhkan lebih banyak memori daripada yang lain untuk dieksekusi, berdasarkan operasi apa yang perlu dilakukan (mis., sortir, hash). Pengoptimal SQL Server memperkirakan berapa banyak memori yang dibutuhkan, dan kueri harus mendapatkan hibah memori untuk mulai mengeksekusi. Ini memegang hibah itu selama eksekusi kueri - yang berarti jika pengoptimal melebih-lebihkan memori, Anda dapat mengalami masalah konkurensi. Jika meremehkan memori, maka Anda dapat melihat tumpahan di tempdb. Keduanya tidak ideal, dan ketika Anda memiliki terlalu banyak kueri yang meminta lebih banyak memori daripada yang tersedia untuk diberikan, Anda akan melihat RESOURCE_SEMAPHORE menunggu. Ada beberapa cara untuk mengatasi masalah ini, dan salah satu metode favorit saya yang baru adalah menggunakan Query Store.
Penyiapan
Kami akan menggunakan salinan WideWorldImporters yang saya kembangkan menggunakan prosedur tersimpan DataLoadSimulation.DailyProcessToCreateHistory. Tabel Sales.Orders memiliki sekitar 4,6 juta baris, dan tabel Sales.OrderLines memiliki sekitar 9,2 juta baris. Kami akan memulihkan cadangan dan mengaktifkan Query Store, dan menghapus semua data Query Store lama sehingga kami tidak mengubah metrik apa pun untuk demo ini.
Pengingat:Jangan jalankan ALTER DATABASE
USE [master]; GO RESTORE DATABASE [WideWorldImporters] FROM DISK = N'C:\Backups\WideWorldImporters.bak' WITH FILE = 1, MOVE N'WWI_Primary' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.mdf', MOVE N'WWI_UserData' TO N'C:\Databases\WideWorldImporters\WideWorldImporters_UserData.ndf', MOVE N'WWI_Log' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.ldf', NOUNLOAD, REPLACE, STATS = 5 GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON; GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE ( OPERATION_MODE = READ_WRITE, INTERVAL_LENGTH_MINUTES = 10 ); GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR; GO
Prosedur tersimpan yang akan kami gunakan untuk menguji kueri tabel Pesanan dan BarisPesanan yang disebutkan di atas berdasarkan rentang tanggal:
USE [WideWorldImporters]; GO DROP PROCEDURE IF EXISTS [Sales].[usp_OrderInfo_OrderDate]; GO CREATE PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; GO
Pengujian
Kami akan menjalankan prosedur tersimpan dengan tiga set parameter input yang berbeda:
EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO
Eksekusi pertama mengembalikan 1958 baris, yang kedua mengembalikan 267.268 baris, dan yang terakhir mengembalikan lebih dari 2,2 juta baris. Jika Anda melihat rentang tanggal, ini tidak mengejutkan – semakin besar rentang tanggal, semakin banyak data yang dikembalikan.
Karena ini adalah prosedur tersimpan, parameter input yang digunakan awalnya menentukan rencana, serta memori yang akan diberikan. Jika kita melihat rencana eksekusi aktual untuk eksekusi pertama, kita melihat loop bersarang dan pemberian memori sebesar 2656 KB.
Eksekusi berikutnya memiliki rencana yang sama (seperti yang di-cache) dan pemberian memori yang sama, tetapi kami mendapat petunjuk bahwa itu tidak cukup karena ada semacam peringatan.
Jika kita melihat di Query Store untuk prosedur tersimpan ini, kita melihat tiga eksekusi dan nilai yang sama untuk memori UsedKB, apakah kita melihat Average, Minimum, Maximum, Last, atau Standard Deviation. Catatan:informasi pemberian memori di Query Store dilaporkan sebagai jumlah halaman 8KB.
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], --memory grant (reported as the number of 8 KB pages) for the query plan within the aggregation interval [rs].[max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs].[stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] = [qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] = [qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] = [rs].[plan_id] WHERE [qsq].[object_id] = OBJECT_ID(N'Sales.usp_OrderInfo_OrderDate');
Jika kami mencari masalah pemberian memori dalam skenario ini – di mana paket di-cache dan digunakan kembali – Query Store tidak akan membantu kami.
Tetapi bagaimana jika kueri tertentu dikompilasi saat dieksekusi, baik karena petunjuk RECOMPILE atau karena ad-hoc?
Kita dapat mengubah prosedur untuk menambahkan petunjuk RECOMPILE ke pernyataan (yang direkomendasikan daripada menambahkan RECOMPILE di tingkat prosedur, atau menjalankan prosedur WITH RECOMIPLE):
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate] OPTION (RECOMPILE); GO
Sekarang kita akan menjalankan kembali prosedur kita dengan parameter input yang sama seperti sebelumnya, dan periksa outputnya:
Perhatikan bahwa kami memiliki query_id baru – teks kueri berubah karena kami menambahkan OPTION (RECOMPILE) ke dalamnya – dan kami juga memiliki dua nilai plan_id baru, dan kami memiliki nomor pemberian memori yang berbeda untuk salah satu paket kami. Untuk plan_id 5 hanya ada satu eksekusi, dan nomor pemberian memori cocok dengan eksekusi awal – jadi rencana itu untuk rentang tanggal kecil. Dua rentang tanggal yang lebih besar menghasilkan paket yang sama, tetapi ada variabilitas yang signifikan dalam pemberian memori – 94.528 untuk minimum, dan 573.568 untuk maksimum.
Jika kita melihat informasi pemberian memori menggunakan laporan Penyimpanan Kueri, variabilitas ini muncul sedikit berbeda. Membuka laporan Konsumen Sumber Daya Teratas dari database, lalu mengubah metrik menjadi Konsumsi Memori (KB) dan Rata-rata, kueri kami dengan RECOMPILE berada di urutan teratas daftar.
Di jendela ini, metrik dikumpulkan berdasarkan kueri, bukan rencana. Kueri yang kami jalankan langsung terhadap tampilan Toko Kueri yang tercantum tidak hanya query_id tetapi juga plan_id. Di sini kami dapat melihat bahwa kueri memiliki dua paket, dan kami dapat melihat keduanya di jendela ringkasan paket, tetapi metrik digabungkan untuk semua paket dalam tampilan ini.
Variabilitas dalam pemberian memori terlihat jelas ketika kita melihat langsung pada tampilan. Kami dapat menemukan kueri dengan variabilitas menggunakan UI dengan mengubah Statistik dari Avg ke StDev:
Kami dapat menemukan informasi yang sama dengan menanyakan tampilan Query Store dan memesan dengan stdev_query_max_used_memory turun. Tapi, kita juga bisa mencari berdasarkan selisih antara pemberian memori minimum dan maksimum, atau persentase selisihnya. Misalnya, jika kami khawatir tentang kasus di mana perbedaan hibah lebih besar dari 512MB, kami dapat menjalankan:
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], [rs].[max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs].[stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] = [qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] = [qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] = [rs].[plan_id] WHERE ([rs].[max_query_max_used_memory]*8) - ([rs].[min_query_max_used_memory]*8) > 524288;
Anda yang menjalankan SQL Server 2017 dengan indeks Columnstore, yang memiliki keuntungan dari umpan balik Memory Grant, juga dapat menggunakan informasi ini di Query Store. Kami pertama-tama akan mengubah tabel Pesanan kami untuk menambahkan indeks Columnstore berkerumun:
ALTER TABLE [Sales].[Invoices] DROP CONSTRAINT [FK_Sales_Invoices_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] DROP CONSTRAINT [FK_Sales_Orders_BackorderOrderID_Sales_Orders]; GO ALTER TABLE [Sales].[OrderLines] DROP CONSTRAINT [FK_Sales_OrderLines_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] DROP CONSTRAINT [PK_Sales_Orders] WITH ( ONLINE = OFF ); GO CREATE CLUSTERED COLUMNSTORE INDEX CCI_Orders ON [Sales].[Orders];
Kemudian kita akan mengatur mode kompatibilitas database ke 140 sehingga kita dapat memanfaatkan umpan balik pemberian memori:
ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140; GO
Terakhir, kami akan mengubah prosedur tersimpan kami untuk menghapus OPTION (RECOMPILE) dari kueri kami dan kemudian menjalankannya beberapa kali dengan nilai input yang berbeda:
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO
Dalam Query Store kita melihat yang berikut:
Kami memiliki paket baru untuk query_id =1, yang memiliki nilai berbeda untuk metrik pemberian memori, dan StDev yang sedikit lebih rendah daripada yang kami miliki dengan plan_id 6. Jika kami melihat paket di Query Store, kami melihat bahwa paket tersebut mengakses indeks Columnstore berkerumun :
Ingat bahwa rencana di Query Store adalah yang dijalankan, tetapi hanya berisi perkiraan. Meskipun paket dalam cache paket memiliki informasi pemberian memori yang diperbarui saat umpan balik memori terjadi, informasi ini tidak diterapkan ke paket yang ada di Query Store.
Ringkasan
Inilah yang saya sukai tentang menggunakan Query Store untuk melihat kueri dengan pemberian memori variabel:data dikumpulkan secara otomatis. Jika masalah ini muncul secara tidak terduga, kami tidak perlu menyiapkan apa pun untuk mencoba dan mengumpulkan informasi, kami sudah menangkapnya di Query Store. Dalam kasus di mana kueri diparameterisasi, mungkin lebih sulit untuk menemukan variabilitas pemberian memori karena potensi nilai statis karena caching paket. Namun, kami juga dapat menemukan bahwa, karena kompilasi ulang, kueri memiliki beberapa paket dengan nilai pemberian memori yang sangat berbeda yang dapat kami gunakan untuk melacak masalah. Ada berbagai cara untuk menyelidiki masalah menggunakan data yang diambil di Query Store, dan ini memungkinkan Anda untuk melihat masalah secara proaktif maupun reaktif.