Al giorno d'oggi, all'interno della comunità DBA di SQL Server, è estremamente probabile che utilizziamo, o almeno abbiamo sentito parlare, della famosa stored procedure sp_WhoIsActive sviluppato da Adam Machanic.
Durante il mio periodo come DBA, ho usato l'SP per controllare immediatamente cosa sta succedendo all'interno di una particolare istanza di SQL Server quando riceve tutto il "dito puntato" che una particolare applicazione sta funzionando lentamente.
Tuttavia, ci sono occasioni in cui tali problemi diventano ricorrenti e richiedono un modo per catturare ciò che sta succedendo per trovare un potenziale colpevole. Esistono anche scenari in cui sono presenti diverse istanze che fungono da back-end per applicazioni di terze parti. La procedura archiviata potrebbe funzionare potenzialmente bene per trovare i nostri colpevoli.
In questo articolo presenterò uno strumento PowerShell che può aiutare qualsiasi DBA di SQL Server a raccogliere le query rilevate da sp_WhoIsActive all'interno di una particolare istanza di SQL Server. Quel SP li abbinerebbe a una determinata stringa di ricerca e li memorizzerebbe in un file di output per la post-analisi.
Considerazioni iniziali
Ecco alcune ipotesi prima di addentrarci nei dettagli dello script:
- Lo script riceve il nome dell'istanza come parametro. Se non viene superato, localhost sarà assunto dallo script.
- Lo script ti chiederà una particolare stringa di ricerca per confrontarla con i testi delle query eseguite nell'istanza di SQL Server. Se c'è una corrispondenza con qualcuno di essi, verrà archiviata in un file .txt che potrai analizzare in seguito.
- Il file di output con tutte le informazioni relative all'istanza viene generato per il percorso esatto in cui si trova e viene attivato PowerShell. Assicurati di avere la scrittura permessi lì.
- Se esegui più volte lo script PowerShell per la stessa istanza, tutti i file di output precedentemente esistenti verranno sovrascritti. Verrà conservato solo il più recente. Pertanto, se devi conservare un file molto specifico, salvalo da qualche altra parte manualmente.
- Il pacchetto include un .sql file con il codice per distribuire la Procedura archiviata WhoIsActive al database principale dell'istanza specificata. Lo script controlla se la procedura memorizzata esiste già nell'istanza e la crea in caso contrario.
- Puoi scegliere di distribuirlo su un altro database. Assicurati solo le modifiche necessarie all'interno dello script.
- Scarica questo .sql file da un hosting sicuro.
- Lo script tenterà di recuperare le informazioni dall'istanza di SQL Server ogni 10 secondi per impostazione predefinita. Ma se vuoi usare un valore diverso, regolalo di conseguenza.
- Assicurati che l'utente richiesto per la connessione all'istanza di SQL Server disponga delle autorizzazioni per creare ed eseguire le stored procedure. In caso contrario, non riuscirà a raggiungere il suo scopo.
Utilizzo dello script PowerShell
Ecco cosa puoi aspettarti dallo script:
Vai alla posizione in cui hai inserito il file di script di PowerShell ed eseguilo in questo modo:
PS C:\temp> .\WhoIsActive-Runner.ps1 SERVER\INSTANCE
Sto usando C:\temp come esempio
L'unica cosa che lo script ti chiederà è il tipo di login che vuoi usare per connetterti all'istanza.
Nota:se utilizzi PowerShell ISE, i prompt appariranno come schermate. Se lo esegui direttamente dalla console di PowerShell, le opzioni verranno visualizzate come testo all'interno della stessa finestra .
Fidato – la connessione all'istanza di SQL Server verrà effettuata con lo stesso utente dell'esecuzione dello script di PowerShell. Non devi specificare alcuna credenziale, le assumerà in base al contesto.
Accesso a Windows – è necessario fornire un login di Windows per la corretta autenticazione.
Accesso SQL – è necessario fornire un login SQL per la corretta autenticazione.
Indipendentemente dall'opzione scelta, assicurati che disponga di privilegi sufficienti nell'istanza per eseguire i controlli .
Se scegli il tipo di accesso che richiede l'inserimento delle credenziali, lo script ti avviserà in caso di errore:
Con le informazioni corrette specificate, lo script verificherà se l'SP esiste nel database principale e procederà a crearlo in caso contrario.
Assicurarsi che il file .sql con il codice T-SQL per creare l'SP si trovi nello stesso percorso in cui si trova lo script. Il .sql il nome del file deve essere sp_WhoIsActive.sql .
Se desideri utilizzare un nome file .sql diverso e un database di destinazione diverso, assicurati le modifiche necessarie all'interno dello script di PowerShell:
Il passaggio successivo sarà il Richiesta stringa di ricerca . Devi inserirlo per raccogliere tutte le corrispondenze restituite da ogni iterazione di esecuzione della stored procedure all'interno dell'istanza di SQL Server.
Dopodiché, devi scegliere quanto tempo desideri consentire per l'esecuzione dello script.
A scopo dimostrativo, sceglierò l'opzione n. 1 (5 minuti). Lascerò una query fittizia in esecuzione nella mia istanza. La query è WAITFOR DELAY '00:10′ . Specifico la stringa di ricerca WAITFOR in modo che tu possa avere un'idea di ciò che lo script farà per te.
Dopo che lo script ha completato la sua esecuzione, vedrai un .txt file che contiene il nome della tua istanza e WhoIsActive come suffisso.
Ecco un esempio di ciò che lo script ha catturato e salvato in quel .txt file:
Codice completo dello script di PowerShell
Se vuoi provare questo script, utilizza il codice seguente:
param(
$instance = "localhost"
)
if (!(Get-Module -ListAvailable -Name "SQLPS")) {
Write-Host -BackgroundColor Red -ForegroundColor White "Module Invoke-Sqlcmd is not loaded"
exit
}
#Function to execute queries (depending on if the user will be using specific credentials or not)
function Execute-Query([string]$query,[string]$database,[string]$instance,[int]$trusted,[string]$username,[string]$password){
if($trusted -eq 1){
try{
Invoke-Sqlcmd -Query $query -Database $database -ServerInstance $instance -ErrorAction Stop -ConnectionTimeout 5 -QueryTimeout 0
}
catch{
Write-Host -BackgroundColor Red -ForegroundColor White $_
exit
}
}
else{
try{
Invoke-Sqlcmd -Query $query -Database $database -ServerInstance $instance -Username $username -Password $password -ErrorAction Stop -ConnectionTimeout 5 -QueryTimeout 0
}
catch{
Write-Host -BackgroundColor Red -ForegroundColor White $_
exit
}
}
}
function Get-Property([string]$property,[string]$instance){
Write-Host -NoNewline "$($property) "
Write-Host @greenCheck
Write-Host ""
switch($loginChoice){
0 {$output = Execute-Query "SELECT SERVERPROPERTY('$($property)')" "master" $instance 1 "" ""}
default {$output = Execute-Query "SELECT SERVERPROPERTY('$($property)')" "master" $instance 0 $login $password}
}
switch($property){
"EngineEdition"{
switch($output[0]){
1 {"$($property): Personal or Desktop Engine" | Out-File -FilePath $filePath -Append}
2 {"$($property): Standard" | Out-File -FilePath $filePath -Append}
3 {"$($property): Enterprise" | Out-File -FilePath $filePath -Append}
4 {"$($property): Express" | Out-File -FilePath $filePath -Append}
5 {"$($property): SQL Database" | Out-File -FilePath $filePath -Append}
6 {"$($property): Microsoft Azure Synapse Analytics" | Out-File -FilePath $filePath -Append}
8 {"$($property): Azure SQL Managed Instance" | Out-File -FilePath $filePath -Append}
9 {"$($property): Azure SQL Edge" | Out-File -FilePath $filePath -Append}
11{"$($property): Azure Synapse serverless SQL pool" | Out-File -FilePath $filePath -Append}
}
}
"HadrManagerStatus"{
switch($output[0]){
0 {"$($property): Not started, pending communication." | Out-File -FilePath $filePath -Append}
1 {"$($property): Started and running." | Out-File -FilePath $filePath -Append}
2 {"$($property): Not started and failed." | Out-File -FilePath $filePath -Append}
default {"$($property): Input is not valid, an error, or not applicable." | Out-File -FilePath $filePath -Append}
}
}
"IsIntegratedSecurityOnly"{
switch($output[0]){
1{"$($property): Integrated security (Windows Authentication)" | Out-File -FilePath $filePath -Append}
0{"$($property): Not integrated security. (Both Windows Authentication and SQL Server Authentication.)" | Out-File -FilePath $filePath -Append}
}
}
default{
if($output[0] -isnot [DBNull]){
"$($property): $($output[0])" | Out-File -FilePath $filePath -Append
}else{
"$($property): N/A" | Out-File -FilePath $filePath -Append
}
}
}
return
}
$filePath = ".\$($instance.replace('\','_'))_WhoIsActive.txt"
Remove-Item $filePath -ErrorAction Ignore
$loginChoices = [System.Management.Automation.Host.ChoiceDescription[]] @("&Trusted", "&Windows Login", "&SQL Login")
$loginChoice = $host.UI.PromptForChoice('', 'Choose login type for instance', $loginChoices, 0)
switch($loginChoice)
{
1 {
$login = Read-Host -Prompt "Enter Windows Login"
$securePassword = Read-Host -Prompt "Enter Password" -AsSecureString
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword))
}
2 {
$login = Read-Host -Prompt "Enter SQL Login"
$securePassword = Read-Host -Prompt "Enter Password" -AsSecureString
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword))
}
}
#Attempt to connect to the SQL Server instance using the information provided by the user
try{
switch($loginChoice){
0{
$spExists = Execute-Query "SELECT COUNT(*) FROM sys.objects WHERE type = 'P' AND name = 'sp_WhoIsActive'" "master" $instance 1 "" ""
if($spExists[0] -eq 0){
Write-Host "The Stored Procedure doesn't exist in the master database."
Write-Host "Attempting its creation..."
try{
Invoke-Sqlcmd -ServerInstance $instance -Database "master" -InputFile .\sp_WhoIsActive.sql
Write-Host -BackgroundColor Green -ForegroundColor White "Success!"
}
catch{
Write-Host -BackgroundColor Red -ForegroundColor White $_
exit
}
}
}
default{
$spExists = Execute-Query "SELECT COUNT(*) FROM sys.objects WHERE type = 'P' AND name = 'sp_WhoIsActive'" "master" $instance 0 $login $password
if($spExists[0] -eq 0){
Write-Host "The Stored Procedure doesn't exist in the master database."
Write-Host "Attempting its creation..."
try{
Invoke-Sqlcmd -ServerInstance $instance -Database "master" -Username $login -Password $password -InputFile .\sp_WhoIsActive.sql
Write-Host -BackgroundColor Green -ForegroundColor White "Success!"
}
catch{
Write-Host -BackgroundColor Red -ForegroundColor White $_
exit
}
}
}
}
}
catch{
Write-Host -BackgroundColor Red -ForegroundColor White $_
exit
}
#If the connection succeeds, then proceed with the retrieval of the configuration for the instance
Write-Host " _______ _______ _______ _________ _______ _______ _______ __________________ _______ "
Write-Host "( ____ \( ____ ) |\ /||\ /|( ___ )\__ __/( ____ \( ___ )( ____ \\__ __/\__ __/|\ /|( ____ \"
Write-Host "| ( \/| ( )| | ) ( || ) ( || ( ) | ) ( | ( \/| ( ) || ( \/ ) ( ) ( | ) ( || ( \/"
Write-Host "| (_____ | (____)| _____ | | _ | || (___) || | | | | | | (_____ | (___) || | | | | | | | | || (__ "
Write-Host "(_____ )| _____)(_____)| |( )| || ___ || | | | | | (_____ )| ___ || | | | | | ( ( ) )| __) "
Write-Host " ) || ( | || || || ( ) || | | | | | ) || ( ) || | | | | | \ \_/ / | ( "
Write-Host "/\____) || ) | () () || ) ( || (___) |___) (___/\____) || ) ( || (____/\ | | ___) (___ \ / | (____/\"
Write-Host "\_______)|/ (_______)|/ \|(_______)\_______/\_______)|/ \|(_______/ )_( \_______/ \_/ (_______/"
Write-Host ""
$searchString = Read-Host "Enter string to lookup"
$timerChoices = [System.Management.Automation.Host.ChoiceDescription[]] @("&1)5m", "&2)10m", "&3)15m","&4)30m","&5)Indefinitely")
$timerChoice = $host.UI.PromptForChoice('', 'How long should the script run?', $timerChoices, 0)
Write-Host -NoNewline "Script will run "
switch($timerChoice){
0{
Write-Host "for 5 minutes."
$limit = 5
}
1{
Write-Host "for 10 minutes."
$limit = 10
}
2{
Write-Host "for 15 minutes."
$limit = 15
}
3{
Write-Host "for 30 minutes."
$limit = 30
}
4{
Write-Host "indefinitely (press ctrl-c to exit)."
$limit = 2000000
}
}
Write-Host "Start TimeStamp: $(Get-Date)"
$StopWatch = [system.diagnostics.stopwatch]::StartNew()
while($StopWatch.Elapsed.TotalMinutes -lt $limit){
$results = Execute-Query "EXEC sp_WhoIsActive" "master" $instance 1 "" ""
Get-Date | Out-File -FilePath $filePath -Append
"####################################################################" | Out-File -FilePath $filePath -Append
foreach($result in $results){
if($result.sql_text -match $searchString){
$result | Out-File -FilePath $filePath -Append
}
"####################################################################" | Out-File -FilePath $filePath -Append
}
Start-Sleep -s 10
}
Get-Date | Out-File -FilePath $filePath -Append
"####################################################################" | Out-File -FilePath $filePath -Append
Write-Host "End TimeStamp : $(Get-Date)"
Conclusione
Tieni presente che WhoIsActive non acquisirà le query eseguite molto velocemente dal motore DB. Tuttavia, lo spirito di questo strumento è rilevare quelle query problematiche che sono lente e potrebbero trarre vantaggio da uno o più cicli di ottimizzazione.
Si potrebbe obiettare che una traccia Profiler o una sessione di eventi estesi potrebbero ottenere la stessa cosa. Tuttavia, trovo molto conveniente che tu possa semplicemente avviare diverse finestre di PowerShell ed eseguirle ciascuna su istanze diverse contemporaneamente. È qualcosa che potrebbe rivelarsi un po' noioso per più istanze.
Utilizzando questo come trampolino di lancio, potresti andare un po' oltre e configurare un meccanismo di avviso per ricevere notifiche su qualsiasi occorrenza rilevata dallo script per qualsiasi query che è stata eseguita per più di X minuti.