عنوان:

‫پروسیجرها و شنود پارامترها در SQL Server - قسمت دوم


نویسنده: امیر هاشم زاده
تاریخ: ۱۳۹۵/۰۹/۰۱ ۹:۵
آدرس: www.dntips.ir
در ادامه مطلب قبلی، آشنایی با شنود پارامترها در اس کیو ال سرور، جهت بهبود زمان پاسخگویی پروسیجرها یا کوئری‌های پارامتری الزامی است.
اس کیو ال سرور بوسیله ایجاد پلن‌های اجرایی کامپایل شده، سعی در بهینه سازی پروسیجرها دارد. هنگامیکه اس کیو ال سرور یک پروسیجر را کامپایل می‌نماید، به پارامترهای ارسال شده توجه دارد و یک پلن اجرایی را بر اساس پارامترهای ارسالی ایجاد می‌کند. به فرآیند تماشا یا توجه به پارامترهای ارسالی به پروسیجر، شنود پارامترها گفته می‌شود. شنود پارامترها می‌تواند بعضی از اوقات به کاهش کارآیی پلن اجرایی منجر شود؛ مخصوصا زمانیکه پارامترهایی با کاردینالیتی متفاوت، فراخوانی شوند.

شنود پارامتر چیست؟

شنود پارامتر، قابلیتی است که اس کیو ال سرور توسط آن یک پلن مناسب و بهینه اجرایی را برای پروسیجری با پارامترهای ارسالی، در اولین مرتبه اجرا، تولید خواهد کرد. در ادامه هر فراخوانی پروسیجر با پارامترهای مشابه، منجر به استفاده از پلن اجرایی ذخیره شده خواهد شد. شاید در اولین نگاه این استفاده مجدد، مناسب به نظر برسد. ولی در صورتیکه پروسیجر با پارامترهای متفاوتی فراخوانی شود، ممکن است پلن اجرایی تولید شده بر اساس آن پارامترهای اولیه، برای پارامترهای جدید بهینه نباشد.

پلن‌های اجرایی بر اساس چیزهایی که از SQL Server خواسته می‌شوند، بهینه سازی می‌شوند. اس کیو ال سرور کوئری را بررسی کرده و یک استراتژی بهینه را برای اجرای آن مشخص می‌کند. به کارهایی که کوئری قرار است انجام دهد نگاه می‌کند؛ از مقادیر پارامترها برای استفاده از اطلاعات آماری استفاده کرده و محاسباتی را انجام خواهد داد.

مثالی از مصرف شدید I/O بدلیل شنود پارامتر

در ادامه، برای درک بهتر شنود پارامتر، با مثالی خواهید دید که پروسیجر ذیل، باعث مصرف بالای منابع، بر اساس پارامترهای ارسالی خواهد شد. در این مثال دو دسته متفاوت پارامتر برای اجرای پروسیجر ارسال خواهند شد و خواهید دید که فراخوانی دوم، منابع I/O بیشتری را نسبت به فراخوانی اول، مصرف خواهد کرد. در ادامه کدهای جدولی را که پروسیجر قرار است بر روی آن فراخوانی اطلاعات را انجام دهد، می‌بینید.

SET NOCOUNT ON;
DROP TABLE BillingInfo;
CREATE TABLE BillingInfo(
ID INT IDENTITY,
BillingDate DATETIME,
BillingAmt MONEY,
BillingDesc varchar(500));
 
DECLARE @I INT;
DECLARE @BD INT;
SET @I = 0;
WHILE @I < 1000000 
BEGIN
  SET @I = @I + 1;
  SET @BD=CAST(RAND()*10000 AS INT)%3650;
  INSERT BillingInfo (BillingDate, BillingAmt) 
  VALUES (DATEADD(DD,@BD,
    CAST('1999/01/01' AS DATETIME)),
    RAND()*5000);
END 
 
ALTER TABLE BillingInfo 
  ADD  CONSTRAINT [PK_BillingInfo_ID] 
  PRIMARY KEY CLUSTERED (ID);
 
CREATE NONCLUSTERED INDEX IX_BillingDate
  ON dbo.BillingInfo(BillingDate);

در جدول BilingInfo بالا، یک میلیون رکورد با مقادیر BilingDate و BilingAmt به صورت تصادفی ایجاد شده است. بر روی ستون ID، ایندکس خوشه‌ای و ستون ایندکس غیر خوشه‌ای بر روی ستون BilingDate ایجاد شده‌است.

بوسیله پروسیجر زیر هم قرار است اطلاعات درخواستی فراهم شود:

CREATE PROC [dbo].[DisplayBillingInfo]
  @BeginDate DATETIME,
  @EndDate DATETIME
AS
SELECT BillingDate, BillingAmt
  FROM BillingInfo
  WHERE BillingDate between @BeginDate AND @EndDate;
سپس پروسیجر را 2 بار، با مقادیر پارامترهای متفاوتی اجرا می‌کنیم:
SET STATISTICS IO ON;
DBCC FREEPROCCACHE;
EXEC dbo.DisplayBillingInfo 
  @BeginDate = '1999-01-01',  
  @EndDate  = '1999-12-31';  
  
EXEC dbo.DisplayBillingInfo 
  @BeginDate = '2005-01-01',  
  @EndDate  = '2005-01-03';
اطلاعات آماری I/O روشن است و اطلاعات I/O در هر بار اجرا، نمایش داده می‌شود. در خط دوم توسط DBCC FREEPROCCACHE، پلن کش خالی خواهد شد؛ جهت اطمینان از عدم وجود پلن اجرایی مشابهی.
در فراخوانی اول، اطلاعات در بازه یک سال و در فراخوانی دوم، در بازه چند روز، درخواست شده‌اند. همانطور که گفته شد، پلن اجرایی بر اساس فراخوانی اول ایجاد خواهد شد و فراخوانی دوم نیز براساس همین پلن اجرایی ایجاد شده، اجرا می‌شود.


همانطور که مشاهده می‌کنید عملیات Clustered Index Scan، اجرا شده و اطلاعات I/O نیز بشرح زیر است (خط اول فراخوانی اول، خط دوم فراخوانی دوم):
Table 'BillingInfo'. Scan count 1, logical reads 3593, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 
Table 'BillingInfo'. Scan count 1, logical reads 3593, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
حال ترتیب فراخوانی‌ها را به‌شرح زیر جابجا می‌کنیم:
SET STATISTICS IO ON;
DBCC FREEPROCCACHE;
EXEC dbo.DisplayBillingInfo 
  @BeginDate = '2005-01-01',  
  @EndDate  = '2005-01-03';
  
EXEC dbo.DisplayBillingInfo 
  @BeginDate = '1999-01-01',  
  @EndDate  = '1999-12-31';
در کد بالا ابتدا فراخوانی که بازه کوچکتری دارد اجرا و پلن اجرایی بر اساس آن ایجاد خواهد شد و فراخوانی دوم از پلن کش شده استفاده می‌کند.

اکنون عملیات Index Seek را بجای Index Scan مشاهده می‌کنید. اطلاعات I/O هم بشرح زیر است:

Table 'BillingInfo'. Scan count 1, logical reads 2965, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 
Table 'BillingInfo'. Scan count 1, logical reads 337040, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
اکنون فراخوانی اول بهینه‌تر اجرا شده و بجای Index Scan از Index Seek استفاده کرده‌است و logical reads آن کاهش یافته‌است. در حالیکه فراخوانی دوم که در بازه یکسال اجرا شده‌است، با عملیات هزینه بر I/O بیشتری نسبت به آزمایش (حدود 100 برابر) انجام شده‌است. انجام این 2 آزمایش، پلن اجرایی متفاوتی را بر اساس پارامترهای ورودی ایجاد کرد و هزینه‌های I/O آن را مشاهده کردید. اکنون درک خوبی را نسبت به این قابلیت و اثرات آن خواهید داشت و در قسمت بعدی به راه حل‌هایی جهت کاهش اثرات منفی آن اشاره خواهد شد.