<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>بایگانی‌های SQL Server | نوین هاب</title>
	<atom:link href="https://tarahanenovin.ir/blog/tag/sql-server/feed/" rel="self" type="application/rss+xml" />
	<link>https://tarahanenovin.ir/blog/tag/sql-server/</link>
	<description>بلاگ طراحان نوین</description>
	<lastBuildDate>Thu, 20 Aug 2026 10:58:48 +0000</lastBuildDate>
	<language>fa-IR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>
	<item>
		<title>بهینه سازی Query های پیچیده SQL Server در پروژه‌های با حجم تراکنش بالا</title>
		<link>https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-query-%d9%87%d8%a7%db%8c-%d9%be%db%8c%da%86%db%8c%d8%af%d9%87-sql-server-%d8%af%d8%b1-%d9%be%d8%b1%d9%88%da%98%d9%87%d9%87%d8%a7%db%8c/</link>
					<comments>https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-query-%d9%87%d8%a7%db%8c-%d9%be%db%8c%da%86%db%8c%d8%af%d9%87-sql-server-%d8%af%d8%b1-%d9%be%d8%b1%d9%88%da%98%d9%87%d9%87%d8%a7%db%8c/#respond</comments>
		
		<dc:creator><![CDATA[TNVN]]></dc:creator>
		<pubDate></pubDate>
				<category><![CDATA[برنامه نویسی]]></category>
		<category><![CDATA[بک‌اند]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[بهینه سازی]]></category>
		<category><![CDATA[بهینه سازی کوئری]]></category>
		<guid isPermaLink="false">https://tarahanenovin.ir/blog/?p=319</guid>

					<description><![CDATA[<p>در پروژه‌های پرتراکنش، کندی SQL Server معمولاً فقط به پیچیده بودن Query مربوط نیست. ترکیبی از ایندکس نامناسب، قفل شدن رکوردها، اجرای مکرر Query، برآورد اشتباه تعداد رکوردها و طراحی نادرست تراکنش می‌تواند زمان پاسخ را از چند میلی ثانیه به چند ثانیه افزایش دهد. در این آموزش، به جای توصیه‌های کلی، یک مسیر عملی برای پیدا کردن و اصلاح Queryهای کند در SQL Server ارائه می‌شود. قبل از تغییر Query، مشکل را اندازه گیری کنید اولین اشتباه در بهینه سازی SQL Server این است که بدون مشاهده Execution Plan یا آمار اجرای Query، شروع به تغییر کد کنیم. دو دستور زیر را فعال کنید: SET STATISTICS IO ON; SET STATISTICS TIME ON; -- Query مورد نظر SELECT * FROM Sales.Orders WHERE CustomerId = 1250; SET STATISTICS IO OFF; SET STATISTICS TIME OFF; خروجی این دستورات اطلاعات مهمی در اختیار شما قرار می‌دهد: تعداد Logical Read تعداد Physical Read زمان CPU زمان سپری شده تعداد Scan و Seek مقدار خواندن از حافظه یا دیسک تفاوت Logical Read و Physical Read Logical Read تعداد صفحات ۸ کیلوبایتی خوانده شده از Buffer Pool است. مقدار زیاد آن معمولاً نشانه اسکن گسترده یا ایندکس نامناسب است. Physical Read نشان می‌دهد SQL Server مجبور شده داده را از دیسک بخواند. در سیستم‌های پرتراکنش، Physical Read بالا می‌تواند باعث رقابت شدید روی دیسک شود. در SSMS، برای مشاهده Execution Plan واقعی از کلیدهای زیر استفاده کنید: Ctrl + M سپس Query را اجرا کنید. در Plan به این موارد توجه داشته باشید: Table Scan Index Scan Key Lookup Sort پرهزینه Hash Match هشدارهای مربوط به Implicit Conversion اختلاف بین Estimated Rows و Actual Rows بر اساس مستندات رسمی مایکروسافت، پایش مداوم Queryها، Execution Planها و Runtime Statistics پایه تصمیم گیری صحیح برای بهینه سازی است. راهنمای مانیتورینگ و بهینه سازی SQL Server در Microsoft Learn ۱. Query را با فیلتر قابل استفاده برای ایندکس بنویسید SQL Server زمانی می‌تواند از ایندکس به شکل مؤثر استفاده کند که شرط WHERE روی خود ستون قابل جستجو باشد. Query نامناسب SELECT OrderId, TotalAmount FROM Sales.Orders WHERE YEAR(OrderDate) = 2026; در این حالت، تابع YEAR روی ستون اجرا شده و SQL Server ممکن است مجبور شود تعداد زیادی از رکوردها را بررسی کند. Query اصلاح شده SELECT OrderId, TotalAmount FROM Sales.Orders WHERE OrderDate &#62;= '20260101' AND OrderDate &#60; '20270101'; این روش به SQL Server اجازه می‌دهد محدوده ایندکس را مستقیماً جستجو کند. چند نمونه رایج دیگر نامناسب WHERE CONVERT(date, CreatedAt) = '2026-08-20' مناسب WHERE CreatedAt &#62;= '20260820' AND CreatedAt &#60; '20260821' نامناسب WHERE ISNULL(Status, 0) = 1 مناسب WHERE Status = 1 در صورت نیاز، مقدار NULL را جداگانه مدیریت کنید: WHERE Status = 1 OR Status IS NULL; ۲. برای Queryهای پرتکرار ایندکس ترکیبی بسازید فرض کنید Query اصلی سیستم سفارش‌ها به شکل زیر است: SELECT OrderId, CustomerId, TotalAmount, CreatedAt FROM Sales.Orders WHERE CustomerId = @CustomerId AND Status = @Status AND CreatedAt &#62;= @FromDate AND CreatedAt &#60; @ToDate ORDER BY CreatedAt DESC; یک ایندکس مناسب می‌تواند این باشد: CREATE INDEX IX_Orders_Customer_Status_CreatedAt ON Sales.Orders ( CustomerId, Status, CreatedAt DESC ) INCLUDE ( OrderId, TotalAmount ); چرا ترتیب ستون‌ها مهم است؟ در این مثال: CustomerId و Status برای فیلتر برابری استفاده می‌شوند. CreatedAt برای فیلتر بازه‌ای و مرتب سازی استفاده می‌شود. ستون‌های OrderId و TotalAmount در بخش INCLUDE قرار گرفته‌اند تا SQL Server برای تکمیل نتیجه به جدول اصلی مراجعه نکند. ستون‌هایی که در WHERE و JOIN استفاده می‌شوند، معمولاً کاندیدهای اصلی بخش کلیدی ایندکس هستند. ستون‌هایی که فقط در SELECT قرار دارند، اغلب برای INCLUDE مناسب‌ترند. ۳. مشکل Key Lookup را برطرف کنید فرض کنید این ایندکس وجود دارد: CREATE INDEX IX_Orders_CustomerId ON Sales.Orders(CustomerId); و Query زیر اجرا می‌شود: SELECT OrderId, CustomerId, TotalAmount, CreatedAt, Status FROM Sales.Orders WHERE CustomerId = @CustomerId; SQL Server ابتدا رکوردها را از ایندکس پیدا می‌کند، اما برای خواندن سایر ستون‌ها به جدول اصلی برمی گردد. این عملیات در Execution Plan با عنوان Key Lookup نمایش داده می‌شود. اگر Query پرتکرار است، ایندکس را پوششی کنید: CREATE INDEX IX_Orders_CustomerId_Covering ON Sales.Orders(CustomerId) INCLUDE ( OrderId, TotalAmount, CreatedAt, Status ); چه زمانی Covering Index نسازیم؟ Covering Index همیشه بهترین راه حل نیست. ایجاد ایندکس بزرگ باعث افزایش هزینه این عملیات می‌شود: INSERT UPDATE DELETE عملیات نگهداری ایندکس مصرف فضای دیسک مصرف حافظه فقط برای Queryهای پرتکرار و حساس به زمان پاسخ، از این روش استفاده کنید. ۴. از SELECT * در مسیرهای پرترافیک استفاده نکنید Query نامناسب SELECT * FROM Sales.Orders WHERE CustomerId = @CustomerId; این Query به تمام ستون‌ها وابسته است. در نتیجه: حجم داده بیشتری از دیسک خوانده می‌شود. امکان استفاده از Covering Index کاهش می‌یابد. انتقال داده بین SQL Server و برنامه افزایش پیدا می‌کند. تغییر ساختار جدول می‌تواند رفتار Query را تغییر دهد. Query بهتر SELECT OrderId, CustomerId, Status, TotalAmount, CreatedAt FROM Sales.Orders WHERE CustomerId = @CustomerId; در APIها و صفحات لیست، فقط ستون‌هایی را دریافت کنید که واقعاً نمایش داده می‌شوند. ۵. Pagination را با Keyset انجام دهید در جدول‌های بزرگ، استفاده از OFFSET برای صفحات انتهایی بسیار پرهزینه است. روش پرهزینه SELECT OrderId, CreatedAt, TotalAmount FROM Sales.Orders ORDER BY CreatedAt DESC, OrderId DESC OFFSET 500000 ROWS FETCH NEXT 50 ROWS ONLY; SQL Server برای رسیدن به صفحه مورد نظر باید تعداد زیادی رکورد را مرتب و رد کند. روش Keyset Pagination در این روش، آخرین رکورد صفحه قبلی را به Query بعدی ارسال می‌کنیم: DECLARE @LastCreatedAt datetime2 = '2026-08-15 12:30:00'; DECLARE @LastOrderId bigint = 987654; SELECT TOP (50) OrderId, CreatedAt, TotalAmount FROM Sales.Orders WHERE CreatedAt &#60; @LastCreatedAt OR ( CreatedAt = @LastCreatedAt AND OrderId &#60; @LastOrderId ) ORDER BY CreatedAt DESC, OrderId DESC; برای این Query ایندکس زیر مناسب است: CREATE INDEX IX_Orders_CreatedAt_OrderId ON Sales.Orders(CreatedAt DESC, OrderId DESC) INCLUDE ( TotalAmount ); این روش برای جدول‌های سفارش، تراکنش مالی، گزارش رخدادها و لاگ‌های حجیم عملکرد پایدارتری دارد. ۶. مراقب Parameter Sniffing باشید SQL Server معمولاً هنگام اولین اجرای Stored Procedure، مقدار پارامترها را بررسی کرده و بر اساس آن Execution Plan می‌سازد. اگر توزیع داده یکنواخت نباشد، یک Plan ممکن است برای یک مقدار مناسب و برای مقدار دیگر بسیار ضعیف باشد. فرض کنید یک مشتری فقط ۱۰ سفارش دارد، اما مشتری دیگری چند میلیون سفارش ثبت کرده است: CREATE OR ALTER PROCEDURE Sales.GetCustomerOrders @CustomerId bigint AS BEGIN SET NOCOUNT ON; SELECT OrderId, CreatedAt, TotalAmount, Status FROM Sales.Orders WHERE CustomerId = @CustomerId; END; ممکن است Plan ساخته شده برای مشتری کم تراکنش، هنگام اجرای Query برای مشتری بزرگ مناسب نباشد. راه حل اول: بازکامپایل در همان اجرا SELECT OrderId, CreatedAt, TotalAmount, Status FROM Sales.Orders WHERE CustomerId = @CustomerId OPTION (RECOMPILE); این روش برای Queryهایی مناسب است که: تعداد اجرای آنها بسیار زیاد نیست. اختلاف حجم داده بین پارامترها زیاد است. زمان ساخت Plan نسبت به هزینه اجرای اشتباه کمتر است. راه حل دوم: استفاده از OPTIMIZE FOR UNKNOWN SELECT OrderId, CreatedAt, TotalAmount, Status FROM Sales.Orders WHERE CustomerId = @CustomerId OPTION (OPTIMIZE FOR UNKNOWN); در این روش SQL Server از مقدار واقعی پارامتر برای ساخت Plan استفاده نمی‌کند و برآورد عمومی‌تری انجام می‌دهد. راه حل سوم: متغیر محلی DECLARE @LocalCustomerId bigint = @CustomerId; SELECT OrderId, CreatedAt, TotalAmount, Status FROM Sales.Orders WHERE CustomerId = @LocalCustomerId; این روش گاهی مشکل را کاهش می‌دهد، اما ممکن است برآورد SQL Server را ضعیف کند. بنابراین باید با Execution Plan و آمار واقعی آزمایش شود. ۷. Implicit Conversion را حذف کنید تبدیل ضمنی نوع داده می‌تواند مانع استفاده صحیح از ایندکس شود. فرض کنید نوع ستون CustomerId از نوع bigint است: CREATE TABLE Sales.Orders ( OrderId bigint NOT NULL, CustomerId bigint NOT NULL ); اما برنامه مقدار را به صورت nvarchar ارسال می‌کند: WHERE CustomerId = N'1250' ممکن است SQL Server مجبور به تبدیل نوع داده شود. در Execution Plan، هشدار CONVERT_IMPLICIT را بررسی کنید. روش صحیح نوع پارامتر در برنامه و Stored Procedure باید با نوع ستون یکسان باشد: CREATE OR ALTER PROCEDURE Sales.GetOrders @CustomerId bigint AS BEGIN SET NOCOUNT ON; SELECT OrderId, CreatedAt, TotalAmount FROM Sales.Orders WHERE CustomerId = @CustomerId; END; در پروژه‌های .NET نیز نوع پارامتر را صحیح مشخص کنید: command.Parameters.Add("@CustomerId", SqlDbType.BigInt).Value = customerId; ۸. JOIN را با ستون‌های درست و ایندکس مناسب اجرا کنید فرض کنید Query زیر برای نمایش سفارش‌های مشتری اجرا می‌شود: SELECT o.OrderId, o.CreatedAt, c.CustomerName, o.TotalAmount FROM Sales.Orders AS o INNER JOIN Sales.Customers AS c ON c.CustomerId = o.CustomerId WHERE o.Status = 1 AND o.CreatedAt &#62;= @FromDate; ایندکس‌های پیشنهادی: CREATE INDEX IX_Orders_Status_CreatedAt_CustomerId ON Sales.Orders(Status, CreatedAt, CustomerId) INCLUDE ( OrderId, TotalAmount ); و در جدول مشتری: CREATE UNIQUE INDEX UX_Customers_CustomerId ON Sales.Customers(CustomerId) INCLUDE ( CustomerName ); از JOIN غیرضروری جلوگیری کنید اگر هیچ ستونی از جدول مشتری استفاده نمی‌شود، این JOIN را حذف کنید: SELECT o.OrderId, o.TotalAmount FROM Sales.Orders AS o INNER JOIN Sales.Customers AS c ON c.CustomerId = o.CustomerId WHERE o.Status = 1; نسخه ساده‌تر: SELECT OrderId, TotalAmount FROM Sales.Orders WHERE Status = 1; هر JOIN اضافی می‌تواند هزینه CPU، حافظه و I/O را افزایش دهد. ۹. به جای IN بزرگ از روش مناسب استفاده کنید روش مشکل ساز SELECT OrderId, TotalAmount FROM Sales.Orders WHERE OrderId IN ( 100001, 100002, 100003, 100004 ); برای تعداد کم شناسه مشکلی ایجاد نمی‌کند، اما در برنامه‌های پرترافیک و فهرست‌های بزرگ، بهتر است از Table-Valued Parameter استفاده شود. تعریف نوع جدولی CREATE TYPE Sales.OrderIdList AS TABLE ( OrderId bigint NOT NULL PRIMARY KEY ); Stored Procedure CREATE OR ALTER PROCEDURE Sales.GetOrdersByIds @OrderIds Sales.OrderIdList READONLY AS BEGIN SET NOCOUNT ON; SELECT o.OrderId, o.CustomerId, o.TotalAmount, o.Status FROM Sales.Orders AS o INNER JOIN @OrderIds AS ids ON ids.OrderId = o.OrderId; END; این روش نسبت به ساختن رشته‌های طولانی و Dynamic SQL قابل کنترل‌تر و امن‌تر است. ۱۰. LIKE را برای جستجوی متنی درست انتخاب کنید Query قابل استفاده از ایندکس WHERE CustomerName LIKE @Search + N'%' Query پرهزینه WHERE CustomerName LIKE N'%' + @Search + N'%' در حالت دوم، SQL Server معمولاً نمی‌تواند از ایندکس معمولی برای پیدا کردن ابتدای عبارت استفاده کند. برای جستجوی واقعی در متن‌های بزرگ، Full-Text Search گزینه مناسب‌تری است: CREATE FULLTEXT CATALOG CustomerCatalog AS DEFAULT; سپس روی ستون مورد نظر Full-Text Index ایجاد کنید و Query را با CONTAINS یا FREETEXT اجرا کنید: SELECT CustomerId, CustomerName FROM Sales.Customers WHERE CONTAINS(CustomerName, @SearchTerm); ۱۱. تراکنش‌ها را کوتاه نگه دارید در سیستم پرتراکنش، تراکنش طولانی فقط یک مشکل عملکردی نیست؛ بلکه باعث Blocking و افزایش زمان انتظار سایر درخواست‌ها می‌شود. الگوی نامناسب BEGIN TRANSACTION; SELECT * FROM Sales.Orders WHERE OrderId = @OrderId; -- پردازش طولانی در برنامه -- ارسال درخواست به سرویس خارجی -- محاسبه سنگین UPDATE Sales.Orders SET Status = 2 WHERE OrderId = @OrderId; COMMIT TRANSACTION; در این فاصله، قفل‌ها ممکن است بیشتر از زمان لازم حفظ شوند. الگوی بهتر ابتدا اطلاعات لازم را بخوانید، پردازش‌های خارج از دیتابیس را انجام دهید و فقط عملیات نهایی را در تراکنش قرار دهید: SELECT OrderId, Status, TotalAmount FROM Sales.Orders WHERE OrderId = @OrderId; سپس: SET XACT_ABORT ON; BEGIN TRY BEGIN TRANSACTION; UPDATE Sales.Orders SET Status = 2, UpdatedAt = SYSUTCDATETIME() WHERE OrderId = @OrderId AND Status = 1; IF @@ROWCOUNT = 0 THROW 50001, 'Order is not available for update.', 1; INSERT INTO Sales.OrderStatusHistory ( OrderId, OldStatus, NewStatus, CreatedAt ) VALUES ( @OrderId, 1, 2, SYSUTCDATETIME() ); COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() &#60;&#62; 0 ROLLBACK TRANSACTION; THROW; END CATCH; نکته مهم شرط وضعیت در دستور UPDATE نقش کنترل همزمانی دارد: WHERE OrderId = @OrderId AND Status = 1 به این ترتیب، اگر درخواست دیگری قبلاً وضعیت سفارش را تغییر داده باشد، عملیات فعلی بدون بررسی اضافی شکست می‌خورد. ۱۲. Blocking و Deadlock را بررسی کنید برای مشاهده درخواست‌های در حال انتظار: SELECT r.session_id, r.status, r.command, r.wait_type, r.wait_time, r.blocking_session_id, r.cpu_time, r.logical_reads, t.text AS SqlText FROM sys.dm_exec_requests AS r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t WHERE r.session_id &#60;&#62; @@SPID ORDER BY r.wait_time DESC; برای پیدا کردن Sessionهای مسدود شده: SELECT session_id, blocking_session_id, wait_type, wait_time, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id &#60;&#62; 0; علت‌های رایج Blocking تراکنش‌های طولانی نبود ایندکس روی شرط UPDATE یا DELETE اسکن گسترده جدول اجرای گزارش سنگین روی جداول عملیاتی ترتیب متفاوت دسترسی به جداول در تراکنش‌های مختلف به روز رسانی دسته‌ای در ساعات شلوغ الگوی کاهش Deadlock در تمام بخش‌های برنامه، ترتیب دسترسی به جداول را ثابت کنید. برای مثال، اگر یک تراکنش ابتدا Customers و سپس Orders را تغییر می‌دهد، تراکنش دیگر نباید ابتدا Orders و بعد Customers را تغییر دهد. همچنین عملیات بزرگ را به بخش‌های کوچک‌تر تقسیم کنید: WHILE 1 = 1 BEGIN DELETE TOP (1000) FROM Audit.Logs WHERE CreatedAt &#60; @BeforeDate; IF @@ROWCOUNT = 0 BREAK; END; ۱۳. سطح Isolation را آگاهانه انتخاب کنید در بسیاری از سیستم‌های OLTP، خواندن‌های طولانی با نوشتن‌ها تداخل ایجاد می‌کنند. یکی از راهکارهای مهم، فعال کردن READ_COMMITTED_SNAPSHOT است: ALTER DATABASE [SalesDb] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; در این حالت، خواندن‌های معمولی از Version Store در tempdb استفاده می‌کنند و معمولاً کمتر منتظر قفل‌های نوشتاری می‌مانند. اما پیش از فعال سازی باید موارد زیر بررسی شود: فضای کافی برای tempdb حجم Update و Delete مدت اجرای تراکنش‌ها مصرف Version Store سازگاری برنامه با رفتار جدید خواندن برای تراکنش‌هایی که نیاز به ثبات کامل داده دارند، سطح Isolation را بدون بررسی تغییر ندهید. ۱۴. Statistics را به روز نگه دارید اگر Statistics قدیمی باشد، SQL Server ممکن است تعداد رکوردهای خروجی را اشتباه تخمین بزند و Join یا ایندکس نامناسب انتخاب کند. به روز رسانی دستی: UPDATE STATISTICS Sales.Orders WITH FULLSCAN; برای جدول‌های بزرگ، FULLSCAN ممکن است پرهزینه باشد. بنابراین آن را در ساعات مناسب و با بررسی زمان اجرا انجام دهید. مشاهده زمان آخرین به روز رسانی Statistics: SELECT OBJECT_SCHEMA_NAME(sp.object_id) AS SchemaName, OBJECT_NAME(sp.object_id) AS TableName, sp.stats_id, sp.last_updated, sp.rows, sp.rows_sampled, sp.modification_counter FROM sys.dm_db_stats_properties ( OBJECT_ID(N'Sales.Orders'), 1 ) AS sp; در پروژه‌هایی که داده به سرعت تغییر می‌کند، فقط فعال بودن Auto Update Statistics کافی نیست. آمار Query Store، زمان اجرای Query و تغییرات داده را نیز بررسی کنید. ۱۵. Query Store را فعال و بررسی کنید Query Store برای پیدا کردن Queryهایی که در طول زمان کند شده‌اند بسیار کاربردی است. فعال سازی: ALTER DATABASE [SalesDb] SET QUERY_STORE = ON; برای پیدا کردن Queryهای پرهزینه: SELECT TOP (20) qt.query_sql_text, rs.execution_count, rs.avg_duration, rs.avg_cpu_time, rs.avg_logical_io_reads, rs.last_execution_time FROM sys.query_store_query_text AS qt INNER JOIN sys.query_store_query AS q ON q.query_text_id = qt.query_text_id INNER JOIN sys.query_store_plan AS p ON p.query_id = q.query_id INNER JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id ORDER BY rs.avg_duration DESC; برای پیدا کردن Queryهایی که بیشترین مجموع زمان را مصرف کرده‌اند: SELECT TOP (20) qt.query_sql_text, SUM(rs.count_executions) AS TotalExecutions, SUM(rs.avg_duration * rs.count_executions) AS TotalDuration FROM sys.query_store_query_text AS qt INNER JOIN sys.query_store_query AS q ON q.query_text_id = qt.query_text_id INNER JOIN sys.query_store_plan AS p ON p.query_id = q.query_id INNER JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id GROUP BY qt.query_sql_text ORDER BY TotalDuration DESC; گاهی Queryای که میانگین زمان کمی دارد، به دلیل تعداد اجرای بسیار زیاد، بیشترین فشار را به SQL Server وارد می‌کند. ۱۶. برای اصلاح Query از این ترتیب استفاده کنید برای هر Query پیچیده، این مراحل را به صورت مشخص انجام دهید: مرحله اول: اجرای فعلی را ثبت کنید زمان متوسط زمان صدک ۹۵ و ۹۹ CPU Logical Read تعداد اجرا تعداد Timeout Wait Type مرحله دوم: Execution Plan را بررسی کنید به دنبال این موارد باشید: Scan روی جدول بزرگ اختلاف Estimated Rows و Actual Rows Key Lookup تکرارشونده Sort بدون ایندکس مناسب Hash Join ناشی از کمبود ایندکس یا برآورد اشتباه Implicit Conversion Spill به tempdb مرحله سوم: Query را اصلاح کنید حذف SELECT * حذف تابع از روی ستون فیلتر اصلاح Pagination حذف JOIN غیرضروری اصلاح نوع پارامترها کاهش تعداد ستون‌های خروجی مرحله چهارم: ایندکس را بررسی کنید آیا ایندکس با الگوی واقعی Query هماهنگ است؟ آیا ترتیب ستون‌ها صحیح است؟ آیا Query نیاز به INCLUDE دارد؟ آیا ایندکس مشابه از قبل وجود دارد؟ هزینه این ایندکس برای عملیات نوشتن چقدر است؟ مرحله پنجم: زیر بار واقعی آزمایش کنید Query فقط در محیط توسعه آزمایش نشود. حجم داده، همزمانی کاربران، پارامترهای مختلف و زمان‌های پرترافیک را شبیه سازی کنید. نمونه کامل بررسی یک Query Query اولیه SELECT * FROM Sales.Orders WHERE CONVERT(date, CreatedAt) = @OrderDate AND CustomerId IN ( SELECT CustomerId FROM Sales.Customers WHERE IsActive = 1 ) ORDER BY CreatedAt DESC OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY; مشکل‌های این Query: استفاده از تابع روی CreatedAt استفاده از SELECT * Pagination با OFFSET در صفحات بزرگ احتمال اسکن جدول Customers نامشخص بودن ایندکس‌های مورد نیاز نسخه اصلاح شده SELECT TOP (@PageSize) o.OrderId, o.CustomerId, o.CreatedAt, o.TotalAmount, o.Status FROM Sales.Orders AS o INNER JOIN Sales.Customers AS c ON c.CustomerId = o.CustomerId WHERE c.IsActive = 1 AND ( o.CreatedAt &#60; @LastCreatedAt OR ( o.CreatedAt = @LastCreatedAt AND o.OrderId &#60; @LastOrderId ) ) ORDER BY o.CreatedAt DESC, o.OrderId DESC; ایندکس‌های پیشنهادی: CREATE INDEX IX_Customers_IsActive_CustomerId ON Sales.Customers(IsActive, CustomerId); CREATE INDEX IX_Orders_CreatedAt_OrderId_CustomerId ON Sales.Orders(CreatedAt DESC, OrderId DESC, CustomerId) INCLUDE ( TotalAmount, Status ); این نسخه برای صفحه‌های انتهایی، خواندن ستون‌های محدود و اجرای پرتکرار، قابلیت مقیاس پذیری بیشتری دارد. چه کارهایی معمولاً نتیجه معکوس دارند؟ ایجاد ایندکس برای هر ستون تعداد زیاد ایندکس، عملیات نوشتن را کند می‌کند و فضای زیادی مصرف می‌کند. استفاده دائمی از NOLOCK SELECT * FROM Sales.Orders WITH (NOLOCK); NOLOCK ممکن است Dirty Read، رکوردهای تکراری یا داده‌های ناپایدار برگرداند. این دستور راه حل عمومی برای مشکل Blocking نیست. استفاده بی دلیل از OPTION (RECOMPILE) برای Queryهای پرتکرار، بازسازی Plan در هر اجرا می‌تواند CPU را افزایش دهد. انتقال همه Queryها به Stored Procedure Stored Procedure مفید است، اما اگر منطق Query، ایندکس یا مدل داده نادرست باشد، صرفاً قرار دادن کد در Stored Procedure مشکل را حل نمی‌کند. افزایش سخت افزار بدون اصلاح Query افزایش RAM یا CPU گاهی کمک می‌کند، اما Query دارای اسکن، Sort یا Blocking همچنان در حجم بالاتر مشکل ایجاد خواهد کرد. چک لیست نهایی بهینه سازی Query در SQL Server پیش از انتشار تغییرات، این موارد را بررسی کنید: Execution Plan واقعی مشاهده شده است. Logical Read قبل و بعد مقایسه شده است. زمان CPU و زمان سپری شده ثبت شده است. Query از SELECT * استفاده نمی‌کند. روی ستون‌های فیلتر تابع اجرا نمی‌شود. نوع پارامتر با نوع ستون یکسان است. ایندکس ترکیبی بر اساس الگوی واقعی Query طراحی شده است. Key Lookup غیرضروری حذف شده است. Pagination برای صفحات بزرگ با Keyset انجام می‌شود. تراکنش‌ها کوتاه هستند. Blocking و Deadlock بررسی شده‌اند. Statistics و Query Store در نظر گرفته شده‌اند. Query با پارامترهای کم حجم و پرحجم آزمایش شده است. تأثیر ایندکس جدید بر INSERT و UPDATE بررسی شده است. جمع بندی بهینه...</p>
<p>نوشته <a href="https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-query-%d9%87%d8%a7%db%8c-%d9%be%db%8c%da%86%db%8c%d8%af%d9%87-sql-server-%d8%af%d8%b1-%d9%be%d8%b1%d9%88%da%98%d9%87%d9%87%d8%a7%db%8c/">بهینه سازی Query های پیچیده SQL Server در پروژه‌های با حجم تراکنش بالا</a> اولین بار در <a href="https://tarahanenovin.ir/blog">نوین هاب</a>. پدیدار شد.</p>
]]></description>
										<content:encoded><![CDATA[<p dir="rtl" lang="fa">در پروژه‌های پرتراکنش، کندی SQL Server معمولاً فقط به پیچیده بودن Query مربوط نیست. ترکیبی از ایندکس نامناسب، قفل شدن رکوردها، اجرای مکرر Query، برآورد اشتباه تعداد رکوردها و طراحی نادرست تراکنش می‌تواند زمان پاسخ را از چند میلی ثانیه به چند ثانیه افزایش دهد.</p>
<p dir="rtl" lang="fa">در این آموزش، به جای توصیه‌های کلی، یک مسیر عملی برای پیدا کردن و اصلاح Queryهای کند در SQL Server ارائه می‌شود.</p>
<h2 dir="rtl" lang="fa">قبل از تغییر Query، مشکل را اندازه گیری کنید</h2>
<p dir="rtl" lang="fa">اولین اشتباه در بهینه سازی SQL Server این است که بدون مشاهده Execution Plan یا آمار اجرای Query، شروع به تغییر کد کنیم.</p>
<p dir="rtl" lang="fa">دو دستور زیر را فعال کنید:</p>
<pre><code class="hljs"><span class="hljs-keyword">SET</span> STATISTICS IO <span class="hljs-keyword">ON</span>;
<span class="hljs-keyword">SET</span> STATISTICS <span class="hljs-type">TIME</span> <span class="hljs-keyword">ON</span>;

<span class="hljs-comment">-- Query مورد نظر</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span>
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-number">1250</span>;

<span class="hljs-keyword">SET</span> STATISTICS IO OFF;
<span class="hljs-keyword">SET</span> STATISTICS <span class="hljs-type">TIME</span> OFF;
</code></pre>
<p dir="rtl" lang="fa">خروجی این دستورات اطلاعات مهمی در اختیار شما قرار می‌دهد:</p>
<ul dir="rtl" lang="fa">
<li>تعداد Logical Read</li>
<li>تعداد Physical Read</li>
<li>زمان CPU</li>
<li>زمان سپری شده</li>
<li>تعداد Scan و Seek</li>
<li>مقدار خواندن از حافظه یا دیسک</li>
</ul>
<h3 dir="rtl" lang="fa">تفاوت Logical Read و Physical Read</h3>
<p dir="rtl" lang="fa"><code>Logical Read</code> تعداد صفحات ۸ کیلوبایتی خوانده شده از Buffer Pool است. مقدار زیاد آن معمولاً نشانه اسکن گسترده یا ایندکس نامناسب است.</p>
<p dir="rtl" lang="fa"><code>Physical Read</code> نشان می‌دهد SQL Server مجبور شده داده را از دیسک بخواند. در سیستم‌های پرتراکنش، Physical Read بالا می‌تواند باعث رقابت شدید روی دیسک شود.</p>
<p dir="rtl" lang="fa">در SSMS، برای مشاهده Execution Plan واقعی از کلیدهای زیر استفاده کنید:</p>
<pre><code class="hljs">Ctrl + M
</code></pre>
<p dir="rtl" lang="fa">سپس Query را اجرا کنید. در Plan به این موارد توجه داشته باشید:</p>
<ul dir="rtl" lang="fa">
<li>Table Scan</li>
<li>Index Scan</li>
<li>Key Lookup</li>
<li>Sort پرهزینه</li>
<li>Hash Match</li>
<li>هشدارهای مربوط به Implicit Conversion</li>
<li>اختلاف بین Estimated Rows و Actual Rows</li>
</ul>
<p dir="rtl" lang="fa">بر اساس مستندات رسمی مایکروسافت، پایش مداوم Queryها، Execution Planها و Runtime Statistics پایه تصمیم گیری صحیح برای بهینه سازی است. <a href="https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitor-and-tune-for-performance?view=sql-server-ver16" target="_blank" rel="nofollow noopener noreferrer">راهنمای مانیتورینگ و بهینه سازی SQL Server در Microsoft Learn</a></p>
<h2 dir="rtl" lang="fa">۱. Query را با فیلتر قابل استفاده برای ایندکس بنویسید</h2>
<p dir="rtl" lang="fa">SQL Server زمانی می‌تواند از ایندکس به شکل مؤثر استفاده کند که شرط <code>WHERE</code> روی خود ستون قابل جستجو باشد.</p>
<h3 dir="rtl" lang="fa">Query نامناسب</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">YEAR</span>(OrderDate) <span class="hljs-operator">=</span> <span class="hljs-number">2026</span>;
</code></pre>
<p dir="rtl" lang="fa">در این حالت، تابع <code>YEAR</code> روی ستون اجرا شده و SQL Server ممکن است مجبور شود تعداد زیادی از رکوردها را بررسی کند.</p>
<h3 dir="rtl" lang="fa">Query اصلاح شده</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> OrderDate <span class="hljs-operator">&gt;=</span> <span class="hljs-string">'20260101'</span>
  <span class="hljs-keyword">AND</span> OrderDate <span class="hljs-operator">&lt;</span>  <span class="hljs-string">'20270101'</span>;
</code></pre>
<p dir="rtl" lang="fa">این روش به SQL Server اجازه می‌دهد محدوده ایندکس را مستقیماً جستجو کند.</p>
<h3 dir="rtl" lang="fa">چند نمونه رایج دیگر</h3>
<h4 dir="rtl" lang="fa">نامناسب</h4>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">CONVERT</span>(<span class="hljs-type">date</span>, CreatedAt) <span class="hljs-operator">=</span> <span class="hljs-string">'2026-08-20'</span>
</code></pre>
<h4 dir="rtl" lang="fa">مناسب</h4>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> CreatedAt <span class="hljs-operator">&gt;=</span> <span class="hljs-string">'20260820'</span>
  <span class="hljs-keyword">AND</span> CreatedAt <span class="hljs-operator">&lt;</span>  <span class="hljs-string">'20260821'</span>
</code></pre>
<h4 dir="rtl" lang="fa">نامناسب</h4>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> ISNULL(Status, <span class="hljs-number">0</span>) <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
</code></pre>
<h4 dir="rtl" lang="fa">مناسب</h4>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
</code></pre>
<p dir="rtl" lang="fa">در صورت نیاز، مقدار <code>NULL</code> را جداگانه مدیریت کنید:</p>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
   <span class="hljs-keyword">OR</span> Status <span class="hljs-keyword">IS</span> <span class="hljs-keyword">NULL</span>;
</code></pre>
<h2 dir="rtl" lang="fa">۲. برای Queryهای پرتکرار ایندکس ترکیبی بسازید</h2>
<p dir="rtl" lang="fa">فرض کنید Query اصلی سیستم سفارش‌ها به شکل زیر است:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, CustomerId, TotalAmount, CreatedAt
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>
  <span class="hljs-keyword">AND</span> Status <span class="hljs-operator">=</span> <span class="hljs-variable">@Status</span>
  <span class="hljs-keyword">AND</span> CreatedAt <span class="hljs-operator">&gt;=</span> <span class="hljs-variable">@FromDate</span>
  <span class="hljs-keyword">AND</span> CreatedAt <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@ToDate</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> CreatedAt <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">یک ایندکس مناسب می‌تواند این باشد:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_Customer_Status_CreatedAt
<span class="hljs-keyword">ON</span> Sales.Orders
(
    CustomerId,
    Status,
    CreatedAt <span class="hljs-keyword">DESC</span>
)
INCLUDE
(
    OrderId,
    TotalAmount
);
</code></pre>
<h3 dir="rtl" lang="fa">چرا ترتیب ستون‌ها مهم است؟</h3>
<p dir="rtl" lang="fa">در این مثال:</p>
<ul dir="rtl" lang="fa">
<li><code>CustomerId</code> و <code>Status</code> برای فیلتر برابری استفاده می‌شوند.</li>
<li><code>CreatedAt</code> برای فیلتر بازه‌ای و مرتب سازی استفاده می‌شود.</li>
<li>ستون‌های <code>OrderId</code> و <code>TotalAmount</code> در بخش <code>INCLUDE</code> قرار گرفته‌اند تا SQL Server برای تکمیل نتیجه به جدول اصلی مراجعه نکند.</li>
</ul>
<p dir="rtl" lang="fa">ستون‌هایی که در <code>WHERE</code> و <code>JOIN</code> استفاده می‌شوند، معمولاً کاندیدهای اصلی بخش کلیدی ایندکس هستند. ستون‌هایی که فقط در <code>SELECT</code> قرار دارند، اغلب برای <code>INCLUDE</code> مناسب‌ترند.</p>
<h2 dir="rtl" lang="fa">۳. مشکل Key Lookup را برطرف کنید</h2>
<p dir="rtl" lang="fa">فرض کنید این ایندکس وجود دارد:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_CustomerId
<span class="hljs-keyword">ON</span> Sales.Orders(CustomerId);
</code></pre>
<p dir="rtl" lang="fa">و Query زیر اجرا می‌شود:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, CustomerId, TotalAmount, CreatedAt, Status
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;
</code></pre>
<p dir="rtl" lang="fa">SQL Server ابتدا رکوردها را از ایندکس پیدا می‌کند، اما برای خواندن سایر ستون‌ها به جدول اصلی برمی گردد. این عملیات در Execution Plan با عنوان <code>Key Lookup</code> نمایش داده می‌شود.</p>
<p dir="rtl" lang="fa">اگر Query پرتکرار است، ایندکس را پوششی کنید:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_CustomerId_Covering
<span class="hljs-keyword">ON</span> Sales.Orders(CustomerId)
INCLUDE
(
    OrderId,
    TotalAmount,
    CreatedAt,
    Status
);
</code></pre>
<h3 dir="rtl" lang="fa">چه زمانی Covering Index نسازیم؟</h3>
<p dir="rtl" lang="fa">Covering Index همیشه بهترین راه حل نیست. ایجاد ایندکس بزرگ باعث افزایش هزینه این عملیات می‌شود:</p>
<ul dir="rtl" lang="fa">
<li><code>INSERT</code></li>
<li><code>UPDATE</code></li>
<li><code>DELETE</code></li>
<li>عملیات نگهداری ایندکس</li>
<li>مصرف فضای دیسک</li>
<li>مصرف حافظه</li>
</ul>
<p dir="rtl" lang="fa">فقط برای Queryهای پرتکرار و حساس به زمان پاسخ، از این روش استفاده کنید.</p>
<h2 dir="rtl" lang="fa">۴. از SELECT * در مسیرهای پرترافیک استفاده نکنید</h2>
<h3 dir="rtl" lang="fa">Query نامناسب</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span>
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;
</code></pre>
<p dir="rtl" lang="fa">این Query به تمام ستون‌ها وابسته است. در نتیجه:</p>
<ul dir="rtl" lang="fa">
<li>حجم داده بیشتری از دیسک خوانده می‌شود.</li>
<li>امکان استفاده از Covering Index کاهش می‌یابد.</li>
<li>انتقال داده بین SQL Server و برنامه افزایش پیدا می‌کند.</li>
<li>تغییر ساختار جدول می‌تواند رفتار Query را تغییر دهد.</li>
</ul>
<h3 dir="rtl" lang="fa">Query بهتر</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OrderId,
    CustomerId,
    Status,
    TotalAmount,
    CreatedAt
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;
</code></pre>
<p dir="rtl" lang="fa">در APIها و صفحات لیست، فقط ستون‌هایی را دریافت کنید که واقعاً نمایش داده می‌شوند.</p>
<h2 dir="rtl" lang="fa">۵. Pagination را با Keyset انجام دهید</h2>
<p dir="rtl" lang="fa">در جدول‌های بزرگ، استفاده از <code>OFFSET</code> برای صفحات انتهایی بسیار پرهزینه است.</p>
<h3 dir="rtl" lang="fa">روش پرهزینه</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OrderId,
    CreatedAt,
    TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> CreatedAt <span class="hljs-keyword">DESC</span>, OrderId <span class="hljs-keyword">DESC</span>
<span class="hljs-keyword">OFFSET</span> <span class="hljs-number">500000</span> <span class="hljs-keyword">ROWS</span>
<span class="hljs-keyword">FETCH</span> NEXT <span class="hljs-number">50</span> <span class="hljs-keyword">ROWS</span> <span class="hljs-keyword">ONLY</span>;
</code></pre>
<p dir="rtl" lang="fa">SQL Server برای رسیدن به صفحه مورد نظر باید تعداد زیادی رکورد را مرتب و رد کند.</p>
<h3 dir="rtl" lang="fa">روش Keyset Pagination</h3>
<p dir="rtl" lang="fa">در این روش، آخرین رکورد صفحه قبلی را به Query بعدی ارسال می‌کنیم:</p>
<pre><code class="hljs"><span class="hljs-keyword">DECLARE</span> <span class="hljs-variable">@LastCreatedAt</span> datetime2 <span class="hljs-operator">=</span> <span class="hljs-string">'2026-08-15 12:30:00'</span>;
<span class="hljs-keyword">DECLARE</span> <span class="hljs-variable">@LastOrderId</span> <span class="hljs-type">bigint</span> <span class="hljs-operator">=</span> <span class="hljs-number">987654</span>;

<span class="hljs-keyword">SELECT</span> TOP (<span class="hljs-number">50</span>)
    OrderId,
    CreatedAt,
    TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span>
    CreatedAt <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@LastCreatedAt</span>
    <span class="hljs-keyword">OR</span>
    (
        CreatedAt <span class="hljs-operator">=</span> <span class="hljs-variable">@LastCreatedAt</span>
        <span class="hljs-keyword">AND</span> OrderId <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@LastOrderId</span>
    )
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> CreatedAt <span class="hljs-keyword">DESC</span>, OrderId <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">برای این Query ایندکس زیر مناسب است:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_CreatedAt_OrderId
<span class="hljs-keyword">ON</span> Sales.Orders(CreatedAt <span class="hljs-keyword">DESC</span>, OrderId <span class="hljs-keyword">DESC</span>)
INCLUDE
(
    TotalAmount
);
</code></pre>
<p dir="rtl" lang="fa">این روش برای جدول‌های سفارش، تراکنش مالی، گزارش رخدادها و لاگ‌های حجیم عملکرد پایدارتری دارد.</p>
<h2 dir="rtl" lang="fa">۶. مراقب Parameter Sniffing باشید</h2>
<p dir="rtl" lang="fa">SQL Server معمولاً هنگام اولین اجرای Stored Procedure، مقدار پارامترها را بررسی کرده و بر اساس آن Execution Plan می‌سازد.</p>
<p dir="rtl" lang="fa">اگر توزیع داده یکنواخت نباشد، یک Plan ممکن است برای یک مقدار مناسب و برای مقدار دیگر بسیار ضعیف باشد.</p>
<p dir="rtl" lang="fa">فرض کنید یک مشتری فقط ۱۰ سفارش دارد، اما مشتری دیگری چند میلیون سفارش ثبت کرده است:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">PROCEDURE</span> Sales.GetCustomerOrders
    <span class="hljs-variable">@CustomerId</span> <span class="hljs-type">bigint</span>
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">SET</span> NOCOUNT <span class="hljs-keyword">ON</span>;

    <span class="hljs-keyword">SELECT</span>
        OrderId,
        CreatedAt,
        TotalAmount,
        Status
    <span class="hljs-keyword">FROM</span> Sales.Orders
    <span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;
<span class="hljs-keyword">END</span>;
</code></pre>
<p dir="rtl" lang="fa">ممکن است Plan ساخته شده برای مشتری کم تراکنش، هنگام اجرای Query برای مشتری بزرگ مناسب نباشد.</p>
<h3 dir="rtl" lang="fa">راه حل اول: بازکامپایل در همان اجرا</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OrderId,
    CreatedAt,
    TotalAmount,
    Status
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>
OPTION (RECOMPILE);
</code></pre>
<p dir="rtl" lang="fa">این روش برای Queryهایی مناسب است که:</p>
<ul dir="rtl" lang="fa">
<li>تعداد اجرای آنها بسیار زیاد نیست.</li>
<li>اختلاف حجم داده بین پارامترها زیاد است.</li>
<li>زمان ساخت Plan نسبت به هزینه اجرای اشتباه کمتر است.</li>
</ul>
<h3 dir="rtl" lang="fa">راه حل دوم: استفاده از OPTIMIZE FOR UNKNOWN</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OrderId,
    CreatedAt,
    TotalAmount,
    Status
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>
OPTION (OPTIMIZE <span class="hljs-keyword">FOR</span> <span class="hljs-literal">UNKNOWN</span>);
</code></pre>
<p dir="rtl" lang="fa">در این روش SQL Server از مقدار واقعی پارامتر برای ساخت Plan استفاده نمی‌کند و برآورد عمومی‌تری انجام می‌دهد.</p>
<h3 dir="rtl" lang="fa">راه حل سوم: متغیر محلی</h3>
<pre><code class="hljs"><span class="hljs-keyword">DECLARE</span> <span class="hljs-variable">@LocalCustomerId</span> <span class="hljs-type">bigint</span> <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;

<span class="hljs-keyword">SELECT</span>
    OrderId,
    CreatedAt,
    TotalAmount,
    Status
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@LocalCustomerId</span>;
</code></pre>
<p dir="rtl" lang="fa">این روش گاهی مشکل را کاهش می‌دهد، اما ممکن است برآورد SQL Server را ضعیف کند. بنابراین باید با Execution Plan و آمار واقعی آزمایش شود.</p>
<h2 dir="rtl" lang="fa">۷. Implicit Conversion را حذف کنید</h2>
<p dir="rtl" lang="fa">تبدیل ضمنی نوع داده می‌تواند مانع استفاده صحیح از ایندکس شود.</p>
<p dir="rtl" lang="fa">فرض کنید نوع ستون <code>CustomerId</code> از نوع <code>bigint</code> است:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE TABLE</span> Sales.Orders
(
    OrderId <span class="hljs-type">bigint</span> <span class="hljs-keyword">NOT NULL</span>,
    CustomerId <span class="hljs-type">bigint</span> <span class="hljs-keyword">NOT NULL</span>
);
</code></pre>
<p dir="rtl" lang="fa">اما برنامه مقدار را به صورت <code>nvarchar</code> ارسال می‌کند:</p>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> N<span class="hljs-string">'1250'</span>
</code></pre>
<p dir="rtl" lang="fa">ممکن است SQL Server مجبور به تبدیل نوع داده شود. در Execution Plan، هشدار <code>CONVERT_IMPLICIT</code> را بررسی کنید.</p>
<h3 dir="rtl" lang="fa">روش صحیح</h3>
<p dir="rtl" lang="fa">نوع پارامتر در برنامه و Stored Procedure باید با نوع ستون یکسان باشد:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">PROCEDURE</span> Sales.GetOrders
    <span class="hljs-variable">@CustomerId</span> <span class="hljs-type">bigint</span>
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">SET</span> NOCOUNT <span class="hljs-keyword">ON</span>;

    <span class="hljs-keyword">SELECT</span> OrderId, CreatedAt, TotalAmount
    <span class="hljs-keyword">FROM</span> Sales.Orders
    <span class="hljs-keyword">WHERE</span> CustomerId <span class="hljs-operator">=</span> <span class="hljs-variable">@CustomerId</span>;
<span class="hljs-keyword">END</span>;
</code></pre>
<p dir="rtl" lang="fa">در پروژه‌های .NET نیز نوع پارامتر را صحیح مشخص کنید:</p>
<pre><code class="hljs">command.Parameters.Add(<span class="hljs-string">"@CustomerId"</span>, SqlDbType.BigInt).Value = customerId;
</code></pre>
<h2 dir="rtl" lang="fa">۸. JOIN را با ستون‌های درست و ایندکس مناسب اجرا کنید</h2>
<p dir="rtl" lang="fa">فرض کنید Query زیر برای نمایش سفارش‌های مشتری اجرا می‌شود:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    o.OrderId,
    o.CreatedAt,
    c.CustomerName,
    o.TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders <span class="hljs-keyword">AS</span> o
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> Sales.Customers <span class="hljs-keyword">AS</span> c
    <span class="hljs-keyword">ON</span> c.CustomerId <span class="hljs-operator">=</span> o.CustomerId
<span class="hljs-keyword">WHERE</span> o.Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
  <span class="hljs-keyword">AND</span> o.CreatedAt <span class="hljs-operator">&gt;=</span> <span class="hljs-variable">@FromDate</span>;
</code></pre>
<p dir="rtl" lang="fa">ایندکس‌های پیشنهادی:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_Status_CreatedAt_CustomerId
<span class="hljs-keyword">ON</span> Sales.Orders(Status, CreatedAt, CustomerId)
INCLUDE
(
    OrderId,
    TotalAmount
);
</code></pre>
<p dir="rtl" lang="fa">و در جدول مشتری:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">UNIQUE</span> INDEX UX_Customers_CustomerId
<span class="hljs-keyword">ON</span> Sales.Customers(CustomerId)
INCLUDE
(
    CustomerName
);
</code></pre>
<h3 dir="rtl" lang="fa">از JOIN غیرضروری جلوگیری کنید</h3>
<p dir="rtl" lang="fa">اگر هیچ ستونی از جدول مشتری استفاده نمی‌شود، این JOIN را حذف کنید:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> o.OrderId, o.TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders <span class="hljs-keyword">AS</span> o
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> Sales.Customers <span class="hljs-keyword">AS</span> c
    <span class="hljs-keyword">ON</span> c.CustomerId <span class="hljs-operator">=</span> o.CustomerId
<span class="hljs-keyword">WHERE</span> o.Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
</code></pre>
<p dir="rtl" lang="fa">نسخه ساده‌تر:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
</code></pre>
<p dir="rtl" lang="fa">هر JOIN اضافی می‌تواند هزینه CPU، حافظه و I/O را افزایش دهد.</p>
<h2 dir="rtl" lang="fa">۹. به جای IN بزرگ از روش مناسب استفاده کنید</h2>
<h3 dir="rtl" lang="fa">روش مشکل ساز</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> OrderId, TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-keyword">IN</span>
(
    <span class="hljs-number">100001</span>, <span class="hljs-number">100002</span>, <span class="hljs-number">100003</span>, <span class="hljs-number">100004</span>
);
</code></pre>
<p dir="rtl" lang="fa">برای تعداد کم شناسه مشکلی ایجاد نمی‌کند، اما در برنامه‌های پرترافیک و فهرست‌های بزرگ، بهتر است از Table-Valued Parameter استفاده شود.</p>
<h3 dir="rtl" lang="fa">تعریف نوع جدولی</h3>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> TYPE Sales.OrderIdList <span class="hljs-keyword">AS</span> <span class="hljs-keyword">TABLE</span>
(
    OrderId <span class="hljs-type">bigint</span> <span class="hljs-keyword">NOT NULL</span> <span class="hljs-keyword">PRIMARY KEY</span>
);
</code></pre>
<h3 lang="en">Stored Procedure</h3>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">PROCEDURE</span> Sales.GetOrdersByIds
    <span class="hljs-variable">@OrderIds</span> Sales.OrderIdList READONLY
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">SET</span> NOCOUNT <span class="hljs-keyword">ON</span>;

    <span class="hljs-keyword">SELECT</span>
        o.OrderId,
        o.CustomerId,
        o.TotalAmount,
        o.Status
    <span class="hljs-keyword">FROM</span> Sales.Orders <span class="hljs-keyword">AS</span> o
    <span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> <span class="hljs-variable">@OrderIds</span> <span class="hljs-keyword">AS</span> ids
        <span class="hljs-keyword">ON</span> ids.OrderId <span class="hljs-operator">=</span> o.OrderId;
<span class="hljs-keyword">END</span>;
</code></pre>
<p dir="rtl" lang="fa">این روش نسبت به ساختن رشته‌های طولانی و Dynamic SQL قابل کنترل‌تر و امن‌تر است.</p>
<h2 dir="rtl" lang="fa">۱۰. LIKE را برای جستجوی متنی درست انتخاب کنید</h2>
<h3 dir="rtl" lang="fa">Query قابل استفاده از ایندکس</h3>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> CustomerName <span class="hljs-keyword">LIKE</span> <span class="hljs-variable">@Search</span> <span class="hljs-operator">+</span> N<span class="hljs-string">'%'</span>
</code></pre>
<h3 dir="rtl" lang="fa">Query پرهزینه</h3>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> CustomerName <span class="hljs-keyword">LIKE</span> N<span class="hljs-string">'%'</span> <span class="hljs-operator">+</span> <span class="hljs-variable">@Search</span> <span class="hljs-operator">+</span> N<span class="hljs-string">'%'</span>
</code></pre>
<p dir="rtl" lang="fa">در حالت دوم، SQL Server معمولاً نمی‌تواند از ایندکس معمولی برای پیدا کردن ابتدای عبارت استفاده کند.</p>
<p dir="rtl" lang="fa">برای جستجوی واقعی در متن‌های بزرگ، Full-Text Search گزینه مناسب‌تری است:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> FULLTEXT CATALOG CustomerCatalog <span class="hljs-keyword">AS</span> <span class="hljs-keyword">DEFAULT</span>;
</code></pre>
<p dir="rtl" lang="fa">سپس روی ستون مورد نظر Full-Text Index ایجاد کنید و Query را با <code>CONTAINS</code> یا <code>FREETEXT</code> اجرا کنید:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> CustomerId, CustomerName
<span class="hljs-keyword">FROM</span> Sales.Customers
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">CONTAINS</span>(CustomerName, <span class="hljs-variable">@SearchTerm</span>);
</code></pre>
<h2 dir="rtl" lang="fa">۱۱. تراکنش‌ها را کوتاه نگه دارید</h2>
<p dir="rtl" lang="fa">در سیستم پرتراکنش، تراکنش طولانی فقط یک مشکل عملکردی نیست؛ بلکه باعث Blocking و افزایش زمان انتظار سایر درخواست‌ها می‌شود.</p>
<h3 dir="rtl" lang="fa">الگوی نامناسب</h3>
<pre><code class="hljs"><span class="hljs-keyword">BEGIN</span> TRANSACTION;

<span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span>
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderId</span>;

<span class="hljs-comment">-- پردازش طولانی در برنامه</span>
<span class="hljs-comment">-- ارسال درخواست به سرویس خارجی</span>
<span class="hljs-comment">-- محاسبه سنگین</span>

<span class="hljs-keyword">UPDATE</span> Sales.Orders
<span class="hljs-keyword">SET</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">2</span>
<span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderId</span>;

<span class="hljs-keyword">COMMIT</span> TRANSACTION;
</code></pre>
<p dir="rtl" lang="fa">در این فاصله، قفل‌ها ممکن است بیشتر از زمان لازم حفظ شوند.</p>
<h3 dir="rtl" lang="fa">الگوی بهتر</h3>
<p dir="rtl" lang="fa">ابتدا اطلاعات لازم را بخوانید، پردازش‌های خارج از دیتابیس را انجام دهید و فقط عملیات نهایی را در تراکنش قرار دهید:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OrderId,
    Status,
    TotalAmount
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderId</span>;
</code></pre>
<p dir="rtl" lang="fa">سپس:</p>
<pre><code class="hljs"><span class="hljs-keyword">SET</span> XACT_ABORT <span class="hljs-keyword">ON</span>;

<span class="hljs-keyword">BEGIN</span> TRY
    <span class="hljs-keyword">BEGIN</span> TRANSACTION;

    <span class="hljs-keyword">UPDATE</span> Sales.Orders
    <span class="hljs-keyword">SET</span>
        Status <span class="hljs-operator">=</span> <span class="hljs-number">2</span>,
        UpdatedAt <span class="hljs-operator">=</span> SYSUTCDATETIME()
    <span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderId</span>
      <span class="hljs-keyword">AND</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;

    IF @<span class="hljs-variable">@ROWCOUNT</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
        THROW <span class="hljs-number">50001</span>, <span class="hljs-string">'Order is not available for update.'</span>, <span class="hljs-number">1</span>;

    <span class="hljs-keyword">INSERT INTO</span> Sales.OrderStatusHistory
    (
        OrderId,
        OldStatus,
        NewStatus,
        CreatedAt
    )
    <span class="hljs-keyword">VALUES</span>
    (
        <span class="hljs-variable">@OrderId</span>,
        <span class="hljs-number">1</span>,
        <span class="hljs-number">2</span>,
        SYSUTCDATETIME()
    );

    <span class="hljs-keyword">COMMIT</span> TRANSACTION;
<span class="hljs-keyword">END</span> TRY
<span class="hljs-keyword">BEGIN</span> CATCH
    IF XACT_STATE() <span class="hljs-operator">&lt;&gt;</span> <span class="hljs-number">0</span>
        <span class="hljs-keyword">ROLLBACK</span> TRANSACTION;

    THROW;
<span class="hljs-keyword">END</span> CATCH;
</code></pre>
<h3 dir="rtl" lang="fa">نکته مهم</h3>
<p dir="rtl" lang="fa">شرط وضعیت در دستور <code>UPDATE</code> نقش کنترل همزمانی دارد:</p>
<pre><code class="hljs"><span class="hljs-keyword">WHERE</span> OrderId <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderId</span>
  <span class="hljs-keyword">AND</span> Status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
</code></pre>
<p dir="rtl" lang="fa">به این ترتیب، اگر درخواست دیگری قبلاً وضعیت سفارش را تغییر داده باشد، عملیات فعلی بدون بررسی اضافی شکست می‌خورد.</p>
<h2 dir="rtl" lang="fa">۱۲. Blocking و Deadlock را بررسی کنید</h2>
<p dir="rtl" lang="fa">برای مشاهده درخواست‌های در حال انتظار:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    r.session_id,
    r.status,
    r.command,
    r.wait_type,
    r.wait_time,
    r.blocking_session_id,
    r.cpu_time,
    r.logical_reads,
    t.text <span class="hljs-keyword">AS</span> SqlText
<span class="hljs-keyword">FROM</span> sys.dm_exec_requests <span class="hljs-keyword">AS</span> r
<span class="hljs-keyword">CROSS</span> APPLY sys.dm_exec_sql_text(r.sql_handle) <span class="hljs-keyword">AS</span> t
<span class="hljs-keyword">WHERE</span> r.session_id <span class="hljs-operator">&lt;&gt;</span> @<span class="hljs-variable">@SPID</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> r.wait_time <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">برای پیدا کردن Sessionهای مسدود شده:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    session_id,
    blocking_session_id,
    wait_type,
    wait_time,
    wait_resource
<span class="hljs-keyword">FROM</span> sys.dm_exec_requests
<span class="hljs-keyword">WHERE</span> blocking_session_id <span class="hljs-operator">&lt;&gt;</span> <span class="hljs-number">0</span>;
</code></pre>
<h3 dir="rtl" lang="fa">علت‌های رایج Blocking</h3>
<ul dir="rtl" lang="fa">
<li>تراکنش‌های طولانی</li>
<li>نبود ایندکس روی شرط <code>UPDATE</code> یا <code>DELETE</code></li>
<li>اسکن گسترده جدول</li>
<li>اجرای گزارش سنگین روی جداول عملیاتی</li>
<li>ترتیب متفاوت دسترسی به جداول در تراکنش‌های مختلف</li>
<li>به روز رسانی دسته‌ای در ساعات شلوغ</li>
</ul>
<h3 dir="rtl" lang="fa">الگوی کاهش Deadlock</h3>
<p dir="rtl" lang="fa">در تمام بخش‌های برنامه، ترتیب دسترسی به جداول را ثابت کنید. برای مثال، اگر یک تراکنش ابتدا <code>Customers</code> و سپس <code>Orders</code> را تغییر می‌دهد، تراکنش دیگر نباید ابتدا <code>Orders</code> و بعد <code>Customers</code> را تغییر دهد.</p>
<p dir="rtl" lang="fa">همچنین عملیات بزرگ را به بخش‌های کوچک‌تر تقسیم کنید:</p>
<pre><code class="hljs">WHILE <span class="hljs-number">1</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">DELETE</span> TOP (<span class="hljs-number">1000</span>)
    <span class="hljs-keyword">FROM</span> Audit.Logs
    <span class="hljs-keyword">WHERE</span> CreatedAt <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@BeforeDate</span>;

    IF @<span class="hljs-variable">@ROWCOUNT</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
        BREAK;
<span class="hljs-keyword">END</span>;
</code></pre>
<h2 dir="rtl" lang="fa">۱۳. سطح Isolation را آگاهانه انتخاب کنید</h2>
<p dir="rtl" lang="fa">در بسیاری از سیستم‌های OLTP، خواندن‌های طولانی با نوشتن‌ها تداخل ایجاد می‌کنند. یکی از راهکارهای مهم، فعال کردن <code>READ_COMMITTED_SNAPSHOT</code> است:</p>
<pre><code class="hljs"><span class="hljs-keyword">ALTER</span> DATABASE [SalesDb]
<span class="hljs-keyword">SET</span> READ_COMMITTED_SNAPSHOT <span class="hljs-keyword">ON</span>
<span class="hljs-keyword">WITH</span> <span class="hljs-keyword">ROLLBACK</span> IMMEDIATE;
</code></pre>
<p dir="rtl" lang="fa">در این حالت، خواندن‌های معمولی از Version Store در <code>tempdb</code> استفاده می‌کنند و معمولاً کمتر منتظر قفل‌های نوشتاری می‌مانند.</p>
<p dir="rtl" lang="fa">اما پیش از فعال سازی باید موارد زیر بررسی شود:</p>
<ul dir="rtl" lang="fa">
<li>فضای کافی برای <code>tempdb</code></li>
<li>حجم Update و Delete</li>
<li>مدت اجرای تراکنش‌ها</li>
<li>مصرف Version Store</li>
<li>سازگاری برنامه با رفتار جدید خواندن</li>
</ul>
<p dir="rtl" lang="fa">برای تراکنش‌هایی که نیاز به ثبات کامل داده دارند، سطح Isolation را بدون بررسی تغییر ندهید.</p>
<h2 dir="rtl" lang="fa">۱۴. Statistics را به روز نگه دارید</h2>
<p dir="rtl" lang="fa">اگر Statistics قدیمی باشد، SQL Server ممکن است تعداد رکوردهای خروجی را اشتباه تخمین بزند و Join یا ایندکس نامناسب انتخاب کند.</p>
<p dir="rtl" lang="fa">به روز رسانی دستی:</p>
<pre><code class="hljs"><span class="hljs-keyword">UPDATE</span> STATISTICS Sales.Orders
<span class="hljs-keyword">WITH</span> FULLSCAN;
</code></pre>
<p dir="rtl" lang="fa">برای جدول‌های بزرگ، <code>FULLSCAN</code> ممکن است پرهزینه باشد. بنابراین آن را در ساعات مناسب و با بررسی زمان اجرا انجام دهید.</p>
<p dir="rtl" lang="fa">مشاهده زمان آخرین به روز رسانی Statistics:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span>
    OBJECT_SCHEMA_NAME(sp.object_id) <span class="hljs-keyword">AS</span> SchemaName,
    OBJECT_NAME(sp.object_id) <span class="hljs-keyword">AS</span> TableName,
    sp.stats_id,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter
<span class="hljs-keyword">FROM</span> sys.dm_db_stats_properties
(
    OBJECT_ID(N<span class="hljs-string">'Sales.Orders'</span>),
    <span class="hljs-number">1</span>
) <span class="hljs-keyword">AS</span> sp;
</code></pre>
<p dir="rtl" lang="fa">در پروژه‌هایی که داده به سرعت تغییر می‌کند، فقط فعال بودن Auto Update Statistics کافی نیست. آمار Query Store، زمان اجرای Query و تغییرات داده را نیز بررسی کنید.</p>
<h2 dir="rtl" lang="fa">۱۵. Query Store را فعال و بررسی کنید</h2>
<p dir="rtl" lang="fa">Query Store برای پیدا کردن Queryهایی که در طول زمان کند شده‌اند بسیار کاربردی است.</p>
<p dir="rtl" lang="fa">فعال سازی:</p>
<pre><code class="hljs"><span class="hljs-keyword">ALTER</span> DATABASE [SalesDb]
<span class="hljs-keyword">SET</span> QUERY_STORE <span class="hljs-operator">=</span> <span class="hljs-keyword">ON</span>;
</code></pre>
<p dir="rtl" lang="fa">برای پیدا کردن Queryهای پرهزینه:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> TOP (<span class="hljs-number">20</span>)
    qt.query_sql_text,
    rs.execution_count,
    rs.avg_duration,
    rs.avg_cpu_time,
    rs.avg_logical_io_reads,
    rs.last_execution_time
<span class="hljs-keyword">FROM</span> sys.query_store_query_text <span class="hljs-keyword">AS</span> qt
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_query <span class="hljs-keyword">AS</span> q
    <span class="hljs-keyword">ON</span> q.query_text_id <span class="hljs-operator">=</span> qt.query_text_id
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_plan <span class="hljs-keyword">AS</span> p
    <span class="hljs-keyword">ON</span> p.query_id <span class="hljs-operator">=</span> q.query_id
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_runtime_stats <span class="hljs-keyword">AS</span> rs
    <span class="hljs-keyword">ON</span> rs.plan_id <span class="hljs-operator">=</span> p.plan_id
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> rs.avg_duration <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">برای پیدا کردن Queryهایی که بیشترین مجموع زمان را مصرف کرده‌اند:</p>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> TOP (<span class="hljs-number">20</span>)
    qt.query_sql_text,
    <span class="hljs-built_in">SUM</span>(rs.count_executions) <span class="hljs-keyword">AS</span> TotalExecutions,
    <span class="hljs-built_in">SUM</span>(rs.avg_duration <span class="hljs-operator">*</span> rs.count_executions) <span class="hljs-keyword">AS</span> TotalDuration
<span class="hljs-keyword">FROM</span> sys.query_store_query_text <span class="hljs-keyword">AS</span> qt
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_query <span class="hljs-keyword">AS</span> q
    <span class="hljs-keyword">ON</span> q.query_text_id <span class="hljs-operator">=</span> qt.query_text_id
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_plan <span class="hljs-keyword">AS</span> p
    <span class="hljs-keyword">ON</span> p.query_id <span class="hljs-operator">=</span> q.query_id
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> sys.query_store_runtime_stats <span class="hljs-keyword">AS</span> rs
    <span class="hljs-keyword">ON</span> rs.plan_id <span class="hljs-operator">=</span> p.plan_id
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> qt.query_sql_text
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> TotalDuration <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">گاهی Queryای که میانگین زمان کمی دارد، به دلیل تعداد اجرای بسیار زیاد، بیشترین فشار را به SQL Server وارد می‌کند.</p>
<h2 dir="rtl" lang="fa">۱۶. برای اصلاح Query از این ترتیب استفاده کنید</h2>
<p dir="rtl" lang="fa">برای هر Query پیچیده، این مراحل را به صورت مشخص انجام دهید:</p>
<h3 dir="rtl" lang="fa">مرحله اول: اجرای فعلی را ثبت کنید</h3>
<ul dir="rtl" lang="fa">
<li>زمان متوسط</li>
<li>زمان صدک ۹۵ و ۹۹</li>
<li>CPU</li>
<li>Logical Read</li>
<li>تعداد اجرا</li>
<li>تعداد Timeout</li>
<li>Wait Type</li>
</ul>
<h3 dir="rtl" lang="fa">مرحله دوم: Execution Plan را بررسی کنید</h3>
<p dir="rtl" lang="fa">به دنبال این موارد باشید:</p>
<ul dir="rtl" lang="fa">
<li>Scan روی جدول بزرگ</li>
<li>اختلاف Estimated Rows و Actual Rows</li>
<li>Key Lookup تکرارشونده</li>
<li>Sort بدون ایندکس مناسب</li>
<li>Hash Join ناشی از کمبود ایندکس یا برآورد اشتباه</li>
<li>Implicit Conversion</li>
<li>Spill به <code>tempdb</code></li>
</ul>
<h3 dir="rtl" lang="fa">مرحله سوم: Query را اصلاح کنید</h3>
<ul dir="rtl" lang="fa">
<li>حذف <code>SELECT *</code></li>
<li>حذف تابع از روی ستون فیلتر</li>
<li>اصلاح Pagination</li>
<li>حذف JOIN غیرضروری</li>
<li>اصلاح نوع پارامترها</li>
<li>کاهش تعداد ستون‌های خروجی</li>
</ul>
<h3 dir="rtl" lang="fa">مرحله چهارم: ایندکس را بررسی کنید</h3>
<ul dir="rtl" lang="fa">
<li>آیا ایندکس با الگوی واقعی Query هماهنگ است؟</li>
<li>آیا ترتیب ستون‌ها صحیح است؟</li>
<li>آیا Query نیاز به <code>INCLUDE</code> دارد؟</li>
<li>آیا ایندکس مشابه از قبل وجود دارد؟</li>
<li>هزینه این ایندکس برای عملیات نوشتن چقدر است؟</li>
</ul>
<h3 dir="rtl" lang="fa">مرحله پنجم: زیر بار واقعی آزمایش کنید</h3>
<p dir="rtl" lang="fa">Query فقط در محیط توسعه آزمایش نشود. حجم داده، همزمانی کاربران، پارامترهای مختلف و زمان‌های پرترافیک را شبیه سازی کنید.</p>
<h2 dir="rtl" lang="fa">نمونه کامل بررسی یک Query</h2>
<h3 dir="rtl" lang="fa">Query اولیه</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span>
<span class="hljs-keyword">FROM</span> Sales.Orders
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">CONVERT</span>(<span class="hljs-type">date</span>, CreatedAt) <span class="hljs-operator">=</span> <span class="hljs-variable">@OrderDate</span>
  <span class="hljs-keyword">AND</span> CustomerId <span class="hljs-keyword">IN</span>
  (
      <span class="hljs-keyword">SELECT</span> CustomerId
      <span class="hljs-keyword">FROM</span> Sales.Customers
      <span class="hljs-keyword">WHERE</span> IsActive <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
  )
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> CreatedAt <span class="hljs-keyword">DESC</span>
<span class="hljs-keyword">OFFSET</span> <span class="hljs-variable">@Offset</span> <span class="hljs-keyword">ROWS</span>
<span class="hljs-keyword">FETCH</span> NEXT <span class="hljs-variable">@PageSize</span> <span class="hljs-keyword">ROWS</span> <span class="hljs-keyword">ONLY</span>;
</code></pre>
<p dir="rtl" lang="fa">مشکل‌های این Query:</p>
<ul dir="rtl" lang="fa">
<li>استفاده از تابع روی <code>CreatedAt</code></li>
<li>استفاده از <code>SELECT *</code></li>
<li>Pagination با <code>OFFSET</code> در صفحات بزرگ</li>
<li>احتمال اسکن جدول Customers</li>
<li>نامشخص بودن ایندکس‌های مورد نیاز</li>
</ul>
<h3 dir="rtl" lang="fa">نسخه اصلاح شده</h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> TOP (<span class="hljs-variable">@PageSize</span>)
    o.OrderId,
    o.CustomerId,
    o.CreatedAt,
    o.TotalAmount,
    o.Status
<span class="hljs-keyword">FROM</span> Sales.Orders <span class="hljs-keyword">AS</span> o
<span class="hljs-keyword">INNER</span> <span class="hljs-keyword">JOIN</span> Sales.Customers <span class="hljs-keyword">AS</span> c
    <span class="hljs-keyword">ON</span> c.CustomerId <span class="hljs-operator">=</span> o.CustomerId
<span class="hljs-keyword">WHERE</span> c.IsActive <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
  <span class="hljs-keyword">AND</span>
  (
      o.CreatedAt <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@LastCreatedAt</span>
      <span class="hljs-keyword">OR</span>
      (
          o.CreatedAt <span class="hljs-operator">=</span> <span class="hljs-variable">@LastCreatedAt</span>
          <span class="hljs-keyword">AND</span> o.OrderId <span class="hljs-operator">&lt;</span> <span class="hljs-variable">@LastOrderId</span>
      )
  )
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> o.CreatedAt <span class="hljs-keyword">DESC</span>, o.OrderId <span class="hljs-keyword">DESC</span>;
</code></pre>
<p dir="rtl" lang="fa">ایندکس‌های پیشنهادی:</p>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Customers_IsActive_CustomerId
<span class="hljs-keyword">ON</span> Sales.Customers(IsActive, CustomerId);
</code></pre>
<pre><code class="hljs"><span class="hljs-keyword">CREATE</span> INDEX IX_Orders_CreatedAt_OrderId_CustomerId
<span class="hljs-keyword">ON</span> Sales.Orders(CreatedAt <span class="hljs-keyword">DESC</span>, OrderId <span class="hljs-keyword">DESC</span>, CustomerId)
INCLUDE
(
    TotalAmount,
    Status
);
</code></pre>
<p dir="rtl" lang="fa">این نسخه برای صفحه‌های انتهایی، خواندن ستون‌های محدود و اجرای پرتکرار، قابلیت مقیاس پذیری بیشتری دارد.</p>
<h2 dir="rtl" lang="fa">چه کارهایی معمولاً نتیجه معکوس دارند؟</h2>
<h3 dir="rtl" lang="fa">ایجاد ایندکس برای هر ستون</h3>
<p dir="rtl" lang="fa">تعداد زیاد ایندکس، عملیات نوشتن را کند می‌کند و فضای زیادی مصرف می‌کند.</p>
<h3 dir="rtl" lang="fa">استفاده دائمی از <code>NOLOCK</code></h3>
<pre><code class="hljs"><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span>
<span class="hljs-keyword">FROM</span> Sales.Orders <span class="hljs-keyword">WITH</span> (NOLOCK);
</code></pre>
<p dir="rtl" lang="fa"><code>NOLOCK</code> ممکن است Dirty Read، رکوردهای تکراری یا داده‌های ناپایدار برگرداند. این دستور راه حل عمومی برای مشکل Blocking نیست.</p>
<h3 dir="rtl" lang="fa">استفاده بی دلیل از <code>OPTION (RECOMPILE)</code></h3>
<p dir="rtl" lang="fa">برای Queryهای پرتکرار، بازسازی Plan در هر اجرا می‌تواند CPU را افزایش دهد.</p>
<h3 dir="rtl" lang="fa">انتقال همه Queryها به Stored Procedure</h3>
<p dir="rtl" lang="fa">Stored Procedure مفید است، اما اگر منطق Query، ایندکس یا مدل داده نادرست باشد، صرفاً قرار دادن کد در Stored Procedure مشکل را حل نمی‌کند.</p>
<h3 dir="rtl" lang="fa">افزایش سخت افزار بدون اصلاح Query</h3>
<p dir="rtl" lang="fa">افزایش RAM یا CPU گاهی کمک می‌کند، اما Query دارای اسکن، Sort یا Blocking همچنان در حجم بالاتر مشکل ایجاد خواهد کرد.</p>
<h2 dir="rtl" lang="fa">چک لیست نهایی بهینه سازی Query در SQL Server</h2>
<p dir="rtl" lang="fa">پیش از انتشار تغییرات، این موارد را بررسی کنید:</p>
<ul dir="rtl" lang="fa">
<li>Execution Plan واقعی مشاهده شده است.</li>
<li>Logical Read قبل و بعد مقایسه شده است.</li>
<li>زمان CPU و زمان سپری شده ثبت شده است.</li>
<li>Query از <code>SELECT *</code> استفاده نمی‌کند.</li>
<li>روی ستون‌های فیلتر تابع اجرا نمی‌شود.</li>
<li>نوع پارامتر با نوع ستون یکسان است.</li>
<li>ایندکس ترکیبی بر اساس الگوی واقعی Query طراحی شده است.</li>
<li>Key Lookup غیرضروری حذف شده است.</li>
<li>Pagination برای صفحات بزرگ با Keyset انجام می‌شود.</li>
<li>تراکنش‌ها کوتاه هستند.</li>
<li>Blocking و Deadlock بررسی شده‌اند.</li>
<li>Statistics و Query Store در نظر گرفته شده‌اند.</li>
<li>Query با پارامترهای کم حجم و پرحجم آزمایش شده است.</li>
<li>تأثیر ایندکس جدید بر INSERT و UPDATE بررسی شده است.</li>
</ul>
<h2 dir="rtl" lang="fa">جمع بندی</h2>
<p dir="rtl" lang="fa">بهینه سازی Queryهای پیچیده SQL Server در پروژه‌های پرتراکنش، با یک تغییر ساده یا افزودن ایندکس تصادفی انجام نمی‌شود. باید ابتدا هزینه واقعی Query را اندازه گیری کرد، سپس Execution Plan، ایندکس، نوع پارامتر، مدت تراکنش و رفتار همزمانی را بررسی کرد.</p>
<p dir="rtl" lang="fa">در عمل، بیشترین نتیجه معمولاً از این اقدامات به دست می‌آید:</p>
<ol dir="rtl" lang="fa">
<li>حذف Scanهای غیرضروری</li>
<li>ایجاد ایندکس ترکیبی متناسب با Query</li>
<li>حذف Key Lookupهای پرتکرار</li>
<li>استفاده از Keyset Pagination</li>
<li>جلوگیری از Parameter Sniffing در Queryهای حساس</li>
<li>کوتاه کردن تراکنش‌ها</li>
<li>بررسی Blocking و Deadlock</li>
<li>استفاده مستمر از Query Store و آمار واقعی اجرا</li>
</ol>
<p dir="rtl" lang="fa">برای مطالعه نکات تکمیلی درباره طراحی ایندکس، مدیریت تراکنش‌ها، <code>tempdb</code> و بهینه سازی دیتابیس، مقاله <a href="https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-%d8%af%db%8c%d8%aa%d8%a7%d8%a8%db%8c%d8%b3%d8%9b-%d8%ac%d8%a7%db%8c%db%8c-%da%a9%d9%87-%db%b8%db%b0%d9%aa-%d8%b3%d8%b1%d8%b9%d8%aa-%d8%b3%d8%a7/" target="_blank" rel="nofollow noopener noreferrer">بهینه سازی دیتابیس؛ جایی که ۸۰٪ سرعت سایت شما در آن نهفته است</a> را نیز بخوانید.</p>
<p dir="rtl" lang="fa">اگر در پروژه خود با Queryهای کند، قفل شدن جداول یا افت عملکرد SQL Server روبه رو هستید، برای <a href="https://tarahanenovin.ir/site/contact" target="_blank" rel="nofollow noopener noreferrer">مشاوره و بررسی فنی پروژه با طراحان نوین تماس بگیرید</a>.</p>
<p>نوشته <a href="https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-query-%d9%87%d8%a7%db%8c-%d9%be%db%8c%da%86%db%8c%d8%af%d9%87-sql-server-%d8%af%d8%b1-%d9%be%d8%b1%d9%88%da%98%d9%87%d9%87%d8%a7%db%8c/">بهینه سازی Query های پیچیده SQL Server در پروژه‌های با حجم تراکنش بالا</a> اولین بار در <a href="https://tarahanenovin.ir/blog">نوین هاب</a>. پدیدار شد.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tarahanenovin.ir/blog/%d8%a8%d9%87%db%8c%d9%86%d9%87-%d8%b3%d8%a7%d8%b2%db%8c-query-%d9%87%d8%a7%db%8c-%d9%be%db%8c%da%86%db%8c%d8%af%d9%87-sql-server-%d8%af%d8%b1-%d9%be%d8%b1%d9%88%da%98%d9%87%d9%87%d8%a7%db%8c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
