[{"content":"Intro psTerminalPerfCounter\nIn the previous article I gave a quick rundown of the history and inner workings of Windows Performance Counters, now comes the hands-on part. The basis is release 0.4.1, which is all about visualization and data export. New additions: CSV export, a terminal GUI replacing the text-based view, and HTML export via PSWriteHTML. Sounds exciting… and it is 🙂 I figure you see it the same way, otherwise you wouldn’t still be reading. 🙂\nInstallation Installing a PowerShell module, nothing special\n# Install psTerminalPerfcounter Install-Module psTerminalPerfCounter # List Cmdlets Get-Command -Module psTerminalPerfCounter # Install PSWriteHtml, required for HTML Export Install-Module PSWriteHtml Basic Structure, Counter Configs psTerminalPerfCounter works with configuration files that describe the counter sets and their configuration: counter IDs, instances (whether, for example, all CPU cores or only core 3 and 4 should be monitored), value conversion, units, and the color coding of the graphs via the so-called ColorMap, which however is only supported by the legacy visualization.\nCounter configs can be stored in any path and called from there. Alternatively, paths can be registered, which allows calling them simply by the config name. The default path is the config folder within the module itself, which already ships with a few predefined configs.\n# List all registered paths Get-tpcConfigPaths # Add custom path Add-tpcConfigPath -Path \u0026#39;C:\\tpcConfig\u0026#39; # List all Counterconfigs and details in all paths Test-tpcAvailableCounterConfig Single Server Monitoring The fastest method is a single server, local or remote.\n# Use CPU Config locally Start-tpcMonitor -Configname CPU Start-tpcMonitor -Confipath \u0026#39;C:\\Users\\labadmin\\Documents\\PowerShell\\Modules\\psterminalperfcounter\\0.4.0\\Config\\tpc_CPU.json\u0026#39; # Use CPU Config remote Start-tpcMonitor -Computername \u0026#39;dev-node1\u0026#39; -Configname CPU Start-tpcMonitor -Computername \u0026#39;dev-node1\u0026#39; -Confipath \u0026#39;C:\\Users\\labadmin\\Documents\\PowerShell\\Modules\\psterminalperfcounter\\0.4.0\\Config\\tpc_CPU.json\u0026#39; # Use CPU Config rmeote with different credentials Start-tpcMonitor -Computername \u0026#39;dev-node1\u0026#39; -Credentials $(Get-Credential) -Configname CPU Since the legacy visualization reaches its limits at the latest with multi-server environments, I use the -TUI switch for this hands-on.\n# Use CPU Config locally with TUI Start-tpcMonitor -Configname CPU -TUI Multi Server Monitoring Multi-server monitoring also works via configuration files, although while writing these lines it occurs to me: why did I never think of simply running a counter config against a server list? Onto the backlog with that. Anyway, in the environment configuration you can define any number of servers with different configs. Since I recently still had trouble making the sparklines in the TUI scrollable, they are disabled in the multi-server view.\n# Start multi server monitoring Start-tpcEnvironmentMonitor -EnvConfigPath \u0026#39;C:\\tpcConfig\\ENV_SERVER_EXAMPLE.json\u0026#39; -TUI LAB-NODE03 is a German installation in this case\nThe Duration column shows the time the function needed on each server to collect all values. The final timestamp of the entire data set is always defined by the slowest server, as only this way is a homogeneous time series possible. A variance of, in this case, 1.3 seconds cannot be avoided here.\nThat’s all there is to running it, since this module only provides the framework. The actual life comes with the counter configs. Let’s now take a look at how these are created.\nCreating Counter Configs In the beginning there is the performance counter. Before we can write the configuration, we need the counter IDs instead of the localized names. For that there is Get-tpcPerformanceCounterInfo, which works in both directions: counter path to ID and vice versa.\n# List all counter and counter objects containing \u0026#39;processor time\u0026#39; Get-tpcPerformanceCounterInfo -ComputerName \u0026#39;dev-node1\u0026#39; -SearchTerm \u0026#39;processor time\u0026#39; The last two columns are important. There are single-instance counters with only one value, but also multi-instance counters like the Processor counter set, where each instance corresponds to a core, or the total (_Total).\nIn the same way you can use the cmdlet to find out which counter is hidden behind a composite ID.\n# Show counter name of composite ID Get-tpcPerformanceCounterInfo -ComputerName \u0026#39;dev-node1\u0026#39; -SearchTerm \u0026#39;238-6\u0026#39; Or on a German system\nLet’s also take counters that are not present by default on a Windows system: SQL Server. DEV22A in this case is the first instance on the system.\n# \u0026#34;Out-String -Stream\u0026#34; converts PowerShell\u0026#39;s internal formatting objects into actual strings line by # line, enabling string operations like `Select-String` to work on them. Get-tpcPerformanceCounterInfo -ComputerName \u0026#39;dev-node1\u0026#39; -SearchTerm \u0026#39;dev22a\u0026#39; | Out-String -Stream | Select-String \u0026#34;lookup\u0026#34; Let’s take Page lookups/sec for testing –\u0026gt; 12222-12272\nGet-tpcPerformanceCounterInfo -ComputerName \u0026#39;dev-node1\u0026#39; -SearchTerm \u0026#39;dev22a\u0026#39; | Out-String -Stream | Select-String \u0026#34;log flushes\u0026#34; And log flush/sec on the TempDB instance –\u0026gt; 12352-12446\nThe minimal config then looks like this. Some values are missing for the SQL Server counters, but that is not SQL Server specific, it shows that most fields receive defaults. Details on this can be found in the docs in the repo. Also worth mentioning: for CPU I use \u0026quot;counterInstance\u0026quot;: \u0026quot;1|2\u0026quot;, which automatically creates two counters, and for the page lookups the conversion by 1000 so that the numbers stay readable.\\\n{ \u0026#34;name\u0026#34;: \u0026#34;CPU and Sql Server Page Usage\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;CPU Usage\u0026#34;, \u0026#34;counters\u0026#34;: [ { \u0026#34;title\u0026#34;: \u0026#34;CPU Usage\u0026#34;, \u0026#34;unit\u0026#34;: \u0026#34;%\u0026#34;, \u0026#34;conversionFactor\u0026#34;: 1, \u0026#34;conversionExponent\u0026#34;: 1, \u0026#34;conversionType\u0026#34;: \u0026#34;D\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;table\u0026#34;, \u0026#34;counterID\u0026#34;: \u0026#34;238-6\u0026#34;, \u0026#34;counterSetType\u0026#34;: \u0026#34;MultiInstance\u0026#34;, \u0026#34;counterInstance\u0026#34;: \u0026#34;0|1\u0026#34;, \u0026#34;colorMap\u0026#34;: { \u0026#34;20\u0026#34;: \u0026#34;Green\u0026#34;, \u0026#34;50\u0026#34;: \u0026#34;Yellow\u0026#34;, \u0026#34;70\u0026#34;: \u0026#34;RED\u0026#34; } }, { \u0026#34;title\u0026#34;: \u0026#34;Sql DEV22A Page Lookups/s\u0026#34;, \u0026#34;unit\u0026#34;: \u0026#34;#K\u0026#34;, \u0026#34;conversionFactor\u0026#34;: 1000, \u0026#34;conversionExponent\u0026#34;: 1, \u0026#34;conversionType\u0026#34;: \u0026#34;D\u0026#34;, \u0026#34;counterID\u0026#34;: \u0026#34;12222-12272\u0026#34;, \u0026#34;counterSetType\u0026#34;: \u0026#34;SingleInstance\u0026#34;, \u0026#34;counterInstance\u0026#34;: \u0026#34;\u0026#34; }, { \u0026#34;title\u0026#34;: \u0026#34;Sql DEV22A TempDB Log Flushes/s\u0026#34;, \u0026#34;unit\u0026#34;: \u0026#34;#\u0026#34;, \u0026#34;counterID\u0026#34;: \u0026#34;12352-12446\u0026#34;, \u0026#34;counterSetType\u0026#34;: \u0026#34;MultiInstance\u0026#34;, \u0026#34;counterInstance\u0026#34;: \u0026#34;tempdb\u0026#34; } ] } Then save the whole thing in a registered path with the prefix tpc_NAME.json and off we go.\nMonitoring an Environment with an Environment File No magic here either. Config names or paths can be specified. A function that is not yet fully documented is the use of SecretVaults: via the PowerShell connectors, credentials can be loaded from almost all common solutions, which also gives non-admins the ability to monitor systems.\n[ { \u0026#34;name\u0026#34;: \u0026#34;SQL_ENVIRONMENT_001\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;SQL Server Environment Production\u0026#34;, \u0026#34;interval\u0026#34;: 2, \u0026#34;secretvaultname\u0026#34;: \u0026#34;SecretStore\u0026#34;, \u0026#34;credentialname\u0026#34;: \u0026#34;integrated\u0026#34;, \u0026#34;servers\u0026#34;: [ { \u0026#34;computername\u0026#34;: \u0026#34;DEV-NODE1\u0026#34;, \u0026#34;comment\u0026#34;: \u0026#34;Sql Server Node A Production\u0026#34;, \u0026#34;counterConfig\u0026#34;: [ \u0026#34;SQLDEMO\u0026#34; ] }, { \u0026#34;computername\u0026#34;: \u0026#34;DEV-NODE2\u0026#34;, \u0026#34;comment\u0026#34;: \u0026#34;Sql Server Node B Production\u0026#34;, \u0026#34;counterConfig\u0026#34;: [ \u0026#34;C:\\Users\\devadmin\\Desktop\\tpc_SQLDEMO.json\u0026#34; ] } ] } ] # without TUI Start-tpcEnvironmentMonitor -EnvConfigPath \u0026#34;C:\\Users\\devadmin\\Desktop\\ENV_SERVER_EXAMPLE2.json\u0026#34; Data Export CSV To reuse the time series, you can export to CSV. Caution, currently still a known bug: the export is comma-separated, so values containing a comma get split as well, fixed in 0.4.1.\n# -CsvPath is default desktop Start-tpcEnvironmentMonitor -EnvConfigPath \u0026#34;C:\\Users\\devadmin\\Desktop\\ENV_SERVER_EXAMPLE2.json\u0026#34; -ExportCsv HTML Report An alternative visualization as HTML, useful when I want to pass on a time series without bothering with Excel or other tools. Grouping can be done by counter or by server, depending on whether, with multiple servers, I want to see all counters of one server in a single chart, or one specific counter across multiple servers. Details on this can be found in the docs in the repo.\nTo note: the HTML contains a rolling data set bound to the maximum number of samples. The more samples, the more sluggish the HTML becomes. For large time series, better use CSV.\n# -HTMLPath is default desktop Start-tpcEnvironmentMonitor -EnvConfigPath \u0026#34;C:\\Users\\devadmin\\Desktop\\ENV_SERVER_EXAMPLE2.json\u0026#34; -ExportHTML That’s it as a quick start, more details on the individual functions can be found, as always, in the docs and the READMEin the repository.\n","permalink":"https://dbavonnebenan.de/en/psterminalperfcounter-counter-in-terminal-handson/","summary":"Hands-on with psTerminalPerfCounter: monitor Windows Performance Counters in the terminal, including a TUI, multi-server monitoring, and CSV/HTML export.","title":"psTerminalPerfCounter – Performance Counter in Terminal – HowTo"},{"content":"Intro psTerminalPerfCounter\nIt has been almost a year now since I started developing psTerminalPerfCounter. Proud of the „graphical“ implementation, language independence via a 3rd-party lib and the holy grail of the time: classes in PowerShell.\nNone of it survived:\nGraphical implementation? Nice and colorful, but… Sluggish as a rock in multi-server environments Dynamic and scalable? Nope Reimplemented as a TUI (BETA) Multi-language via 3rd-party lib Turns out it all works directly through the Windows registry, stumbled across it by accident Classes in PowerShell? Very bad decision. Not practical, instances aren’t easily serializable, methods get lost, parallel remoting with runspaces barely doable Rebuild in C# With the current release 0.4.1, all the features I had in my backlog are implemented for now. Since I will be lacking the time for this project over the next few months, this is a good moment to write a HowTo, because the README.md in the repo can be a bit challenging at times… as it seems 🙂 But before we get to the hands-on, let’s first talk a little about the technology and history of performance counters.\nHistory Windows performance counters have been around since NT 3.1 (1993), back then I didn’t even own a computer. At the time, Microsoft needed a standardized way to expose system states without every application having to implement its own monitoring mechanisms. But that also meant scraping measurement values out of the registry and calculating deltas yourself.\nWindows 2000 added the Performance Data Helper (PDH) as a convenient high-level API, which shaped access to performance counters the way we know it today. At the same time, WMI was introduced, making performance data accessible through an object-oriented interface. CIM just wasn’t proprietary enough and a bit too compatible 😛\nWindows Vista brought the revised Provider V2 model: instead of a DLL that was loaded by the system into a foreign process (error-prone, provider crash = crash of the querying process), providers now register as standalone executables or services. That is the model still in use today.\nStructure and Function Metadata in the registry All known counters are registered under HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib. That’s where the name, description and a numeric index number per counter live, separated by language (subkey 009 for English, 007 for German, etc.).\n# List available language packs (counter IDs) Get-ChildItem \u0026#34;HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib\u0026#34; Providers deliver the raw data Every component (kernel, drivers, services like IIS or SQL Server, the .NET runtime) can register its own counters. The provider waits passively and only delivers data once someone queries it. There is no permanent collection service running in the background.\n# List all services that have a Performance subkey Get-ChildItem \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\u0026#34; | Where-Object { Test-Path \u0026#34;$($_.PSPath)\\Performance\u0026#34; } | ForEach-Object { $perf = Get-ItemProperty \u0026#34;$($_.PSPath)\\Performance\u0026#34; [PSCustomObject]@{ Service = $_.PSChildName FirstCounter = $perf.\u0026#39;First Counter\u0026#39; LastCounter = $perf.\u0026#39;Last Counter\u0026#39; Library = $perf.Library } } | Format-Table -AutoSize APIs for applications (CONSUMER) PDH (Performance Data Helper): The convenient API that returns values already formatted (e.g. percentages calculated, intervals taken into account). Get-Counter in PowerShell uses PDH. PerfLib / registry directly: Faster, but raw. WMI uses this path internally. Structure\n\\Processor(_Total)\\% Processor Time\n└─ Object: „Processor“\n└─ Instance: „_Total“ (sum of all cores)\n└─ Counter: „% Processor Time“\nMicrosoft’s Tools The grandfather: PerfMon The Performance Monitor has been Microsoft’s official tool since the NT days and, for what it was built for, it is still excellent: interactive, graphical deep-dive analysis of individual systems, with recording via Data Collector Sets, CSV export via relog and basic remoting. The limits show up the moment you want to scale, automate or simply work in the terminal. Portable configuration files, keeping an eye on multiple servers at once or a structured console workflow are not its strengths.\nThe newcomer: Windows Admin Center Microsoft’s browser-based management interface also comes with a performance view, cleaner than Perfmon and without a local installation on the managed system. For a quick look at CPU, memory and network that’s perfectly fine. Two things limit its practical value though: first, WAC requires a central installation, just quickly firing it up on a client or at a customer’s site isn’t an option. Second, the counter selection is based, as everywhere, on the localized PDH API, which makes workspaces not really portable.\nThe evergreen: Get-Counter Get-Counter is the PowerShell equivalent for console use and therefore the most flexible of the three tools, as long as you’re up for some fiddling willing to get your hands dirty. Counters can be queried directly, repeated in a loop and sent down the pipeline. Remoting works via the -ComputerName parameter. The problem is the same as everywhere: the counter paths are language-dependent, a script written on a German system fails on an English one. On top of that, Get-Counter returns raw data. Formatting, thresholds, history visualization and structured output are entirely manual work. For one-off queries and quick checks it’s ideal, for reproducible, multi-language environments it quickly hits its limits.\n# Query CPU, memory and disk counters every 5 seconds, format output as table $counters = @( \u0026#39;\\Processor(_Total)\\% Processor Time\u0026#39;, \u0026#39;\\Memory\\Available MBytes\u0026#39;, \u0026#39;\\PhysicalDisk(_Total)\\Disk Bytes/sec\u0026#39; ) # German Counternames # $counters = @( #\t\u0026#39;\\Processor(_Total)\\% Processor Time\u0026#39;, #\t\u0026#39;\\Memory\\Available MBytes\u0026#39;, #\t\u0026#39;\\PhysicalDisk(_Total)\\Disk Bytes/sec\u0026#39; # ) Get-Counter -Counter $counters -SampleInterval 5 -Continuous | ForEach-Object { $sample = $_.CounterSamples [PSCustomObject]@{ Timestamp = $_.Timestamp \u0026#39;CPU %\u0026#39; = [math]::Round(($sample | Where-Object Path -like \u0026#39;*processor*\u0026#39;).CookedValue, 1) \u0026#39;RAM frei (MB)\u0026#39; = [math]::Round(($sample | Where-Object Path -like \u0026#39;*available*\u0026#39;).CookedValue, 0) \u0026#39;Disk B/s\u0026#39; = [math]::Round(($sample | Where-Object Path -like \u0026#39;*disk bytes*\u0026#39;).CookedValue, 0) } | Format-Table -AutoSize } The path to language independence As you can see, language dependence is a big problem across all implementations. And it’s not even necessary. Under the hood, every counter object and every counter has a unique ID. Take a look at the screenshots above, there’s always a First and Last Counter. So \\Processor(_Total)\\Processor Time (%) –\u0026gt; \\238(_Total)\\6 .\n# Read counter ID/name mapping from registry (English - key 009, German - key 007) Get-ItemProperty -Path \u0026#39;HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib\\009\u0026#39; -Name Counter | Select-Object -ExpandProperty Counter | Select-Object -First 20 That’s it. The counter object and the path just need to be translated once and I’ve got my language-independent counter path. Before execution it gets translated on the target system and then Get-Counter -Counter \u0026lt;TRANSLATION\u0026gt; is run. In psTerminalPerfCounter this is handled by the cmdlet get-tpcPerformanceCounterInfo\npsTerminalPerfCounter – Performance Counter in Terminal – HowTo\n","permalink":"https://dbavonnebenan.de/en/windows-performance-counters-how-the-os-monitors-itself/","summary":"Performance counters have been an integral part of the operating system since Windows NT 3.1. In this article I briefly explain how the system works internally, why counter names are language-dependent and how my PowerShell module psTerminalPerfCounter solves exactly that problem.","title":"Windows Performance Counters: How the OS Monitors Itself"},{"content":"GitHub – gabrielkoehl/DBAScriptBox: Useful stuff for DBAs\nIntro Who hasn’t been there? Before you can even start installing SQL Server, you’ve either repetitively clicked through pages of bad GUIs or, if available, juggled a loose collection of scripts to semi-automatically set up all dependencies and best practices.\nEven worse is the case when, as a consultant, you don’t have Domain Admin access (which is how it should be) and you have to guide the customer — who may have never done this before — via Teams \u0026amp; Co. If this isn’t already a planned installation with knowledge transfer, something like this creates pain. 🤭\nAccordingly, I’ve now gathered all my loose scripts and developed a function for each topic area that automates all these tasks and logs them for archival purposes. Thanks also to Claude for the support. Nobody reviews and documents code better than Claude. 😋\nNow you might ask: Aren’t there dbatools and DSC? Sure, I like using those too. But transparent, traceable scripts without framework overhead convey knowledge better — especially when aspiring DBAs lack the big-picture perspective. Operating SQL Server starts at the bare metal, goes through storage, Windows Server, Active Directory, and ends at a Certificate Authority. And for critical infrastructure companies or small mid-sized businesses with 3 clusters, I don’t want to start a fundamental discussion about dbatools or DSC right from the beginning.\nLet’s start with a production-ready Windows Server Failover Cluster — including CNO/VCO permissions, DNS registration, and quorum configuration. Everything repeatable, everything logged, everything traceable.\nThe Lab Scenario To make the steps tangible, the following environment and naming conventions are used\nNodes LAB-NODE01 (192.168.100.12) LAB-NODE02 (192.168.100.13) Cluster Name MSSQL-CL-01 (192.168.100.14) SQL Instance LAB22A Domain lab.local Service Accounts Group Managed Service Accounts (gMSA) g-LAB22A-oltp g-LAB22A-agnt Windows Server Failover Cluster Deployment ⚒️ cluster_manageSQLCluster.ps1\n📓 cluster_manageSQLCluster.md\nThe WSFC forms the foundation for AlwaysOn Availability Groups. The script manages the entire lifecycle from feature installation to witness configuration.\nFeature Installation \u0026amp; Cluster Creation First, the Failover-Clustering feature is installed on the individual nodes and the cluster is created. An important aspect of automation is the Cluster Name Object (CNO): It needs write permissions in DNS and Active Directory to be able to automatically create listener records later.\n.\\cluster_manageSQLCluster.ps1 ` -Operation InstallFeature ` -Nodes \u0026#34;LAB-NODE01\u0026#34;,\u0026#34;LAB-NODE02\u0026#34; ` -LogPath \u0026#34;C:\\Temp\\Cluster_Feature_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; Afterwards, the cluster is created. The script additionally sets the corresponding permissions in AD and DNS.\n.\\cluster_manageSQLCluster.ps1 ` -Operation CreateCluster ` -ClusterName \u0026#34;MSSQL-CL-01\u0026#34; ` -Nodes \u0026#34;LAB-NODE01\u0026#34;,\u0026#34;LAB-NODE02\u0026#34; ` -StaticIP \u0026#34;192.168.100.14\u0026#34; ` -DnsDomain \u0026#34;lab.local\u0026#34; ` -TargetOU \u0026#34;OU=lab_computer,DC=lab,DC=local\u0026#34; ` -LogPath \u0026#34;C:\\Temp\\Cluster_Create_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; File Share Witness (Quorum) For a 2-node cluster, a witness is mandatory to avoid split-brain scenarios. Here, a file share on the domain controller (\\\\LAB-DC\\quorum$) is used.\nManual Intermediate Step\nThe Cluster Computer Object (MSSQL-CL-01$) requires Full Control at both the share and NTFS permission level of the witness path (\\\\LAB-DC\\quorum$). This must be ensured before running the following command.\n.\\cluster_manageSQLCluster.ps1 ` -Operation SetWitness ` -ClusterName \u0026#34;MSSQL-CL-01\u0026#34; ` -WitnessShare \u0026#34;\\\\LAB-DC\\quorum$\u0026#34; ` -LogPath \u0026#34;C:\\Temp\\Cluster_Witness_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; Microsoft Cmdlets Used The script primarily uses the FailoverClusters module\nNew-Cluster Creates the cluster, adds the nodes, and sets the management IP Set-ClusterQuorum Configures the witness mode (here: File Share Witness) Install-WindowsFeature Installs the RSAT tools and the cluster service Identity Management: gMSA ⚒️ security_createGMSA.ps1\n📓 security_createGMSA.md\nUsing standard users as service accounts can lead to expired passwords and downtime. The recommended method for SQL Server is Group Managed Service Accounts (gMSA). Here, the Key Distribution Service (KDS) of Active Directory handles password rotation; nobody needs to know the password.\nKDS Root Key Before the first gMSA can be created, a KDS Root Key must exist once per forest. If a key already exists, the creation will be skipped.\n.\\security_createGMSA.ps1 ` -InitializeKDS ` -LogPath \u0026#34;C:\\Temp\\KDS_Init.log\u0026#34; gMSA Creation Instead of assigning servers directly to a gMSA, it is best practice to create a security group (SQL_LAB_CL01) that contains the computer accounts of the cluster nodes. In addition to setting up the actual gMSA, this script can also create the security groups and handle the assignments. And always keep the descriptions of AD objects well-maintained.\n.\\security_createGMSA.ps1 ` -AccountName @( ` @(\u0026#34;g-LAB22A-oltp\u0026#34;, \u0026#34;SQL Server Engine Account for LAB22A\u0026#34;), ` @(\u0026#34;g-LAB22A-agnt\u0026#34;, \u0026#34;SQL Server Agent Account for LAB22A\u0026#34;) ) ` -SecurityGroupName \u0026#34;SQL_LAB_CL01\u0026#34; ` -SecurityGroupDescription \u0026#34;SQL Server Cluster CL01 gMSA Group\u0026#34; ` -ServerNames \u0026#34;LAB-NODE01\u0026#34;,\u0026#34;LAB-NODE02\u0026#34; ` -CreateSecurityGroup ` -SecurityGroupOU \u0026#34;OU=lab_secgrp,DC=lab,DC=local\u0026#34; ` -PasswordIntervalDays 90 ` -LogPath \u0026#34;C:\\Temp\\gMSA_Creation_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; After this step, a restart of the nodes is required for the new group membership to take effect.\nLocal Registration After creation, the gMSAs must be registered locally on the nodes. The following script can be used for this purpose.\n# Required for local setup (Local Admin) Add-WindowsFeature -Name RSAT-AD-PowerShell $gMSAs = @( \u0026#39;g-LAB22A-oltp\u0026#39;, \u0026#39;g-LAB22A-agnt\u0026#39; ) foreach ($gMSA in $gMSAs) { if (Test-ADServiceAccount -Identity $gMSA -ErrorAction SilentlyContinue) { try { Install-ADServiceAccount -Identity $gMSA -ErrorAction Stop Write-Host \u0026#34;$gMSA installed\u0026#34; -ForegroundColor Green } catch { Write-Host \u0026#34;$gMSA - Installation error: $($_.Exception.Message)\u0026#34; -ForegroundColor Red } } else { Write-Host \u0026#34;$gMSA does not exist\u0026#34; -ForegroundColor Yellow } } After this step, a restart of the nodes is required for the new group membership to take effect.\nMicrosoft Cmdlets Used The script primarily uses the ActiveDirectory module\nAdd-KdsRootKey Creates the root key for the Group Key Distribution Service New-ADServiceAccount Creates the gMSA object in AD Set-ADServiceAccount Configures which computers (PrincipalsAllowedToRetrieveManagedPassword) are allowed to read the password Kerberos and Service Principal Names (SPN) ⚒️ security_setSqlSpn.ps1\n📓 security_setSqlSpn.md\nA gMSA does not have the right to write its own SPNs in AD by default. When the SQL service starts, the registration often fails, leading to NTLM fallbacks. Even though I prefer to set SPNs in a controlled and manual manner, the automatic registration must be technically possible to avoid fallbacks.\nThe script solves this problem by granting the gMSA the rights in AD to manage its own SPNs, in addition to creating the SPNs. It sets ACLs on the nodes for Read, Write, and Validated write on the servicePrincipalName attribute.\n.\\security_setSqlSpn.ps1 ` -ServiceAccount \u0026#34;LAB\\g-LAB22A-oltp\u0026#34; ` -InstanceName \u0026#34;LAB22A\u0026#34; ` -Port 51101 ` -Hostnames \u0026#34;LAB-NODE01\u0026#34;,\u0026#34;LAB-NODE02\u0026#34; ` -Domain \u0026#34;lab.local\u0026#34; ` -IsGMSA ` -SetADPermissions ` -LogPath \u0026#34;C:\\Temp\\SPN_AlwaysOn_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; Microsoft Cmdlets Used On one hand, setspn.exe is used for SPN management, and on the other hand, the ActiveDirectory module is used again to verify accounts and extract SIDs.\nSince none of the modules can manipulate Active Directory permissions, the .NET Framework System.DirectoryServices is used directly for this purpose, which provides various classes to completely turn AD inside out.\nLocal Security Policies ⚒️ security_setSqlLocalSec.ps1\n📓 security_setSqlLocalSec.md\nAs everywhere in IT, SQL Server should also follow the PoLP, which is why no service account gets local administrator privileges. For SQL Server to still run flawlessly and performantly, the service account needs specific rights in the operating system („User Rights Assignment“). Anyone who has done this manually a few times will be very happy about this solution. This must be executed locally on the respective node.\n# ===== Set Engine Permissions for gMSA ===== .\\security_setSqlLocalSec.ps1 ` -ServiceAccount \u0026#34;g-LAB22A-oltp$\u0026#34; ` -ServiceType \u0026#34;engine\u0026#34; ` -IsGMSA ` -LogPath \u0026#34;C:\\Temp\\LAB22A_Engine_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; # ===== Set Agent Permissions for gMSA ===== .\\security_setSqlLocalSec.ps1 ` -ServiceAccount \u0026#34;g-LAB22A-agnt$\u0026#34; ` -ServiceType \u0026#34;agent\u0026#34; ` -IsGMSA ` -LogPath \u0026#34;C:\\Temp\\LAB22A_Agent_$(Get-Date -Format \u0026#39;yyyyMMdd_HHmmss\u0026#39;).log\u0026#34; Microsoft Cmdlets Used Since PowerShell has no native cmdlets for editing local security policies, the Windows tool secedit.exe is used and its export is manipulated.\nSummary With these four building blocks, the foundation for a SQL Server installation has been laid. It wasn’t just an installation that was performed, but a valid, documented configuration that considers identity (gMSA), network (DNS/SPN), and OS performance. In the next step, the actual SQL Server instance can be installed.\nAlso check out the linked MD files — they contain background information on best practices and further articles from Microsoft\n","permalink":"https://dbavonnebenan.de/en/automate-sql-server-ha-ad-spn-and-cluster-setup/","summary":"Production-Ready Windows Cluster Deployment: Automation of Failover Cluster, gMSA, Kerberos and local rights via PowerShell.","title":"Automate SQL Server HA: AD, SPN, and Cluster Setup"},{"content":"Current Release ( 2026-06-08 ) psTerminalPerfCounter 0.4.0\nChangelog Documentation Intro Windows Performance Counters are absolutely powerful, but when I need something quick or monitoring tailored to a specific problem, I get frustrated fast. Usually I fall back on other options – PowerShell native Get-Counter, CIM, or existing monitoring solutions at my clients. But that’s never truly ad-hoc either, and I’ve accepted it.\nRecently I had the chance to join Uwe Ricken’s Performance Tuning Workshop at DataSaturday, and he clearly named Performance Counters as a must-have in every DBA’s toolkit. What I didn’t know until then: SQL Server creates individual counters like Compile/sec, AdHoc Queries/sec, etc.\nThe possibilities to visualize dependencies directly – endless. But how long should I sit there configuring that in Windows Performance Monitor, especially ad-hoc at a client site?\nUwe showed the capabilities in Windows Admin Center, which enables templates you can use flexibly across servers. For systems or environments where Admin Center is used strategically, definitely a great choice.\nBut two problems remain: As a consultant, I can’t install software at every client, and if Admin Center happens to be running, my English templates fail on German systems.\nStill, I was so incredibly hooked by the template idea and the SQL Server possibilities that 4 weeks of after-work crunchtime were spinning in my head. So let me introduce my first module in the PowerShell Gallery:\npsTerminalPerfCounter 0.1.0-preview A PowerShell module for displaying real-time graphs of Windows Performance Counters directly in the terminal console. This module provides an easy way to visualize system performance metrics without requiring external graphing tools or GUI applications by using templates and multilanguage support.\npsTerminalPerfCounter 0.1.0-preview\nYou’ll find all technical details and development status in the repository – I just want to show the different possibilities here.\nMain Features 100% PowerShell no external dependencies Terminal Graphics obviously it’s not a real engine, the more counters the choppier it gets, but it’s sufficient The foundation comes from Prateek Singh solution. Without finding this, I would have had to solve this problem first, for which I probably wouldn’t have had time. THANKS!!! Configuration Templates every output element is controlled via configuration files flexible, portable Data Output Customization each counter in the configuration file can use its own output Table, Graph, or Both arbitrary axis scaling in steps and max values arbitrary value coloring based on user-defined ColorMap per counter value conversion via configuration file (Byte -\u0026gt; KB -\u0026gt; MB -\u0026gt; GB) Language Independent configuration files are based on counter IDs, not paths portable, regardless of system language translation from ID to counter path happens at runtime no external dependencies I had no idea this was possible with Windows built-in tools. Here too I stumbled across a blog at PowerShell.ONE by chance. Again, a big THANKS!!! My original solution had multiple language variants (DE, EN, FR, ES) which would be set by reading system locale. Demo with CPU Configuration # Install module with required dependency (GripDevJsonSchemaValidator) Install-Module -Name psTerminalPerfCounter -AllowPrerelease # Import the module Import-Module psTerminalPerfCounter # Start Default Cpu Configuration Start-tpcMonitor -ConfigName CPU When loading the configuration, counter IDs are translated to counter paths and tested\nIf successful, the first samples are collected so a graph can be displayed\nThats IT!\nWithin 10 seconds, 2 Performance Counters running, no installation, no language issues.\nDemo Counter Translation # Translate ID -\u0026gt; Path Get-tpcPerformanceCounterInfo 238-6 # Search Counter -\u0026gt; Including ID Get-tpcPerformanceCounterInfo \u0026#34;Queue\u0026#34; Output is heavily truncated since the ResultSet is enormous\n# Search Counter -\u0026gt; Including ID Get-tpcPerformanceCounterInfo \u0026#34;Processor Queue\u0026#34; What’s next? The foundation works, the planned feature list is long, and what I really need for SQL Server is still far off. I hope I haven’t solved a problem that doesn’t exist, but this module simply addresses all the issues I’ve always had with Performance Counters, as mentioned above.\nFurthermore, this is my first larger project that I’m developing publicly, so GitHub might not always meet expected standards. I’m always happy about feedback and suggestions.\nGitHub – gabrielkoehl/psTerminalPerfCounter: psTerminalPerfCounter is a PowerShell module that provides predefined, language-independent performance counter configurations for real-time terminal-based monitoring, enabling admins to quickly deploy tailored system performance presets with graphical visualization instead of manually configuring Windows Performance Monitor.\n","permalink":"https://dbavonnebenan.de/en/psterminalperfcounter-speedup-your-performance-counters/","summary":"Visualize Windows Performance Counters directly in terminal with psTerminalPerfCounter. Language-independent templates, maximum customization. Perfect for quick AdHoc monitoring.","title":"psTerminalPerfCounter – SpeedUp your Performance Counters in Terminal"},{"content":"GitHub – gabrielkoehl/DBAScriptBox: Useful stuff for DBAs\nIntro Currently, I’m involved in a code review at a client. We’re examining not only the application but also all interfaces. This naturally includes the SQL Server landscape, which I set up at the beginning of the year.\nThe result was more findings in my direction than expected, and I immediately felt challenged. I was able to refute most of it, some topics were the client’s responsibility, and one topic caught my interest purely out of curiosity.\nRead- and Write Latencies of TempDB ( and every other DB )\nBackground The client doesn’t operate their own data center, so they’re dependent on the hoster, who unfortunately isn’t very communicative. Therefore, I had to get a picture of the totally overcommitted hardware through various other means and challenge the hoster. This was also the reason why I was quite precisely informed about what the hoster’s hosts and storage systems were capable of delivering.\nThat’s why I was very surprised that 32ms Write Latency was listed as a finding. No question, that’s a bad value for AllFlash NVMe, but it just stood there in the report without differentiation or deeper analysis. Just the statement „bad storage.“ So I unpacked my tools, took another look at it, and also developed a new solution that I want to share with you.\nDISKSPD Repo: perf_diskspeed/DISKSPD\nMicrosoft: https://github.com/microsoft/diskspd\nWith DISKSPD, you should always start on unknown systems; often a large portion of problems is hidden here. At this client, for example, a DB with high workload was running on spindles, with latencies in the .2 percentile of 15 seconds latency, plus 14MB throughput. I don’t need to worry about indexes etc. anymore.\nTo quickly get results, I built a wrapper for DISKSPD that always executes 3 standard tests:\nSQL Server Workload Random 64KB Workload Sequential 64KB Workload The files are stored separately in the _Output subfolder and thus provide quite good hints about storage problems. Now these were, as expected, great values that correspond to NVMe storage. So let’s continue to SQL Server and see what it tells us. welche NVMe Storage entsprechen. Also weiter zu Sql Server und wir schauen mal was uns dieser so sagt.\nsys.dm_io_virtual_file_stats SQL Server provides a view where all file metrics are cleanly listed.\nselect * from sys.dm_io_virtual_file_stats(null,null) Microsoft Learn Based on this, I built an analysis script that gives me more info and I can also draw further conclusions like ReadWrite Ratio, probable IOPS based on the number of operations, and also quantities. This allows you to build storage systems quite well for specific workloads.\nAnd this value also showed me 32ms Write Latency of the TempDB. Then I started wondering, how does this value even arise?\nBasic Calculation SQL Server measures I/O latency through simple division of cumulative wait times by the number of operations:\n-- Read Latency in ms io_stall_read_ms / num_of_reads = durchschnittliche Read-Latenz -- Write Latency in ms io_stall_write_ms / num_of_writes = durchschnittliche Write-Latenz That’s it!!! And now it should be clear that the point-in-time use of this DMV is not really meaningful, on the contrary. The values can, as with the review company that specializes in such processes, lead to false conclusions and action recommendations. Scary.\nIf the storage system had a hard hiccup since the start of SQL Server, the metrics are contaminated. According to my research, you can’t flush these, only by taking the DB offline, which I usually avoid in productive environments.\nAfter a few more rounds in my head, the solution came quite quickly. These are all snapshot values, I just need the delta between two values and I have real values. And I can also put the whole thing in a time series and have a long-term analysis. Mindblowing 😝\nGoogle also quickly showed corresponding solutions that I used as a base idea. Thanks to Paul Randal ( sqlskills.com )\nFor analyzing a complete week, this is also not suitable. So I extended the whole thing to a long-term analysis with persistent table.\nThe Solution –\u0026gt; snapDiskLatency Repo: perf_diskspeed/snapDiskLatency\nWhat does snapDiskLatency do? The problem is well-known – the standard DMV sys.dm_io_virtual_file_stats shows cumulative values since SQL Server startup. One single storage hiccup weeks ago and the latency values are no longer representative.\nThis solves the problem with two simple stored procedures:\nThe first collects the raw data from the DMV hourly and writes it to a permanent table. Why hourly? Shorter intervals lead to unreliable delta calculations – too little I/O between snapshots makes the values inaccurate. However, this can be individually adjusted since the agent triggers the procedure.\nThe second does the delta calculations between snapshots and creates the result. Thus, real latency values are generated for defined time periods, without historical hiccups, which can then be compared with other metrics.\nHere we see, for example, in the TRUNCATE TABLE an outlier (NVMe is also underlying here). On the LAB system there’s no load, DOXIS is just a table with 2 columns and was artificially worked with workload.\nThe Result Instead of possibly contaminated values:\nAverage latencies per interval Read/Write ratios Breakdown by database and file type Setup Create Agent Job, run hourly, done. The complete instructions are in the README, copy \u0026amp; paste \u0026amp; enjoy\nDiagrams Since every good proprietary database monitoring tool does this and presents it graphically, I want to at least show one possibility in Excel in the result. For this, the result of the DataSource or Copy must be pivoted and you can easily recognize your daily peaks.\n","permalink":"https://dbavonnebenan.de/en/sql-server-disk-latency/","summary":"How to get correct SQL Server Disk Latency values instead of falsified DMV data. Open Source snapDiskLatency solution for long-term analysis.","title":"Measuring SQL Server Disk Latency Correctly – snapDiskLatency"},{"content":"Intro I’ve always used PowerShell following the same pattern, that’s roughly how my learning path followed the PowerShell Best Practices.\nQuick and Dirty one-liners in the console, nowadays Terminal Gradually, those one-liners evolved into PowerShell scripts with better organized one-liners Variables came along, then functions, reusability increased Functions became modules, persistent data storage in JSON and technologically that was the end for a while The last highlight was threading and runspaces I’ve been operating on these last two levels for quite a long time now, of course getting more efficient and generally my PowerShell knowledge became broader.\nDuring this development, however, I noticed a problem: I’m a very organized person, at least in my professional environment 😉, and so should my code be, structured. I don’t think much of Clean Code, comments are just important at certain points, for me, for others, for my customers. Besides comments, structure is also important – what good do comments do me in a ‚bucket of chars‘? I also love symmetrical code, yeah go ahead and laugh, but it immediately makes clear what’s a variable, what’s an assignment and what’s the value. When I see something like this…\n$userInfo = @{ Name = $varSurename Address = $varStreet Data = $varRandomLongVar } $configSettings = @{ ServerName = \u0026#39;localhost\u0026#39; Port = 5432 Database = \u0026#39;mydb\u0026#39; Timeout = 30 } $employee = @{ Personal = @{ FirstName = $varSurename LastName = \u0026#39;Doe\u0026#39; Street = $varStreet } Work = @{ Department = \u0026#39;IT\u0026#39; Position = \u0026#39;Developer\u0026#39; } } … my insides turn outward 🤣 Maybe it’s also a disease. This is a normal writing style that’s considered clean, I know, but it should look like this …\n$userInfo = @{ Name = $varSurename Address = $varStreet Data = $varRandomLongVar } $configSettings = @{ ServerName = \u0026#39;localhost\u0026#39; Port = 5432 Database = \u0026#39;mydb\u0026#39; Timeout = 30 } $employee = @{ Personal = @{ FirstName = $varSurename LastName = \u0026#39;Doe\u0026#39; Street = $varStreet } Work = @{ Department = \u0026#39;IT\u0026#39; Position = \u0026#39;Developer\u0026#39; } } $users = @( @{ Name = \u0026#39;Alice\u0026#39;; Role = \u0026#39;Admin\u0026#39; } @{ Name = $varSurename; Role = \u0026#39;User\u0026#39; } ) Anyway, that’s not what this is about. It’s basically the case that structure and order gradually decrease in larger projects and classes play exactly into this topic. But why only now? Classes have existed since PowerShell 5.0.\nAI Assisted Coding Of course I was an early adopter of all the AI chatbots and was amazed at what ChatGPT could already do 2 years ago. Of course the code, regardless of language, was unusable, but definitely useful for knowledge generation. As the quality progressed, the code got better and the solution approaches became more and more interesting.\nAt some point, classes started appearing in the suggestions and I consistently ignored them 😝. Through my C# journeys, I knew that classes are simply the standard, but you should fundamentally know what you want to do with them in the project to design them cleanly. And I didn’t want to make that effort.\nThe New Project It was time to adapt a 4-year-old solution to new requirements, as is sometimes the case. Since my knowledge doesn’t stand still either, after briefly skimming through the code, I decided:\nI’M NOT TOUCHING THIS ANYMORE, THIS ALL HAS TO BE NEW!!\nNow I was in the relaxed position that everything was available for the solution:\nProgram flow chart Function descriptions Programmatic solution of the required functions at the lowest level 4 years of lessons learned So I only had to worry about orchestrating all the steps and decided to give classes a chance. In theory, this is namely the cleanest and most structured way to code.\nThe result hasn’t completely changed my way of coding, but it has sustainably changed it – but more on that later.\nConclusion After 5 Weeks in the Project Classes in PowerShell are so incredibly simple, so structured, so beautiful❤️. Of course the use case has to fit, since the implementation initially requires more structure, but when that’s given, there will be no way around classes for me anymore.\nWhy This Article, Aren’t There Enough Already? There are some articles about classes in PowerShell and object orientation, but still this topic is hardly covered in a classic tutorial. It’s always the functions and you have to know about this topic to find it. So I’m simply writing another article, also in german this time, to maybe reach one or the other additionally, because…\nBackground Info on PowerShell Classes – OOP Since Version 5.0 Classes have existed in PowerShell since PowerShell 5.0, so 2016… and I only learned about them a year ago 🤦‍♂️. PowerShell classes bring object-oriented programming concepts to PowerShell and thus also have all the basic features of a class.\nProperties: Properties are strongly typed and support most validation attributes like [ValidateNotNull()], [ValidateRange()], etc. Methods: Methods are functions bound to an object and must explicitly define the return type Constructors: Enable initialization of objects with specific values Inheritance: Allows creating new classes from existing classes and extending their functionality A class, after loading, is its own data type – more on that later.\nAnd How Does This Work Now? I still remember how I struggled with OOP many years ago and it’s generally assumed that rather admins than full-blood software developers initially deal with PowerShell. So I’ll try to keep it totally simple.\nI’ll take a fictional module that queries server properties via remoting and evaluates them locally, a classic admin task.\nATTENTION: Of course this example is very constructed and not necessarily practical, but in my opinion it shows the relationships quite well.\nThis PowerShell classes tutorial shows you the most important concepts for object-oriented programming with PowerShell.\nProperties A class has a firmly defined set of properties at all times. This cannot be changed at runtime, but must be described in the definition of the class.\nThe Class class Server { # All Possible properties for the Server class [string] $ServerName [string] $Status [ipaddress] $IP [string] $Location [datetime] $LastBootTime [string] $OperatingSystem # ------------------------------------------- } Constructors A constructor creates the instance of a class. The class can receive default values, or the constructor expects parameters. In our case, a server name makes sense. So I give the property ServerName a value directly when loading.\nclass Server { # All Possible properties for the Server class [string] $ServerName [string] $Status [ipaddress] $IP [string] $Location [datetime] $LastBootTime [string] $OperatingSystem # ------------------------------------------- # Constructor Server([string]$name) { $this.ServerName = $name } } Now we have a class, but how do we get a server in there? By loading the class into a variable and preferably also dynamically for all servers.\nclass Server { # All Possible properties for the Server class [string] $ServerName [string] $Status [ipaddress] $IP [string] $Location [datetime] $LastBootTime [string] $OperatingSystem # ------------------------------------------- # Constructor Server([string]$name) { $this.ServerName = $name } } $serverlist = @(\u0026#39;Server1\u0026#39;,\u0026#39;Server2\u0026#39;,\u0026#39;Server3\u0026#39;) foreach ( $server in $serverlist ) { # Create a new Server object for each server name New-Variable -Name \u0026#34;obj_$server\u0026#34; -Value ([Server]::new($server)) -Force } Methods Methods make classes really interesting in my opinion. Methods are the functions with which I can manipulate properties of classes or generate RETURNS based on arguments that I add to the call. Here I already specify the type of return in the method definition. VOID thus logically creates no return.\nclass Server { [string] $ServerName [string] $Status [ipaddress] $IP [string] $Location [datetime] $LastBootTime [string] $OperatingSystem Server([string]$name) { $this.ServerName = $name } [void] GetServerInfo() { try { $osInfo = Invoke-Command -ComputerName $this.ServerName -ScriptBlock { Get-CimInstance Win32_OperatingSystem | Select-Object Caption, LastBootUpTime } -ErrorAction Stop $this.IP = Invoke-Command -ComputerName $this.ServerName -ScriptBlock { (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.IPAddress -notlike \u0026#39;169.*\u0026#39;} | Select-Object -First 1).IPAddress } -ErrorAction Stop $this.OperatingSystem = $osInfo.Caption $this.LastBootTime = $osInfo.LastBootUpTime $this.Status = \u0026#34;Online\u0026#34; } catch { $this.Status = \u0026#34;Offline\u0026#34; } } } $serverlist = @(\u0026#39;Server1\u0026#39;,\u0026#39;Server2\u0026#39;,\u0026#39;Server3\u0026#39;) foreach ($server in $serverlist) { New-Variable -Name \u0026#34;obj_$server\u0026#34; -Value ([Server]::new($server)) -Force } $obj_Server1.GetServerInfo() $obj_Server2.GetServerInfo() $obj_Server3.GetServerInfo() Here I’ve inserted the method GetServerInfo(), which now collects the information about the server. I don’t need to pass an argument or start a cmdlet, since the instance (server name) of the class (Server) has inherited everything. I don’t need external functions, variables or anything else, everything is available in the class. ❤️ Following an example from the LAB:\nLAB Results\nAnd Now, Where’s the Benefit? Of course this isn’t the most optimal use case, but it’s understandable. One possibility would be to create a logging class. Here from my project:\nclass PatchLogger { [string] $ServerName [string] $LogPathServer [string] $LogLevel = \u0026#39;INFO\u0026#39; [hashtable] $LogTargets [bool] $ConsoleOutput [bool] $StreamOutput [string] $VerbosePreference [string] $DebugPreference # Constructor PatchLogger(\t[string]$LogPathServer, [string]$ServerName, [string]$MessageFile, [string]$ErrorFile, [string]$VerboseFile, [string]$DebugFile, [string]$SqlServerFile, [bool]$ConsoleOutput = $true, [bool]$StreamOutput = $false) { $this.LogPathServer = $LogPathServer $this.ServerName = $ServerName $this.ConsoleOutput = $ConsoleOutput $this.StreamOutput = $StreamOutput $this.LogTargets = @{ Main = $MessageFile Error = $ErrorFile Verbose = $VerboseFile Debug = $DebugFile SqlServer = $SqlServerFile } $this.VerbosePreference = $Global:VerbosePreference $this.DebugPreference = $Global:DebugPreference if ( -not (Test-Path $this.LogPathServer) ) { New-Item -Path $this.LogPathServer -ItemType Directory -Force | Out-Null } } } An instance of the class is loaded directly into the variable $PatchLogger when the module loads, and from now on I can start right away with $PatchLogger.Info($message). How? Such a short command with such a complex class? Let’s get to the last thing, which in my opinion is the advantage par excellence: Overloading.\nOverloading Overloading means nothing other than the number of arguments I pass to a method. Based on the number, the engine can distinguish which method is used, since I can have several with the same name. The technical term would be Method Overloading.\nThe added value lies in the fact that I don’t have to work with BoundParams, Switch or IF loops like with functions. When a helper method is called with only one argument, the missing arguments are filled in internally and the main method is called.\nLet’s take the example $PatchLogger.Info($message)\n# Hauptmethode [hashtable] WriteLog([string]$Level, [string]$Message, [string]$MessageCode, [int]$Severity, [string]$Target, [bool]$ReturnObject) {...} # Helfermethode [hashtable] Info([string]$Message) { return $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $null, 0, \u0026#39;Main\u0026#39;, $true) } # Helfermethode 2 [hashtable] Info([string]$Message, [string]$MessageCode) { return $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $MessageCode, 0, \u0026#39;Main\u0026#39;, $true) } What will be most needed in a large module? A simple message to the main log, without message code and frills. So the helper method calls the main method with the default values that do exactly that.\nLet’s look at helper method 2. This expects 2 arguments, namely a MessageCode. Now I can write a registered message with $PatchLogger.Info(„Process XY completed“,“I0002″).\nOf course I can also implement all this with functions, but I don’t think it’s as structured and pretty.\nComplete PatchLogger Class class PatchLogger { [string] $ServerName [string] $LogPathServer [string] $LogLevel = \u0026#39;INFO\u0026#39; [hashtable] $LogTargets [bool] $ConsoleOutput [bool] $StreamOutput [string] $VerbosePreference [string] $DebugPreference # Constructor PatchLogger([string]$LogPathServer, [string]$ServerName, [string]$MessageFile, [string]$ErrorFile, [string]$VerboseFile, [string]$DebugFile, [string]$SqlServerFile, [bool]$ConsoleOutput = $true, [bool]$StreamOutput = $false) { $this.LogPathServer = $LogPathServer $this.ServerName = $ServerName $this.ConsoleOutput = $ConsoleOutput $this.StreamOutput = $StreamOutput $this.LogTargets = @{ Main = $MessageFile Error = $ErrorFile Verbose = $VerboseFile Debug = $DebugFile SqlServer = $SqlServerFile } $this.VerbosePreference = $Global:VerbosePreference $this.DebugPreference = $Global:DebugPreference if ( -not (Test-Path $this.LogPathServer) ) { New-Item -Path $this.LogPathServer -ItemType Directory -Force | Out-Null } } [void] UpdatePreferences([string]$VerbosePreference, [string]$DebugPreference) { $this.VerbosePreference = $VerbosePreference $this.DebugPreference = $DebugPreference } # Central WriteLog method - all other methods call this one [object] WriteLog([string]$Level, [string]$Message, [string]$MessageCode, [int]$Severity, [string]$Target, [bool]$ReturnObject) { $timestamp = Get-Date -Format \u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34; $severityText = if ($Severity -ne 9999) { \u0026#34;S$Severity\u0026#34; } else { \u0026#34;\u0026#34; } $messageCodeText = if ($MessageCode) { \u0026#34; $MessageCode\u0026#34; } else { \u0026#34;\u0026#34; } $paddedLevel = \u0026#34;{0,-9}\u0026#34; -f \u0026#34;[$Level]\u0026#34; $additionalInfo = \u0026#34;$severityText$messageCodeText\u0026#34; $logEntry = if ( $level -eq \u0026#39;BLANK\u0026#39; ) { \u0026#34;\u0026#34; } elseif ( $Level -notin @(\u0026#39;VERBOSE\u0026#39;,\u0026#39;DEBUG\u0026#39;) -and -not [string]::IsNullOrEmpty($additionalInfo) ) { \u0026#34;[$timestamp] $paddedLevel [$($this.ServerName)] $Message ($additionalInfo)\u0026#34; } else { \u0026#34;[$timestamp] $paddedLevel [$($this.ServerName)] $Message\u0026#34; } $targetFile = $this.LogTargets[$Target] if ( $targetFile ) { try { Add-Content -Path $targetFile -Value $logEntry -Encoding UTF8 $verboseFile = $this.LogTargets[\u0026#39;Verbose\u0026#39;] if ( $verboseFile -and $Level -ne \u0026#39;VERBOSE\u0026#39; ) { Add-Content -Path $verboseFile -Value $logEntry -Encoding UTF8 # for unbroken verbose stream } } catch { Invoke-ScriptError $_ } } if ( $this.ConsoleOutput ) { switch ( $Level ) { \u0026#39;ERROR\u0026#39; { Write-Host $logEntry -ForegroundColor Red } \u0026#39;WARNING\u0026#39; { Write-Host $logEntry -ForegroundColor Yellow } \u0026#39;INFO\u0026#39; { Write-Host $logEntry -ForegroundColor Green } \u0026#39;BLANK\u0026#39; { Write-Host $logEntry} \u0026#39;VERBOSE\u0026#39; { if ( $this.VerbosePreference -ne \u0026#39;SilentlyContinue\u0026#39; ) { Write-Host $logEntry -ForegroundColor Cyan } } \u0026#39;DEBUG\u0026#39; { if ( $this.DebugPreference -ne \u0026#39;SilentlyContinue\u0026#39; ) { Write-Host $logEntry -ForegroundColor Magenta } } } } # PowerShell streams for integration - only if explicitly enabled if ( $this.StreamOutput ) { switch ($Level) { \u0026#39;ERROR\u0026#39; { Write-Error $Message -ErrorAction Continue } \u0026#39;WARNING\u0026#39; { Write-Warning $Message } \u0026#39;VERBOSE\u0026#39; { Write-Verbose $Message } \u0026#39;DEBUG\u0026#39; { Write-Debug $Message } } } # Return object only when requested if ( $ReturnObject ) { return @{ HasError = $Level -eq \u0026#39;ERROR\u0026#39; HasWarning = $Level -eq \u0026#39;WARNING\u0026#39; MessageCode = if ( $MessageCode ) { $MessageCode } else { $null } Message = $Message Severity = $Severity Level = $Level Target = $Target } } return $null } # BLANK Line [void] BlankLine() { $this.WriteLog(\u0026#39;BLANK\u0026#39;, \u0026#39;\u0026#39;, $null, 0, \u0026#39;Main\u0026#39;, $false) } # INFO Methods - standard return hashtable [hashtable] Info([string]$Message) { return $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $null, 0, \u0026#39;Main\u0026#39;, $true) } [hashtable] Info([string]$Message, [string]$MessageCode) { return $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $MessageCode, 0, \u0026#39;Main\u0026#39;, $true) } # INFO NoReturn [void] InfoNoReturn([string]$Message) { $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $null, 0, \u0026#39;Main\u0026#39;, $false) } [void] InfoNoReturn([string]$Message, [string]$MessageCode) { $this.WriteLog(\u0026#39;INFO\u0026#39;, $Message, $MessageCode, 0, \u0026#39;Main\u0026#39;, $false) } # WARNING Methods - standard return hashtable [hashtable] Warning([string]$Message) { return $this.WriteLog(\u0026#39;WARNING\u0026#39;, $Message, $null, 1, \u0026#39;Main\u0026#39;, $true) } [hashtable] Warning([string]$Message, [string]$MessageCode) { return $this.WriteLog(\u0026#39;WARNING\u0026#39;, $Message, $MessageCode, 1, \u0026#39;Main\u0026#39;, $true) } # WARNING NoReturn [void] WarningNoReturn([string]$Message) { $this.WriteLog(\u0026#39;WARNING\u0026#39;, $Message, $null, 1, \u0026#39;Main\u0026#39;, $false) } [void] WarningNoReturn([string]$Message, [string]$MessageCode) { $this.WriteLog(\u0026#39;WARNING\u0026#39;, $Message, $MessageCode, 1, \u0026#39;Main\u0026#39;, $false) } # ERROR Methods - standard return hashtable [hashtable] Error([string]$Message) { return $this.WriteLog(\u0026#39;ERROR\u0026#39;, $Message, $null, 2, \u0026#39;Error\u0026#39;, $true) } [hashtable] Error([string]$Message, [string]$MessageCode) { return $this.WriteLog(\u0026#39;ERROR\u0026#39;, $Message, $MessageCode, 2, \u0026#39;Error\u0026#39;, $true) } # ERROR NoReturn [void] ErrorNoReturn([string]$Message) { $this.WriteLog(\u0026#39;ERROR\u0026#39;, $Message, $null, 2, \u0026#39;Error\u0026#39;, $false) } [void] ErrorNoReturn([string]$Message, [string]$MessageCode) { $this.WriteLog(\u0026#39;ERROR\u0026#39;, $Message, $MessageCode, 2, \u0026#39;Error\u0026#39;, $false) } # VERBOSE \u0026amp; DEBUG - NoReturn [void] Verbose([string]$Message) { $this.WriteLog(\u0026#39;VERBOSE\u0026#39;, $Message, $null, 0, \u0026#39;Verbose\u0026#39;, $false) } [void] Debug([string]$Message) { $this.WriteLog(\u0026#39;DEBUG\u0026#39;, $Message, $null, 0, \u0026#39;Debug\u0026#39;, $false) } # Step/SQL/AG Methods - NoReturn [void] StepStart([int]$Step, [string]$StepName) { $this.BlankLine() $message = \u0026#34;========================= STEP $Step START: $StepName =========================\u0026#34; $this.WriteLog(\u0026#39;INFO\u0026#39;, $message, $null, 9999, \u0026#39;Main\u0026#39;, $false) } [void] StepEnd([int]$Step, [string]$StepName, [bool]$Success) { $status = if ( $Success ) { \u0026#34;SUCCESS\u0026#34; } else { \u0026#34;FAILED\u0026#34; } $message = \u0026#34;========================= STEP $Step END: $StepName - $status =========================\u0026#34; $this.WriteLog(\u0026#39;INFO\u0026#39;, $message, $null, 9999, \u0026#39;Main\u0026#39;, $false) $this.BlankLine() } } Summary PowerShell classes revolutionize how you structure larger projects. Available since PowerShell 5.0, they offer object-oriented programming with properties, methods, constructors, and method overloading. The initial extra effort pays off through clean, maintainable code – especially for modules and more complex function collections.\nThe most important advantages at a glance:\nStrongly typed properties with validation Encapsulated functionality through methods Elegant overloading instead of complex parameter logic and a structure that remains understandable even after months For daily-doing scripts, the classic approach remains more practical, but as soon as reusability and structure become important, there’s no way around classes for me anymore. 🫡\n","permalink":"https://dbavonnebenan.de/en/powershell-classes-for-beginner/","summary":"Intro I’ve always used PowerShell following the same pattern, that’s roughly how my learning path followed the PowerShell Best Practices. I’ve been operating on these last two levels for quite a long time now, of course getting more efficient and generally my PowerShell knowledge became broader. During this development, however, I noticed a problem: I’m","title":"PowerShell Classes for Beginners – Structured Code Instead of Function Chaos"},{"content":"Recently, I stumbled across a LinkedIn post about truncate vs. delete, just like many others out there. Typically, random members post simple SQL Server tips, nicely packaged in graphics and kept general. Although for beginners this is absolutely legitimate – since these simple entry points can help many newcomers understand and provide a great starting point – nevertheless, the fact that the same topics then go viral across various accounts is clearly another matter entirely.\nAnyway. If you’re a bit further along in the topic and already know that the pronunciation isn’t „SQL“ but „sequel“ 😊, you should fundamentally question these posts, as they mostly generalize.\nThis was also the case in a post about the top 3 SQL mistakes that „everyone“ makes. And here we are, getting to the point.\nLinkedIn\nThe same happened with Vedran Kesegic, who gave the hint that TRUNCATE is apparently rollback-capable when encapsulated in a transaction. Then Carsten Saastamoinen-Jakobsen jumped in and things got wild. So wild that the whole topic hooked me so much that I did a deep dive into it. Furthermore, so much knowledge dropped in this exchange that it would be a shame if it disappeared in a comment section.\nWhat to expect here I compared the exchange and looked at the core statements. I also texted separately with both of them a bit more, and Carsten gave me excerpts from one of his publications where the topic is covered technically.\nTechnically speaking, both agree, even if they don’t say it 🤣. But what was really wild in the end was the question:\nI can say it upfront. This question is an philosophical one. I was able to form an opinion based on my research, more on that later, but basically it’s up to everyone to decide for themselves.\nThe Discussion – DML or DDL ? Vedran Kesegic Core Position: TRUNCATE is DDL\n„TRUNCATE modifies internal system tables sys.sysallocunits and sys.sysrowsets. DELETE modifies indexes and stats“\n„DML writes data (rows in user tables), while DDL writes metadata (rows in system tables, data that describes other/user objects). Truncate happens to write metadata (system tables) and thus is classified as DDL.“\nTechnical Arguments\nUses SCH-M (Schema Modification) lock Modifies system metadata tables Microsoft documentation classifies it as DDL Logs page deallocations, not row deletions Key Statement\n„TRUNCATE modifies internal system tables sys.sysallocunits and sys.sysrowsets. DELETE modifies indexes and stats – see attached“\nCarsten Saastamoinen-Jakobsen Core Position: TRUNCATE is DML\n„It is not an argument to call TRUNCATE a DDL statement because a certain lock is used! When statements are categorized, it must be based on what happens and not how a given database system solves the task.“\n„We may have to recognize that a previous error categorization should not continue to call TRUNCATE a DDL, but recognize that it is a DML.“\nLogical Arguments\nData is deleted, structure is preserved No table definition changes occur Follows semantic rules of data manipulation Can delete only some partitions (not all data) Key Statement\n„I say that MS is right in the documentations because they are not telling that TRUNCATE is as DDL, because they know that TRUNCATE is a DML!“\nNOTE Gabriel\nI think this is really a bit questionable because the documentation is absolutely clear on this point –\u0026gt; DDL (microsoft.com)\nQuote from publication\n„As of version 2016, it is possible to delete only some of the data in a table with TRUNCATE, so it is even more cor­rect to call it manipulation than definition, because it is changing some of the data but not changing the structure.“\nPoints of Agreement TRUNCATE can be rolled back in a transaction general demo at sqlauthority.com deep dive demo sqlshack.com TRUNCATE is a logged operation TRUNCATE is faster than DELETE for large datasets Both understand the technical implementation Core Disagreement Classification criteria Implementation details Authority Vendor documentation (Vedran) vs. Logical reasoning (Carsten) Perspective How it works internally (Vedran) vs. What it does functionally (Carsten) My 50 Cent So as you can see, technically it fits, but how do you view the whole thing? Maybe it’s good that I’m not preloaded with as much knowledge as both of them because from my perspective it’s quite clear: DDL!!\nContrary to Carsten’s statement that no system tables are changed, Vedran’s statement is absolutely correct. See why…\n… and of course because MS says it’s DDL 😊 ( microsoft.com )\nAnd partitioning is DML… I don’t know, here too it’s at least system columns that define the partitioning.\nHow TRUNCATE works I’d like to say I’ll describe it simply, but that’s just my normal description. The topic is really complex ✌️\n1 – TRUNCATE Command Initiated TRUNCATE TABLE dbo.MyTable; First action: Acquire SCH-M (Schema Modification) lock on entire table Effect: Table is completely locked – no reads, no writes, nothing 2 – Identify Extents DEEP DIVE Extents –\u0026gt; microsoft.com SQL Server reads IAM pages Lists all extents belonging to the table Still holding: SCH-M lock on table 3- Lock Individual Extents Places X (Exclusive) locks on each extent These are regular data locks, NOT schema locks Now holding: SCH-M lock on table (prevents access) X locks on extents (marks them for deallocation) 4 – Log the Operation Writes to transaction log: „Deallocate extents 8:8, 8:16, etc.“ Still holding: Both lock types 5a – If COMMIT Updates IAM – marks extents as free Releases all X locks on extents Releases SCH-M lock on table Table is accessible again 5b – If ROLLBACK Discards deallocation markers Releases all X locks on extents Releases SCH-M lock on table Everything unchanged Why Two Different Lock Types? SCH-M Lock:\n„Nobody touch this table AT ALL“ Typical for DDL operations Prevents even metadata queries X Locks on Extents:\n„These specific data blocks are being modified“ Typical for data operations Ensures extents aren’t reused until commit/rollback by other sessions My humble Opinion Given this knowledge, the case is actually clear for me, but how do you see it? Indeed, here no user data is being manipulated; instead, system-level assignments are being adjusted. Consequently, I’ll allow myself, despite my new half-knowledge, to make the following comparison:\n„I also don’t say I’m deleting text in a Word document when I quick format the disk and only delete the block allocation.“\nYes, I think that’s a nice comparison… Case closed, now a few technically interesting aspects and solutions\nTechnical Part, Hacks and How2 … practical, not technical 😉\nTRUNCATE with Survivor Table (Temporary Table) Data that is still required is simply moved to a temporary table and retrieved after TRUNCATE\n-- Step 1: Create temp table with rows to keep SELECT * INTO #SurvivorRows FROM MyTable WHERE DateColumn \u0026gt;= \u0026#39;2024-01-01\u0026#39; -- Step 2: Truncate the entire table TRUNCATE TABLE MyTable -- Step 3: Re-insert survivor rows INSERT INTO MyTable SELECT * FROM #SurvivorRows -- Step 4: Clean up DROP TABLE #SurvivorRows TRUNCATE with PARTITION option (2016++) Since SQL Server 2016 (every edition) it is possible to specify partitions or ranges of partitions\n-- Truncate specific partitions TRUNCATE TABLE MyPartitionedTable WITH (PARTITIONS (2, 4 TO 6)) -- Check partition status SELECT partition_number ,rows FROM sys.partitions WHERE OBJECT_NAME(OBJECT_ID) = \u0026#39;MyPartitionedTable\u0026#39; TRUNCATE is always a transaction… …. and must be considered during RESTORING. Carsten illustrated this whole thing very nicely in his publication. I’m just describing it, that has to suffice at this point 😝\nEven though we only write TRUNCATE TABLE, SQL Server still uses BEGIN and COMMIT TRANSACTION, which of course is recorded in the TLog. Accordingly, during a POINT IN TIME RECOVERY, the data in the table remains available until the TLog with the corresponding TRANSACTION is restored, or the time of the TRUNCATE transaction has been exceeded.\nSo you can also restore the data 👌\nContinuously read Transaction log file data with fn_dblog and fn_dump_dblog Vedran wrote in one of his posts that there’s an undocumented function where you can read log records at runtime. There’s also a very cool article about this on sqlshack.com\nNOT recommended for production but only for test / education.\nHow to continuously read Transaction log file data directly in a SQL Server database with fn_dblog and fn_dump_dblog\nFinally Let’s get to my initially mentioned point that all these SQL Server tips from randoms should be questioned. Because the statement that truncate is not rollback safe is simply wrong. The path to this realization was long for me, initiated by the conversation between Vedran and Carsten. I really learned a lot left and right, which I also wanted to record here again for posterity.\nIn the end, it doesn’t matter whether it’s DDL or DML. In the general SQL Server standard, for example, it’s described as DML, most RDBMSs in dialect with DDL. We’ll just agree on DDL and that’s it. Much more exciting is what happens under the hood.\nBest comment from Vedran 🤣\n„Or you are trying to argue with SQL Certified Master about SQL? Good luck Mr :)“\nLinks from the text Transact-SQL statements – SQL Server\nTRUNCATE TABLE (Transact-SQL) – SQL Server\nSQL SERVER – Rollback TRUNCATE Command in Transaction – SQL Authority with Pinal Dave\nThe internals of SQL Truncate and SQL Delete statements\nHow to continuously read Transaction log file data directly in a SQL Server database with fn_dblog and fn_dump_dblog\nTruncate (SQL) – Wikipedia\nPages and Extents Architecture Guide – SQL Server\nFull Conversation Expand Vedran Kesegic\nIn sql server, truncate can be rolled back (if encapsulated within transaction). It is a logged operation.\nCarsten Saastamoinen-Jakobsen\nWith DELETE we can specify the WHERE clause and some or all rows can be removed. All deleted rows are logged with BEFORE IMAGE/the data values. With TRUNCATE we can delete all rows or only the rows in one or more partitions, if we have a partition table. The only information logged is the pageid. The physical removing/releasing to unused pages happens in a background task after COMMIT. No data is stored in the log, so less and faster! And it is not possible to restore to point in time because no data is logged. The pageid in the log can already have being ‚released‘ to unused pages and therefore already used to data from another object – table/index. Before COMMIT the pages is still allocated to the table and still with data. If no explicit transaction is specified the transaction is already committed!\nVedran Kesegic\n„No data is stored in the log“ and „not possible to restore to point in time“ is both incorrect for TRUNCATE. The difference is, truncate logs only deallocation of pages, which is multiple orders of magnitude less (basically insignifficant) compared to DELETE which logs data that is deleted. That is why truncate is „instant“. If you delete eg. 10GB of data, you wait for 10GB to be written to transaction log, which takes time. Truncate logs maybe 1KB for deallocation info, which is instant. If you do not want to delete all rows, you can still use TRUNCATE by storing survivor rows aside before truncating, and inserting them back after truncate. Even better, you can use SWITCH into some dummy table (even with not-partitioned tables) to remove rows quickly, and then insert survivor rows back from that dummy into original table. That is the fastest way to delete when a smaller percent of rows needs to survive.\nVedran Kesegic\nObvious difference is that truncate removes all rows from table („truncate table“) or from partition(s), when using „trancate table xy with partitions (…)“. You cannot choose which rows to delete, they all are gone. With delete you can have a WHERE clause to choose which rows to delete. Delete is more appropriate if you need to delete a small percentage of table rows (or partition rows).\nVedran Kesegic\nTruncate is a metadata operation, requires a different lock (SCH_M, schema modification lock). If also has some restrictins regarding foreign keys etc. Check the documentation for the list of limitations: https://learn.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql\nCarsten Saastamoinen-Jakobsen\nIt is not an argument to call TRUNCATE a DDL statement because a certain lock is used! When statements are categorized, it must be based on what happens and not how a given database system solves the task. Data is deleted, the structure is preserved and only some data may be deleted. When one or more partitions are deleted, the condition is also well-known, but just limited. In previous versions of database systems – and perhaps still – a TRUNCATE was performed by a DROP TABLE + CREATE TABLE. Hence DDL rights. It just shows that it is problematic to categorize based on how it is performed and not based on what actually happens. If we look at the documentation for SQL Server, it is not mentioned that TRUNCATE is a DDL. We may have to recognize that a previous error categorization should not continue to call TRUNCATE a DDL, but recognize that it is a DML. We will also be free from erroneous claims about only deleting all rows, that space is not released, that the transaction cannot be rolled back, …. A DELETE operation without a user-defined transaction cannot be rolled back either! But DELETE is DML and not DDL. TRUNCATE is ‚created‘ for having af fast way of deleting rows, and extended for only deleting some of the rows – DML!!!\nVedran Kesegic\nNo, the SCH-M lock (the „king of all locks“) is not the sole reason why TRUNCATE is DDL. DML writes data (rows in user tables), while DDL writes metadata (rows in system tables, data that describes other/user objects). Truncate happens to write metadata (system tables) and thus is classified as DDL. See the doc: https://learn.microsoft.com/en-us/previous-versions/sql/sql-server-2012/ff848799(v=sql.110)\nCarsten Saastamoinen-Jakobsen\nCan you tell which data is written to the system tables that is not written when removing rows by DELETE? The only additional information is, that with TRUNCATE of all rows without specifying partitions is, that the ‚identity‘ is reset to the initial values. Removing some or all rows with specifying partitions, this value is not reset to the initial value. Empty page are handled in the same way as empty page after DELETE! All indexes are preserved, the table definition is NOT changed, ….\nVedran Kesegic\nBetter than that – you can see for yourself! The exact difference and details are present in transaction log records after you do TRUNCATE and compare that to log records that DELETE generates. Use fn_dblog() function, but only on the test system because it is undocumented and might have an impact.\nOr you are trying to say that Microsoft is wrong in documentation? If so, report it to MS so doc can be fixed. But in this case there is nothing to fix.\nOr you are trying to argue with SQL Certified Master about SQL? Good luck Mr 🙂\nCarsten Saastamoinen-Jakobsen\nI say that MS is right in the documentations because they are not telling that TRUNCATE is as DDL, because they know that TRUNCATE is a DML!. Of course there is less in the log – it is the idea wit TRUNCATE!!!!!!!! Just te functionality and te reason for implementing TRUNCATE as an alternative to DELETE. But the log is NOT the system tables!!!!! So please tell which system tables are updated as they are updated when CRAETE, ALTER and DROP but NOT updated when INSERT, DELETE or UPDATE.\nVedran Kesegic\nTRUNCATE modifies internal system tables sys.sysallocunits and sys.sysrowsets. DELETE modifies indexes and stats – see attached pictures. You could do that yourself using fn_dblog as instructed, but I did that for you so you don’t have to. If you want to know more about deep SQL Server internals you can attend one of my trainings, or trainings of other SQL Masters like Brent Ozar, Paul Randal (sqlskills), etc.\nCarsten Saastamoinen-Jakobsen\nOf course they are modified as INSERT DELETE and UPDATE modify this informations. But INSERT, DELETE and UPDATE are still only DML and not DDL. And I am not impressed of your job by showing me this information. If you was showing the same for the other DML statements it would be fine. So maybe you should modify your course material and also shows the same for the other DML statements!!!!\n","permalink":"https://dbavonnebenan.de/en/sql-server-truncate-vs-delete-dml-or-ddl/","summary":"Recently, I stumbled across a LinkedIn post about truncate vs. delete, just like many others out there. Typically, random members post simple SQL Server tips, nicely packaged in graphics and kept general. Although for beginners this is absolutely legitimate – since these simple entry points can help many newcomers understand and provide a great starting","title":"TRUNCATE TABLE – DDL vs DML – deep dive in operations and practical tips"},{"content":"Today I had another call on my desk where an error message from an application stated that an object in the Sql Server DB couldn’t be accessed. The vendor claimed „connection problem“, I said „DB or at least server problem“, customer says „everything used to work before“.\nThe Extended Events were quickly set up, „Remote Procedure Call Starting“ shows me a statement without a fully qualified table name. The target object is in the database under the db_owner schema. Welllll, that’s already questionable, but OK. Why not keep it as dbo?\nAnyway, the behavior at this point was really incomprehensible to me since everything was configured correctly: Default schema of the user, db_owner on the DB. And the SysAdmin. I had noticed that, but well, customer’s decision. I’ll note things like this in a session, but won’t change anything to avoid endangering grown dependencies.\nAfter searching for a long time, I almost gave up, referred to the vendor and ended the call. Unfortunately, I can’t let go of stuff like this, so I fired up the lab, rebuilt the environment and lo and behold, found the error.\nThe problem is the SysAdmin role. It directly maps the login to the dbo user of the database – regardless of whether the actual user exists or not, thus inheriting dbo’s default schema. Mindblowing, didn’t know that. 🙂\nSysAdmin Role InActive SysAdmin Role Active T-Sql CODE I used for the screenshots DECLARE @IsSysAdmin INT SELECT @IsSysAdmin = IS_SRVROLEMEMBER(\u0026#39;sysadmin\u0026#39;) SELECT SYSTEM_USER AS CurrentLoginName; SELECT \u0026#39;Current User\u0026#39; AS Info, USER_NAME() AS Username, SCHEMA_NAME() AS CurrentSchema, CASE WHEN @IsSysAdmin = 1 THEN \u0026#39;Yes\u0026#39; ELSE \u0026#39;No\u0026#39; END AS IsSysAdmin SELECT \u0026#39;Default Schema\u0026#39; AS Info, dp.name AS Username, dp.default_schema_name AS DefaultSchema FROM sys.database_principals dp WHERE dp.name = USER_NAME() SELECT \u0026#39;DB Role Membership\u0026#39; AS Info, USER_NAME() AS Username, r.name AS RoleName FROM sys.database_principals u JOIN sys.database_role_members m ON u.principal_id = m.member_principal_id JOIN sys.database_principals r ON m.role_principal_id = r.principal_id WHERE u.name = USER_NAME() -------------------- -- select * from [customSchema].[userTable] \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; ALWAYS WORKING !!!!! select * from [userTable] Always use fully qualified object names, someday you’ll be glad you did. You can read this in many blogs. Always work according to POLP (Principle of Least Privilege), an app user shouldn’t have SysAdmin rights! 🙂 Gabriel, der DBAvonNebenan\n","permalink":"https://dbavonnebenan.de/en/sql-server-sysadmin-role-can-override-the-default-schema/","summary":"Today I had another call on my desk where an error message from an application stated that an object in the Sql Server DB couldn’t be accessed. The vendor claimed „connection problem“, I said „DB or at least server problem“, customer says „everything used to work before“. The Extended Events were quickly set up, „Remote","title":"SysAdmin Role ignores the Default User Schema – A confusing story"},{"content":"Knowing that SQL Server has blocking or deadlock problems is still quite easy. You usually notice this in the application, and with the right queries, you can also display the corresponding blocking chains. The simplest way to do this is with http://whoisactive.com.\nSELECT blocking_session_id AS blocking_session, session_id AS blocked_session, wait_time / 1000.0 AS waiting_sec, wait_type AS waittype, resource_description AS ressource FROM sys.dm_os_waiting_tasks WHERE blocking_session_id IS NOT NULL ORDER BY wait_time DESC I also have a custom development that I will present in a later blog post, which better meets my individual requirements.\nThe Problem Now I can display AdHoc blocks, but with deadlocks it gets more interesting. For this, the System Health Session must be checked, which is also volatile.\n-- Query System Health Session for Deadlocks SELECT event_data.value(\u0026#39;(event/@timestamp)[1]\u0026#39;, \u0026#39;datetime2\u0026#39;) AS DeadlockDateTime, event_data.query(\u0026#39;.\u0026#39;) AS DeadlockGraph FROM ( SELECT CAST(target_data AS XML) AS target_data FROM sys.dm_xe_session_targets AS st JOIN sys.dm_xe_sessions AS s ON s.address = st.event_session_address WHERE s.name = \u0026#39;system_health\u0026#39; AND st.target_name = \u0026#39;ring_buffer\u0026#39; ) AS data CROSS APPLY target_data.nodes(\u0026#39;//RingBufferTarget/event[@name=\u0026#34;xml_deadlock_report\u0026#34;]\u0026#39;) AS XEventData(event_data) ORDER BY DeadlockDateTime DESC; I encountered this problem with a client who was struggling enormously with deadlocks in a 3rd party application, but the support was a bit… let’s say reluctant. 😉\nWe had to report everything transparently to them so they would start finding a solution. Of course, query result sets can be considered transparent if you understand them or want to understand them. Unfortunately, the XML of Health Sessions or Extended Events are not exactly famous for being readable. Yet the client simply wanted the problem to be fixed.\nSo together we developed a reporting solution.\nThe Idea With Extended Events I have XML data, and with PowerShell I can parse XML however I want, including into a report of my choice.\nThe Goal We wanted a way, apart from SQL Server result sets, to overlay application problems with anomalies in the SQL Server metrics to quickly bring about solutions.\nIn the week after the solution went live, there were many, many „aha“ moments. From incorrectly configured ERP parameters to poor transaction handling.\nThe Way Persistent Data The most important thing is to persistently store blocking session and deadlock events. This works perfectly with Extended Events. There are two areas for this:\n-- SNIPPET!! ADD EVENT sqlserver.blocked_process_report( ACTION(sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name) ), ADD EVENT sqlserver.xml_deadlock_report( ACTION(sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name) ) The whole thing is configured in its own Extended Event and the recordings are already running. Nearly! We still need to configure SQL Server to treat Blocked Process as Extended Events.\nEXEC sp_configure \u0026#39;show advanced options\u0026#39;, 1; GO RECONFIGURE; GO EXEC sp_configure \u0026#39;blocked process threshold\u0026#39;, \u0026#39;5\u0026#39;; -- 5 Secconds RECONFIGURE GO Read Data 100% PowerShell After the events are logged, they need to be evaluated. What immediately came to my mind were the dbatools with Read-DbaXEFile. The problem is that the currently written file cannot be read with it. You can imagine that blocking and deadlock events don’t occur in such masses that dozens of file rotations are performed within 24 hours. If they do, you definitely have a different problem than just reporting it . So we took the detour via SQL Server, since the current file can also be read here.\nPowerShell + TSql I still find it very difficult to read XML with queries, but THANKS to Brent Ozar, known to all of us, for thebasic query I could build upon. At least I now had a result set for orientation and a separate XML for each event in the log.\n-- Snippet select * FROM sys.fn_xe_file_target_read_file (\u0026#39;C:\\temp\\XEventSessions\\blocked_process*.xel\u0026#39;, \u0026#39;C:\\temp\\XEventSessions\\blocked_process*.xem\u0026#39;, null, null) CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) as xevents Finalization with PowerShell Since I finally had my XML event, I could parse it with PowerShell, transform it, and finally package it into a nice text report.\nSql Server Deadlock Analysis | Event Time: 2025-04-08T11:16:07.447 ================================================================================ SQL STATEMENTS INVOLVED -------------------------------------------------------------------------------- Process ID: process261b00cfc28 SPID: 643 Database: DATABASENAME Query: **DETAILED QUERY** ---------------------------------------- Process ID: process258294fbc28 SPID: 650 Database: DATABASENAME Query: **DETAILED QUERY** ---------------------------------------- VICTIM LIST -------------------------------------------------------------------------------- Process ID: process261b00cfc28 SPID: 643 Database: DATABASENAME PROCESS LIST -------------------------------------------------------------------------------- Process ID: process261b00cfc28 SPID: 643 Isolation Level: read uncommitted (1) Logical Reads: 700 Wait Resource: KEY: 6:72058005407662080 (fb04e1bc7788) Wait Time (ms): 2247 Transaction: user_transaction Last Transaction Started: 2025-04-08T11:16:07.447 Client: \u0026#39;dbserver_dll \u0026amp;lt;ID@APPSERVER:34784\u0026gt;\u0026#39; Hostname: APPSERVER User: USERNAME EXECUTED SQL STATEMENT ---------------------------------------- **DETAILED QUERY** Process Details ================================================================================ Process ID: process258294fbc28 SPID: 650 Isolation Level: read uncommitted (1) Logical Reads: 1904 Wait Resource: KEY: 6:72057996178358272 (2a9726d2d061) Wait Time (ms): 2278 Transaction: user_transaction Last Transaction Started: 2025-04-08T11:16:07.330 Client: \u0026#39;dbserver_dll \u0026amp;lt;ID@APPSERVER:11664\u0026gt;\u0026#39; Hostname: APPSERVER User: USERNAME EXECUTED SQL STATEMENT ---------------------------------------- **DETAILED QUERY** Process Details ================================================================================ RESOURCE LIST -------------------------------------------------------------------------------- Resource: Type: keylock Database: 6 Object: DATABASENAME.dbo.tcprmp300100 Index: Itcprmp300100_1a Lock Mode: X HOBT ID: 72058005407662080 Associated Object ID: 72058005407662080 Owners: Process ID: process258294fbc28, Mode: X Waiting Processes: Process ID: process261b00cfc28, Mode: U, Request Type: wait ================================================================================ Resource: Type: keylock Database: 6 Object: DATABASENAME.dbo.tcprrp011100 Index: Itcprrp011100_1a Lock Mode: X HOBT ID: 72057996178358272 Associated Object ID: 72057996178358272 Owners: Process ID: process261b00cfc28, Mode: X Waiting Processes: Process ID: process258294fbc28, Mode: U, Request Type: wait ================================================================================ Implementation It was requested to add the results to the daily report, which we had already implemented with PowerShell. Now, it’s not great to potentially attach 200 text files to an email every morning, so the PowerShell script returns a table showing how blocks and locks occurred in time period X, and if needed, one can proactively check the folder structure.\nFor dramatic effect and a quick overview, an Excel file is also generated. In this example, there was a nice collateral finding that the DWH jobs had serious problems 🙂\nAnd just like that, we had a few reports that we could hand over to the vendor, and then things moved very quickly.\nDie Ordner Struktur _report/ ├── text_reports/ │ └── YYYY/ │ └── MM/ │ └── DD/ │ ├── 20231120_123456_dbname_blocked.txt │ ├── 20231120_123456_dbname_blocked.xml │ └── 20231120_123456_dbname_deadlock.txt └── excel_reports/ └── BlockReport_20231120_123456.xlsx Unfortunately, I experience again and again that application vendors think their customers are stupid and cannot operate the application. Stop doing that and believe your customers sometimes without 101% data, which you can only collect if you already have knowledge of the subject matter.\nSee Repository GitHub – gabrielkoehl/get-snapBlockEvents: SQL Server Block Event Reporter that automatically captures, analyzes, and visualizes blocking events and deadlocks using PowerShell and Extended Events, providing detailed text reports and aggregated Excel summaries.\nThe script has various parameters and options, details are in the README.MD. If you have questions, leave them in the comments or contact me.\nOther Erik Darling has also developed a solution to make Extended Events readable, in case other approaches are needed. (sp_HumanEvents)\nGabriel, der DBAvonNebenan\n","permalink":"https://dbavonnebenan.de/en/reporting-sql-server-blocks/","summary":"Knowing that SQL Server has blocking or deadlock problems is still quite easy. You usually notice this in the application, and with the right queries, you can also display the corresponding blocking chains. The simplest way to do this is with \u003ca href=\"http://whoisactive.com\"\u003ehttp://whoisactive.com\u003c/a\u003e. I also have a custom development that I will present in a later","title":"Reporting Blocks and Deadlocks: When the 3rd-Party Vendor Demands Evidence"},{"content":"About me I\u0026rsquo;m Gabriel Köhl – SQL Server DBA and PowerShell enthusiast. After years in in-house IT, where everything became increasingly predictable, I moved into consulting at the beginning of 2021. Here there are new challenges every day in different environments.\nWhat I do SQL Server administration, performance tuning, migrations – the full spectrum. PowerShell for everything that can be automated. And yes, sometimes that creates new problems which then need to be solved.\nWhy this blog? The community lives from sharing. Tools like dbatools or sp_Blitz have helped me many times. Now I\u0026rsquo;m giving something back – with practical solutions instead of theoretical treatises.\nDespite the quality of these big solutions, for special requirements beyond standard administration I often prefer my own approaches. sp_Blitz is excellent, no question, but for certain cases I often only need five percent of the hundred percent of metrics – but those with more depth. Similar with dbatools: the overhead is sometimes too big, so I\u0026rsquo;d rather write it myself. That way a lot more knowledge sticks, too.\nWhat to expect Real stories from everyday DBA life. No perfect solutions, but ones that work. Sometimes elegant, sometimes pragmatically put together – but they get the job done.\nIf you\u0026rsquo;ve become an accidental DBA or are simply looking for pragmatic approaches: you\u0026rsquo;re in the right place.\nAI and stuff Of course I use AI tools. They make me more efficient. But it takes time until new knowledge really sticks and is production-ready. Experience still beats hype. By the way, the articles are also translated into English with AI. 🙂\nContact Questions, feedback or just talking shop? LinkedIn or GitHub – I\u0026rsquo;d love to hear from you.\nDo you have SQL Server challenges in a business environment or want to automate processes? Feel free to drop by Opitz Consulting – that\u0026rsquo;s where you can book me.\n","permalink":"https://dbavonnebenan.de/en/about/","summary":"\u003ch2 id=\"about-me\"\u003eAbout me\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m Gabriel Köhl – SQL Server DBA and PowerShell enthusiast. After years in in-house IT, where everything became increasingly predictable, I moved into consulting at the beginning of 2021. Here there are new challenges every day in different environments.\u003c/p\u003e\n\u003ch2 id=\"what-i-do\"\u003eWhat I do\u003c/h2\u003e\n\u003cp\u003eSQL Server administration, performance tuning, migrations – the full spectrum. PowerShell for everything that can be automated. And yes, sometimes that creates new problems which then need to be solved.\u003c/p\u003e","title":"About Me – Gabriel Köhl"}]