عنوان:

‫کدام برنامه را برای مانیتور کردن SQL پیشنهاد می‌کنید؟


نویسنده: هادی مزارعی
تاریخ: ۱۴۰۵/۰۶/۲۹ ۱۱:۰۵
آدرس: www.dntips.ir
قصد دارم برنامه‌ی مورد نظر مانیتورینگ را در یک 24 ساعت بصورت مستمر انجام بده و در انتها در یک گزارش بتونم فهرست تمام Stored Procedureهای برنامه را با جزئیات مد نظر بررسی کنیم:
  1. زمان صرف شده از شروع تا پایان هر SP
  2. حافظه مصرف شده در هر بار اجرای SP

نظرات

  • وحید نصیری در ۱۴۰۵/۰۶/۲۹ ۱۷:۴۷
    خود SQL Server به‌صورت پیش‌فرض چندین ابزار داخلی دارد که دقیقاً همین اطلاعات (زمان اجرا و میزان حافظه/منابع مصرفی) را بدون نیاز به نرم‌افزارهای جانبی ثبت و نگهداری می‌کنند. بسته به اینکه تحلیل آماری تجمعی بخواهید یا ردگیری لحظه‌به‌لحظه و دقیق هر بار اجرا، از ۳ روش اصلی می‌توانید استفاده کنید:

    روش اول: استفاده از DMVها (سریع‌ترین و بدون بار پردازشی اضافه)
    SQL Server آمار اجرای رویه‌ها را در حافظه کش (Plan Cache) نگه می‌دارد. با کوئری گرفتن از نمای آماری sys.dm_exec_procedure_stats می‌توانید مجموع، میانگین و آخرین زمان اجرا و منابع مصرفی را ببینید:
    SELECT 
        DB_NAME(ps.database_id) AS DatabaseName,
        OBJECT_SCHEMA_NAME(ps.object_id, ps.database_id) AS SchemaName,
        OBJECT_NAME(ps.object_id, ps.database_id) AS ProcedureName,
        ps.execution_count AS TotalExecutions,
        
        -- زمان کل و میانگین زمان اجرا (به میلی‌ثانیه)
        (ps.total_elapsed_time / 1000.0) AS TotalDuration_ms,
        (ps.total_elapsed_time / ps.execution_count / 1000.0) AS AvgDuration_ms,
        (ps.last_elapsed_time / 1000.0) AS LastDuration_ms,
    
        -- زمان پردازنده (CPU Time به میلی‌ثانیه)
        (ps.total_worker_time / ps.execution_count / 1000.0) AS AvgCPUTime_ms,
    
        -- مصرف حافظه و دیسک (Logical Reads بر حسب Pageهای 8 کیلوبایتی)
        -- این شاخص دقیق‌ترین معیار فشار روی حافظه Cache است:
        (ps.total_logical_reads / ps.execution_count) AS AvgLogicalReads_Pages,
        (ps.total_logical_reads / ps.execution_count * 8 / 1024.0) AS AvgMemoryRead_MB,
        (ps.last_logical_reads * 8 / 1024.0) AS LastMemoryRead_MB,
    
        -- آخرین باری که اجرا شده
        ps.last_execution_time
    FROM 
        sys.dm_exec_procedure_stats ps
    WHERE 
        ps.database_id = DB_ID('YourDatabaseName') -- نام دیتابیس خود را اینجا وارد کنید
    ORDER BY 
        AvgDuration_ms DESC;
    نکته در مورد حافظه مصرفی: در دیتابیس‌های رابطه‌ای، مصرف حافظه در طول اجرای پروسیجر معمولاً با شاخص Logical Reads (تعداد صفحات ۸ کیلوبایتی خوانده‌شده از Buffer Cache) سنجیده می‌شود. همچنین برای پروسیجرهایی که نیاز به مرتب‌سازی سنگین دارند، ستون total_grant_kb (میزان حافظه رزرو شده یا Memory Grant) در نسخه‌های جدید SQL Server در همین DMV در دسترس است.
    محدودیت DMV: اگر سرویس SQL Server ری‌استارت شود یا پروسیجر تغییر کند، آمار کش پاک می‌شود.

    روش دوم: قابلیت Query Store (پیشنهاد اصلی از SQL Server 2016 به بعد)
    اگر نسخه SQL Server شما 2016 یا بالاتر است، قابلیت Query Store را فعال کنید. مزیت بزرگ آن:
    • اطلاعات حتی با ری‌استارت شدن سرور پاک نمی‌شوند و در دیسک ذخیره می‌مانند.
    • گزارش‌های تصویری آماده (از جمله Longest Duration و Memory Consuming Queries) در SSMS در اختیارتان می‌گذارد.

    برای فعال‌سازی:
    ALTER DATABASE [YourDatabaseName] SET QUERY_STORE = ON;
    پس از فعال‌سازی، در Object Explorer در SSMS، زیرشاخه دیتابیس خود پوشه‌ای به نام Query Store می‌بینید که گزارش‌های آماده‌ای مانند:
    • Top Resource Consuming Queries (برای بررسی مدت‌زمان و CPU)
    • گزارش مصرف Memory Grants برای ردیابی مصرف مستقیم RAM

    روش سوم: استفاده از Extended Events (برای ثبت تک‌تک اجراها با تاریخچه دقیق)
    اگر می‌خواهید «به ازای هر بار اجرای مشخص»، لاگ دقیقی از زمان شروع، پایان و حافظه ثبت شود (مثلاً مانند یک فایل لاگ یا جدول تاریخچه)، یک سشن Extended Events با ایونت‌های زیر بسازید:
    • module_end (هنگام اتمام اجرای هر SP تریگر می‌شود)
    • فیلدهای دریافتی: duration، cpu_time، logical_reads و memory_grant
    این روش جایگزین مدرن و بسیار کم‌مصرف SQL Server Profiler قدیمی است و کمترین سربار را روی سرور می‌گذارد.
    در اینجا برای ثبت تک‌تک دفعات اجرای Stored Procedureها، ما از رویدادی به نام sqlserver.module_end استفاده می‌کنیم. این رویداد دقیقاً در لحظه پایان اجرای SP شلیک می‌شود و مواردی چون زمان صرف‌شده (Duration)، زمان پردازنده (CPU) و دسترسی به حافظه (Logical Reads) را ثبت می‌کند.

    مرحله ۱: ایجاد Session در Extended Events
    ابتدا اسکریپت زیر را اجرا کنید تا یک Session ایجاد شود. در این کد نام دیتابیس خود را به‌جای YourDatabaseName قرار دهید تا رویدادهای مربوط به دیتابیس‌های دیگر فیلتر شوند:
    CREATE EVENT SESSION [Track_SP_Execution] ON SERVER 
    ADD EVENT sqlserver.module_end(
        ACTION(
            sqlserver.database_name,
            sqlserver.client_app_name,
            sqlserver.username,
            sqlserver.sql_text
        )
        WHERE (
            [package0].[equal_uint64]([sqlserver].[database_id], DB_ID(N'YourDatabaseName')) -- فیلتر روی دیتابیس مشخص
            AND [object_type] = 'P ' -- فیلتر فقط برای Stored Procedureها
        )
    )
    ADD TARGET package0.event_file(
        SET filename = N'Track_SP_Execution.xel', -- مسیر ذخیره فایل گزارش
        max_file_size = (50),                      -- حداکثر حجم هر فایل: ۵۰ مگابایت
        max_rollover_files = (5)                   -- نگهداری تا حداکثر ۵ فایل چرخشی
    )
    WITH (
        MAX_MEMORY = 4096 KB,
        EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
        MAX_DISPATCH_LATENCY = 5 SECONDS,
        TRACK_CAUSALITY = OFF
    );
    GO

    مرحله ۲: فعال‌سازی Session
    پس از ساخت، سشن به‌صورت پیش‌فرض خاموش است. با دستور زیر آن را روشن کنید:
    ALTER EVENT SESSION [Track_SP_Execution] ON SERVER 
    STATE = START;
    GO

    مرحله ۳: خواندن گزارش اجراها و استخراج آمار
    برای مشاهده اطلاعات ذخیره‌شده و تحلیل زمان اجرا و حافظه، کوئری زیر را اجرا کنید:
    SELECT 
        event_data.value('(/event/@timestamp)[1]', 'datetime2') AS [EndTime_UTC],
        event_data.value('(/event/data[@name="object_name"]/value)[1]', 'sysname') AS [ProcedureName],
        
        -- زمان کل اجرا (میکروثانیه به میلی‌ثانیه تبدیل شده)
        event_data.value('(/event/data[@name="duration"]/value)[1]', 'bigint') / 1000.0 AS [Duration_ms],
        
        -- زمان مصرف CPU (میلی‌ثانیه)
        event_data.value('(/event/data[@name="cpu_time"]/value)[1]', 'bigint') / 1000.0 AS [CpuTime_ms],
        
        -- مصرف حافظه کش (تعداد صفحات ۸ کیلوبایتی و تبدیل به مگابایت)
        event_data.value('(/event/data[@name="logical_reads"]/value)[1]', 'bigint') AS [LogicalReads_Pages],
        (event_data.value('(/event/data[@name="logical_reads"]/value)[1]', 'bigint') * 8) / 1024.0 AS [MemoryRead_MB],
        
        -- تعداد نوشتن روی دیسک
        event_data.value('(/event/data[@name="writes"]/value)[1]', 'bigint') AS [Writes],
        
        event_data.value('(/event/action[@name="username"]/value)[1]', 'sysname') AS [UserName],
        event_data.value('(/event/action[@name="client_app_name"]/value)[1]', 'nvarchar(max)') AS [ApplicationName]
    FROM (
        SELECT CAST(event_data AS XML) AS event_data
        FROM sys.fn_xe_file_target_read_file(N'Track_SP_Execution*.xel', NULL, NULL, NULL)
    ) AS xed
    ORDER BY [EndTime_UTC] DESC;

    روش گرافیکی (بدون کدنویسی در SSMS)
    اگر ترجیح می‌دهید از محیط گرافیکی استفاده کنید:
    • در Object Explorer منوی Management و سپس Extended Events را باز کنید.
    • روی پوشه Sessions کلیک راست کرده و New Session Wizard یا New Session را انتخاب کنید.
    • در تب Events رویداد module_end را پیدا و اضافه کنید.
    • در تب Data Storage گزینه event_file را انتخاب کنید تا لاگ‌ها روی دیسک بنویسند.
    • پس از استارت سشن، با کلیک راست روی سشن و انتخاب Watch Live Data، اجرای SPها را به‌صورت لحظه‌ای در جدول زنده تماشا کنید.

    مرحله ۴: توقف یا حذف Session (در صورت اتمام عیب‌یابی)
    پس از اینکه تحلیل شما به پایان رسید، سشن را متوقف یا حذف کنید تا فضای دیسک مصرف نشود:
    -- توقف موقت
    ALTER EVENT SESSION [Track_SP_Execution] ON SERVER STATE = STOP;
    
    -- یا حذف کامل سشن
    DROP EVENT SESSION [Track_SP_Execution] ON SERVER;

    برای ذخیره خودکار و زمان‌بندی‌شده داده‌های فایل‌های .xel در یک جدول دائمی، روال استاندارد ساخت یک جدول لاگ، یک پروسیجر انتقال داده (ETL) با قابلیت جلوگیری از ثبت رکوردهای تکراری، و زمان‌بندی آن توسط SQL Server Agent است.

    گام اول: ساخت جدول دائمی برای نگهداری تاریخچه
    ابتدا در دیتابیس مدنظر یک جدول برای ذخیره اطلاعات پردازش‌شده بسازید:
    USE [YourDatabaseName];
    GO
    
    CREATE TABLE dbo.SP_ExecutionHistory (
        HistoryID BIGINT IDENTITY(1,1) PRIMARY KEY,
        EventTimestampUTC DATETIME2(3) NOT NULL,
        ProcedureName SYSNAME NOT NULL,
        DurationMs DECIMAL(18, 2) NOT NULL,
        CpuTimeMs DECIMAL(18, 2) NOT NULL,
        LogicalReads BIGINT NOT NULL,
        MemoryReadMB DECIMAL(18, 2) NOT NULL,
        PhysicalWrites BIGINT NOT NULL,
        UserName SYSNAME NULL,
        ApplicationName NVARCHAR(500) NULL,
        InsertDate DATETIME2(3) DEFAULT SYSUTCDATETIME()
    );
    
    -- ایجاد ایندکس روی ستون زمان برای جلوگیری از تکرار و سرعت در جستجوها
    CREATE NONCLUSTERED INDEX IX_SP_ExecutionHistory_Timestamp 
    ON dbo.SP_ExecutionHistory(EventTimestampUTC);
    GO

    گام دوم: ساخت Stored Procedure برای انتقال داده‌های جدید
    این پروسیجر آخرین زمان ثبت‌شده در جدول را بررسی کرده و تنها رکوردهایی را که بعد از آن زمان ایجاد شده‌اند، منتقل می‌کند (Incremental Load):
    USE [YourDatabaseName];
    GO
    
    CREATE OR ALTER PROCEDURE dbo.usp_Archive_SP_Executions
    AS
    BEGIN
        SET NOCOUNT ON;
    
        -- پیدا کردن زمان آخرین رکورد درج‌شده
        DECLARE @LastRecordedTime DATETIME2(3);
        SELECT @LastRecordedTime = ISNULL(MAX(EventTimestampUTC), '1900-01-01') 
        FROM dbo.SP_ExecutionHistory;
    
        -- خواندن داده‌ها از فایل‌های XEL و درج در جدول
        INSERT INTO dbo.SP_ExecutionHistory (
            EventTimestampUTC,
            ProcedureName,
            DurationMs,
            CpuTimeMs,
            LogicalReads,
            MemoryReadMB,
            PhysicalWrites,
            UserName,
            ApplicationName
        )
        SELECT 
            parsed.EventTimestampUTC,
            parsed.ProcedureName,
            parsed.DurationMs,
            parsed.CpuTimeMs,
            parsed.LogicalReads,
            (parsed.LogicalReads * 8.0) / 1024.0 AS MemoryReadMB,
            parsed.PhysicalWrites,
            parsed.UserName,
            parsed.ApplicationName
        FROM (
            SELECT 
                event_data.value('(/event/@timestamp)[1]', 'datetime2(3)') AS EventTimestampUTC,
                event_data.value('(/event/data[@name="object_name"]/value)[1]', 'sysname') AS ProcedureName,
                event_data.value('(/event/data[@name="duration"]/value)[1]', 'bigint') / 1000.0 AS DurationMs,
                event_data.value('(/event/data[@name="cpu_time"]/value)[1]', 'bigint') / 1000.0 AS CpuTimeMs,
                event_data.value('(/event/data[@name="logical_reads"]/value)[1]', 'bigint') AS LogicalReads,
                event_data.value('(/event/data[@name="writes"]/value)[1]', 'bigint') AS PhysicalWrites,
                event_data.value('(/event/action[@name="username"]/value)[1]', 'sysname') AS UserName,
                event_data.value('(/event/action[@name="client_app_name"]/value)[1]', 'nvarchar(500)') AS ApplicationName
            FROM (
                SELECT CAST(event_data AS XML) AS event_data
                -- در صورت لزوم مسیر کامل فایل را مشخص کنید: N'C:\LogPath\Track_SP_Execution*.xel'
                FROM sys.fn_xe_file_target_read_file(N'Track_SP_Execution*.xel', NULL, NULL, NULL)
            ) AS raw_xed
        ) AS parsed
        WHERE parsed.EventTimestampUTC > @LastRecordedTime
          AND parsed.ProcedureName IS NOT NULL
        ORDER BY parsed.EventTimestampUTC ASC;
    END;
    GO

    گام سوم: زمان‌بندی دوره ای با SQL Server Agent
    برای اجرای خودکار این عملیات (مثلاً هر ۳۰ دقیقه یا روزانه):
    • در محیط SSMS، بخش SQL Server Agent را گسترش داده و روی پوشه Jobs راست‌کلیک کرده و New Job را بزنید.
    • در تب General یک نام انتخاب کنید (مثلاً DBA - Archive SP Execution Logs).
    • در تب Steps دکمه New را بزنید:
    • Step name:Run usp_Archive_SP_Executions
    • Database: دیتابیس مربوطه را انتخاب کنید.
    • Command: عبارت EXEC dbo.usp_Archive_SP_Executions; را درج کنید.
    • در تب Schedules دکمه New را بزنید و بازه زمانی اجرا (مثلاً هر ۱۵ یا ۳۰ دقیقه یک‌بار) را تنظیم نمایید.
    • روی OK کلیک کنید تا جاب ذخیره و فعال شود.

    نکته تکمیلی: مدیریت حجم داده‌ها (Purge)
    از آنجا که این جدول پیوسته رشد می‌کند، مناسب است یک شرط پاکسازی دوره‌ای (Data Retention) نیز در نظر بگیرید. برای نمونه، افزودن دستور زیر به انتهای پروسیجر، داده‌های قدیمی‌تر از ۶۰ روز را به‌صورت خودکار پاک می‌کند:
    DELETE FROM dbo.SP_ExecutionHistory
    WHERE EventTimestampUTC < DATEADD(DAY, -60, SYSUTCDATETIME());

    برای تحلیل کارآمد داده‌های ذخیره‌شده در جدول dbo.SP_ExecutionHistory، سناریوهای گزارش‌گیری را می‌توان بر اساس نوع بار کاری (مدت زمان، مصرف RAM، پردازنده و نوسان کارایی) دسته‌بندی کرد.

    در ادامه، ۴ کوئری استاندارد مدیریتی و تحلیلی آورده شده است:

    ۱. ۱۰ پروسیجر با بیشترین میانگین زمان اجرا (کندترین پروسیجرها)
    این گزارش پروسیجرهایی را نشان می‌دهد که به‌طور میانگین بیشترین معطلی را برای کاربر یا برنامه ایجاد می‌کنند (با شرط حداقل ۵ بار اجرا جهت حذف خطاهای موردی):
    SELECT TOP 10
        ProcedureName,
        COUNT(*) AS ExecutionCount,
        -- زمان بر حسب ثانیه
        CAST(AVG(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS AvgDuration_Sec,
        CAST(MIN(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS MinDuration_Sec,
        CAST(MAX(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS MaxDuration_Sec,
        CAST(AVG(CpuTimeMs) / 1000.0 AS DECIMAL(10, 2))  AS AvgCpu_Sec,
        CAST(AVG(MemoryReadMB) AS DECIMAL(10, 2))        AS AvgMemory_MB
    FROM dbo.SP_ExecutionHistory
    WHERE EventTimestampUTC >= DATEADD(DAY, -7, SYSUTCDATETIME()) -- بازه ۷ روز اخیر
    GROUP BY ProcedureName
    HAVING COUNT(*) >= 5
    ORDER BY AvgDuration_Sec DESC;

    ۲. پرمصرف‌ترین پروسیجرها از نظر کل منابع حافظه (I/O و RAM)
    این گزارش پروسیجرهایی را استخراج می‌کند که بیشترین حجم داده را در کش پردازش کرده‌اند؛ این رویه‌ها معمولاً کاندیدای اصلی کمبود ایندکس (Index Seek به جای Index Scan) هستند:
    SELECT TOP 10
        ProcedureName,
        COUNT(*) AS ExecutionCount,
        -- مجموع حافظه خوانده شده از دیسک/کش در کل دوره
        CAST(SUM(MemoryReadMB) / 1024.0 AS DECIMAL(10, 2)) AS TotalMemoryRead_GB,
        CAST(AVG(MemoryReadMB) AS DECIMAL(10, 2))          AS AvgMemoryRead_MB,
        CAST(MAX(MemoryReadMB) AS DECIMAL(10, 2))          AS MaxMemoryRead_MB,
        SUM(PhysicalWrites)                                AS TotalWrites
    FROM dbo.SP_ExecutionHistory
    WHERE EventTimestampUTC >= DATEADD(DAY, -7, SYSUTCDATETIME())
    GROUP BY ProcedureName
    ORDER BY TotalMemoryRead_GB DESC;

    ۳. بیشترین بار تجمعی روی سرور (Total Impact on Server)
    یک پروسیجر ممکن است در هر بار اجرا سریع باشد (مثلاً ۲۰۰ میلی‌ثانیه)، اما به دلیل فراخوانی مکرر (مثلاً صدهزار بار در روز)، بخش اعظم توان CPU و دیسک سرور را ببلعد:
    SELECT TOP 10
        ProcedureName,
        COUNT(*) AS ExecutionCount,
        -- مجموع کل زمانی که CPU درگیر این پروسیجر بوده
        CAST(SUM(CpuTimeMs) / 1000.0 / 60.0 AS DECIMAL(10, 2))   AS TotalCpu_Minutes,
        CAST(SUM(DurationMs) / 1000.0 / 60.0 AS DECIMAL(10, 2))  AS TotalDuration_Minutes,
        CAST(SUM(MemoryReadMB) / 1024.0 AS DECIMAL(10, 2))       AS TotalRead_GB,
        -- درصد سهم از کل زمان CPU در کل دیتابیس
        CAST(100.0 * SUM(CpuTimeMs) / SUM(SUM(CpuTimeMs)) OVER() AS DECIMAL(5, 2)) AS PctOfTotalCpu
    FROM dbo.SP_ExecutionHistory
    WHERE EventTimestampUTC >= DATEADD(DAY, -1, SYSUTCDATETIME()) -- ۲۴ ساعت گذشته
    GROUP BY ProcedureName
    ORDER BY TotalCpu_Minutes DESC;

    ۴. پروسیجرهای ناپایدار با نوسان شدید کارایی (Parameter Sniffing Candidates)
    اگر یک پروسیجر گاهی در چند میلی‌ثانیه تمام شود اما گاهی چند دقیقه طول بکشد، معمولاً نشان‌دهنده مشکل Parameter Sniffing یا قفل‌شدگی (Locking / Blocking) است. این کوئری نسبت بیشترین زمان به کمترین زمان را محاسبه می‌کند:
    SELECT TOP 10
        ProcedureName,
        COUNT(*) AS ExecutionCount,
        CAST(MIN(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS MinDuration_Sec,
        CAST(MAX(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS MaxDuration_Sec,
        CAST(AVG(DurationMs) / 1000.0 AS DECIMAL(10, 2)) AS AvgDuration_Sec,
        -- نسبت بدترین حالت به بهترین حالت
        CASE 
            WHEN MIN(DurationMs) > 0 THEN CAST(MAX(DurationMs) / MIN(DurationMs) AS DECIMAL(10, 1))
            ELSE NULL 
        END AS PerformanceVarianceRatio
    FROM dbo.SP_ExecutionHistory
    WHERE EventTimestampUTC >= DATEADD(DAY, -7, SYSUTCDATETIME())
    GROUP BY ProcedureName
    HAVING COUNT(*) >= 10 AND MIN(DurationMs) > 100 -- حذف اجراهای خطای آنی
    ORDER BY PerformanceVarianceRatio DESC;

    • هادی مزارعی در ۱۴۰۵/۰۶/۲۹ ۱۸:۲۰
      ممنون بابت پاسخ ارائه شده. آیا پیشنهاد میشه که برای برخی از SPها که به ندرت اجرا می‌شوند ولی حافظه زیادی مصرف می‌کنند، در پایان اجرای کدهای SP، حافظه را بابت اجرای SP جاری آزاد کرد؟ علت پرسیدن این سوال بخاطر پیغام زیر است:

      ODBC SQL Server Driver - SQL Server
      
      Could not get the memory grant of 4643232 KB because it exceeds the maximum configuation limit in workload group 'default' (2) and resource pool 'default' (2). contact the server administratior to increase the memory usage limit (#8657)
      • وحید نصیری در ۱۴۰۵/۰۶/۲۹ ۱۹:۴۷
        خیر، آزادسازی دستی حافظه در انتهای Stored Procedure نه ممکن است و نه به این خطا کمکی می‌کند؛ چرا که در SQL Server حافظه اختصاص‌یافته به اجرای کوئری (Memory Grant) بلافاصله پس از اتمام اجرا به‌صورت خودکار آزاد می‌شود. خطای Error 8657 به این معناست که کوئری حتی نتوانسته اجرا را شروع کند، زیرا مقدار حافظه‌ای که قبل از اجرا برای پردازش (Memory Grant) درخواست کرده (~۴.۴ گیگابایت)، از سقف مجاز برای یک کوئری واحد در Workload Group پیش‌فرض عبور کرده است.
        علت این درخواست حجم غیرمنطقی حافظه، تقریباً همیشه تخمین اشتباه تعداد رکوردها (Cardinality Estimation) یا عملیات سنگین مرتب‌سازی/هش در کوئری است. برای رفع اصولی این مشکل مراحل زیر را دنبال کنید:

        ۱. اصلاح علت ریشه‌ای (درون کوئری و ایندکس‌ها)
        - بروزرسانی آمارها (Update Statistics): اگر آمارها قدیمی باشند، SQL Server تعداد رکوردها را بسیار بیشتر از واقعیت تخمین زده و حافظه نجومی درخواست می‌کند:
        UPDATE STATISTICS YourTableName WITH FULLSCAN;
        - حذف مرتب‌سازی‌های سنگین: دستورات غیرضروری ORDER BY، DISTINCT و GROUP BY را بررسی کنید. مرتب‌سازی‌های سنگین در حافظه (Sort Spills / Sort Grants) متهم ردیف اول درخواست‌های حافظه بالا هستند.
        - بررسی ایندکس‌ها: اطمینان حاصل کنید فیلترها (WHERE) و اتصالات (JOIN) دارای ایندکس مناسب هستند تا از اسکن کامل جدول (Table Scan) و Hash Joinهای سنگین جلوگیری شود.

        ۲. محدود کردن حافظه کوئری با Query Hint (سریع‌ترین راهکار برای این SP خاص)
        اگر این پروسیجر به ندرت اجرا می‌شود و نمی‌خواهید کل سرور یا تنظیمات دیتابیس را دستکاری کنید، می‌توانید سقف مجاز حافظه برای این کوئری را با استفاده از max_grant_percent محدود کنید تا کوئری اجازه اجرا پیدا کند:
        SELECT ...
        FROM ...
        OPTION (min_grant_percent = 5, max_grant_percent = 25);
        نکته: با این کار کوئری به جای درخواست بیش از حد حافظه، در محدوده مشخصی اجرا می‌شود (حتی اگر نیاز باشد داده‌ها در tempdb اسپیلیت شوند)، و ارور ۸۶۵۷ رخ نمی‌دهد.

        ۳. مدیریت از طریق Resource Governor (در صورت نیاز به تفکیک منابع)
        اگر ساختار کوئری بهینه‌سازی شده اما واقعاً به دلیل پردازش میلیون‌ها رکورد به چنین حافظه‌ای نیاز دارد: می‌توانید با استفاده از Resource Governor یک Workload Group اختصاصی تعریف کنید و سقف مجاز حافظه برای یک درخواست (REQUEST_MAX_MEMORY_GRANT_PERCENT) را افزایش دهید تا سقف گروه پیش‌فرض (Default) شکسته نشود و سایر پروسه‌های سرور دچار کمبود منابع نشوند.
        برای افزایش سقف Memory Grant بدون برهم‌زدن منابع سایر پردازش‌ها، پیاده‌سازی Resource Governor را در ۴ مرحله زیر انجام دهید (این قابلیت در نسخه‌های Enterprise یا Developer موجود است):
        1.ایجاد Resource Pool و Workload Group اختصاصی: گام ۱: تخصیص منابع.ابتدا یک Pool جداگانه برای کارهای سنگین و یک Workload Group روی آن ایجاد کنید. با ویژگی REQUEST_MAX_MEMORY_GRANT_PERCENT سقف مجاز حافظه برای یک کوئری را افزایش می‌دهید (به‌طور پیش‌فرض ۲۵٪ است که در اینجا مثلاً به ۵۰٪ تغییر می‌دهیم):
        USE master;
        GO
        
        -- ۱. ساخت Resource Pool با سقف حافظه کلی مجاز
        CREATE RESOURCE POOL HeavyBatchPool
        WITH (
            MIN_MEMORY_PERCENT = 0,
            MAX_MEMORY_PERCENT = 80 -- حداکثر ۸۰ درصد از رم کل سرور در اختیار این Pool باشد
        );
        GO
        
        -- ۲. ساخت Workload Group و تنظیم سقف درخواست حافظه برای یک کوئری
        CREATE WORKLOAD GROUP HeavyBatchGroup
        WITH (
            REQUEST_MAX_MEMORY_GRANT_PERCENT = 50 -- مجاز به دریافت تا ۵۰٪ حافظه Pool
        )
        USING HeavyBatchPool;
        GO
        نحوه اعتبارسنجی: با کوئری از sys.dm_resource_governor_workload_groups ستون request_max_memory_grant_percent باید مقدار ۵۰ را برای گروه جدید نمایش دهد.

        2.ایجاد تابع طبقه‌بندی (Classifier Function): گام ۲: تفکیک بر اساس کاربر یا برنامه.یک تابع اسکالر در دیتابیس master بنویسید که تعیین کند ارتباط ورودی باید به کدام Workload Group متصل شود. می‌توانید بر اساس نام کاربر (SUSER_NAME()) یا نام کلاینت (APP_NAME()) فیلتر کنید:
        USE master;
        GO
        
        CREATE OR ALTER FUNCTION dbo.rgClassifier()
        RETURNS sysname
        WITH SCHEMABINDING
        AS
        BEGIN
            DECLARE @GroupName sysname;
        
            -- اگر کاربر خاصی بود (مثلاً کاربری که این SP سنگین را اجرا می‌کند)
            IF SUSER_NAME() = 'ReportRunnerUser'
                SET @GroupName = 'HeavyBatchGroup';
            
            -- یا بر اساس Application Name در کانکشن استرینگ
            ELSE IF APP_NAME() = 'MonthlyHeavyBatchApp'
                SET @GroupName = 'HeavyBatchGroup';
            
            ELSE
                SET @GroupName = 'default';
        
            RETURN @GroupName;
        END;
        GO
        نحوه اعتبارسنجی: اجرای دستور SELECT dbo.rgClassifier(); به صورت تستی با اجرای EXECUTE AS USER = 'ReportRunnerUser' نام گروه HeavyBatchGroup را برگرداند.

        3.ثبت تابع و فعال‌سازی Resource Governor: گام ۳: اعمال تنظیمات.تابع را به Resource Governor متصل کرده و پیکربندی را مجدداً بارگذاری کنید:
        USE master;
        GO
        
        -- اتصال تابع طبقه‌بندی
        ALTER RESOURCE GOVERNOR 
        WITH (CLASSIFIER_FUNCTION = dbo.rgClassifier);
        GO
        
        -- اعمال تغییرات در حافظه سرور
        ALTER RESOURCE GOVERNOR RECONFIGURE;
        GO
        نحوه اعتبارسنجی: دستور SELECT is_enabled FROM sys.resource_governor_configuration; باید مقدار 1 را بازگرداند.

        اعتبارسنجی نهایی در زمان اجرا
        برای اطمینان از اینکه نشست (Session) شما با موفقیت به گروه سنگین اختصاص یافته است، با کاربر تعریف‌شده متصل شده و کوئری زیر را اجرا کنید:
        SELECT 
            s.session_id,
            s.login_name,
            s.program_name,
            wg.name AS WorkloadGroupName
        FROM sys.dm_exec_sessions s
        JOIN sys.dm_resource_governor_workload_groups wg 
            ON s.group_id = wg.group_id
        WHERE s.session_id = @@SPID;
        اگر ستون WorkloadGroupName برابر با HeavyBatchGroup باشد، سقف حافظه مجاز برای کوئری‌های این نشست طبق تنظیمات جدید عمل خواهد کرد و خطای ۸۶۵۷ برطرف می‌شود.

        چه خطرات و عوارض جانبی‌ای ممکن است با افزایش سقف Memory Grant در سرورهای پربار (OLTP) رخ دهد؟
        افزایش سقف Memory Grant در یک سیستم OLTP می‌تواند مستقیماً پایداری و زمان پاسخ‌دهی (Latency) کل سرور را به خطر بیندازد. در محیط‌های OLTP، هدف اصلی اجرای سریع هزاران تراکنش سبک در ثانیه است؛ اختصاص یک قطعه حافظه چند گیگابایتی به یک درخواست واحد، تعادل موتور دیتابیس را بر هم می‌زند.

        مهم‌ترین خطرات و عوارض جانبی این کار عبارتند از:

        ۱. تشکیل صف و انتظار سایر کوئری‌ها (RESOURCE_SEMAPHORE)
        حافظه اختصاص‌یافته برای اجرای کوئری‌ها (Workspace Memory) نامحدود نیست. وقتی یک کوئری بخش بزرگی از این فضا را رزرو می‌کند:
        • کوئری‌های دیگر برای دریافت سهمیه حافظه خود در صف انتظار قرار می‌گیرند.
        • این حالت سبب افزایش شدید Wait Type از نوع RESOURCE_SEMAPHORE می‌شود.
        • در ترافیک بالای OLTP، این صف‌بندی ظرف چند ثانیه باعث انباشت صدها نشست (Connection Exhaustion)، کندی سراسری و در نهایت خطای Timeout در سمت کلاینت‌ها/اپلیکیشن‌ها خواهد شد.

        ۲. تخلیه بافر کش و افت شاخص Page Life Expectancy (PLE)
        اختصاص حجم بالایی از رم به Query Workspace به این معناست که بخش کمتری از حافظه در اختیار Buffer Pool (داده‌های کش‌شده در رم) قرار دارد:
        • اگر دیتابیس تحت فشار حافظه قرار بگیرد، صفحات پرکاربرد جداول از حافظه پاک می‌شوند تا جا برای پردازش‌های موقت باز شود.
        • شاخص PLE به‌شدت سقوط می‌کند.
        • در نتیجه، کوئری‌های سبک OLTP که پیش‌تر داده‌ها را مستقیماً از حافظه (RAM) می‌خواندند، ناچار به خواندن مکرر از دیسک (Physical I/O) می‌شوند که افت عملکرد محسوسی را رقم می‌زند.

        ۳. خطر اجرای هم‌زمان (Concurrent Execution Hazard)
        اگر کاربر مجاز یا جاب مربوطه به هر دلیلی دو یا چند بار هم‌زمان فراخوانی شود (مثلاً اجرای دستی هم‌زمان با Job خودکار، یا کلیک‌های مکرر کاربر در پنل گزارش‌گیری):
        • هر اجرا تلاش می‌کند سهمیه بالای مجاز (مثلاً ۵۰٪ حافظه Pool) را دریافت کند.
        • اجرای دوم فوراً در صف مسدود شده یا خطای کمبود حافظه رخ می‌دهد، و تمامی منابع محاسباتی سرور معطل این دو پردازش باقی می‌مانند.

        ۴. پنهان ماندن و تشدید مشکلات پایه‌ای کد
        افزایش سقف Memory Grant معمولاً پاک کردن صورت‌مسئله است:
        • اگر کوئری به دلیل پارامترهای نامناسب (Parameter Sniffing)، ایندکس نامناسب یا محاسبات ناکارآمد حافظه بالا می‌خواهد، بالا بردن سقف فقط به آن اجازه می‌دهد منابع بیشتری را بدون بازدهی تلف کند.
        • با رشد حجم جداول در طول زمان، حتی سقف افزایش‌یافته نیز پاسخگو نخواهد بود و سیستم مجدداً با خطای کمبود حافظه مواجه می‌شود.

        اقدامات پیشگیرانه هنگام نیاز اجباری به افزایش سقف
        • ایجاد سقف برای اجرای هم‌زمان: در Workload Group مربوطه، پارامتر GROUP_MAX_REQUESTS را محدود کنید تا بیش از ۱ یا ۲ نمونه از این پردازش سنگین به‌صورت هم‌زمان شروع نشود.
        • اجرا در ساعات خلوت (Off-Peak): اجرای این گزارش‌ها یا SPها را با استفاده از SQL Server Agent به ساعاتی منتقل کنید که سرور کمترین بار تراکنشی OLTP را تجربه می‌کند.
        • جداسازی کامل در سطح Read-Replica: بهترین راهکار در معماری‌های تجاری، هدایت این نوع گزارش‌ها و پردازش‌های سنگین به یک رپلیکای ثانویه (مانند Always On Availability Groups - Readable Secondary) است تا حافظه سرور عملیاتی درگیر نشود.