Using sp_msForeachTable in SQL Server

You can use the sp_msForeachTable stored proc in SQL Server to execute a query/command against all tables in the database. To give you an idea about how to use this, here’s an example of using it to do a SELECT query against all tables:

EXEC sp_msForeachTable 'SELECT * FROM ?'
Code language: plaintext (plaintext)

Internally, sp_msForeachTable gets all user table names (from dbo.sysobjects) and opens a cursor to loop through them. For each table, it swaps in the table name with the ? placeholder and executes the query/command you specified.

Alternatively, if you want to loop through the tables in code, you can query INFORMATION_SCHEMA to get all table names. Then you can loop through the table names and execute a SQL query against one table at a time. Using sp_msForeachTable is just a nice convenient way to do this.

Could not find stored procedure ‘sp_msForeachTable’

sp_msForeachTable is an “unofficial stored proc” by Microsoft. It’s not guaranteed to exist. It’s entirely possible that when you go to execute it, you’ll be greeted by the Could not find stored procedure ‘sp_msForeachTable’ error. Not to worry, you can create it yourself. You have to create both dbo.sp_msForeachTable and the stored proc it depends on called dbo.sp_msForeach_Worker. I’ll show how below.

1 – Create dbo.sp_msForeachTable

In your database, create dbo.sp_msForeachTable stored procedure with the following SQL script:

CREATE proc [dbo].[sp_MSforeachtable]
	@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null,
   @command3 nvarchar(2000) = null, @whereand nvarchar(2000) = null,
	@precommand nvarchar(2000) = null, @postcommand nvarchar(2000) = null
as
	/* This proc returns one or more rows for each table (optionally, matching @where), with each table defaulting to its own result set */
	/* @precommand and @postcommand may be used to force a single result set via a temp table. */

	/* Preprocessor won't replace within quotes so have to use str(). */
	declare @mscat nvarchar(12)
	select @mscat = ltrim(str(convert(int, 0x0002)))

	if (@precommand is not null)
		exec(@precommand)

	/* Create the select */
   exec(N'declare hCForEachTable cursor global for select ''['' + REPLACE(schema_name(syso.schema_id), N'']'', N'']]'') + '']'' + ''.'' + ''['' + REPLACE(object_name(o.id), N'']'', N'']]'') + '']'' from dbo.sysobjects o join sys.all_objects syso on o.id = syso.object_id '
         + N' where OBJECTPROPERTY(o.id, N''IsUserTable'') = 1 ' + N' and o.category & ' + @mscat + N' = 0 '
         + @whereand)
	declare @retval int
	select @retval = @@error
	if (@retval = 0)
		exec @retval = dbo.sp_MSforeach_worker @command1, @replacechar, @command2, @command3, 0

	if (@retval = 0 and @postcommand is not null)
		exec(@postcommand)

	return @retval

Code language: SQL (Structured Query Language) (sql)

2 – Create dbo.sp_msForeach_Worker

dbo.sp_msForeachTable requires stored proc dbo.sp_msForeach_Worker, so create it in your database with the following SQL script:

CREATE proc dbo.[sp_MSforeach_worker]
	@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null, @command3 nvarchar(2000) = null, @worker_type int =1
as

	create table #qtemp (	/* Temp command storage */
		qnum				int				NOT NULL,
		qchar				nvarchar(2000)	COLLATE database_default NULL
	)

	set nocount on
	declare @name nvarchar(517), @namelen int, @q1 nvarchar(2000), @q2 nvarchar(2000)
   declare @q3 nvarchar(2000), @q4 nvarchar(2000), @q5 nvarchar(2000)
	declare @q6 nvarchar(2000), @q7 nvarchar(2000), @q8 nvarchar(2000), @q9 nvarchar(2000), @q10 nvarchar(2000)
	declare @cmd nvarchar(2000), @replacecharindex int, @useq tinyint, @usecmd tinyint, @nextcmd nvarchar(2000)
   declare @namesave nvarchar(517), @nametmp nvarchar(517), @nametmp2 nvarchar(258)

	declare @local_cursor cursor
	if @worker_type=1	
		set @local_cursor = hCForEachDatabase
	else
		set @local_cursor = hCForEachTable
	
	open @local_cursor
	fetch @local_cursor into @name

	/* Loop for each database */
	while (@@fetch_status >= 0) begin
		/* Initialize. */

      /* save the original dbname */
      select @namesave = @name
		select @useq = 1, @usecmd = 1, @cmd = @command1, @namelen = datalength(@name)
		while (@cmd is not null) begin		/* Generate @q* for exec() */
			/*
			 * Parse each @commandX into a single executable batch.
			 * Because the expanded form of a @commandX may be > OSQL_MAXCOLLEN_SET, we'll need to allow overflow.
			 * We also may append @commandX's (signified by '++' as first letters of next @command).
			 */
			select @replacecharindex = charindex(@replacechar, @cmd)
			while (@replacecharindex <> 0) begin

            /* 7.0, if name contains ' character, and the name has been single quoted in command, double all of them in dbname */
            /* if the name has not been single quoted in command, do not doulbe them */
            /* if name contains ] character, and the name has been [] quoted in command, double all of ] in dbname */
            select @name = @namesave
            select @namelen = datalength(@name)
            declare @tempindex int
            if (substring(@cmd, @replacecharindex - 1, 1) = N'''') begin
               /* if ? is inside of '', we need to double all the ' in name */
               select @name = REPLACE(@name, N'''', N'''''')
            end else if (substring(@cmd, @replacecharindex - 1, 1) = N'[') begin
               /* if ? is inside of [], we need to double all the ] in name */
               select @name = REPLACE(@name, N']', N']]')
            end else if ((@name LIKE N'%].%]') and (substring(@name, 1, 1) = N'[')) begin
               /* ? is NOT inside of [] nor '', and the name is in [owner].[name] format, handle it */
               /* !!! work around, when using LIKE to find string pattern, can't use '[', since LIKE operator is treating '[' as a wide char */
               select @tempindex = charindex(N'].[', @name)
               select @nametmp  = substring(@name, 2, @tempindex-2 )
               select @nametmp2 = substring(@name, @tempindex+3, len(@name)-@tempindex-3 )
               select @nametmp  = REPLACE(@nametmp, N']', N']]')
               select @nametmp2 = REPLACE(@nametmp2, N']', N']]')
               select @name = N'[' + @nametmp + N'].[' + @nametmp2 + ']'
            end else if ((@name LIKE N'%]') and (substring(@name, 1, 1) = N'[')) begin
               /* ? is NOT inside of [] nor '', and the name is in [name] format, handle it */
               /* j.i.c., since we should not fall into this case */
               /* !!! work around, when using LIKE to find string pattern, can't use '[', since LIKE operator is treating '[' as a wide char */
               select @nametmp = substring(@name, 2, len(@name)-2 )
               select @nametmp = REPLACE(@nametmp, N']', N']]')
               select @name = N'[' + @nametmp + N']'
            end
            /* Get the new length */
            select @namelen = datalength(@name)

            /* start normal process */
				if (datalength(@cmd) + @namelen - 1 > 2000) begin
					/* Overflow; put preceding stuff into the temp table */
					if (@useq > 9) begin
						close @local_cursor
						if @worker_type=1	
							deallocate hCForEachDatabase
						else
							deallocate hCForEachTable
							
						RAISERROR(55555, 16, 1); -- N'sp_MSforeach_worker assert failed:  command too long'
						return 1
					end
					if (@replacecharindex < @namelen) begin
						/* If this happened close to beginning, make sure expansion has enough room. */
						/* In this case no trailing space can occur as the row ends with @name. */
						select @nextcmd = substring(@cmd, 1, @replacecharindex)
						select @cmd = substring(@cmd, @replacecharindex + 1, 2000)
						select @nextcmd = stuff(@nextcmd, @replacecharindex, 1, @name)
						select @replacecharindex = charindex(@replacechar, @cmd)
						insert #qtemp values (@useq, @nextcmd)
						select @useq = @useq + 1
						continue
					end
					/* Move the string down and stuff() in-place. */
					/* Because varchar columns trim trailing spaces, we may need to prepend one to the following string. */
					/* In this case, the char to be replaced is moved over by one. */
					insert #qtemp values (@useq, substring(@cmd, 1, @replacecharindex - 1))
					if (substring(@cmd, @replacecharindex - 1, 1) = N' ') begin
						select @cmd = N' ' + substring(@cmd, @replacecharindex, 2000)
						select @replacecharindex = 2
					end else begin
						select @cmd = substring(@cmd, @replacecharindex, 2000)
						select @replacecharindex = 1
					end
					select @useq = @useq + 1
				end
				select @cmd = stuff(@cmd, @replacecharindex, 1, @name)
				select @replacecharindex = charindex(@replacechar, @cmd)
			end

			/* Done replacing for current @cmd.  Get the next one and see if it's to be appended. */
			select @usecmd = @usecmd + 1
			select @nextcmd = case (@usecmd) when 2 then @command2 when 3 then @command3 else null end
			if (@nextcmd is not null and substring(@nextcmd, 1, 2) = N'++') begin
				insert #qtemp values (@useq, @cmd)
				select @cmd = substring(@nextcmd, 3, 2000), @useq = @useq + 1
				continue
			end

			/* Now exec() the generated @q*, and see if we had more commands to exec().  Continue even if errors. */
			/* Null them first as the no-result-set case won't. */
			select @q1 = null, @q2 = null, @q3 = null, @q4 = null, @q5 = null, @q6 = null, @q7 = null, @q8 = null, @q9 = null, @q10 = null
			select @q1 = qchar from #qtemp where qnum = 1
			select @q2 = qchar from #qtemp where qnum = 2
			select @q3 = qchar from #qtemp where qnum = 3
			select @q4 = qchar from #qtemp where qnum = 4
			select @q5 = qchar from #qtemp where qnum = 5
			select @q6 = qchar from #qtemp where qnum = 6
			select @q7 = qchar from #qtemp where qnum = 7
			select @q8 = qchar from #qtemp where qnum = 8
			select @q9 = qchar from #qtemp where qnum = 9
			select @q10 = qchar from #qtemp where qnum = 10
			truncate table #qtemp
			exec (@q1 + @q2 + @q3 + @q4 + @q5 + @q6 + @q7 + @q8 + @q9 + @q10 + @cmd)
			select @cmd = @nextcmd, @useq = 1
		end /* while @cmd is not null, generating @q* for exec() */

		/* All commands done for this name.  Go to next one. */
		fetch @local_cursor into @name
	end /* while FETCH_SUCCESS */
	close @local_cursor
	if @worker_type=1	
		deallocate hCForEachDatabase
	else
		deallocate hCForEachTable
		
	return 0

Code language: SQL (Structured Query Language) (sql)

Using the @whereand parameter

The @whereand parameter enables you to filter out tables by name (and schema). Here’s an example of using the @whereand parameter with sp_msForeachTable. This executes a query for all tables with names starting with “People” (using LIKE):

EXEC sp_msForeachTable 'SELECT * FROM ?', 
	@whereand='AND object_name(o.id) LIKE ''People%'''
Code language: plaintext (plaintext)

The @whereand parameter gets appended to sp_msForeachTable’s query against dbo.sysobjects. That’s why you have to start it with AND and specify the table name with object_name(o.id).

Example 1 – Get count for all tables

This query saves record counts to a temp table, then shows the counts.

CREATE TABLE #counts (
    TableName VARCHAR(128),
    RecordCount INT 
);

EXEC sp_msForeachTable "INSERT INTO #counts (TableName, RecordCount) SELECT '?', COUNT(*) FROM ?"

SELECT * FROM #counts
Code language: plaintext (plaintext)

This outputs all of the table names and their record counts.

SQL query results showing record counts for all tables

Example 2 – Delete all rows from all tables

Here’s how to delete all rows from all tables. Notice that you have to disable constraints before deleting, then re-enable them after.

EXEC sp_msForeachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'

EXEC sp_msForeachTable 'DELETE FROM ?'

EXEC sp_msForeachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'
Code language: plaintext (plaintext)

Warning: Only do this if you don’t actually need the data, such as in a local dev environment.

I have the following two tables. The Games table has a foreign key constraint on the Users table.

Users table with two columns: Id, Name
Games table with 3 columns: Id, UserId, Name. Foreign Key on Users table.

Running this query successfully deletes all data from my tables.

Why do I need to disable constraints?

If I try running sp_msForeachTable with just the delete statement, I get the following error:

The DELETE statement conflicted with the REFERENCE constraint “FK_Games_Users”. The conflict occurred in database “TestDatabase”, table “dbo.Games”, column ‘UserId’.

This is because the Games table has a foreign key constraint on the Users table.

If you’re doing a delete the normal way, you’d have to delete data in the correct sequence. For example, I would need to delete data from the Games table first, then the Users table. The sp_msForeachTable stored proc does not guarantee the order that it operates on tables, which is why I simply need to disable all constraints before deleting.

Leave a Comment