I dag er det inden for SQL Server DBA-fællesskabet yderst sandsynligt, at vi bruger, eller i det mindste har hørt om, den berømte lagrede procedure sp_WhoIsActive udviklet af Adam Machanic.
I min tid som DBA brugte jeg SP til med det samme at tjekke, hvad der foregår inde i en bestemt SQL Server-instans, når den får al den "fingerpegning", at en bestemt applikation kører langsomt.
Der er dog lejligheder, hvor sådanne problemer bliver tilbagevendende og kræver en måde at fange, hvad der foregår, for at finde en potentiel skyldig. Der er også scenarier, hvor du har flere instanser, der fungerer som backend for 3-parts applikationer. Den lagrede procedure kunne potentielt fungere godt for at finde vores skyldige.
I denne artikel vil jeg præsentere et PowerShell-værktøj, der kan hjælpe enhver SQL Server DBA med at indsamle forespørgsler opdaget af sp_WhoIsActive inde i en bestemt SQL Server-instans. Denne SP ville matche dem til en bestemt søgestreng og gemme dem i en outputfil til efteranalyse.
Indledende overvejelser
Her er nogle antagelser, før du dykker ned i detaljerne i scriptet:
- Scriptet modtager navnet på forekomsten som en parameter. Hvis ingen er bestået, localhost vil blive antaget af scriptet.
- Scriptet vil bede dig om en bestemt søgestreng for at sammenligne den med teksten af forespørgsler udført i SQL Server-forekomsten. Hvis der er et match med nogen af dem, vil det blive gemt i en .txt-fil, som du kan analysere senere.
- Outputfilen med al information relateret til din instans genereres for den nøjagtige sti, hvor PowerShell er placeret og udløst. Sørg for, at du har write tilladelser der.
- Hvis du udfører PowerShell-scriptet flere gange for den samme instans, vil alle tidligere eksisterende outputfiler blive overskrevet. Kun den seneste beholdes. Derfor, hvis du har brug for at beholde en meget specifik fil, skal du gemme den manuelt et andet sted.
- Bundten indeholder en .sql fil med koden for at implementere WhoIsActive Stored Procedure til masterdatabasen for den instans, du angiver. Scriptet kontrollerer, om den lagrede procedure allerede findes i instansen, og opretter den, hvis den ikke gør det.
- Du kan vælge at implementere den til en anden database. Bare sørg for de nødvendige ændringer i scriptet.
- Download denne .sql fil fra sikker hosting.
- Scriptet vil som standard forsøge at hente oplysningerne fra SQL Server-instansen hvert 10. sekund. Men hvis du vil bruge en anden værdi, skal du justere den i overensstemmelse hermed.
- Sørg for, at den bruger, der ansøgte om at oprette forbindelse til SQL Server-instansen, har tilladelser til at oprette og udføre de lagrede procedurer. Ellers vil den ikke nå sit formål.
Brug af PowerShell-scriptet
Her er, hvad du kan forvente af scriptet:
Gå til det sted, hvor du har lagt PowerShell-scriptfilen, og kør den sådan her:
PS C:\temp> .\WhoIsActive-Runner.ps1 SERVER\INSTANCE
Jeg bruger C:\temp som eksempel
Det eneste, scriptet vil spørge dig om, er den type login, du vil bruge til at oprette forbindelse til instansen.
Bemærk:Hvis du bruger PowerShell ISE, vil meddelelserne se ud som skærmbilleder. Hvis du kører det direkte fra PowerShell-konsollen, vil indstillingerne blive bedt om som tekst i det samme vindue .
Pålidelig – forbindelsen til SQL Server-instansen oprettes med den samme bruger som til udførelse af PowerShell-scriptet. Du behøver ikke at angive nogen legitimationsoplysninger, det vil antage dem baseret på konteksten.
Windows-login – du skal angive et Windows-login for den korrekte godkendelse.
SQL-login – du skal angive et SQL-login for den korrekte godkendelse.
Uanset hvilken mulighed du vælger, skal du sørge for, at den har nok privilegier i instansen til at udføre kontroller .
Hvis du vælger den login-type, der kræver, at du indtaster legitimationsoplysninger, vil scriptet give dig besked i tilfælde af fejl:
Med de korrekte oplysninger specificeret vil scriptet kontrollere, om SP'en findes i masterdatabasen og fortsætter med at oprette den, hvis den ikke gør det.
Sørg for, at .sql-filen med T-SQL-koden til at oprette SP'en er placeret på samme sti, hvor scriptet er placeret. .sql filnavnet skal være sp_WhoIsActive.sql .
Hvis du vil bruge et andet .sql-filnavn og en anden måldatabase, skal du sørge for de nødvendige ændringer i PowerShell-scriptet:
Det næste trin vil være søgestrengprompten . Du skal indtaste den for at indsamle matchninger, der returneres af hver eksekveringsiteration af den lagrede procedure i SQL Server-instansen.
Derefter skal du vælge, hvor meget tid du vil tillade til scriptet eksekvering.
Til demonstrationsformål vil jeg vælge mulighed #1 (5 minutter). Jeg vil lade en dummy-forespørgsel køre i min instans. Forespørgslen er WAITFOR DELAY ’00:10′ . Jeg vil angive søgestrengen WAITFOR så du kan få en fornemmelse af, hvad scriptet vil gøre for dig.
Når scriptet har fuldført sin udførelse, vil du se en .txt fil, der indeholder navnet på din forekomst og WhoIsActive som suffiks.
Her er et eksempel på, hvad scriptet fangede og gemte i den .txt fil:
Fuldstændig kode for PowerShell-scriptet
Hvis du vil prøve dette script, skal du bruge nedenstående kode:
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)"
Konklusion
Lad os huske på, at WhoIsActive ikke vil fange forespørgsler, der udføres meget hurtigt af DB Engine. Men ånden i dette værktøj er at opdage de problematiske forespørgsler, der er langsomme og kunne drage fordel af en optimeringsrunde (eller -runder).
Du kan hævde, at en Profiler-sporing eller en udvidet begivenhedssession kunne opnå det samme. Jeg finder det dog meget praktisk, at du blot kan tænde flere PowerShell-vinduer og udføre hver mod forskellige instanser på samme tid. Det er noget, der kunne vise sig at være en smule kedeligt i flere tilfælde.
Ved at bruge dette som et springbræt, kan du gå lidt længere og konfigurere en advarselsmekanisme til at blive underrettet om enhver hændelse, der er registreret af scriptet for enhver forespørgsel, der har kørt i mere end X antal minutter.