You can run queries against multiple servers at once, and it’s quite useful for a number of reasons. I use it to check settings, verify backups and DBCC ran recently on unmonitored servers, make sure all servers rebooted during the maintenance window, and many other reasons.
This is all done through registering servers on SQL Server and opening a new query for the group. I’ll walk you through that then run a couple queries that I find useful.
Unfortunately, this method is so easy that you’ll only ever have to see it once and won’t be back to visit this blog post again. Hopefully the scripts below will inspire a return visit.
Setting It Up
Registered servers are local to your SSMS install. You’re not changing server settings by setting this up, and this will be lost if you reinstall SSMS without exporting your settings.
First, open the Registered Servers pane by going to View / Registered Servers.
Add a new server group and call it anything you want. I’ll call mine blog because that’s what I’m using it for. I love the idea of having Prod and Non-Prod groups, especially since I can run a query against all my Non-Prod servers that aren’t monitored to verify backups and DBCC checks are being done.
It’s important to note at this point that you can have a server in more than one group and groups can be nested. So in my prod group I have groups for each data center, then the servers are at that level. I could also have the same servers in functional groups, such as Finance, IT, and Why_Me.
Right-click and do a New Server Registration, and the options should be pretty natural to you. For this example, I used aliases for “Registered Server Name”, but I stick to the default when doing this for myself.
At the most basic level, it should look like this.
That’s it, you’re set up.
Running Queries
This is easier than the setup.
Right-click on a group and click on New Query.
It opens a new query window with the only oddity being instead of a yellowish bar saying “Connected. (1/1)”, now you have a pink bar saying “Connected. (2/2)” along with the group name.
This will be connected to all servers directly in the group and in groups nested within that group. There will be a long delay if one of the servers isn’t available, so it’s worth while to keep these groups cleaned up.
Now you run a query. Here’s how the results are grouped by default. Although they showed up in order for me, that was a coincidence. They’ll actually be in the order they came back, so a server that returned results in 1 second will come before a server that returned results in 2 seconds.
You can go to Tools / Options and change a couple things, but the defaults tend to work great.
Now that it’s set up, right-click on your group and go to Tasks / Export… to save this off somewhere. The only place this is saved right now is on your workstation, and those words will make any DBA cringe.
Practical Uses
Doing “SELECT 1” like I did above is always a blast, but it’s not all that productive. As always, I encourage you to play around with it yourself, but here’s some things to get you started.
Server (services) last started
When I do Windows Updates I reboot all the database servers every month, even if the updates don’t say the needed the reboot at that time. However, can be difficult to tell which servers I rebooted and which ones just aren’t begging for a reboot. Since TempDB is recreated each time services start, I check to make sure that happened during the WSUS window.
SELECT create_date FROM sys.databases WHERE database_id = 2
If I was doing this for hundreds of servers, I’d simply change the query to say AND create_date < GetDate()-1 then only my problem servers would return results.
DBCC and Backups up-to-date
My non-prod servers aren’t monitored, so things go missed sometimes. Backup jobs may be failing and I don’t know until I have time to check everything on all my non-prod servers. Unfortunately, that doesn’t happen very often. As with everything else in life, I cheat as much as possible to make it easier. This time by running this as a multiserver query:
--Leaving some overlap to avoid false alarms due to timing issues --We want to know if there hasn't been: --Full backup in the last 8 days (should happen every 7) --Full or diff backup in the last 2 days (should happen every 1) -- Log backup in the last 4 hours (should happen every 1 or more often) -- DBCC CheckDB run successfully in the last 8 days (should happen every 7) SET NOCOUNT ON DECLARE @FullDays Int SELECT @FullDays = 8 DECLARE @FullDiffDays Int SELECT @FullDiffDays = 2 DECLARE @LogHours Int SELECT @LogHours = 4 DECLARE @DBCCDays Int SELECT @DBCCDays = 8 DECLARE @SQLText NVarChar(4000) IF OBJECT_ID('tempdb..#Results') IS NOT NULL BEGIN DROP TABLE #Results END CREATE TABLE #Results ( ResultText NVarChar(2000) ) IF Object_ID('TempDB..#DBCC_Temp') IS NOT NULL BEGIN DROP TABLE #DBCC_Temp END CREATE TABLE #DBCC_Temp ( ParentObject VARCHAR(255), [Object] VARCHAR(255), Field VARCHAR(255), [Value] VARCHAR(255) ) --When was the last full backup? INSERT INTO #Results (ResultText) SELECT CAST(serverproperty('ComputerNamePhysicalNetBIOS') as sysname) + ISNULL(NULLIF('\' + CAST(serverproperty('InstanceName') as sysname),'\MSSQLSERVER'),'') + '.' + d.name + ' hasn''t had a full backup in over ' + Cast(@FullDays as VarChar(2)) + ' day(s). Last full backup was ' + ISNULL('on ' + Convert(VarChar(16), max(b.backup_finish_date), 120), 'NEVER') + '.' FROM sys.databases d LEFT JOIN msdb..backupset b ON d.name = b.database_name AND b.type = 'D' WHERE d.name <> 'tempdb' AND d.state = 0 GROUP BY d.name HAVING ISNULL(max(b.backup_finish_date), '2000-01-01') < GetDate()-@FullDays --When was the last full or differential backup? INSERT INTO #Results (ResultText) SELECT CAST(serverproperty('ComputerNamePhysicalNetBIOS') as sysname) + ISNULL(NULLIF('\' + CAST(serverproperty('InstanceName') as sysname),'\MSSQLSERVER'),'') + '.' + d.name + ' hasn''t had a full or diff backup in over ' + Cast(@FullDiffDays as VarChar(2)) + ' day(s). Last full or diff backup was ' + ISNULL('on ' + Convert(VarChar(16), max(b.backup_finish_date), 120), 'NEVER') + '.' FROM sys.databases d LEFT JOIN msdb..backupset b ON d.name = b.database_name AND b.type IN ('D', 'I') WHERE d.name <> 'tempdb' AND d.state = 0 GROUP BY d.name HAVING ISNULL(max(b.backup_finish_date), '2000-01-01') < GetDate()-@FullDiffDays --When was the last log backup? INSERT INTO #Results (ResultText) SELECT CAST(serverproperty('ComputerNamePhysicalNetBIOS') as sysname) + ISNULL(NULLIF('\' + CAST(serverproperty('InstanceName') as sysname),'\MSSQLSERVER'),'') + '.' + d.name + ' hasn''t had a log backup in over ' + Cast(@LogHours as VarChar(3)) + ' hour(s). Last log backup was ' + ISNULL('on ' + Convert(VarChar(16), max(b.backup_finish_date), 120), 'NEVER') + '.' FROM sys.databases d LEFT JOIN msdb..backupset b ON d.name = b.database_name AND b.type = 'L' WHERE d.name NOT IN ('tempdb', 'model')--Model is by default in full, rarely backed up, and not modified enough to complain about. AND d.recovery_model_desc <> 'SIMPLE' AND d.state = 0 GROUP BY d.name HAVING ISNULL(max(b.backup_finish_date), '2000-01-01') < DateAdd(Hour, -1 * @LogHours, GetDate()) SELECT @SQLText = N'use [?]; DECLARE @DBName sysname DECLARE @HasRetried bit SELECT @DBName = CAST(''?'' as VarChar(256)) SELECT @HasRetried = 0 TRUNCATE TABLE #DBCC_Temp IF EXISTS (SELECT d.state FROM sys.databases d WHERE d.name = @DBName AND d.State = 0 /*Online*/ AND d.is_read_only = 0 AND d.create_date < GetDate()-7) AND ''tempdb'' <> @DBName BEGIN INSERT INTO #DBCC_Temp EXECUTE(''DBCC PAGE (['' + @DBName + ''], 1, 9, 3)WITH TABLERESULTS, NO_INFOMSGS'') INSERT INTO #Results (ResultText) SELECT CAST(serverproperty(''ComputerNamePhysicalNetBIOS'') as sysname) + ISNULL(NULLIF(''\'' + CAST(serverproperty(''InstanceName'') as sysname),''\MSSQLSERVER''),'''') + ''.'' + @DBName + '' hasn''''t had a successful DBCC check in the last ' + CAST(@DBCCDays as VARCHAR(2)) + ' day(s). Last successful check was '' + ISNULL(NULLIF(MAX([Value]),''1900-01-01 00:00:00.000''), ''NEVER'') FROM #DBCC_Temp WHERE Field = ''dbi_dbccLastKnownGood'' HAVING MAX([Value]) < GetDate()-' + CAST(@DBCCDays as VARCHAR(2)) + ' END ' exec sp_MSforeachdb @SQLText SELECT ResultText FROM #Results DROP TABLE #Results DROP TABLE #DBCC_Temp
If I get any results from this, I have something to fix. This also works for daily full backups because it does diff or full, but you may want to change it around to fit your maintenance schedules.
Version Check
If you’re evaluating what version all of your servers are on so you can make plans to patch them, just run this:
SELECT @@VERSION
You have everything you need to compare it to SQLServerBuilds.BlogSpot.com. The only thing is that I tend to copy/paste the results into Excel so I can sort them there.
More Uses
I use registered servers and multiserver queries for more than this. In fact, I wrote this post so I could link to it in a future post on Instant File Initialization. Once I have that one here, I’ll have four examples of uses.
Other readers and I would benefit from hearing more uses of this feature. Let us hear the best uses you have, preferably with a script.
Entry-Level Content Challenge
This is my third post in Tim Ford’s Entry Level Content Challenge. I was tempted to write a post and just tell people to run a script as a multiserver query. After all, it’s very simple after you see it for the first time. However, you have to see it for the first time before you get to that point.
This is the exact purpose of his challenge. Read over it and consider joining in.