sp ForEachDB اجرای دستورات SQL در تمامی دیتابیس‌ها جایگزین sp MSForEachDB

اجرای دستورات SQL در تمامی دیتابیس‌ها: راهکار بهینه و جایگزین sp_MSForEachDB

اغلب اوقات نیاز به اجرای دستورات SQL بر روی تمامی دیتابیس‌های موجود در یک نمونه SQL Server داریم. متداول‌ترین روشی که بسیاری از توسعه‌دهندگان و مدیران دیتابیس برای این کار استفاده می‌کنند، رویه ذخیره‌شده (Stored Procedure) مستند نشده `sp_MSForEachDB` است. این رویه سیستمی به شما امکان می‌دهد تا یک دستور را برای هر دیتابیس اجرا کنید.

برای مثال، می‌توانید از دستور زیر برای اجرای یک کوئری ساده در تمامی دیتابیس‌ها استفاده کنید:


EXEC sp_MSForEachDB 'SELECT DB_NAME() AS DatabaseName, COUNT(*) AS TotalRows FROM ?.dbo.MyTable;'

این دستور کوئری `SELECT DB_NAME() AS DatabaseName, COUNT(*) AS TotalRows FROM ?.dbo.MyTable;` را در هر دیتابیس اجرا می‌کند و `؟` به نام دیتابیس فعلی اشاره دارد.

با این حال، رویه `sp_MSForEachDB` دارای محدودیت‌ها و مشکلاتی است که استفاده از آن را در محیط‌های تولیدی با چالش مواجه می‌کند. این محدودیت‌ها شامل موارد زیر می‌شوند:

* **نادیده گرفتن دیتابیس‌های سیستمی:** این رویه ممکن است دیتابیس‌های سیستمی را نادیده بگیرد، یا برعکس، ممکن است دیتابیس‌های سیستمی را نیز شامل شود که همیشه مطلوب نیست.
* **دیتابیس‌های آفلاین یا محدود:** `sp_MSForEachDB` به طور پیش‌فرض دیتابیس‌هایی که در وضعیت آفلاین (Offline) یا دسترسی محدود (Restricted Access) هستند را شامل نمی‌شود، که می‌تواند منجر به عدم اجرای دستور در تمام دیتابیس‌های مورد نظر شود.
* **مدیریت خطا:** مدیریت خطا در این رویه به خوبی صورت نمی‌گیرد. به عنوان مثال، استفاده از بلوک‌های `TRY/CATCH` همیشه به درستی کار نمی‌کند و ممکن است خطاهای یک دستور را نادیده بگیرد.
* **خروجی و ترکیب نتایج:** ترکیب نتایج خروجی از هر دیتابیس به یک مجموعه نتایج واحد دشوار است. برای مثال، اگر بخواهید اطلاعاتی را از هر دیتابیس جمع‌آوری کرده و در یک جدول موقت ذخیره کنید، `sp_MSForEachDB` این قابلیت را به راحتی ارائه نمی‌دهد.

با توجه به این مشکلات، یک راهکار سفارشی می‌تواند جایگزین مناسبی باشد. در ادامه، یک رویه ذخیره‌شده سفارشی به نام `sp_ForEachDB` را معرفی می‌کنیم که این محدودیت‌ها را برطرف می‌کند و انعطاف‌پذیری بیشتری را برای اجرای دستورات در دیتابیس‌های مختلف فراهم می‌آورد.

این رویه ذخیره‌شده سفارشی پارامترهای مختلفی را برای کنترل دقیق نحوه اجرای دستورات ارائه می‌دهد:

* `@command nvarchar(MAX)`: دستوری که قرار است در هر دیتابیس اجرا شود.
* `@captureOutput bit = 0`: اگر ۱ باشد، خروجی دستورات را در یک جدول موقت به نام `#sp_ForEachDB_Output` ذخیره می‌کند.
* `@userDatabasesOnly bit = 0`: اگر ۱ باشد، دستور فقط در دیتابیس‌های کاربری اجرا می‌شود و دیتابیس‌های سیستمی را نادیده می‌گیرد.
* `@onlineOnly bit = 0`: اگر ۱ باشد، دستور فقط در دیتابیس‌های آنلاین اجرا می‌شود.
* `@suppressErrors bit = 0`: اگر ۱ باشد، خطاهای رخ داده در اجرای دستورات را نادیده می‌گیرد و روند اجرا ادامه می‌یابد.
* `@printCommands bit = 0`: اگر ۱ باشد، دستورات تولید شده برای هر دیتابیس را چاپ می‌کند اما آن‌ها را اجرا نمی‌کند (برای دیباگ).
* `@delimiter nvarchar(10) = N”`: یک جداکننده اختیاری که برای ترکیب نتایج در صورت `captureOutput = 1` استفاده می‌شود.
* `@debug bit = 0`: اگر ۱ باشد، اطلاعات دیباگ بیشتری را چاپ می‌کند.

در ادامه کد کامل رویه `sp_ForEachDB` آمده است:


USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

IF OBJECT_ID('sp_ForEachDB') IS NOT NULL
    DROP PROCEDURE sp_ForEachDB
GO

CREATE PROCEDURE [dbo].[sp_ForEachDB]
    @command nvarchar(MAX),
    @captureOutput bit = 0,
    @userDatabasesOnly bit = 0,
    @onlineOnly bit = 0,
    @suppressErrors bit = 0,
    @printCommands bit = 0,
    @delimiter nvarchar(10) = N'',
    @debug bit = 0
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @SQL nvarchar(MAX);
    DECLARE @db_name sysname;
    DECLARE @db_id int;
    DECLARE @db_state tinyint; -- 0=ONLINE, 1=RESTORING, 2=RECOVERING, 3=RECOVERY_PENDING, 4=SUSPECT, 5=EMERGENCY, 6=OFFLINE, 7=COPYING, 10=OFFLINE_BY_SCRIPT, 11=ONLINE_BY_SCRIPT, 12=STANDBY_READONLY, 13=STANDBY_FULLACCESS
    DECLARE @is_read_only bit;
    DECLARE @is_in_standby bit;
    DECLARE @is_shutdown bit;
    DECLARE @is_online bit;
    DECLARE @is_user_database bit;

    IF @captureOutput = 1
    BEGIN
        IF OBJECT_ID('tempdb..#sp_ForEachDB_Output') IS NOT NULL
            DROP TABLE #sp_ForEachDB_Output;

        SET @SQL = N'CREATE TABLE #sp_ForEachDB_Output (
                        DatabaseName sysname,
                        OutputValue nvarchar(MAX)
                    );';
        EXEC (@SQL);

        IF @debug = 1 PRINT 'Created #sp_ForEachDB_Output table.';
    END

    DECLARE db_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT database_id, name, state, is_read_only, is_in_standby, is_shutdown
        FROM sys.databases
        ORDER BY name;

    OPEN db_cursor;
    FETCH NEXT FROM db_cursor INTO @db_id, @db_name, @db_state, @is_read_only, @is_in_standby, @is_shutdown;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @is_online = CASE WHEN @db_state = 0 THEN 1 ELSE 0 END;
        SET @is_user_database = CASE WHEN @db_id > 4 THEN 1 ELSE 0 END; -- 1=master, 2=tempdb, 3=model, 4=msdb

        IF @debug = 1
        BEGIN
            PRINT N'Processing database: ' + @db_name + N' (ID: ' + CAST(@db_id AS nvarchar) + N')';
            PRINT N'  State: ' + CAST(@db_state AS nvarchar) + N' (Online: ' + CAST(@is_online AS nvarchar) + N')';
            PRINT N'  User DB: ' + CAST(@is_user_database AS nvarchar);
        END

        IF (@userDatabasesOnly = 1 AND @is_user_database = 0)
        BEGIN
            IF @debug = 1 PRINT N'  Skipping (User Databases Only is 1 and this is a system database).';
            GOTO NextDB;
        END

        IF (@onlineOnly = 1 AND @is_online = 0)
        BEGIN
            IF @debug = 1 PRINT N'  Skipping (Online Only is 1 and this database is not online).';
            GOTO NextDB;
        END

        SET @SQL = N'USE ' + QUOTENAME(@db_name) + N';' + @command;

        IF @printCommands = 1
        BEGIN
            PRINT N'--- Command for ' + @db_name + N' ---';
            PRINT @SQL;
        END
        ELSE
        BEGIN
            IF @debug = 1 PRINT N'  Executing command: ' + @SQL;

            IF @captureOutput = 1
            BEGIN
                BEGIN TRY
                    -- Use INSERT...EXEC to capture results from @command, if it returns a single string result
                    SET @SQL = N'USE ' + QUOTENAME(@db_name) + N';
                                  INSERT INTO #sp_ForEachDB_Output (DatabaseName, OutputValue)
                                  SELECT ''' + REPLACE(REPLACE(@db_name, '''', ''''''), N'\', N'\\') + N''' AS DatabaseName,
                                         COALESCE(CAST((' + @command + N') AS nvarchar(MAX)), N''NULL'') AS OutputValue;';
                    IF @debug = 1 PRINT N'  Capture Output Command: ' + @SQL;
                    EXEC (@SQL);
                END TRY
                BEGIN CATCH
                    IF @suppressErrors = 0
                    BEGIN
                        INSERT INTO #sp_ForEachDB_Output (DatabaseName, OutputValue)
                        SELECT QUOTENAME(@db_name) AS DatabaseName,
                               N'ERROR: ' + ERROR_MESSAGE();
                        IF @debug = 1 PRINT N'  Error capturing output for ' + @db_name + N': ' + ERROR_MESSAGE();
                        -- If @suppressErrors is 0, we still want to log the error but not re-throw if it's for capturing output.
                        -- The main command execution (without captureOutput) will handle re-throwing if @suppressErrors is 0.
                    END
                    ELSE
                    BEGIN
                        IF @debug = 1 PRINT N'  Error capturing output for ' + @db_name + N': ' + ERROR_MESSAGE() + N' (Errors suppressed).';
                    END
                END CATCH
            END
            ELSE
            BEGIN
                BEGIN TRY
                    EXEC (@SQL);
                END TRY
                BEGIN CATCH
                    IF @suppressErrors = 0
                    BEGIN
                        DECLARE @ErrorMessage NVARCHAR(MAX) = ERROR_MESSAGE();
                        DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
                        DECLARE @ErrorState INT = ERROR_STATE();

                        IF @debug = 1 PRINT N'  Error executing command for ' + @db_name + N': ' + @ErrorMessage;
                        
                        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
                    END
                    ELSE
                    BEGIN
                        IF @debug = 1 PRINT N'  Error executing command for ' + @db_name + N': ' + ERROR_MESSAGE() + N' (Errors suppressed).';
                    END
                END CATCH
            END
        END

        NextDB:
        FETCH NEXT FROM db_cursor INTO @db_id, @db_name, @db_state, @is_read_only, @is_in_standby, @is_shutdown;
    END

    CLOSE db_cursor;
    DEALLOCATE db_cursor;

    IF @captureOutput = 1
    BEGIN
        IF @delimiter IS NULL OR @delimiter = N''
        BEGIN
            SELECT DatabaseName, OutputValue
            FROM #sp_ForEachDB_Output
            ORDER BY DatabaseName;
        END
        ELSE
        BEGIN
            SELECT STUFF(
                (SELECT @delimiter + N'DB: ' + DatabaseName + N' Val: ' + OutputValue
                 FROM #sp_ForEachDB_Output
                 ORDER BY DatabaseName
                 FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
            ,1,LEN(@delimiter),N'') AS ConcatenatedOutput;
        END

        IF @debug = 1 PRINT 'Displaying captured output from #sp_ForEachDB_Output table.';
    END
END
GO

این رویه `sp_ForEachDB` با استفاده از یک `CURSOR` بر روی دیتابیس‌های `sys.databases` پیمایش می‌کند و دستور ارسالی را در هر دیتابیس اجرا می‌کند. قابلیت‌های کلیدی آن عبارتند از:

* **فیلتر کردن دیتابیس‌ها:** با پارامترهای `@userDatabasesOnly` و `@onlineOnly` می‌توانید انتخاب کنید که دستور فقط در دیتابیس‌های کاربری یا دیتابیس‌های آنلاین اجرا شود.
* **مدیریت خروجی:** پارامتر `@captureOutput` امکان ذخیره خروجی هر دستور را در یک جدول موقت `#sp_ForEachDB_Output` فراهم می‌کند. این قابلیت به خصوص برای جمع‌آوری داده‌ها از دیتابیس‌های مختلف بسیار مفید است.
* **کنترل خطا:** این رویه از بلوک‌های `TRY/CATCH` داخلی استفاده می‌کند. پارامتر `@suppressErrors` به شما اجازه می‌دهد تا تصمیم بگیرید که آیا خطاها باید منجر به توقف اجرا شوند یا نادیده گرفته شوند.
* **حالت دیباگ و چاپ دستور:** پارامترهای `@printCommands` و `@debug` به شما کمک می‌کنند تا دستورات SQL تولید شده را مشاهده کرده و فرآیند اجرا را اشکال‌زدایی (Debug) کنید بدون اینکه واقعاً دستوری اجرا شود.

**نحوه استفاده از `sp_ForEachDB`**

در اینجا چند مثال برای نشان دادن نحوه استفاده از `sp_ForEachDB` آورده شده است:

**مثال ۱: اجرای یک دستور ساده در تمامی دیتابیس‌ها**

برای بررسی فضای استفاده شده توسط فایل‌های لاگ در هر دیتابیس، می‌توانید از دستور زیر استفاده کنید:


EXEC sp_ForEachDB @command = 'DBCC SQLPERF(LOGSPACE);'

این دستور گزارش فضای لاگ را برای هر دیتابیس نمایش می‌دهد.

**مثال ۲: اجرای دستور فقط در دیتابیس‌های کاربری**

اگر می‌خواهید فقط دیتابیس‌های کاربری را هدف قرار دهید، پارامتر `@userDatabasesOnly` را روی `1` تنظیم کنید:


EXEC sp_ForEachDB @command = 'SELECT DB_NAME() AS DatabaseName;', @userDatabasesOnly = 1

این دستور نام هر دیتابیس کاربری را برمی‌گرداند.

**مثال ۳: جمع‌آوری خروجی در یک جدول موقت**

برای جمع‌آوری خروجی یک دستور (مانند شمارش ردیف‌های یک جدول) از هر دیتابیس در یک جدول موقت، از پارامتر `@captureOutput` استفاده کنید:


EXEC sp_ForEachDB @command = 'SELECT COUNT(*) FROM sys.objects;', @captureOutput = 1

پس از اجرای این دستور، می‌توانید نتایج جمع‌آوری شده را از جدول `#sp_ForEachDB_Output` بازیابی کنید:


SELECT DatabaseName, OutputValue FROM #sp_ForEachDB_Output;

**مثال ۴: مدیریت خطا و مشاهده دستورات تولید شده**

برای چاپ دستورات تولید شده بدون اجرای آن‌ها و همچنین دیدن جزئیات دیباگ، می‌توانید از پارامترهای `@printCommands` و `@debug` استفاده کنید:


EXEC sp_ForEachDB @command = 'SELECT 1/0;', @suppressErrors = 1, @printCommands = 1, @debug = 1;

در این مثال، دستوری که منجر به خطا می‌شود (تقسیم بر صفر) اجرا نخواهد شد زیرا `printCommands` روی `1` تنظیم شده است. اگر `printCommands` صفر باشد و `suppressErrors` روی `1` باشد، خطاها نادیده گرفته می‌شوند.

**مثال ۵: استفاده از جداکننده برای خروجی ترکیب شده**

اگر می‌خواهید خروجی را با یک جداکننده مشخص ترکیب کنید، از پارامتر `@delimiter` استفاده کنید:


EXEC sp_ForEachDB @command = 'SELECT DB_NAME() + N''-Test'';', @captureOutput = 1, @delimiter = N' | '

این دستور خروجی را به صورت یک رشته واحد و با جداکننده ` | ` برمی‌گرداند.

با استفاده از `sp_ForEachDB` می‌توانید به راحتی و با انعطاف‌پذیری بالا، دستورات SQL خود را در تمامی دیتابیس‌های SQL Server اجرا کنید و مشکلات مربوط به `sp_MSForEachDB` را دور بزنید. این رویه، ابزاری قدرتمند برای اتوماسیون وظایف مدیریتی و توسعه‌ای در محیط‌های پیچیده SQL Server است.

من علی دستجردی‌ام؛ عاشق کار با دیتا، از SQL Server تا بیگ‌دیتا و هوش مصنوعی. دغدغه‌ام کشف ارزش داده‌ها و به‌اشتراک‌گذاری تجربه‌هاست. ✦ رزومه من: alidastjerdi.com ✦

عضویت
منو باخبر کن!!!
guest
نام
ایمیل

0 دیدگاه
Inline Feedbacks
دیدن تمامی کامنتها

فوتر سایت

ورود به سایت

sqlyar

هنوز عضو نیستید؟

ورود به سایت

هنوز تبت نام نکردید ؟