Skip to content

Commit 4cd29f2

Browse files
authored
Merge pull request BrentOzarULTD#3944 from BrentOzarULTD/claude/gracious-euclid
sp_ineachdb: support Azure SQL DB as sp_MSforeachdb drop-in
2 parents f35ede8 + ee0bb63 commit 4cd29f2

2 files changed

Lines changed: 83 additions & 4 deletions

File tree

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,47 @@ For information about how this works, see [Tara Kizer's white paper on Log Shipp
535535

536536

537537

538+
## sp_ineachdb: Run a Command in Each Database
539+
540+
`sp_ineachdb` is a drop-in replacement for the undocumented `sp_MSforeachdb` with saner filtering and error handling. Pass a command string using `?` as a placeholder for the database name:
541+
542+
```tsql
543+
EXEC sp_ineachdb @command = N'SELECT DB_NAME() AS db, COUNT(*) AS tables FROM [?].sys.tables;';
544+
```
545+
546+
Useful parameters:
547+
548+
* `@command` - the T-SQL to run. `?` (or whatever you set `@replace_character` to) gets replaced with the quoted database name.
549+
* `@database_list`, `@exclude_list` - comma-separated lists of databases to include or exclude. Bracket-quote names that contain special characters.
550+
* `@name_pattern`, `@exclude_pattern` - LIKE patterns applied to database names.
551+
* `@system_only`, `@user_only` - limit to system DBs (master/model/msdb/tempdb/distribution) or user DBs.
552+
* `@recovery_model_desc`, `@compatibility_level`, `@is_read_only`, `@is_auto_close_on`, `@is_auto_shrink_on`, `@is_broker_enabled`, `@is_query_store_on`, `@user_access`, `@state_desc` - filter by database property.
553+
* `@is_ag_writeable_copy = 1` - skip Availability Group secondaries.
554+
* `@print_dbname`, `@print_command`, `@print_command_only`, `@select_dbname` - diagnostics for debugging your command string without running it.
555+
556+
### Azure SQL DB support
557+
558+
Azure SQL DB forbids cross-database calls and 3-part object names, so "run in each database" collapses to "run in the current database" (Azure SQL DB sessions are bound to one user database anyway). `sp_ineachdb` detects Azure SQL DB via `SERVERPROPERTY('EngineEdition') = 5` and adapts automatically:
559+
560+
* Seeds the database list with just the current database.
561+
* Executes `@command` via `EXEC sys.sp_executesql` instead of a 3-part dynamic call.
562+
* Rewrites common `sp_MSforeachdb`-style patterns in `@command` so they don't have to be changed:
563+
* `USE [?];` and `USE ?;` (with or without brackets or semicolons) are stripped - you can't change database context in Azure SQL DB.
564+
* `[?].schema.object` and `?.schema.object` are collapsed to `schema.object`, turning 3-part names into 2-part names.
565+
566+
This means callers that already work with `sp_MSforeachdb` on box SQL (like `sp_Blitz` internals) can call `sp_ineachdb` on Azure SQL DB and Just Work.
567+
568+
**Things to look out for on Azure SQL DB:**
569+
570+
* **Placeholders inside string literals get rewritten too.** `PRINT 'See [?].sys.tables'` will have `[?].` stripped out - avoid putting the placeholder inside quoted strings. (This caveat also applies to box SQL, since `@command` text substitution happens everywhere.)
571+
* **Non-canonical whitespace isn't matched.** `USE [?];` (double space) or `[?] . sys . tables` (spaces between name parts) won't be rewritten. Stick to the canonical `USE [?];` and `[?].sys.tables` forms.
572+
* **Managed Instance is not affected.** Azure SQL Managed Instance reports `EngineEdition = 8` and supports cross-database calls, so it uses the same code path as box SQL.
573+
* **Filter parameters still apply** - the current database is added to the list, then the usual `@name_pattern`, `@user_only`, property filters, etc. can still exclude it. If nothing matches, you get the normal `No databases to process.` message.
574+
575+
[*Back to top*](#header1)
576+
577+
578+
538579
## Parameters Common to Many of the Stored Procedures
539580

540581
* @Help = 1 - returns a result set or prints messages explaining the stored procedure's input and output. Make sure to check the Messages tab in SSMS to read it.

sp_ineachdb.sql

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,27 @@ BEGIN
105105
@cr char(2) = CHAR(13) + CHAR(10),
106106
@SQLVersion AS tinyint = (@@microsoftversion / 0x1000000) & 0xff, -- Stores the SQL Server Version Number(8(2000),9(2005),10(2008 & 2008R2),11(2012),12(2014),13(2016),14(2017),15(2019)
107107
@ServerName AS sysname = CONVERT(sysname, SERVERPROPERTY('ServerName')), -- Stores the SQL Server Instance name.
108-
@NoSpaces nvarchar(20) = N'%[^' + CHAR(9) + CHAR(32) + CHAR(10) + CHAR(13) + N']%'; --Pattern for PATINDEX
108+
@NoSpaces nvarchar(20) = N'%[^' + CHAR(9) + CHAR(32) + CHAR(10) + CHAR(13) + N']%', --Pattern for PATINDEX
109+
@IsAzureSqlDb bit = CASE WHEN CONVERT(int, SERVERPROPERTY('EngineEdition')) = 5 THEN 1 ELSE 0 END;
110+
111+
/* Azure SQL DB forbids cross-database calls and 3-part names. Rewrite the
112+
incoming @command so sp_MSforeachdb-style inputs work unchanged:
113+
- strip USE [?]; / USE ?; variants (can't change database context)
114+
- collapse [?].schema.object / ?.schema.object to schema.object
115+
Force CI collation so USE/Use/use all match regardless of server collation. */
116+
IF @IsAzureSqlDb = 1 AND @command IS NOT NULL
117+
BEGIN
118+
DECLARE @rc nvarchar(10) = @replace_character;
119+
120+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, N'USE [' + @rc + N'];', N'');
121+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, N'USE [' + @rc + N']', N'');
122+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, N'USE ' + @rc + N';', N'');
123+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, N'USE ' + @rc, N'');
124+
125+
/* Bracketed form first — otherwise '?.' would turn '[?].' into '[]'. */
126+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, N'[' + @rc + N'].', N'');
127+
SET @command = REPLACE(@command COLLATE SQL_Latin1_General_CP1_CI_AS, @rc + N'.', N'');
128+
END
109129

110130

111131
CREATE TABLE #ineachdb(id int, name nvarchar(512), is_distributor bit);
@@ -162,7 +182,16 @@ BEGIN
162182
3)If we find a [, we begin to accumulate the result until we reach closing ], (jumping over escaped ]]).
163183
4)Finally, tabs, line breaks and spaces are removed from unquoted names
164184
*/
165-
WITH C
185+
IF @IsAzureSqlDb = 1
186+
BEGIN
187+
/* Azure SQL DB: the session is bound to one user database. Seed with it and
188+
let the downstream filter DELETEs decide whether it survives. */
189+
INSERT #ineachdb(id, name, is_distributor)
190+
SELECT DB_ID(), DB_NAME(), 0;
191+
END
192+
ELSE
193+
BEGIN
194+
;WITH C
166195
AS (SELECT V.SrcList
167196
, CAST('' AS nvarchar(MAX)) AS Name
168197
, V.DBList
@@ -213,6 +242,7 @@ WHERE ( EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'In')
213242
OR @database_list IS NULL)
214243
AND NOT EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'Out')
215244
OPTION (MAXRECURSION 0);
245+
END
216246
;
217247
-- next, let's delete any that *don't* match various criteria passed in
218248
DELETE dbs FROM #ineachdb AS dbs
@@ -356,8 +386,16 @@ OPTION (MAXRECURSION 0);
356386

357387
IF COALESCE(@print_command_only,0) = 0
358388
BEGIN
359-
SET @exec = @dbq + @sx;
360-
EXEC @exec @cmd;
389+
IF @IsAzureSqlDb = 1
390+
BEGIN
391+
/* Azure SQL DB: no 3-part names allowed; just run in current DB. */
392+
EXEC sys.sp_executesql @cmd;
393+
END
394+
ELSE
395+
BEGIN
396+
SET @exec = @dbq + @sx;
397+
EXEC @exec @cmd;
398+
END
361399
END
362400
END TRY
363401

0 commit comments

Comments
 (0)