foreach is for Everyone!
By Paul Munkenbeck
Back in the May 1998 issue of
SQL Server Professional (see "SQL Essentials: Locks, Ownership
Chains, and sp_MSs"), Kalen Delaney described the undocumented stored
procedure sp_MSforeachtable. Inspired by this, Paul's article explores the
family of MSforeach procedures, shows how useful they are, and even shows
you how to write your own.
Kalen's article last year showed briefly how to use the sp_MSforeachtable
procedure to carry out repeated processing against each of the tables in
a database, and without the need for any messy cursor processing. The resulting
code seemed so elegant and easy to follow that I decided to investigate
further. I found that there's a family of sp_MSforeach procedures that are
simple and effective to use -- once you understand their parameters and
prerequisites.
sp_MSforeachtable
The procedure sp_MSforeachtable will repeat
a set of commands against every table in the current database. You code
the command as normal, but with a "?" character in place of the
varying table name. The placeholder character "?" can be changed
to something else if necessary (see the syntax definition in the sidebar
"Syntax for sp_MSforeachtable"). As a simple example, here's an update of index
statistics for all tables:
exec sp_MSforeachtable "UPDATE STATISTICS ?"
|
Table selection The sp_MSforeachtable procedure works
by establishing a cursor called hCForEach (more on this later) on
the sysobjects system table in the current database. Procedure sp_MSforeachtable
automatically includes a WHERE condition that restricts the type of object
to user-defined tables. You can further restrict the scope of the procedure
by adding additional AND clauses to select just those tables you want to
process. This is done by including selection criteria in a parameter named
@whereand (see the sidebar "Syntax for
sp_MSforeachtable
"). When developing code
using selection criteria, it's a good idea to test it by using the PRINT
command to list the selected tables. As an example, the following code lists
all tables with names that start with "pub":
exec sp_MSforeachtable "PRINT '?'",
@whereand="AND name like 'pub%'"
|
The results, when run on the pubs database under SQL Server 7.0, are as
follows. Note that the placeholder character is replaced by the table name
qualified by its owner name. As you can see, version 7.0 automatically uses
delimited identifiers with the new square brackets to ensure that any names
with special characters are interpreted correctly. For the same reason,
in version 6.5, you often need to put quotes around the placeholder character
yourself.
[dbo].[pub_info]
[dbo].[publishers]
|
In the previous example, selection was by table name. In fact, any AND clause
that is valid on sysobjects will work (find a complete list of sysobjects
columns in Books Online and in the T-SQL online Help). In practice,
the most useful columns are name, uid (the user id of the
table owner), crdate (date table created), and id (the table's
object id). The user id uid can be used in conjunction with the USER_ID
function. In the next example, all tables that belong to database user "Paul"
are dropped. Note that I've included a PRINT statement in order to see a
list of the tables that were successfully dropped (SQL Server doesn't issue
a message confirming success of its DROPs):
exec sp_MSforeachtable
"DROP TABLE ? PRINT '? dropped' ",
@whereand="AND uid=USER_ID('Paul')"
|
I ran this code on a pubs database that had copies of some of the tables
created under the user name `Paul.' The results were as follows:
[Paul].[pub_info] dropped
[Paul].[publishers] dropped
|
Each database object is given a unique system-generated identifier -- its
object id -- whose value is held in the id column in sysobjects.
Many system tables include id as a foreign key, which allows you
to define quite sophisticated table selection criteria with this object
id, usually by using subselects to reference those other system tables.
When you reference the id column in such a subselect, you have to
specify it as o.id (you're qualifying it with the alias name that's
used for sysobjects in the hCForEach cursor).
As an example, consider the sysindexes system table. This holds one
row for each index over each table. (Again, refer to Books Online
or the T-SQL online Help for more info on sysindexes.) There's also
a summary row for each table (column indid = 0 or 1) that gives the
number of rows in the table and the total space (excluding text/image datatypes)
used by the table and all of its indexes. Hence, it's possible to select
tables based either on row counts or space used. For instance, the next
example shows how to carry out a DBCC check, but only on those tables with
less than 100,000 rows:
exec sp_MSforeachtable "DBCC CHECKTABLE('?')",
@whereand=" AND (select rows from sysindexes
where sysindexes.id = o.id
and indid < 2)
< 100000"
|
Controlling table sequence
The value in the @whereand parameter
is added to the end of the SELECT statement that defines the hCForEach cursor.
This means that you can add any other SELECT clauses that follow the WHERE
... AND statements. I often include an `ORDER BY name' clause to ensure
that tables are processed in alphabetic order (see the next example). This
makes it easy to compare results between two different databases with similar
structures.
pre and post commands
Procedure sp_MSforeachtable provides
optional parameters that allow you to specify two further commands: @precommand
is executed once before starting the table loop, and @postcommand is run
once afterwards. The best use I've found for these is for printing messages,
as can be seen in the following example. This one grants public read access
to all tables in a database:
exec sp_MSforeachtable
"PRINT '?' GRANT SELECT ON ? TO Paul",
@whereand="ORDER BY uid, name",
@precommand="PRINT 'Granting permissions on...'",
@postcommand = "PRINT '... procedure complete.'"
|
When run against the pubs database, the results look like this:
Granting permissions on...
[dbo].[authors]
[dbo].[discounts]
[dbo].[employee]
[dbo].[jobs]
[dbo].[pub_info]
[dbo].[publishers]
[dbo].[roysched]
[dbo].[sales]
[dbo].[stores]
[dbo].[titleauthor]
[dbo].[titles]
... procedure complete.
|
The programmer's comments in the source for sp_MSforeachtable suggest that
the @precommand and @postcommand parameters can be used to employ a temporary
table to capture and process the results from the repeated commands. For
example, if procedure sp_spaceused is used as the command, it would produce
a separate result set for each table. This is okay, but it would be more
effective if the individual results could be combined into a single table.
The way to do this is to use a temporary table that's populated with repeated
executions of an INSERT statement using sp_spaceused to get the data. Of
course, the temporary table has to be created beforehand (presumably using
@precommand) to contain all of the columns needed to hold the results of
sp_spaceused. After all of the tables have been processed, you can then
produce your final report from the temporary table in @postcommand. By the
way, the command in @postcommand is executed only if the previous commands
have all run successfully.
The drawback is that sp_MSforeachtable actually executes @precommand within
a direct EXEC() statement, and any local temporary table created like this
is unavailable outside of the EXEC(). A solution is to use global temporary
tables (prefix the table name with ##). You can try this for yourself, but
I feel that all of the extra parameters make the code look more confusing.
I prefer to create and use the temporary table outside of the sp_MSforeachtable
procedure, as follows:
CREATE TABLE #msforeach
(name varchar(30),
rows varchar(12),
reserved varchar(18),
data varchar(18),
index_size varchar(18),
unused varchar(18) )
GO
exec sp_MSforeachtable "INSERT INTO #msforeach
EXEC sp_spaceused '?'"
GO
SELECT name,rows FROM #msforeach
GO
|
The results are as follows. Unfortunately, sp_spaceused converts all of
its numeric values to variable strings for display output, but it wouldn't
be too difficult to convert the temporary table columns back in order to
do arithmetic on them if required.
name rows
------------------------------ ------------
authors 23
publishers 8
titles 18
titleauthor 25
stores 6
sales 21
roysched 86
discounts 3
jobs 14
pub_info 8
employee 43
(11 row(s) affected)
|
Complex commands
In each of the preceding examples,
the command to be executed has been fairly simple. In fact, there is
a limit to the size of the command parameter: 255 characters in version
6.5, 2,000 in 7.0. If you need to execute a command set that's longer than
this, such as a large SELECT statement or an IF ... ELSE block, then you
can use up to three command parameters, each with the same size restriction.
By default, each of these command parameters are executed as separate T-SQL
batches. If an actual T-SQL command extends across two parameters, this
will cause a syntax error. To avoid this, you can code `++' in the first
two characters of a command to specify that it's a continuation of the previous
parameter.
In the previous examples, the value of the first parameter @command1 has
been provided positionally -- that is, without specifying @command1= explicitly.
The additional command parameters, @command2 and @command3, are actually
the third and fourth parameters positionally. The second parameter is the
placeholder value. So to continue coding parameter values positionally,
you have to code the second parameter -- the placeholder -- explicitly,
as in the next example. (If you prefer, you can name the parameters and
specify them in whatever order you like.) This example shows a large SELECT
with UNIONs that's used to list all of the triggers on each table:
sp_MSforeachtable
"print '--------------------------------------'
print 'Triggers defined on ?'
print '--------------------------------------'",
"?",
"SELECT tr.name,'FOR INSERT' from sysobjects so
join sysobjects tr on tr.id=so.instrig
where so.id=OBJECT_ID('?')
UNION
SELECT tr.name,'FOR UPDATE' from sysobjects so
join sysobjects tr on tr.id=so.updtrig
where so.id=OBJECT_ID('?')",
"++UNION
SELECT tr.name,'FOR DELETE' from sysobjects so
join sysobjects tr on tr.id=so.deltrig
where so.id=OBJECT_ID('?')",
@whereand='AND (instrig+updtrig+deltrig)>0'
GO
|
The results from the pubs database illustrate that there's only one table
with triggers:
--------------------------------------
Triggers defined on dbo.employee
--------------------------------------
name
------------------------------ ----------
employee_insupd FOR INSERT
employee_insupd FOR UPDATE
|
sp_MSforeach_worker
So, how does this all work? In fact, it's
implemented as two procedures. Procedure sp_MSforeachtable creates the hCForEach
cursor against sysobjects and then passes all of the command strings
to a second procedure sp_MSforeach_worker. This "worker" procedure
does all of the work of evaluating and running the commands within a loop
controlled by the cursor.
As described previously, the main command parameters use a placeholder character
that's replaced by the real object name at execution time. Remember that
there's a limit to the size of each command parameter. But what if this
length is exceeded after the placeholder character has been evaluated? To
cope with this possibility, sp_MSforeach_worker uses a work area of nine
strings, each of the maximum size, into which it expands each of the command
parameters at a time. In the unlikely event that the placeholder replacement
uses up all of this workspace, then the procedure will fail with the message:
"sp_MSforeach_worker assert failed: command too long."
The flexibility of sp_MSforeach_worker is that it can be exploited by any
procedure that loops around a set of database object names. In sp_Msforeachtable,
this is obviously table names, but it isn't difficult to write your own
procedures that use sp_MSforeach_worker to handle other object types such
as views, indexes, stored procedures, and so forth. In fact, supplied with
SQL Server, there's another procedure that uses the same principle: sp_Msforeachdb,
which executes a set of commands against each database in turn.
#SQLOLEDbUserProfile and sp_MSdbuserprofile
Under SQL Server 6.5, Enterprise Manager
uses the sp_MSforeachdb procedure to gather basic information about each
database; first, when you click on the database folder to expand it, and
then again, when you use the Database Maintenance Plan wizard. Its operation
depends on a special temporary table called #SQLOLEDbUserProfile. This holds
a list of all databases on the server and, against each database, an indication
of its availability and what permissions the current user has to access
it (if you're interested, the structure and profile values are shown in
the sidebar "Structure of #SQLOLEDbUserProfile
in SQL Server 6.5"). Enterprise Manager
clearly uses the values in this profile to determine which databases it
includes in the list that you see.
The profile table is maintained with the procedure sp_MSdbuserprofile. Its
default processing is to initialize the profile table by emptying it and
rebuilding the list of databases. When run from the master database, it
lists all databases on the server. But if the current database is any other
than the master database, then it contains only that database in its list.
Unfortunately, being a local temporary table, #SQLOLEDbUserProfile is only
available to the connection under which it was created. Opening a query
window under Enterprise Manager or ISQL/W opens a new connection that doesn't
have access to any existing profile table. To use the sp_MSforeachdb procedure,
you have to create it explicitly, as follows:
create table #SQLOLEDbUserProfile
(dbid int NOT NULL PRIMARY KEY,
profilebits int NOT NULL)
exec sp_MSdbuserprofile 'init'
go
|
In SQL Server 7.0, #SQLOLEDbUserProfile is no longer used by sp_MSforeachdb.
It's been replaced with the new has_dbaccess property and Metadata Function
DATABASEPROPERTY().
sp_MSforeachdb
Procedure sp_MSforeachdb will repeat a
set of commands against every database on the server. Prior to version 6.5,
such a procedure was impossible, as it relies on the direct EXEC() syntax,
which is the only way of allowing a database name to be parameterized in
a command. It takes all of the same parameters as sp_MSforeachtable (see
the sidebar "Syntax for sp_MSforeachtable" for its syntax), with the notable exception of @whereand.
No database filtering facility has been provided; sp_MSforeachdb operates
on all available databases to which you have access.
I first used sp_MSforeachdb when I needed to calculate the total number
of database objects on a server. I'd noticed messages in the error log such
as "Warning: OPEN OBJECTS parameter may be too low...Run sp_configure
to increase parameter value." The solution is to increase the configuration
parameter to be at least as high as the total number of database objects
on the server -- that is, sum up the counts of rows in sysobjects across
all databases. With more than 40 databases on the server, I didn't want
to do this manually, but I found a neat solution using sp_MSforeachdb:
-- In 6.5 need #SQLOLEDbUserProfile (see above)
CREATE TABLE #msforeach
(name varchar(30), objects int)
go
exec sp_MSforeachdb
"INSERT INTO #msforeach
select '?',count(*) from ?..sysobjects"
go
select * from #msforeach compute sum(objects)
go
|
As you can see, I used the temporary table method to force a single result
set so that I could compute an overall total. The results might look something
like this:
name objects
------------------------------ -----------
master 925
model 40
msdb 457
Northwind 120
pubs 89
tempdb 42
sum
==========
1673
|
In the preceding code, all I needed the placeholder for was to replace the
database name in the qualifier for the sysobjects table. The master database
was the current database throughout. Sometimes this isn't appropriate and
you need a "USE database" statement to switch current database
before executing the repeated command. This is no problem, as long as you
note that you don't need a GO after the USE. In the following example, I
wanted to identify those databases with a user named guest:
-- In 6.5 need #SQLOLEDbUserProfile (see above)
exec sp_MSforeachdb
"USE ? select db_name()exec sp_helpuser guest"
|
Because of the difference in the security models for versions 6.x and 7.0,
the output from this looks slightly different. Listing
1 shows the first part of the output on a version
7.0 server.
Listing 1. The first part of the output on a 7.0 server.
-------------------------------------------------
master
UserName GroupName LoginName DefDBName UserID SUserID
---------- ------------ --------- --------- ------ -------
guest public NULL NULL 2 NULL
-------------------------------------------------
model
Server: Msg 15198, Level 16, State 1, Procedure sp_helpuser, Line 189
The name supplied (guest) is not a user, role, or aliased login.
|
Each to their own
I've found the MSforeach procedures to
be particularly useful -- so much so that it's a shame they operate only
on the database and table collections. However, thanks to the flexibility
of a design that incorporates the generic sp_MSforeach_worker procedure,
you can easily write your own versions against other types of database objects.
All you need to do is create your procedure with the appropriate command
and placeholder parameters, declare a cursor called hCForEach that returns
the names of the objects to be processed, and then call sp_MSforeach_worker.
Add some error handling and you end up with something like the following
procedure that operates on user view objects:
create proc sp_myforeachview
@command1 varchar(255),
@replacechar char(1) = '?',
@command2 varchar(255) = null,
@command3 varchar(255) = null,
@whereand varchar(255) = null
as
exec("declare hCForEach cursor for
select user_name(uid) + '.'
+ object_name(id) from sysobjects o"
+ " where type = 'V' "
+ " and o.category & 0x0002=0 "
+ @whereand)
declare @retval int
select @retval = @@error
if (@retval = 0) exec @retval=
sp_MSforeach_worker @command1,@replacechar,
@command2,@command3
return @retval
GO
|
The @precommand and @postcommand parameters aren't needed for sp_MSforeach_worker,
so I've omitted them for simplicity (and because I rarely use them!). If
you want to include them, just take a look at the code for sp_MSforeachtable
in master. By creating the new procedure in master and giving it a name
starting with lower case "sp_", it can then be executed from within
any other database.
In the following example, sp_myforeachview is used to list all of the dependencies
of each view in a database. The PRINT statement helps to separate the output:
exec sp_myforeachview
"PRINT '*** ? ***' exec sp_depends '?'"
|
The pubs database has only one view defined, and Listing
2 shows the results under version 7.0. Under
version 6.5, sp_depends doesn't show referenced columns, and the output
is even smaller! To give the code a real test, try it out on the Northwind
database supplied with version 7.0.
Listing 2. Output showing the results under version 7.0.
*** dbo.titleview ***
In the current database, the specified object references the following:
name type updated selected column
---------------- ---------------- ------- -------- ---------
dbo.authors user table no no au_id
dbo.authors user table no no au_lname
dbo.titles user table no no title_id
dbo.titles user table no no title
dbo.titles user table no no pub_id
dbo.titles user table no no price
dbo.titles user table no no ytd_sales
dbo.titleauthor user table no no au_id
dbo.titleauthor user table no no title_id
dbo.titleauthor user table no no au_ord
|
foreach is for me
As long as you understand their parameters,
the MSforeach procedures are simple and effective to use. They're present
in versions 6.5 and 7.0, and they take the same parameters and operate in
essentially the same way. However, they aren't documented and aren't guaranteed
to work the same way in future releases, particularly as they were originally
designed specifically for Enterprise Manager in 6.5.
Sidebar: Syntax for sp_MSforeachtable
sp_MSforeachtable
command1 [,replacechar] [,command2] [,command3] [,whereand] [,precommand]
[,postcommand]
where
- command1, command2, and command3 -- This specifies a set of commands to be executed once
for each selected table in the current database. You can pass up to three
command parameters, and each can be up to 255 characters in length. Where
the command syntax requires a tablename, you can take advantage of the
special placeholder character, which is a "?" by default.
- replacechar
-- This enables a different character to be used as placeholder if "?"
is unsuitable. Every occurrence of replacechar will be replaced
by a selected tablename in the format owner.tablename.
- whereand
-- This is an optional parameter that can contain an AND clause to be added
to the WHERE statement that selects tables to be processed.
- precommand
-- This is an optional command of up to 255 characters that will be executed
once before any of the other commands.
- postcommand
-- This is an optional command of up to 255 characters that will be executed
once after all of the other commands.
Sidebar: Structure of #SQLOLEDbUserProfile
in SQL Server 6.5
#SQLOLEDbUserProfile contains one row for
each database, indicating whether the user is able to access it. There isn't
necessarily an entry for every database on the server.
Column |
Datatype |
Description |
dbid |
smallint |
Database ID |
profilebits |
int |
Bitmap showing access user has to database: |
|
0x8.......means inaccessible (in loading, recovery,
offline, or locked out of single-user db) |
0x4.......means login isn't a user in db |
0x.......ff means user is sa or dbo -- full access |
0x.......00 means no special permissions |
0x.......02 means user can CREATE TABLE |
0x.......04 means user can CREATE VIEW |
0x.......08 means user can CREATE PROCEDURE |
0x.......10 means user can DUMP DATABASE |
0x.......20 means user can CREATE DEFAULT |
0x.......40 means user can DUMP TRANSACTION |
0x.......80 means user can CREATE RULE |
Paul Munkenbeck has been a DBA
for nearly 15 years. He has worked on all types of databases, from desktop
to multi-gigabyte mainframe. He was first exposed to Microsoft SQL Server
as a database consultant on a project early in 1995. Having gained two years'
experience of strategic SQL Server applications with a leading U.K. real
estate company, Paul is now DBA for a market research company that builds
and runs a variety of database applications for its clients. MUNKY@compuserve.com.