آموزش پرفورمنس در EF Core

آموزش پرفورمنس در EF Core

توسط admin | گروه برنامه نویسی | 1405/04/29

نظرات 0

آموزش EF Core 10 & C# Performance Tuning — نسخه فارسی
۵۰۰ نمونه کد۱۵ SVG درون‌خطیBefore / AfterRTL + Responsive

راهنمای جامع کارایی و بهینه‌سازی#

EF Core 10 & C# Performance Tuning

مرجع فارسی مبتنی بر اندازه‌گیری برای دات‌نت ۱۰ و SQL Server — شامل دقیقاً ۵۰۰ نمونه کد

ویرایش: ۱٫۰ | تاریخ تولید: 2026-07-19 | سطح: متوسط تا پیشرفته | حجم هدف: حدود ۵۶٬۰۰۰ واژه فارسی

این متن یک راهنمای فنی است. اعداد، Pool size، Batch size، Timeout و Indexهای نمونه نسخه عمومی ندارند و باید با Benchmark و داده واقعی پروژه تنظیم شوند.

چکیده و شیوه استفاده#

کارایی در EF Core تنها به سریع نوشتن یک عبارت LINQ محدود نیست. زمان پاسخ از لایه‌های متعددی ساخته می‌شود: صف Thread، ساخت DbContext، گرفتن Connection، رفت‌وبرگشت شبکه، انتخاب Plan، خواندن صفحه‌های داده، Materialization، Tracking، تبدیل به DTO و Serialization. اگر این اجزا جدا نشوند، تیم ممکن است بخشی را بهینه کند که سهم ناچیزی از زمان کل دارد. این کتاب راهنما هر تکنیک را به یک نشانه، روش اندازه‌گیری، نمونه کد و هشدار عملی متصل می‌کند تا تصمیم‌ها بر پایه داده باشند.

هدف، حذف EF Core یا تبدیل همه Queryها به SQL خام نیست. EF Core در بسیاری از سامانه‌ها هزینه‌ای بسیار کوچک‌تر از شبکه و IO دیتابیس دارد و مزیت نگهداری، ترجمه LINQ، مهاجرت و Unit of Work را فراهم می‌کند. رویکرد حرفه‌ای این است که ابتدا Query و مدل را درست طراحی کنیم، سپس نقاط داغ اثبات‌شده را با Projection، Index، Batch، Query Compileشده یا SQL اختصاصی بهبود دهیم. هر بهینه‌سازی باید تست صحت، Benchmark و مسیر بازگشت داشته باشد.

نمونه‌ها برای EF Core 10، .NET 10 و C# 14 نوشته شده‌اند و Provider اصلی SQL Server است. بخش بزرگی از اصول برای EF Core 8 و 9 و Providerهایی مانند PostgreSQL نیز صدق می‌کند، اما ترجمه تابع‌ها، Batch، نوع JSON، Vector، Collation و جزئیات Execution Plan وابسته به Provider و نسخه دیتابیس هستند. بنابراین SQL تولیدشده و مستندات Provider باید در محیط واقعی بررسی شوند. EF Core 10 یک نسخه LTS است و استفاده از آن نیازمند Runtime و SDK دات‌نت 10 است.

پنج قاعده در سراسر متن تکرار می‌شوند: پیش از تغییر اندازه بگیرید؛ تعداد Roundtrip و حجم داده را کم کنید؛ Query را برای Index قابل استفاده نگه دارید؛ طول عمر Tracking و Transaction را کوتاه کنید؛ و پس از هر تغییر هم کارایی و هم صحت را دوباره آزمایش کنید. عدد خوب بدون زمینه معنا ندارد. کاهش میانگین زمان در کنار بدتر شدن صدک ۹۹، افزایش خطا، اشباع Pool اتصال یا مصرف حافظه بالا یک موفقیت واقعی نیست.

ساختار هر فصل شامل توضیح معماری، نشانه‌های خطا، روش اندازه‌گیری و بیست نمونه شماره‌گذاری‌شده است. شماره نمونه‌ها در کل سند پیوسته‌اند تا بتوان در Code Review، Issue یا جلسه Performance به آن‌ها ارجاع داد. کدها قطعه‌های آموزشی‌اند و نام Entity، Property و سرویس باید با مدل پروژه جایگزین شود. برای هر قطعه ابتدا SQL را مشاهده کنید، سپس تست صحت بنویسید و در نهایت Benchmark قبل و بعد را اجرا کنید.

فهرست فصل‌ها#

بخش اول: آزمایشگاه فنی، مدل هزینه و ابزارهای Profiling

بخش دوم: ده پرونده Before/After با نمودار و روش بازتولید

بخش سوم: اعتبارسنجی Production و Performance Regression

فصل 1: روش علمی اندازه‌گیری و یافتن گلوگاه

فصل 2: مشاهده SQL، Logging و برچسب‌گذاری Query

فصل 3: Projection و دریافت فقط ستون‌های لازم

فصل 4: Tracking، NoTracking و Identity Resolution

فصل 5: بارگذاری داده‌های مرتبط و Include هدفمند

فصل 6: Split Query و جلوگیری از انفجار ضرب دکارتی

فصل 7: حذف الگوی N+1 و کنترل Lazy Loading

فصل 8: صفحه‌بندی Offset، Keyset و Cursor

فصل 9: طراحی Index بر اساس Query واقعی

فصل 10: Query قابل Seek، ترجمه LINQ و SARGability

فصل 11: Query Cache، پارامتردهی و LINQ پویا

فصل 12: Compiled Query برای مسیرهای بسیار داغ

فصل 13: DbContext Pooling و PooledDbContextFactory

فصل 14: طول عمر DbContext، Async و هم‌زمانی

فصل 15: Streaming، Buffering و Cancellation

فصل 16: SaveChanges، Batching و ChangeTracker

فصل 17: ExecuteUpdate و ExecuteDelete مجموعه‌ای

فصل 18: Transaction، Retry و کنترل هم‌زمانی

فصل 19: SQL خام، Stored Procedure و Function Mapping

فصل 20: مدل‌سازی برای Performance و Compiled Model

فصل 21: Caching در لایه برنامه و معماری خواندن

فصل 22: ASP.NET Core، Async و محدودکردن فشار

فصل 23: بهینه‌سازی C#، Allocation و GC در مسیر داده

فصل 24: Interceptor، Metrics، Benchmark و Load Test

فصل 25: قابلیت‌های EF Core 10 با اثر عملکردی

بخش اول: آزمایشگاه فنی و مدل سیستمی Performance#

این بخش هسته مهندسی سند است. نمودارهای قبل و بعد بر پایه یک سناریوی آزمایشگاهی کنترل‌شده ساخته شده‌اند، نه وعده عمومی برای همه پروژه‌ها. هدف نشان‌دادن روش اندازه‌گیری، رابطه علت و معلول و نوع اثری است که باید در نمودار واقعی پروژه جست‌وجو شود. هر نمودار باید با همان داده، همان بار، همان محدودیت اتصال و همان نسخه برنامه بازتولید شود. اگر فایل BenchmarkDotNet، Query Store یا NBomber پروژه در دسترس باشد، اعداد آزمایشگاهی باید با داده واقعی جایگزین شوند.

۱. مدل هزینه End-to-End و محل واقعی گلوگاه#

زمان درخواست را می‌توان به‌صورت بودجه اجزا نوشت: Trequest برابر است با زمان صف، middleware و کد برنامه، ساخت یا دریافت Context، انتظار Connection Pool، مجموع رفت‌وبرگشت‌های شبکه، اجرای SQL، خواندن ردیف‌ها، Materialization، Tracking و Serialization. این جمع همیشه ساده نیست؛ بعضی بخش‌ها هم‌پوشانی دارند و زیر concurrency صف می‌سازند. بااین‌حال مدل مانع یک خطای رایج می‌شود: کندی Endpoint به‌تنهایی ثابت نمی‌کند SQL کند است و پایین بودن CPU نیز ثابت نمی‌کند سیستم ظرفیت خالی دارد.

2026-07-20T16:07:42.454829 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ HTTP request queue + middleware DbContext scope + model LINQ expression tree shape Query compiler cache + translation ADO.NET provider command + parameters Connection pool wait + checkout SQL Server plan + locks + I/O Data reader network + rows Materializer objects + GC Tracking / DTO fix-up + serialization Measure each boundary separately; a slow request does not prove a slow SQL statement. EF Core query pipeline and measurable cost centers

اگر CPU برنامه پایین ولی P95 بالا باشد، احتمال انتظار شبکه، Connection Pool، lock، thread pool starvation یا downstream وجود دارد. اگر SQL در SSMS سریع ولی Endpoint کند باشد، اختلاف ممکن است از payload، materialization، GC یا serialization باشد. اجرای Query در SSMS با پارامتر، isolation، SET options و Cache متفاوت نیز ممکن است Plan دیگری بسازد. بنابراین شواهد باید از همان Request واقعی با TraceId و TagWith جمع شوند.

برای تفکیک CPU time و wall-clock time، CPU profiler را کنار trace زمان‌محور استفاده کنید. متدی که Total CPU کمی دارد اما wall time زیادی صرف می‌کند ممکن است منتظر I/O یا lock باشد. Thread count رو به رشد همراه CPU پایین می‌تواند starvation را نشان دهد. Allocation rate بالا همراه time-in-GC و افت throughput نشان می‌دهد CPU در GC مصرف می‌شود. هیچ Counter منفردی بدون هم‌بستگی زمانی تفسیر قطعی ندارد.

۲. قرارداد آزمایش قبل و بعد#

در همه نمودارهای آزمایشگاهی این سند فرض شده است: build در حالت Release، دات‌نت ۱۰ و EF Core 10، SQL Server با آمار به‌روز، داده ثابت با حداقل دو میلیون ردیف در جدول اصلی، latency شبکه نزدیک یک میلی‌ثانیه، warm-up پنج دقیقه و سپس ده دقیقه اندازه‌گیری. اندازه Connection Pool و تعداد core ثابت می‌ماند. اعداد برای آموزش انتخاب شده‌اند و نتیجه واقعی سخت‌افزار شما نیستند.

# Build and run the exact release artifact dotnet publish -c Release -o ./publish DOTNET_ENVIRONMENT=Performance ./publish/MyApi  # Capture runtime and EF counters dotnet-counters monitor --process-id <PID> \   --counters System.Runtime,Microsoft.EntityFrameworkCore  # Capture a CPU/EventPipe trace for the same time window dotnet-trace collect --process-id <PID> \   --profile cpu-sampling --output before.nettrace

قبل و بعد باید با snapshot یکسان دیتابیس، پارامترهای یکسان، ترتیب تصادفی یا کنترل‌شده درخواست‌ها و نرخ ورود ثابت اجرا شوند. اجرای قبل در ساعت شلوغ و بعد در ساعت خلوت مقایسه نیست. Cache سرد و گرم را جدا گزارش کنید. حداقل میانه، P95، P99، throughput، error rate، allocation/request، query/request، logical reads و pool wait را ثبت کنید. فاصله اطمینان یا پراکندگی نیز لازم است؛ یک عدد منفرد ممکن است outlier یا نویز باشد.

2026-07-20T16:07:44.926149 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 1. Baseline P50/P95/P99 + RPS 2. Correlate trace → SQL tag 3. Explain plan + reads + waits 4. Change one thing query / index / model 5. Re-test same data + load 6. Verify production regression guard Stop when the evidence no longer supports the hypothesis — not when the chart looks attractive. Performance investigation loop

۳. جعبه‌ابزار تشخیص؛ هر ابزار برای کدام لایه است؟#

2026-07-20T16:07:42.516809 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ User / API P50, P95, P99, RPS, errors NBomber / k6 / OpenTelemetry ASP.NET Core queue, threads, activities VS Profiler / dotnet-counters EF Core queries, cache hit, SaveChanges EF metrics / logs / interceptors ADO.NET pool wait, roundtrips, payload MiniProfiler / traces SQL Server CPU, reads, locks, plans Query Store / XE / actual plan OS / Runtime GC, allocation, CPU, I/O dotnet-trace / gcdump / PerfMon What changed? Which tool proves it? Observability stack: symptom → evidence → root cause

لایه | ابزار | شاخص اصلی | کاربرد |

— | — | — | — |

درخواست و API | NBomber یا k6 | P50/P95/P99، RPS، نرخ خطا | تست نرخ ورود ثابت و Spike |

Trace توزیع‌شده | OpenTelemetry | Span، وابستگی SQL، صف و خطا | ارتباط Endpoint با Command |

محیط توسعه | Visual Studio Performance Profiler | CPU، Allocation، Memory، Async، Database | Alt+F2 و اجرای Release |

Runtime دات‌نت | dotnet-counters | CPU، GC heap، allocation rate، thread pool | پایش سریع فرایند زنده |

نمونه‌برداری CPU | dotnet-trace / PerfView | Hot path، EventPipe، contention | Trace کوتاه در بازه مسئله |

Heap مدیریت‌شده | dotnet-gcdump / dotnet-dump | نوع‌های زنده، root، LOH، dump | نشت حافظه و رشد پایدار |

خود EF Core | Metrics، Logging، Interceptor | Query/sec، cache hit، SaveChanges | برچسب‌گذاری و شمارش Command |

درخواست توسعه | MiniProfiler | زمان SQL و Stepهای برنامه | یافتن N+1 در محیط امن |

SQL Server | Query Store | تاریخچه Plan، CPU، Duration، Reads | تشخیص regression و تغییر Plan |

SQL Server | Actual Plan و Live Query Stats | Actual rows، warnings، operators | Scan، Sort، Spill و mismatch |

آزمایش خرد | BenchmarkDotNet | Mean، error، allocation، GC | مقایسه Query compile و materialization |

Visual Studio Performance Profiler برای CPU Usage، .NET Object Allocation، Memory Usage، .NET Async، .NET Counters و Database دید یکپارچه می‌دهد. تست را با Release و بدون debugger اجرا کنید؛ debugger، tiered compilation و instrumentation می‌توانند رفتار را تغییر دهند. Database tool برای یافتن Query طولانی، تعداد Record و متن SQL مفید است، اما Actual Plan و Logical Read همچنان باید در SQL Server بررسی شوند.

dotnet-counters ابزار پایش مرحله اول است و برای مشاهده CPU، allocation rate، GC heap، exception rate و thread pool مناسب است. dotnet-trace بر پایه EventPipe trace قابل تحلیل می‌سازد؛ dotnet-gcdump نمای heap مدیریت‌شده و dotnet-dump dump کامل برای SOS فراهم می‌کنند. جمع‌آوری dump یا trace طولانی سربار و ریسک داده حساس دارد؛ بازه، دسترسی، محل ذخیره و حذف فایل باید کنترل شود.

در EF Core، Logging متن SQL و زمان Command را می‌دهد؛ TagWith Query را به use case متصل می‌کند؛ Metrics نرخ Query، SaveChanges، optimistic concurrency failure و Query Cache Hit Rate را نشان می‌دهند؛ Interceptor برای شمارش، زمان‌سنجی یا سیاست سطح پایین است. Interceptor می‌تواند Command را تغییر یا suppress کند، پس برای لاگ ساده انتخاب اول نیست. EnableSensitiveDataLogging فقط در محیط امن و کوتاه‌مدت مجاز است.

Query Store تاریخچه Query، Plan و runtime statistics را در بازه‌های زمانی نگه می‌دارد و برای regression ناشی از تغییر Plan مناسب است. Actual Execution Plan اطلاعات زمان اجرا، actual rows و warningها را می‌دهد. Live Query Statistics برای Query درحال‌اجرا مفید است. SET STATISTICS IO/TIME عدد دقیق Logical Read و CPU/Elapsed را سریع نشان می‌دهد. Extended Events برای رخدادهای هدفمند بهتر از Trace قدیمی است، اما session پرحجم می‌تواند سربار بسازد.

2026-07-20T16:07:44.955724 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ Performance capture — terminal $ dotnet-counters monitor -p 4280 --counters System.Runtime,Microsoft.EntityFrameworkCore [System.Runtime] cpu-usage 82% | alloc-rate 146 MB/s | gc-heap-size 1,024 MB [EF Core] queries/sec 3,840 | compiled-query-cache-hit-rate 63% $ dotnet-trace collect -p 4280 --profile cpu-sampling -o before.nettrace Trace completed: before.nettrace SET STATISTICS IO, TIME ON; Table 'Orders'. Scan count 1, logical reads 18420, physical reads 0. SQL Server Execution Times: CPU time = 282 ms, elapsed time = 412 ms. -- after projection + covering index Table 'Orders'. Scan count 1, logical reads 1150, physical reads 0. SQL Server Execution Times: CPU time = 38 ms, elapsed time = 74 ms.
ALTER DATABASE [AppDb] SET QUERY_STORE = ON; GO SET STATISTICS IO, TIME ON; SET STATISTICS XML ON;  -- SQL copied from EF Core ToQueryString/command log EXEC sys.sp_executesql     N'SELECT ... FROM dbo.Orders WHERE TenantId = @tenantId',     N'@tenantId int', @tenantId = 42;  SET STATISTICS XML OFF; SET STATISTICS IO, TIME OFF;

۴. چگونه نمودار قبل و بعد را فنی بخوانیم#

کاهش latency بدون افزایش throughput ممکن است فقط نتیجه کم‌شدن concurrency یا حذف خطاها از نمونه باشد. افزایش throughput همراه CPU صددرصد و رشد P99 نیز ظرفیت پایدار نیست. برای هر نمودار یک شاخص نتیجه و حداقل یک شاخص علت رسم کنید؛ مثلاً P95 در کنار Logical Reads، Allocation در کنار Gen0، یا RPS در کنار pool wait. وقتی علت و نتیجه هم‌جهت‌اند، استدلال قوی‌تر می‌شود.

درصد تغییر از رابطه (بعد منهای قبل) تقسیم بر قبل به‌دست می‌آید. برای معیارهایی که کمتر بهتر است، مقدار منفی بهبود است؛ برای throughput، مقدار مثبت بهبود است. در محور لگاریتمی فاصله بصری خطی نیست و باید مقدار عددی روی نمودار نوشته شود. محور بریده یا مقیاس دوگانه می‌تواند اختلاف را بزرگ‌نمایی کند. همه نمودارهای این سند مقدار خام و واحد را نشان می‌دهند.

P95 یعنی ۹۵ درصد مشاهده‌ها کمتر یا مساوی آن زمان‌اند؛ درباره پنج درصد کندتر جزئیات نمی‌دهد. P99 برای tail latency مهم است ولی نمونه بیشتری می‌خواهد. average در توزیع دنباله‌دار ممکن است تجربه کاربر را پنهان کند. warm-up، JIT، مدل EF، connection creation و plan compilation باید جدا از steady state گزارش شوند، نه این‌که بدون توضیح حذف شوند.

بخش دوم: پرونده‌های فنی قبل و بعد#

2026-07-20T16:07:44.879688 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ BEFORE AFTER Index Scan 1,000,000 rows Residual Filter 12,400 rows Sort 12,400 rows Top 50 response Index Seek 50 rows range Ordered index no sort Top 50 response 18,420 logical reads 1,150 logical reads Execution-plan shape: scan/filter/sort vs. selective seek

پرونده ۱: Projection به‌جای بارگذاری Entity و گراف کامل#

ریشه مشکل، تفاوت میان مدل دامنه و قرارداد خروجی است. Entity برای رفتار، تغییر و رابطه طراحی شده است؛ اما صفحه فهرست فقط چهار مقدار می‌خواهد. Include باعث می‌شود ستون‌های غیرمصرفی، کلیدهای تکراری و گاهی متن یا باینری حجیم از شبکه عبور کنند. سپس EF برای هر ردیف Entity می‌سازد، Navigationها را fix-up می‌کند و در حالت Tracking snapshot نگه می‌دارد. این هزینه‌ها در SQL Server دیده نمی‌شوند و فقط با Allocation profiler و اندازه payload آشکار می‌شوند.

2026-07-20T16:07:42.720682 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ Before After 0 100 200 300 400 ms 412 74 -82% P95 latency Before After 0 2 4 6 8 10 12 14 MB 14.8 1.9 -87% Allocated / request Before After 0 2500 5000 7500 10000 12500 15000 17500 8 KB pages 18420 1150 -94% Logical reads Before After 0 5 10 15 20 25 MB 28.4 2.2 -92% Payload Projection instead of full entity graph Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

var orders = await db.Orders     .Include(o => o.Customer)     .Include(o => o.Items).ThenInclude(i => i.Product)     .Where(o => o.TenantId == tenantId)     .OrderByDescending(o => o.CreatedAt)     .Take(1000)     .ToListAsync(ct);

کد بعد از بهینه‌سازی#

var orders = await db.Orders.AsNoTracking()     .Where(o => o.TenantId == tenantId)     .OrderByDescending(o => o.CreatedAt)     .Select(o => new OrderListRow(         o.Id, o.CreatedAt, o.Customer.Name,         o.Items.Sum(i => i.Quantity * i.UnitPrice)))     .Take(1000)     .ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در سناریوی نمونه، P95 از ۴۱۲ به ۷۴ میلی‌ثانیه، Allocation از ۱۴٫۸ به ۱٫۹ مگابایت، Logical Read از ۱۸٬۴۲۰ به ۱٬۱۵۰ و payload از ۲۸٫۴ به ۲٫۲ مگابایت رسید. یعنی به‌ترتیب حدود ۸۲، ۸۷، ۹۴ و ۹۲ درصد کاهش. این هم‌جهتی مهم است: اگر فقط زمان کم می‌شد ولی Reads ثابت می‌ماند، احتمالاً نتیجه از Cache یا نویز محیط بود. کاهش چهار شاخص مستقل نشان می‌دهد کار واقعی در دیتابیس، شبکه و Runtime حذف شده است.

ابزار و روش اثبات#

برای اثبات، Query را TagWith کنید؛ در Visual Studio ابزار Database و .NET Object Allocation را هم‌زمان اجرا کنید؛ در SSMS گزینه Actual Execution Plan و SET STATISTICS IO, TIME را فعال کنید. تعداد ستون‌های SELECT، Actual Rows، Key Lookup، اندازه پاسخ HTTP و Allocated Bytes/request را قبل و بعد ثبت کنید. اگر DTO شامل مجموعه فرزند است، SQL نهایی را ببینید تا Projection نیز انفجار ردیف ایجاد نکرده باشد.

هزینه جانبی و شرط استفاده#

Projection قرارداد خواندن را از مدل نوشتن جدا می‌کند و کد DTO بیشتری می‌خواهد. این هزینه نگهداری معمولاً در مسیرهای پرترافیک قابل توجیه است. اگر خروجی بعداً ویرایش می‌شود، Entity tracked را در Unit of Work جدا بخوانید؛ DTO را دوباره Attach نکنید مگر سیاست دقیق برای propertyهای modified، امنیت mass assignment و concurrency token داشته باشید.

پرونده ۲: Tracking، Identity Resolution و فشار GC#

ChangeTracker یک دیکشنری ساده نیست. برای Entityهای tracked، EF باید identity map، original values یا snapshot لازم، state و رابطه‌ها را مدیریت کند. DetectChanges نیز روی Entryهای موجود حرکت می‌کند. در یک Query خواندنی بزرگ، این ساختارها عمر اشیا را افزایش می‌دهند و فشار Gen0 و گاهی Gen1 را بالا می‌برند. وقتی همان کلید در نتیجه چندبار تکرار می‌شود، AsNoTrackingWithIdentityResolution می‌تواند نمونه‌های تکراری را در محدوده Query یکی کند، اما خودش یک tracker موقت و هزینه محدود دارد.

2026-07-20T16:07:42.959234 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ Before After 0 50 100 150 200 ms 210 135 -36% P95 latency Before After 0 2 4 6 8 10 12 MB 12.6 6.1 -52% Allocated / request Before After 0 2 4 6 8 10 12 14 count 14 6 -57% Gen0 collections / 1k req Before After 0 2000 4000 6000 8000 10000 entries 10000 0 -100% Tracked entries Read-only query: tracking vs. no-tracking projection Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

var products = await db.Products     .Where(p => p.TenantId == tenantId && p.IsActive)     .Take(10_000)     .ToListAsync(ct);

کد بعد از بهینه‌سازی#

var products = await db.Products     .AsNoTracking()     .Where(p => p.TenantId == tenantId && p.IsActive)     .Select(p => new ProductRow(p.Id, p.Name, p.Price))     .Take(10_000)     .ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در آزمایش نمونه، P95 از ۲۱۰ به ۱۳۵ میلی‌ثانیه، Allocation از ۱۲٫۶ به ۶٫۱ مگابایت و Gen0 در هر هزار درخواست از ۱۴ به ۶ کاهش یافت. تعداد Entryهای پایدار از ده‌هزار به صفر رسید. کاهش زمان فقط ۳۶ درصد است، در حالی‌که Allocation حدود ۵۲ درصد کم شده؛ این تفاوت طبیعی است چون SQL و شبکه تغییر زیادی نکرده‌اند و عمده اثر در Materialization و GC است.

ابزار و روش اثبات#

در Visual Studio از .NET Object Allocation و Memory Usage snapshot استفاده کنید. در خط فرمان allocation-rate، gc-heap-size، gen-0-gc-count و time-in-gc را با dotnet-counters ببینید. هم‌زمان تعداد db.ChangeTracker.Entries() را در پایان Request ثبت کنید. یک رشد مداوم Entry در Context طولانی‌عمر نشانه طراحی اشتباه است، نه صرفاً نیاز به فراخوانی GC.Collect.

هزینه جانبی و شرط استفاده#

NoTracking نباید به‌عنوان تنظیم جهانی بدون شناخت جریان نوشتن اعمال شود. اگر Entity بعداً ویرایش و SaveChanges می‌شود، دوباره Attach کردن آن می‌تواند ستون‌های ناخواسته را Modified کند یا کنترل هم‌زمانی را دور بزند. راه امن، Context کوتاه برای فرمان نوشتن و Query جدا برای خواندن است. در گرافی با referenceهای تکراری، هر دو حالت NoTracking ساده و identity resolution را benchmark کنید.

پرونده ۳: N+1، Lazy Loading و انفجار Roundtrip#

در N+1 هر Command ممکن است کمتر از چند میلی‌ثانیه باشد و در فهرست Slow Query دیده نشود؛ مشکل از جمع تأخیر شبکه، checkout اتصال، parse/execute و serialization اجرای پشت‌سرهم ایجاد می‌شود. Lazy Loading دسترسی معمولی به property را به IO تبدیل می‌کند و Serialization یا ToString می‌تواند ناخواسته Query اجرا کند. این الگو زیر بار، Connection Pool را نیز پر می‌کند و P99 بسیار بیشتر از میانگین می‌شود.

2026-07-20T16:07:43.133024 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 20 40 60 80 100 Parent rows 0 20 40 60 80 100 commands / request Database commands N+1 Batched 0 20 40 60 80 100 Parent rows 0 200 400 600 800 ms P95 latency N+1 Batched N+1 grows with result cardinality Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

var customers = await db.Customers.Take(100).ToListAsync(ct); foreach (var customer in customers) {     // Lazy loading: one SQL command per customer     Console.WriteLine(customer.Orders.Count); }

کد بعد از بهینه‌سازی#

var customers = await db.Customers.AsNoTracking()     .Select(c => new     {         c.Id,         c.Name,         OrderCount = c.Orders.Count(o => o.Status == OrderStatus.Open)     })     .Take(100)     .ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

با صد والد، تعداد Command از ۱۰۱ به یک یا دو، و P95 آزمایشگاهی از ۸۹۰ به ۱۱۸ میلی‌ثانیه رسید؛ حدود ۸۷ درصد کاهش. RPS در همان ظرفیت اتصال از ۴۲ به ۲۹۶ افزایش یافت. نمودار نشان می‌دهد در ده والد تفاوت هنوز کوچک به نظر می‌رسد، اما هزینه نسخه N+1 با cardinality رشد می‌کند؛ پس تست دیتای کوچک توسعه نمی‌تواند این ایراد را رد کند.

ابزار و روش اثبات#

یک DbCommandInterceptor بسازید و تعداد Command را در Activity هر Request ذخیره کنید. آستانه‌ای مانند بیش از پنج Command برای Endpoint فهرست، هشدار ایجاد کند؛ اما عدد را با نیاز واقعی تنظیم کنید. MiniProfiler در محیط توسعه، EF logging و Query Store برای دیدن SQLهای پرتکرار مکمل‌اند. در Load Test، pool wait و active connection را کنار P95 رسم کنید تا اثر سیستمی روشن شود.

هزینه جانبی و شرط استفاده#

جایگزین N+1 همیشه Include نیست. Include چند Collection می‌تواند مشکل ضرب دکارتی بسازد. برای Count یا Sum از Projection تجمیعی، برای داده محدود از filtered include و برای چند مجموعه بزرگ از Query دسته‌ای بر اساس کلیدها استفاده کنید. تصمیم باید بر اساس شکل پاسخ، تعداد فرزندان و محدودیت consistency میان چند Query باشد.

پرونده ۴: Cartesian Explosion و انتخاب Split Query#

وقتی دو Collection sibling با JOIN در یک SQL خوانده می‌شوند، هر Post با هر Contributor همان Blog ترکیب می‌شود. ده Post و ده Contributor برای یک Blog صد ردیف فیزیکی می‌سازند. ستون‌های Blog نیز در هر ردیف تکرار می‌شوند. اگر ستون متن یا تصویر وجود داشته باشد، payload شدیداً بزرگ می‌شود. این مسئله با تعداد Entity نهایی در حافظه دیده نمی‌شود؛ باید physical rows و bytes transferred را بررسی کرد.

2026-07-20T16:07:43.370008 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 20 40 60 80 100 Rows in each child collection 1 0 3 1 0 4 1 0 5 1 0 6 Physical rows transferred (log scale) Cartesian explosion from sibling collection JOINs Single query: 100 × A × B Split/projection: 100 + A + B Illustrative cardinality model for 100 parent rows.

کد قبل از بهینه‌سازی#

var blogs = await db.Blogs     .Include(b => b.Posts)     .Include(b => b.Contributors)     .AsSingleQuery()     .Take(100)     .ToListAsync(ct);

کد بعد از بهینه‌سازی#

var blogs = await db.Blogs     .AsNoTracking()     .AsSplitQuery()     .Include(b => b.Posts.Where(p => p.IsPublished))     .Include(b => b.Contributors)     .Take(100)     .ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در مدل صد والد، اگر هر مجموعه پنجاه ردیف داشته باشد، Query یکپارچه حدود ۲۵۰ هزار ردیف می‌فرستد؛ Split یا Projection نزدیک ۱۰٬۱۰۰ ردیف نیاز دارد. در آزمایش نمونه payload از ۹۲ به ۷٫۸ مگابایت و P95 از ۱٫۴۶ ثانیه به ۲۲۰ میلی‌ثانیه کاهش یافت. در عوض تعداد Roundtrip از یک به سه رسید. در شبکه با latency بالا ممکن است این معامله متفاوت باشد.

ابزار و روش اثبات#

در Actual Plan تعداد Actual Rows خروجی هر Join را ببینید و با تعداد Entity نهایی مقایسه کنید. در ابزار Database رکوردهای خوانده‌شده و زمان شبکه را ثبت کنید. برای Split Query تعداد Command و فاصله زمانی آن‌ها را بررسی کنید. اگر paging دارید، Order By باید کاملاً یکتا باشد و نسخه EF/Provider را از نظر رفتار Split مرور کنید.

هزینه جانبی و شرط استفاده#

Split Query یک snapshot اتمیک تضمین نمی‌کند مگر Transaction و isolation مناسب داشته باشید؛ میان Commandها داده ممکن است تغییر کند. Transaction قوی‌تر نیز Lock و version store هزینه دارد. برای APIهای فهرست اغلب Projection بهترین راه است، چون هم شکل داده و هم تعداد Collectionها را محدود می‌کند. AsSplitQuery درمان خودکار همه Includeها نیست.

پرونده ۵: Offset در برابر Keyset Pagination#

OFFSET باید ردیف‌های قبل از صفحه هدف را پیدا و کنار بگذارد. Index می‌تواند Sort را حذف کند، اما موتور همچنان متناسب با عمق کار می‌کند. Keyset به‌جای شماره صفحه، آخرین کلید دیده‌شده را به Predicate تبدیل می‌کند و Range Seek می‌سازد. ترتیب باید کاملاً یکتا باشد؛ CreatedAt به‌تنهایی کافی نیست، زیرا چند ردیف مقدار برابر دارند و زیر تغییر هم‌زمان ممکن است تکرار یا حذف شوند.

2026-07-20T16:07:43.698478 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 1 0 0 1 0 1 1 0 2 1 0 3 1 0 4 Page depth (log scale) 1 0 1 1 0 2 1 0 3 P95 latency, ms (log scale) Deep pagination cost OFFSET / Skip Keyset cursor Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

var rows = await db.Events.AsNoTracking()     .OrderBy(e => e.CreatedAt).ThenBy(e => e.Id)     .Skip((pageNumber - 1) * pageSize)     .Take(pageSize)     .ToListAsync(ct);

کد بعد از بهینه‌سازی#

var rows = await db.Events.AsNoTracking()     .Where(e => e.CreatedAt > cursor.Date ||         (e.CreatedAt == cursor.Date && e.Id > cursor.Id))     .OrderBy(e => e.CreatedAt).ThenBy(e => e.Id)     .Take(pageSize)     .ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در صفحه اول هر دو روش حدود ۱۲ تا ۱۳ میلی‌ثانیه بودند. در عمق ۱۰٬۰۰۰، Offset به حدود ۲٫۹ ثانیه رسید اما Keyset نزدیک ۱۵ میلی‌ثانیه ماند. نمودار با محور لگاریتمی نشان می‌دهد اختلاف در صفحات کم پنهان است. این بهبود برای Next/Previous عالی است، ولی پرش مستقیم به صفحه ۸۷۳ را به‌طور طبیعی حل نمی‌کند.

ابزار و روش اثبات#

Index مرکب مطابق WHERE و ORDER BY بسازید؛ برای مثال TenantId, CreatedAt, Id. Actual Plan باید Seek و نبود Sort بزرگ را نشان دهد. Logical Reads را در عمق‌های ۱، ۱۰، ۱۰۰، ۱۰۰۰ و ۱۰٬۰۰۰ ثبت کنید. تست صحت باید درج و حذف هم‌زمان، جهت برگشت، timezone و رمزنگاری/امضای Cursor را پوشش دهد.

هزینه جانبی و شرط استفاده#

اگر محصول واقعاً شماره صفحه و تعداد کل می‌خواهد، Offset ممکن است برای عمق کم قابل قبول باشد و Count جداگانه هزینه خود را دارد. می‌توان UI را به Cursor تغییر داد، عمق مجاز را محدود کرد یا snapshot جست‌وجو ساخت. هدف حذف قابلیت محصول نیست؛ هدف آشکارکردن هزینه و انتخاب آگاهانه است.

پرونده ۶: SaveChanges در حلقه، Batching و ExecuteUpdate#

SaveChanges داخل حلقه مرز Transaction و Roundtrip را برای هر Entity تکرار می‌کند. انتقال SaveChanges به بیرون حلقه از batching Provider استفاده می‌کند، ولی ChangeTracker، snapshot و Statementهای متعدد باقی می‌مانند. ExecuteUpdate شرط و تغییر را به یک UPDATE مجموعه‌ای تبدیل می‌کند و Materialization را حذف می‌کند. تفاوت سه روش باید جدا سنجیده شود؛ عنوان کلی Bulk Update این تفاوت‌ها را پنهان می‌کند.

2026-07-20T16:07:44.010834 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ SaveEach SaveChanges batch ExecuteUpdate 1 0 3 1 0 4 ms (log scale) 18,000 2,200 180 Duration SaveEach SaveChanges batch ExecuteUpdate 1 0 0 1 0 1 1 0 2 1 0 3 1 0 4 count (log scale) 10,000 239 1 Database roundtrips Updating 10,000 rows Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

foreach (var employee in employees) {     employee.Salary += 1_000;     await db.SaveChangesAsync(ct); }

کد بعد از بهینه‌سازی#

var affected = await db.Employees     .Where(e => e.DepartmentId == departmentId)     .ExecuteUpdateAsync(s => s         .SetProperty(e => e.Salary, e => e.Salary + 1_000)         .SetProperty(e => e.UpdatedAt, now), ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در ده‌هزار ردیف، روش SaveEach حدود ۱۸ ثانیه و ده‌هزار Roundtrip، SaveChanges دسته‌ای حدود ۲٫۲ ثانیه و نزدیک ۲۳۹ Batch، و ExecuteUpdate حدود ۱۸۰ میلی‌ثانیه و یک Roundtrip داشت. کاهش از ۱۸ ثانیه به ۱۸۰ میلی‌ثانیه حدود ۹۹ درصد است؛ اما Lock footprint و transaction log همچنان می‌توانند در جدول بزرگ گلوگاه باشند.

ابزار و روش اثبات#

تعداد DbCommand، مدت Transaction، rows affected، log bytes flushed، lock wait و deadlock را اندازه بگیرید. در SQL Server از Query Store، Extended Events و DMVهای transaction log استفاده کنید. Batch size را بدون Benchmark تغییر ندهید؛ اندازه بهتر به Provider، latency، trigger، constraint و اندازه Statement بستگی دارد. برای عملیات طولانی، chunk و نقطه ادامه طراحی کنید.

هزینه جانبی و شرط استفاده#

ExecuteUpdate رویداد دامنه، validation داخل Entity و ChangeTracker را دور می‌زند و Entityهای tracked موجود را تازه نمی‌کند. Concurrency token را باید در Predicate وارد کنید و صفر بودن affected row را conflict بدانید. اگر business rule فقط در کد C# است، آن را به SQL تبدیل نکنید مگر صحت و تست مهاجرت آن روشن باشد.

پرونده ۷: DbContext Pooling و Compiled Query در مسیر فوق‌داغ#

Context Pooling هزینه ساخت و آماده‌سازی نمونه DbContext را کم می‌کند، اما Connection Pooling نیست. اتصال معمولاً درست پیش از Command گرفته و پس از آن آزاد می‌شود. Compiled Query نیز lookup و ترجمه expression را دور می‌زند، اما I/O، plan و انتقال داده را تغییر نمی‌دهد. این دو تکنیک برای سرویس کم‌تأخیر و Query بسیار پرتکرار مفیدند؛ در Query ۲۰۰ میلی‌ثانیه‌ای با Scan سنگین، چند ده میکروثانیه اهمیتی ندارد.

2026-07-20T16:07:44.285797 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ Default Optimized 0 50 100 150 200 250 300 350 µs 350 45 Context setup Default Optimized 0 10 20 30 40 50 KB 50 5 Allocation Default Optimized 0 100 200 300 400 500 600 700 µs 671 564 Hot query mean DbContext pooling and compiled hot query Illustrative setup figures; compiled-query values align with the order of magnitude in EF documentation.

کد قبل از بهینه‌سازی#

builder.Services.AddDbContext<AppDbContext>(o =>     o.UseSqlServer(connectionString));  var item = await db.Products.AsNoTracking()     .FirstOrDefaultAsync(p => p.Id == id, ct);

کد بعد از بهینه‌سازی#

builder.Services.AddDbContextPool<AppDbContext>(o =>     o.UseSqlServer(connectionString), poolSize: 128);  private static readonly Func<AppDbContext, int, Product?> ById =     EF.CompileQuery((AppDbContext db, int id) =>         db.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در نمودار آزمایشگاهی setup Context از ۳۵۰ به ۴۵ میکروثانیه و Allocation از ۵۰ به ۵ کیلوبایت رسید. Query داغ compileشده از حدود ۶۷۱ به ۵۶۴ میکروثانیه کاهش یافت. این نمودار عمداً microbenchmark است؛ اگر latency شبکه ۵ میلی‌ثانیه باشد، سهم این بهبود بسیار کوچک می‌شود. به همین دلیل نتیجه باید در load test end-to-end نیز دیده شود.

ابزار و روش اثبات#

BenchmarkDotNet با MemoryDiagnoser برای هزینه درون‌فرایند مناسب است. برای سرویس، RPS، P99، pool saturation و allocation rate را اندازه بگیرید. Query Cache Hit Rate عادی پس از warm-up باید نزدیک صد درصد باشد؛ اگر نیست، ابتدا شکل Query پویا را اصلاح کنید. اندازه pool را با concurrency واقعی تنظیم کنید، نه بر اساس تعداد هسته به‌تنهایی.

هزینه جانبی و شرط استفاده#

State درخواست مانند TenantId نباید در OnConfiguring یک Context poolشده باقی بماند؛ OnConfiguring برای نمونه بازیافتی هر درخواست اجرا نمی‌شود. State باید از factory scoped اعمال و پیش از بازگشت پاک شود. Compiled Query فقط با یک مدل EF سازگار است و پارامترهای پیچیده محدودیت دارند. هر Query را compile نکنید؛ فقط hot path ثابت و اندازه‌گیری‌شده.

پرونده ۸: Query Cache، پارامتردهی و Expression Tree پویا#

EF خروجی ترجمه Query را بر اساس شکل Expression Tree cache می‌کند. Constant جدید شکل جدید می‌سازد و علاوه بر EF، متن SQL متفاوت می‌تواند Plan Cache دیتابیس را آلوده کند. در سازنده فیلتر پویا، استفاده از Expression.Constant برای مقدار کاربر رایج است. راه بهتر capture پارامتر، ترکیب predicateهای ازپیش‌تعریف‌شده یا ساخت expression parameterized است. ثابت بودن شکل به معنی ثابت بودن مقدار نیست.

2026-07-20T16:07:44.393568 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 2.5 5.0 7.5 10.0 12.5 15.0 17.5 20.0 Warm-up batch 40 50 60 70 80 90 100 Compiled query cache hit rate (%) Stable expression shape reaches a warm cache Dynamic constants Parameterized shape Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

// Anti-pattern: a different ConstantExpression per value var constant = Expression.Constant(request.Name); var body = Expression.Equal(nameProperty, constant); var predicate = Expression.Lambda<Func<Product, bool>>(body, parameter);

کد بعد از بهینه‌سازی#

var wantedName = request.Name; IQueryable<Product> query = db.Products.AsNoTracking(); query = query.Where(p => p.Name == wantedName); var rows = await query.Take(50).ToListAsync(ct);

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در نمودار، Query پارامتری پس از چند batch به cache hit نزدیک صد درصد می‌رسد، اما نسخه constant-driven حوالی ۶۳ درصد متوقف می‌شود. در آزمایش نمونه سهم CPU مربوط به compilation از ۲۸ به ۳ درصد و P95 از ۱۸۰ به ۱۲۵ میلی‌ثانیه رسید. اثر دقیق به پیچیدگی expression و نرخ تنوع فیلتر بستگی دارد.

ابزار و روش اثبات#

EF metric مربوط به compiled-query-cache-hit-rate را با dotnet-counters یا Meter listener بخوانید. در SQL Server تعداد planهای مشابه را در Query Store یا plan cache بررسی کنید. برای فیلترساز پویا unit test روی ToQueryString بنویسید و مطمئن شوید مقدار به پارامتر تبدیل می‌شود. نرخ صد درصد در ثانیه اول انتظار درستی نیست؛ دوره warm-up را جدا کنید.

هزینه جانبی و شرط استفاده#

پارامتردهی همیشه بهترین Plan واحد را تضمین نمی‌کند. توزیع بسیار نامتوازن داده و parameter sniffing ممکن است برای مقادیر مختلف Plan متفاوت بخواهد. این مسئله باید با Query Store، actual rows و plan history اثبات شود. استفاده بی‌قاعده از recompile یا تبدیل همه چیز به Constant، هزینه CPU و cache churn دارد و درمان عمومی نیست.

پرونده ۹: Streaming در برابر Buffering و هزینه نگه‌داشتن Connection#

ToListAsync تمام نتیجه را پیش از پردازش در حافظه نگه می‌دارد. برای یک میلیون ردیف، Entity، رشته‌ها و آرایه داخلی List می‌توانند LOH و GC سنگین بسازند. Streaming ردیف را پس از خواندن مصرف می‌کند و peak memory را محدود می‌سازد. اما DbDataReader و Connection تا پایان باز می‌مانند؛ اگر مصرف‌کننده کند باشد، pool تحت فشار قرار می‌گیرد. پس حافظه تنها محور تصمیم نیست.

2026-07-20T16:07:44.479953 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0.0 0.2 0.4 0.6 0.8 1.0 Rows processed 1e6 200 400 600 800 1000 Peak working set, MB Streaming controls peak memory but holds the reader longer ToListAsync buffering AsAsyncEnumerable streaming Illustrative lab benchmark; reproduce on your own hardware and dataset.

کد قبل از بهینه‌سازی#

var rows = await db.AuditRecords.AsNoTracking()     .Where(x => x.CreatedAt >= from && x.CreatedAt < to)     .ToListAsync(ct); foreach (var row in rows)     await writer.WriteAsync(row, ct);

کد بعد از بهینه‌سازی#

await foreach (var row in db.AuditRecords.AsNoTracking()     .Where(x => x.CreatedAt >= from && x.CreatedAt < to)     .Select(x => new AuditExportRow(x.Id, x.Action, x.CreatedAt))     .AsAsyncEnumerable()     .WithCancellation(ct)) {     await writer.WriteAsync(row, ct); }

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در سناریوی صادرات، peak working set از حدود ۹۸۰ به ۸۲ مگابایت رسید، ولی زمان کل از ۴۱ به ۴۵ ثانیه افزایش یافت و Connection چهار ثانیه بیشتر نگه داشته شد. این یک نمونه مهم است که بهینه‌سازی یک شاخص می‌تواند شاخص دیگر را بدتر کند. راه‌حل نهایی ممکن است Keyset batch، فایل موقت یا صف background باشد تا هم حافظه و هم زمان اشغال اتصال کنترل شود.

ابزار و روش اثبات#

working set، gc-heap-size، LOH size، allocation rate، duration و connection checkout duration را با هم ثبت کنید. CancellationToken باید تا enumerator و writer منتقل شود. مصرف‌کننده کند را در تست شبیه‌سازی کنید؛ benchmark با sink حافظه‌ای سریع واقعیت شبکه یا دیسک را نشان نمی‌دهد. در retry strategy یا split query امکان buffering داخلی را نیز در نظر بگیرید.

هزینه جانبی و شرط استفاده#

برای پاسخ کوچک، Buffering ساده‌تر است و اتصال را سریع‌تر آزاد می‌کند. برای نتیجه بسیار بزرگ، Streaming یا batch ضروری است. اگر endpoint HTTP مستقیماً stream می‌کند، شکست میان پاسخ قابل rollback نیست و format خروجی باید تحمل قطع شدن داشته باشد. محدوده زمانی و حداکثر ردیف را محدود کنید تا Query صادرات تبدیل به denial of service داخلی نشود.

پرونده ۱۰: نتیجه سیستمی زیر Load Test#

Microbenchmark ثابت نمی‌کند سیستم زیر concurrency بهتر شده است. ممکن است Query سریع‌تر شود اما تعداد درخواست بیشتر، Connection Pool یا CPU SQL Server را به saturation ببرد. تست بار باید مدل ورود مشخص داشته باشد: closed model تعداد user ثابت و open/arrival-rate نرخ ورود ثابت را شبیه‌سازی می‌کند. برای latency service، نرخ ورود ثابت معمولاً صف‌سازی و شکست واقعی را بهتر آشکار می‌کند.

2026-07-20T16:07:44.768633 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ Before After 0 200 400 600 800 1000 1200 1400 ms 1450 180 P95 latency Before After 0 200 400 600 800 1000 1200 1400 RPS 320 1450 Throughput Before After 0 20 40 60 80 % 82 58 App CPU Before After 0 10 20 30 40 50 60 70 % 76 44 SQL CPU Before After 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 % 3.8 0.2 Errors Before After 0 50 100 150 200 250 300 350 ms 340 18 Pool wait End-to-end load test: system-level effect Illustrative 10-minute constant-arrival-rate test after warm-up; same payload and database snapshot.

کد قبل از بهینه‌سازی#

var scenario = Scenario.Create("orders-before", async context => {     var response = await http.GetAsync("/api/orders?size=1000");     return response.IsSuccessStatusCode ? Response.Ok() : Response.Fail(); }) .WithLoadSimulations(Simulation.Inject(     rate: 400, interval: TimeSpan.FromSeconds(1),     during: TimeSpan.FromMinutes(10)));

کد بعد از بهینه‌سازی#

// Run the identical scenario against the optimized build. // Keep arrival rate, payload, database snapshot, connection limits, // warm-up, host size and network placement unchanged. // Compare percentile distributions and saturation signals, not only averages.

اثر اندازه‌گیری‌شده در سناریوی آزمایشگاهی#

در تست ده‌دقیقه‌ای نمونه، P95 از ۱۴۵۰ به ۱۸۰ میلی‌ثانیه، pool wait از ۳۴۰ به ۱۸ میلی‌ثانیه، خطا از ۳٫۸ به ۰٫۲ درصد، CPU برنامه از ۸۲ به ۵۸ و CPU SQL از ۷۶ به ۴۴ درصد رسید. Throughput پایدار از ۳۲۰ به ۱۴۵۰ RPS افزایش یافت. این اعداد جمع اثر Projection، حذف N+1، Index، NoTracking و set-based update هستند و سهم هر تغییر باید در آزمایش‌های جدا ثبت شود.

ابزار و روش اثبات#

گزارش NBomber یا k6 را با OpenTelemetry traces، EF metrics، dotnet-counters و Query Store هم‌زمان کنید. timestamp و شناسه نسخه build مشترک باشند. Warm-up، snapshot دیتابیس، MAXDOP، اندازه connection pool و host limits را ثابت نگه دارید. نمودار percentile را به جای یک average رسم کنید و خطا یا timeout را از محاسبه latency حذف نکنید؛ حذف درخواست‌های شکست‌خورده نمودار را مصنوعی خوب می‌کند.

هزینه جانبی و شرط استفاده#

افزایش RPS تا لحظه saturation مفید است، اما تست نباید محیط تولید مشترک را مختل کند. capacity test، stress test، soak test و spike test هدف‌های متفاوت دارند. پس از استقرار canary، معیار بازگشت تعریف کنید: مثلاً P99 بیش از ۵۰۰ میلی‌ثانیه، خطا بیش از نیم درصد یا pool wait بیش از پنجاه میلی‌ثانیه برای پنج دقیقه. Performance بدون regression guard پایدار نمی‌ماند.

بخش سوم: از نمودار آزمایشگاهی تا تصمیم Production#

بهینه‌سازی زمانی کامل است که در تولید قابل مشاهده و قابل بازگشت باشد. برای هر تغییر، dashboard باید نسخه build، endpoint، tenant class، database و region را dimension کند؛ اما cardinality برچسب‌ها را کنترل کنید. Trace sampling برای مسیرهای عادی و نگهداری کامل traceهای کند یا خطادار معمولاً تعادل خوبی می‌سازد. لاگ SQL خام یا پارامتر حساس را وارد telemetry عمومی نکنید.

بودجه Performance را به تست regression تبدیل کنید. برای مثال، endpoint فهرست سفارش در داده ثابت نباید بیش از دو Command، ۲۵۰ کیلوبایت Allocation، ۱۵۰۰ Logical Read و P95 برابر ۲۰۰ میلی‌ثانیه داشته باشد. این thresholdها از اعداد پروژه شما می‌آیند. BenchmarkDotNet برای regression درون‌فرایند و NBomber/k6 برای قرارداد end-to-end مکمل‌اند؛ یکی جای دیگری را نمی‌گیرد.

در canary، نسخه قبل و بعد باید هم‌زمان روی درصد کمی از ترافیک مشابه مقایسه شوند. اگر توزیع مشتریان متفاوت باشد، مقایسه خام گمراه‌کننده است. معیار بازگشت، مدت پنجره و حداقل تعداد نمونه را پیش از استقرار تعریف کنید. Query Store امکان مشاهده plan regression را می‌دهد؛ plan forcing می‌تواند اقدام موقت باشد، اما علت آمار، پارامتر، index یا تغییر compatibility level باید حل شود.

نمودار خوب باید امکان ردکردن فرضیه را نیز بدهد. اگر پس از Projection، payload کم شده ولی Logical Reads ثابت مانده است، شاید Index پوششی لازم باشد. اگر Reads کم شده ولی P95 تغییر نکرده، گلوگاه احتمالاً شبکه، lock یا صف است. اگر Allocation نصف شده ولی time-in-GC ثابت است، heap شاید مسئله اصلی نبوده است. گزارش باید این نتیجه‌های منفی را نیز نگه دارد تا تیم دوباره همان آزمایش را تکرار نکند.

فصل 1: روش علمی اندازه‌گیری و یافتن گلوگاه#

در فصل 1 تمرکز بر «روش علمی اندازه‌گیری و یافتن گلوگاه» است. اصل مرکزی این فصل چنین است: بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Execution Plan و آمار IO/Time مشخص می‌کنند هزینه واقعاً در اسکن، Join، Sort یا انتقال داده است. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 001: روش علمی اندازه‌گیری و یافتن گلوگاه در فهرست محصولات#

در فهرست محصولات اصل «بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var sw = Stopwatch.StartNew(); var rows = await db.Products.Where(x => x.IsActive).Take(10).ToListAsync(ct); sw.Stop(); metrics.Record("Products.list", sw.Elapsed, rows.Count);

نمونه 002: روش علمی اندازه‌گیری و یافتن گلوگاه در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. راهبرد نمونه این است که زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. SQL و پارامترها را ببینید و مقایسه را با میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Orders.TagWith("UseCase:Orders:Sample2").Where(x => x.TenantId == tenantId); var sql = query.ToQueryString(); var result = await query.CountAsync(ct);

نمونه 003: روش علمی اندازه‌گیری و یافتن گلوگاه در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Execution Plan و آمار IO/Time مشخص می‌کنند هزینه واقعاً در اسکن، Join، Sort یا انتقال داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

GC.Collect(); var before = GC.GetAllocatedBytesForCurrentThread(); var result = await db.Customers.AsNoTracking().Take(30).ToListAsync(ct); var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

نمونه 004: روش علمی اندازه‌گیری و یافتن گلوگاه در مدیریت وبلاگ‌ها#

نمونه 4 برای مدیریت وبلاگ‌ها است. نشانه اولیه: تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. نسخه پیشنهادی زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var db = await factory.CreateDbContextAsync(ct); var started = TimeProvider.System.GetTimestamp(); var count = await db.Blogs.CountAsync(x => x.IsActive, ct); var elapsed = TimeProvider.System.GetElapsedTime(started);

نمونه 005: روش علمی اندازه‌گیری و یافتن گلوگاه در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var samples = new List<double>(); for (var i = 0; i < 30; i++) {     var t = Stopwatch.GetTimestamp();     _ = await db.Posts.AsNoTracking().Take(50).ToArrayAsync(ct);     samples.Add(Stopwatch.GetElapsedTime(t).TotalMilliseconds); }

نمونه 006: روش علمی اندازه‌گیری و یافتن گلوگاه در گزارش کارکنان#

در گزارش کارکنان اصل «بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var sw = Stopwatch.StartNew(); var rows = await db.Employees.Where(x => x.IsActive).Take(10).ToListAsync(ct); sw.Stop(); metrics.Record("Employees.list", sw.Elapsed, rows.Count);

نمونه 007: روش علمی اندازه‌گیری و یافتن گلوگاه در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. راهبرد نمونه این است که زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. SQL و پارامترها را ببینید و مقایسه را با میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Invoices.TagWith("UseCase:Invoices:Sample7").Where(x => x.TenantId == tenantId); var sql = query.ToQueryString(); var result = await query.CountAsync(ct);

نمونه 008: روش علمی اندازه‌گیری و یافتن گلوگاه در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Execution Plan و آمار IO/Time مشخص می‌کنند هزینه واقعاً در اسکن، Join، Sort یا انتقال داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

GC.Collect(); var before = GC.GetAllocatedBytesForCurrentThread(); var result = await db.Tickets.AsNoTracking().Take(30).ToListAsync(ct); var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

نمونه 009: روش علمی اندازه‌گیری و یافتن گلوگاه در صندوق پیام‌ها#

نمونه 9 برای صندوق پیام‌ها است. نشانه اولیه: تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. نسخه پیشنهادی زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var db = await factory.CreateDbContextAsync(ct); var started = TimeProvider.System.GetTimestamp(); var count = await db.Messages.CountAsync(x => x.IsActive, ct); var elapsed = TimeProvider.System.GetElapsedTime(started);

نمونه 010: روش علمی اندازه‌گیری و یافتن گلوگاه در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var samples = new List<double>(); for (var i = 0; i < 30; i++) {     var t = Stopwatch.GetTimestamp();     _ = await db.Events.AsNoTracking().Take(50).ToArrayAsync(ct);     samples.Add(Stopwatch.GetElapsedTime(t).TotalMilliseconds); }

نمونه 011: روش علمی اندازه‌گیری و یافتن گلوگاه در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var sw = Stopwatch.StartNew(); var rows = await db.Documents.Where(x => x.IsActive).Take(10).ToListAsync(ct); sw.Stop(); metrics.Record("Documents.list", sw.Elapsed, rows.Count);

نمونه 012: روش علمی اندازه‌گیری و یافتن گلوگاه در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. راهبرد نمونه این است که زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. SQL و پارامترها را ببینید و مقایسه را با میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Payments.TagWith("UseCase:Payments:Sample12").Where(x => x.TenantId == tenantId); var sql = query.ToQueryString(); var result = await query.CountAsync(ct);

نمونه 013: روش علمی اندازه‌گیری و یافتن گلوگاه در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Execution Plan و آمار IO/Time مشخص می‌کنند هزینه واقعاً در اسکن، Join، Sort یا انتقال داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

GC.Collect(); var before = GC.GetAllocatedBytesForCurrentThread(); var result = await db.Shipments.AsNoTracking().Take(30).ToListAsync(ct); var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

نمونه 014: روش علمی اندازه‌گیری و یافتن گلوگاه در مرور رخدادها#

نمونه 14 برای مرور رخدادها است. نشانه اولیه: تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. نسخه پیشنهادی زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var db = await factory.CreateDbContextAsync(ct); var started = TimeProvider.System.GetTimestamp(); var count = await db.LogEntries.CountAsync(x => x.IsActive, ct); var elapsed = TimeProvider.System.GetElapsedTime(started);

نمونه 015: روش علمی اندازه‌گیری و یافتن گلوگاه در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var samples = new List<double>(); for (var i = 0; i < 30; i++) {     var t = Stopwatch.GetTimestamp();     _ = await db.AuditRecords.AsNoTracking().Take(50).ToArrayAsync(ct);     samples.Add(Stopwatch.GetElapsedTime(t).TotalMilliseconds); }

نمونه 016: روش علمی اندازه‌گیری و یافتن گلوگاه در نشست‌های فعال#

در نشست‌های فعال اصل «بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var sw = Stopwatch.StartNew(); var rows = await db.Sessions.Where(x => x.IsActive).Take(10).ToListAsync(ct); sw.Stop(); metrics.Record("Sessions.list", sw.Elapsed, rows.Count);

نمونه 017: روش علمی اندازه‌گیری و یافتن گلوگاه در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. راهبرد نمونه این است که زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. SQL و پارامترها را ببینید و مقایسه را با میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Courses.TagWith("UseCase:Courses:Sample17").Where(x => x.TenantId == tenantId); var sql = query.ToQueryString(); var result = await query.CountAsync(ct);

نمونه 018: روش علمی اندازه‌گیری و یافتن گلوگاه در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Execution Plan و آمار IO/Time مشخص می‌کنند هزینه واقعاً در اسکن، Join، Sort یا انتقال داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بهینه‌سازی باید از فرضیه قابل‌آزمایش، خط مبنا و معیار موفقیت شروع شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

GC.Collect(); var before = GC.GetAllocatedBytesForCurrentThread(); var result = await db.Enrollments.AsNoTracking().Take(30).ToListAsync(ct); var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

نمونه 019: روش علمی اندازه‌گیری و یافتن گلوگاه در فهرست مقاله‌ها#

نمونه 19 برای فهرست مقاله‌ها است. نشانه اولیه: تیم تنها بر اساس حس کاربر یا یک اجرای محلی درباره کندی قضاوت می‌کند. نسخه پیشنهادی زمان کل درخواست را به زمان برنامه، شبکه، صف اتصال، اجرای SQL و ساخت اشیا تفکیک کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var db = await factory.CreateDbContextAsync(ct); var started = TimeProvider.System.GetTimestamp(); var count = await db.Articles.CountAsync(x => x.IsActive, ct); var elapsed = TimeProvider.System.GetElapsedTime(started);

نمونه 020: روش علمی اندازه‌گیری و یافتن گلوگاه در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: میانه، صدک ۹۵، صدک ۹۹، نرخ خطا، تعداد Query و تخصیص حافظه را کنار هم ثبت کنید. اعداد محیط توسعه را بدون بار، حجم داده و تنظیمات مشابه تولید مبنا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var samples = new List<double>(); for (var i = 0; i < 30; i++) {     var t = Stopwatch.GetTimestamp();     _ = await db.Notifications.AsNoTracking().Take(50).ToArrayAsync(ct);     samples.Add(Stopwatch.GetElapsedTime(t).TotalMilliseconds); }

فصل 2: مشاهده SQL، Logging و برچسب‌گذاری Query#

در فصل 2 تمرکز بر «مشاهده SQL، Logging و برچسب‌گذاری Query» است. اصل مرکزی این فصل چنین است: SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، پارامترها، ترتیب Join، ستون‌های Select و تعداد Roundtrip در SQL از خود LINQ مهم‌تر هستند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 021: مشاهده SQL، Logging و برچسب‌گذاری Query در فهرست محصولات#

در فهرست محصولات اصل «SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString)            .LogTo(Console.WriteLine, LogLevel.Information)            .EnableDetailedErrors());

نمونه 022: مشاهده SQL، Logging و برچسب‌گذاری Query در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. راهبرد نمونه این است که سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Orders     .TagWith("Endpoint=/orders;Sample=2")     .Where(x => x.TenantId == tenantId)     .Take(20)     .ToListAsync(ct);

نمونه 023: مشاهده SQL، Logging و برچسب‌گذاری Query در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا پارامترها، ترتیب Join، ستون‌های Select و تعداد Roundtrip در SQL از خود LINQ مهم‌تر هستند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var query = db.Customers.Where(x => x.IsActive).OrderBy(x => x.Id).Take(30); logger.LogDebug("Generated SQL: {Sql}", query.ToQueryString()); var rows = await query.ToListAsync(ct);

نمونه 024: مشاهده SQL، Logging و برچسب‌گذاری Query در مدیریت وبلاگ‌ها#

نمونه 24 برای مدیریت وبلاگ‌ها است. نشانه اولیه: لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. نسخه پیشنهادی سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.ConfigureWarnings(w =>     w.Log(RelationalEventId.MultipleCollectionIncludeWarning));

نمونه 025: مشاهده SQL، Logging و برچسب‌گذاری Query در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

options.LogTo(     message => logger.LogInformation("{EfMessage}", message),     new[] { DbLoggerCategory.Database.Command.Name },     LogLevel.Information);

نمونه 026: مشاهده SQL، Logging و برچسب‌گذاری Query در گزارش کارکنان#

در گزارش کارکنان اصل «SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString)            .LogTo(Console.WriteLine, LogLevel.Information)            .EnableDetailedErrors());

نمونه 027: مشاهده SQL، Logging و برچسب‌گذاری Query در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. راهبرد نمونه این است که سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Invoices     .TagWith("Endpoint=/invoices;Sample=7")     .Where(x => x.TenantId == tenantId)     .Take(20)     .ToListAsync(ct);

نمونه 028: مشاهده SQL، Logging و برچسب‌گذاری Query در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا پارامترها، ترتیب Join، ستون‌های Select و تعداد Roundtrip در SQL از خود LINQ مهم‌تر هستند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var query = db.Tickets.Where(x => x.IsActive).OrderBy(x => x.Id).Take(30); logger.LogDebug("Generated SQL: {Sql}", query.ToQueryString()); var rows = await query.ToListAsync(ct);

نمونه 029: مشاهده SQL، Logging و برچسب‌گذاری Query در صندوق پیام‌ها#

نمونه 29 برای صندوق پیام‌ها است. نشانه اولیه: لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. نسخه پیشنهادی سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.ConfigureWarnings(w =>     w.Log(RelationalEventId.MultipleCollectionIncludeWarning));

نمونه 030: مشاهده SQL، Logging و برچسب‌گذاری Query در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

options.LogTo(     message => logger.LogInformation("{EfMessage}", message),     new[] { DbLoggerCategory.Database.Command.Name },     LogLevel.Information);

نمونه 031: مشاهده SQL، Logging و برچسب‌گذاری Query در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString)            .LogTo(Console.WriteLine, LogLevel.Information)            .EnableDetailedErrors());

نمونه 032: مشاهده SQL، Logging و برچسب‌گذاری Query در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. راهبرد نمونه این است که سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Payments     .TagWith("Endpoint=/payments;Sample=12")     .Where(x => x.TenantId == tenantId)     .Take(20)     .ToListAsync(ct);

نمونه 033: مشاهده SQL، Logging و برچسب‌گذاری Query در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا پارامترها، ترتیب Join، ستون‌های Select و تعداد Roundtrip در SQL از خود LINQ مهم‌تر هستند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var query = db.Shipments.Where(x => x.IsActive).OrderBy(x => x.Id).Take(30); logger.LogDebug("Generated SQL: {Sql}", query.ToQueryString()); var rows = await query.ToListAsync(ct);

نمونه 034: مشاهده SQL، Logging و برچسب‌گذاری Query در مرور رخدادها#

نمونه 34 برای مرور رخدادها است. نشانه اولیه: لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. نسخه پیشنهادی سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.ConfigureWarnings(w =>     w.Log(RelationalEventId.MultipleCollectionIncludeWarning));

نمونه 035: مشاهده SQL، Logging و برچسب‌گذاری Query در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

options.LogTo(     message => logger.LogInformation("{EfMessage}", message),     new[] { DbLoggerCategory.Database.Command.Name },     LogLevel.Information);

نمونه 036: مشاهده SQL، Logging و برچسب‌گذاری Query در نشست‌های فعال#

در نشست‌های فعال اصل «SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString)            .LogTo(Console.WriteLine, LogLevel.Information)            .EnableDetailedErrors());

نمونه 037: مشاهده SQL، Logging و برچسب‌گذاری Query در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. راهبرد نمونه این است که سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Courses     .TagWith("Endpoint=/courses;Sample=17")     .Where(x => x.TenantId == tenantId)     .Take(20)     .ToListAsync(ct);

نمونه 038: مشاهده SQL، Logging و برچسب‌گذاری Query در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا پارامترها، ترتیب Join، ستون‌های Select و تعداد Roundtrip در SQL از خود LINQ مهم‌تر هستند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: SQL تولیدشده باید قابل مشاهده و به درخواست تجاری قابل انتساب باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var query = db.Enrollments.Where(x => x.IsActive).OrderBy(x => x.Id).Take(30); logger.LogDebug("Generated SQL: {Sql}", query.ToQueryString()); var rows = await query.ToListAsync(ct);

نمونه 039: مشاهده SQL، Logging و برچسب‌گذاری Query در فهرست مقاله‌ها#

نمونه 39 برای فهرست مقاله‌ها است. نشانه اولیه: لاگ‌ها یا خاموش‌اند یا آن‌قدر پرحجم هستند که Query کند در میان نویز گم می‌شود. نسخه پیشنهادی سطح لاگ، TagWith، ToQueryString و هم‌بستگی درخواست را هدفمند به کار بگیرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.ConfigureWarnings(w =>     w.Log(RelationalEventId.MultipleCollectionIncludeWarning));

نمونه 040: مشاهده SQL، Logging و برچسب‌گذاری Query در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command، زمان هر Command، اندازه پارامتر و شناسه Trace را بررسی کنید. نمایش داده حساس و پارامترهای واقعی را فقط در محیط امن و برای بازه کوتاه فعال کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

options.LogTo(     message => logger.LogInformation("{EfMessage}", message),     new[] { DbLoggerCategory.Database.Command.Name },     LogLevel.Information);

فصل 3: Projection و دریافت فقط ستون‌های لازم#

در فصل 3 تمرکز بر «Projection و دریافت فقط ستون‌های لازم» است. اصل مرکزی این فصل چنین است: هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Select محدود، پهنای ردیف و فشار شبکه و حافظه را کم می‌کند و گاهی Index پوششی را ممکن می‌سازد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 041: Projection و دریافت فقط ستون‌های لازم در فهرست محصولات#

در فهرست محصولات اصل «هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking()     .Where(x => x.IsActive)     .Select(x => new ProductListItem(x.Id, x.Name, x.CreatedAt))     .Take(10).ToListAsync(ct);

نمونه 042: Projection و دریافت فقط ستون‌های لازم در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. راهبرد نمونه این است که با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. SQL و پارامترها را ببینید و مقایسه را با حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var names = await db.Orders.AsNoTracking()     .OrderBy(x => x.Id).Select(x => x.Name)     .Take(20).ToArrayAsync(ct);

نمونه 043: Projection و دریافت فقط ستون‌های لازم در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Select محدود، پهنای ردیف و فشار شبکه و حافظه را کم می‌کند و گاهی Index پوششی را ممکن می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Customers.AsNoTracking()     .Select(x => new { x.Id, x.Name, ChildCount = x.Children.Count() })     .Take(30).ToListAsync(ct);

نمونه 044: Projection و دریافت فقط ستون‌های لازم در مدیریت وبلاگ‌ها#

نمونه 44 برای مدیریت وبلاگ‌ها است. نشانه اولیه: صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. نسخه پیشنهادی با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Blogs.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Select(x => new BlogDto { Id = x.Id, Name = x.Name })     .ToListAsync(ct);

نمونه 045: Projection و دریافت فقط ستون‌های لازم در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var summary = await db.Posts.AsNoTracking()     .GroupBy(x => x.IsActive)     .Select(g => new { g.Key, Count = g.Count() })     .ToListAsync(ct);

نمونه 046: Projection و دریافت فقط ستون‌های لازم در گزارش کارکنان#

در گزارش کارکنان اصل «هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking()     .Where(x => x.IsActive)     .Select(x => new EmployeeListItem(x.Id, x.Name, x.CreatedAt))     .Take(10).ToListAsync(ct);

نمونه 047: Projection و دریافت فقط ستون‌های لازم در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. راهبرد نمونه این است که با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. SQL و پارامترها را ببینید و مقایسه را با حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var names = await db.Invoices.AsNoTracking()     .OrderBy(x => x.Id).Select(x => x.Name)     .Take(20).ToArrayAsync(ct);

نمونه 048: Projection و دریافت فقط ستون‌های لازم در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Select محدود، پهنای ردیف و فشار شبکه و حافظه را کم می‌کند و گاهی Index پوششی را ممکن می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Tickets.AsNoTracking()     .Select(x => new { x.Id, x.Name, ChildCount = x.Children.Count() })     .Take(30).ToListAsync(ct);

نمونه 049: Projection و دریافت فقط ستون‌های لازم در صندوق پیام‌ها#

نمونه 49 برای صندوق پیام‌ها است. نشانه اولیه: صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. نسخه پیشنهادی با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Messages.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Select(x => new MessageDto { Id = x.Id, Name = x.Name })     .ToListAsync(ct);

نمونه 050: Projection و دریافت فقط ستون‌های لازم در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var summary = await db.Events.AsNoTracking()     .GroupBy(x => x.IsActive)     .Select(g => new { g.Key, Count = g.Count() })     .ToListAsync(ct);

نمونه 051: Projection و دریافت فقط ستون‌های لازم در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking()     .Where(x => x.IsActive)     .Select(x => new DocumentListItem(x.Id, x.Name, x.CreatedAt))     .Take(10).ToListAsync(ct);

نمونه 052: Projection و دریافت فقط ستون‌های لازم در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. راهبرد نمونه این است که با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. SQL و پارامترها را ببینید و مقایسه را با حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var names = await db.Payments.AsNoTracking()     .OrderBy(x => x.Id).Select(x => x.Name)     .Take(20).ToArrayAsync(ct);

نمونه 053: Projection و دریافت فقط ستون‌های لازم در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Select محدود، پهنای ردیف و فشار شبکه و حافظه را کم می‌کند و گاهی Index پوششی را ممکن می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Shipments.AsNoTracking()     .Select(x => new { x.Id, x.Name, ChildCount = x.Children.Count() })     .Take(30).ToListAsync(ct);

نمونه 054: Projection و دریافت فقط ستون‌های لازم در مرور رخدادها#

نمونه 54 برای مرور رخدادها است. نشانه اولیه: صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. نسخه پیشنهادی با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.LogEntries.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Select(x => new LogEntryDto { Id = x.Id, Name = x.Name })     .ToListAsync(ct);

نمونه 055: Projection و دریافت فقط ستون‌های لازم در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var summary = await db.AuditRecords.AsNoTracking()     .GroupBy(x => x.IsActive)     .Select(g => new { g.Key, Count = g.Count() })     .ToListAsync(ct);

نمونه 056: Projection و دریافت فقط ستون‌های لازم در نشست‌های فعال#

در نشست‌های فعال اصل «هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking()     .Where(x => x.IsActive)     .Select(x => new SessionListItem(x.Id, x.Name, x.CreatedAt))     .Take(10).ToListAsync(ct);

نمونه 057: Projection و دریافت فقط ستون‌های لازم در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. راهبرد نمونه این است که با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. SQL و پارامترها را ببینید و مقایسه را با حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var names = await db.Courses.AsNoTracking()     .OrderBy(x => x.Id).Select(x => x.Name)     .Take(20).ToArrayAsync(ct);

نمونه 058: Projection و دریافت فقط ستون‌های لازم در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Select محدود، پهنای ردیف و فشار شبکه و حافظه را کم می‌کند و گاهی Index پوششی را ممکن می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر Query باید کوچک‌ترین شکل داده متناسب با مصرف‌کننده را برگرداند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Enrollments.AsNoTracking()     .Select(x => new { x.Id, x.Name, ChildCount = x.Children.Count() })     .Take(30).ToListAsync(ct);

نمونه 059: Projection و دریافت فقط ستون‌های لازم در فهرست مقاله‌ها#

نمونه 59 برای فهرست مقاله‌ها است. نشانه اولیه: صفحه‌ای با چند فیلد، Entity کامل و ستون‌های حجیم متن یا باینری را می‌خواند. نسخه پیشنهادی با Select به DTO، رکورد یا مقدار Scalar نگاشت کنید و محاسبه قابل ترجمه را به سرور بسپارید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Articles.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Select(x => new ArticleDto { Id = x.Id, Name = x.Name })     .ToListAsync(ct);

نمونه 060: Projection و دریافت فقط ستون‌های لازم در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: حجم Result Set، Allocated Bytes، زمان Materialization و تعداد ستون‌ها را مقایسه کنید. Projection پیچیده‌ای که بخشی از آن روی Client اجرا شود باید با SQL و تست ترجمه کنترل شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var summary = await db.Notifications.AsNoTracking()     .GroupBy(x => x.IsActive)     .Select(g => new { g.Key, Count = g.Count() })     .ToListAsync(ct);

فصل 4: Tracking، NoTracking و Identity Resolution#

در فصل 4 تمرکز بر «Tracking، NoTracking و Identity Resolution» است. اصل مرکزی این فصل چنین است: ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، NoTracking SQL را الزاماً عوض نمی‌کند اما هزینه Snapshot، Fix-up و نگهداری Entity را کاهش می‌دهد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 061: Tracking، NoTracking و Identity Resolution در فهرست محصولات#

در فهرست محصولات اصل «ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Take(10).ToListAsync(ct);

نمونه 062: Tracking، NoTracking و Identity Resolution در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. راهبرد نمونه این است که برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

نمونه 063: Tracking، NoTracking و Identity Resolution در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا NoTracking SQL را الزاماً عوض نمی‌کند اما هزینه Snapshot، Fix-up و نگهداری Entity را کاهش می‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var graph = await db.Customers.AsNoTrackingWithIdentityResolution()     .Include(x => x.Children).Take(30).ToListAsync(ct);

نمونه 064: Tracking، NoTracking و Identity Resolution در مدیریت وبلاگ‌ها#

نمونه 64 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. نسخه پیشنهادی برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var item = new Blog { Id = id }; db.Attach(item); item.Name = newName; db.Entry(item).Property(x => x.Name).IsModified = true; await db.SaveChangesAsync(ct);

نمونه 065: Tracking، NoTracking و Identity Resolution در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var trackedCount = db.ChangeTracker.Entries<Post>().Count(); logger.LogInformation("Tracked Post count: {Count}", trackedCount);

نمونه 066: Tracking، NoTracking و Identity Resolution در گزارش کارکنان#

در گزارش کارکنان اصل «ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Take(10).ToListAsync(ct);

نمونه 067: Tracking، NoTracking و Identity Resolution در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. راهبرد نمونه این است که برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

نمونه 068: Tracking، NoTracking و Identity Resolution در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا NoTracking SQL را الزاماً عوض نمی‌کند اما هزینه Snapshot، Fix-up و نگهداری Entity را کاهش می‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var graph = await db.Tickets.AsNoTrackingWithIdentityResolution()     .Include(x => x.Children).Take(30).ToListAsync(ct);

نمونه 069: Tracking، NoTracking و Identity Resolution در صندوق پیام‌ها#

نمونه 69 برای صندوق پیام‌ها است. نشانه اولیه: Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. نسخه پیشنهادی برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var item = new Message { Id = id }; db.Attach(item); item.Name = newName; db.Entry(item).Property(x => x.Name).IsModified = true; await db.SaveChangesAsync(ct);

نمونه 070: Tracking، NoTracking و Identity Resolution در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var trackedCount = db.ChangeTracker.Entries<Event>().Count(); logger.LogInformation("Tracked Event count: {Count}", trackedCount);

نمونه 071: Tracking، NoTracking و Identity Resolution در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Take(10).ToListAsync(ct);

نمونه 072: Tracking، NoTracking و Identity Resolution در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. راهبرد نمونه این است که برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

نمونه 073: Tracking، NoTracking و Identity Resolution در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا NoTracking SQL را الزاماً عوض نمی‌کند اما هزینه Snapshot، Fix-up و نگهداری Entity را کاهش می‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var graph = await db.Shipments.AsNoTrackingWithIdentityResolution()     .Include(x => x.Children).Take(30).ToListAsync(ct);

نمونه 074: Tracking، NoTracking و Identity Resolution در مرور رخدادها#

نمونه 74 برای مرور رخدادها است. نشانه اولیه: Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. نسخه پیشنهادی برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var item = new LogEntry { Id = id }; db.Attach(item); item.Name = newName; db.Entry(item).Property(x => x.Name).IsModified = true; await db.SaveChangesAsync(ct);

نمونه 075: Tracking، NoTracking و Identity Resolution در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var trackedCount = db.ChangeTracker.Entries<AuditRecord>().Count(); logger.LogInformation("Tracked AuditRecord count: {Count}", trackedCount);

نمونه 076: Tracking، NoTracking و Identity Resolution در نشست‌های فعال#

در نشست‌های فعال اصل «ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking()     .Where(x => x.TenantId == tenantId)     .Take(10).ToListAsync(ct);

نمونه 077: Tracking، NoTracking و Identity Resolution در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. راهبرد نمونه این است که برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

نمونه 078: Tracking، NoTracking و Identity Resolution در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا NoTracking SQL را الزاماً عوض نمی‌کند اما هزینه Snapshot، Fix-up و نگهداری Entity را کاهش می‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ردیابی تغییر فقط زمانی ارزش دارد که همان Entity در همان Unit of Work ویرایش شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var graph = await db.Enrollments.AsNoTrackingWithIdentityResolution()     .Include(x => x.Children).Take(30).ToListAsync(ct);

نمونه 079: Tracking، NoTracking و Identity Resolution در فهرست مقاله‌ها#

نمونه 79 برای فهرست مقاله‌ها است. نشانه اولیه: Query خواندنی هزاران Entity را وارد ChangeTracker می‌کند و حافظه و CPU بالا می‌رود. نسخه پیشنهادی برای خواندن از AsNoTracking و در گراف‌های تکراری از AsNoTrackingWithIdentityResolution استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var item = new Article { Id = id }; db.Attach(item); item.Name = newName; db.Entry(item).Property(x => x.Name).IsModified = true; await db.SaveChangesAsync(ct);

نمونه 080: Tracking، NoTracking و Identity Resolution در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Entryهای ChangeTracker، Gen0/Gen1، Allocated Bytes و زمان Materialization را بسنجید. NoTracking را کورکورانه برای جریانی که بعداً SaveChanges دارد فعال نکنید؛ مرز خواندن و نوشتن را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var trackedCount = db.ChangeTracker.Entries<Notification>().Count(); logger.LogInformation("Tracked Notification count: {Count}", trackedCount);

فصل 5: بارگذاری داده‌های مرتبط و Include هدفمند#

در فصل 5 تمرکز بر «بارگذاری داده‌های مرتبط و Include هدفمند» است. اصل مرکزی این فصل چنین است: رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Include می‌تواند Join یا Query جدا بسازد؛ شکل دقیق SQL و Cardinality تعیین‌کننده است. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 081: بارگذاری داده‌های مرتبط و Include هدفمند در فهرست محصولات#

در فهرست محصولات اصل «رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive).OrderBy(c => c.Id).Take(5))     .Take(10).ToListAsync(ct);

نمونه 082: بارگذاری داده‌های مرتبط و Include هدفمند در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. راهبرد نمونه این است که Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Orders.AsNoTracking()     .Select(x => new { x.Id, x.Name, Children = x.Children.Where(c => c.IsActive).Select(c => c.Name).Take(5) })     .Take(20).ToListAsync(ct);

نمونه 083: بارگذاری داده‌های مرتبط و Include هدفمند در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Include می‌تواند Join یا Query جدا بسازد؛ شکل دقیق SQL و Cardinality تعیین‌کننده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var item = await db.Customers.SingleAsync(x => x.Id == id, ct); await db.Entry(item).Collection(x => x.Children).Query()     .Where(c => c.IsActive).LoadAsync(ct);

نمونه 084: بارگذاری داده‌های مرتبط و Include هدفمند در مدیریت وبلاگ‌ها#

نمونه 84 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. نسخه پیشنهادی Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Blogs.IgnoreAutoIncludes()     .AsNoTracking().Take(40).ToListAsync(ct);

نمونه 085: بارگذاری داده‌های مرتبط و Include هدفمند در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Posts.AsNoTracking()     .Include(x => x.Owner)     .OrderByDescending(x => x.CreatedAt).Take(50).ToListAsync(ct);

نمونه 086: بارگذاری داده‌های مرتبط و Include هدفمند در گزارش کارکنان#

در گزارش کارکنان اصل «رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive).OrderBy(c => c.Id).Take(5))     .Take(10).ToListAsync(ct);

نمونه 087: بارگذاری داده‌های مرتبط و Include هدفمند در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. راهبرد نمونه این است که Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Invoices.AsNoTracking()     .Select(x => new { x.Id, x.Name, Children = x.Children.Where(c => c.IsActive).Select(c => c.Name).Take(5) })     .Take(20).ToListAsync(ct);

نمونه 088: بارگذاری داده‌های مرتبط و Include هدفمند در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Include می‌تواند Join یا Query جدا بسازد؛ شکل دقیق SQL و Cardinality تعیین‌کننده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var item = await db.Tickets.SingleAsync(x => x.Id == id, ct); await db.Entry(item).Collection(x => x.Children).Query()     .Where(c => c.IsActive).LoadAsync(ct);

نمونه 089: بارگذاری داده‌های مرتبط و Include هدفمند در صندوق پیام‌ها#

نمونه 89 برای صندوق پیام‌ها است. نشانه اولیه: Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. نسخه پیشنهادی Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Messages.IgnoreAutoIncludes()     .AsNoTracking().Take(40).ToListAsync(ct);

نمونه 090: بارگذاری داده‌های مرتبط و Include هدفمند در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Events.AsNoTracking()     .Include(x => x.Owner)     .OrderByDescending(x => x.CreatedAt).Take(50).ToListAsync(ct);

نمونه 091: بارگذاری داده‌های مرتبط و Include هدفمند در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive).OrderBy(c => c.Id).Take(5))     .Take(10).ToListAsync(ct);

نمونه 092: بارگذاری داده‌های مرتبط و Include هدفمند در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. راهبرد نمونه این است که Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Payments.AsNoTracking()     .Select(x => new { x.Id, x.Name, Children = x.Children.Where(c => c.IsActive).Select(c => c.Name).Take(5) })     .Take(20).ToListAsync(ct);

نمونه 093: بارگذاری داده‌های مرتبط و Include هدفمند در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Include می‌تواند Join یا Query جدا بسازد؛ شکل دقیق SQL و Cardinality تعیین‌کننده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var item = await db.Shipments.SingleAsync(x => x.Id == id, ct); await db.Entry(item).Collection(x => x.Children).Query()     .Where(c => c.IsActive).LoadAsync(ct);

نمونه 094: بارگذاری داده‌های مرتبط و Include هدفمند در مرور رخدادها#

نمونه 94 برای مرور رخدادها است. نشانه اولیه: Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. نسخه پیشنهادی Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.LogEntries.IgnoreAutoIncludes()     .AsNoTracking().Take(40).ToListAsync(ct);

نمونه 095: بارگذاری داده‌های مرتبط و Include هدفمند در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.AuditRecords.AsNoTracking()     .Include(x => x.Owner)     .OrderByDescending(x => x.CreatedAt).Take(50).ToListAsync(ct);

نمونه 096: بارگذاری داده‌های مرتبط و Include هدفمند در نشست‌های فعال#

در نشست‌های فعال اصل «رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive).OrderBy(c => c.Id).Take(5))     .Take(10).ToListAsync(ct);

نمونه 097: بارگذاری داده‌های مرتبط و Include هدفمند در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. راهبرد نمونه این است که Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Courses.AsNoTracking()     .Select(x => new { x.Id, x.Name, Children = x.Children.Where(c => c.IsActive).Select(c => c.Name).Take(5) })     .Take(20).ToListAsync(ct);

نمونه 098: بارگذاری داده‌های مرتبط و Include هدفمند در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Include می‌تواند Join یا Query جدا بسازد؛ شکل دقیق SQL و Cardinality تعیین‌کننده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: رابطه‌ها باید متناسب با شکل خروجی و در کمترین حجم لازم بارگذاری شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var item = await db.Enrollments.SingleAsync(x => x.Id == id, ct); await db.Entry(item).Collection(x => x.Children).Query()     .Where(c => c.IsActive).LoadAsync(ct);

نمونه 099: بارگذاری داده‌های مرتبط و Include هدفمند در فهرست مقاله‌ها#

نمونه 99 برای فهرست مقاله‌ها است. نشانه اولیه: Includeهای زنجیره‌ای گراف بزرگ می‌سازند یا داده‌های غیرمصرفی را از دیتابیس می‌کشند. نسخه پیشنهادی Projection، Filtered Include، Explicit Loading و AutoInclude کنترل‌شده را بر اساس سناریو انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Articles.IgnoreAutoIncludes()     .AsNoTracking().Take(40).ToListAsync(ct);

نمونه 100: بارگذاری داده‌های مرتبط و Include هدفمند در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف فیزیکی، اندازه پاسخ، تعداد Navigation و زمان Fix-up را اندازه بگیرید. Filtered Include روی Context ردیابی‌شونده ممکن است به دلیل Navigation Fix-up نتایج قبلی را وارد مجموعه کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Notifications.AsNoTracking()     .Include(x => x.Owner)     .OrderByDescending(x => x.CreatedAt).Take(50).ToListAsync(ct);

فصل 6: Split Query و جلوگیری از انفجار ضرب دکارتی#

در فصل 6 تمرکز بر «Split Query و جلوگیری از انفجار ضرب دکارتی» است. اصل مرکزی این فصل چنین است: بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Split Query تکثیر داده را کم می‌کند اما چند Command اجرا می‌کند و هزینه تأخیر شبکه دارد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 101: Split Query و جلوگیری از انفجار ضرب دکارتی در فهرست محصولات#

در فهرست محصولات اصل «بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking().AsSplitQuery()     .Include(x => x.Children).Include(x => x.Tags)     .Take(10).ToListAsync(ct);

نمونه 102: Split Query و جلوگیری از انفجار ضرب دکارتی در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. راهبرد نمونه این است که AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Orders.AsNoTracking()     .Select(x => new { x.Id, ChildIds = x.Children.Select(c => c.Id), TagNames = x.Tags.Select(t => t.Name) })     .Take(20).ToListAsync(ct);

نمونه 103: Split Query و جلوگیری از انفجار ضرب دکارتی در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Split Query تکثیر داده را کم می‌کند اما چند Command اجرا می‌کند و هزینه تأخیر شبکه دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

options.UseSqlServer(connectionString, sql =>     sql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));

نمونه 104: Split Query و جلوگیری از انفجار ضرب دکارتی در مدیریت وبلاگ‌ها#

نمونه 104 برای مدیریت وبلاگ‌ها است. نشانه اولیه: دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. نسخه پیشنهادی AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Blogs.AsSingleQuery()     .Include(x => x.Owner).Take(40).ToListAsync(ct);

نمونه 105: Split Query و جلوگیری از انفجار ضرب دکارتی در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var ids = await db.Posts.OrderBy(x => x.Id).Select(x => x.Id).Take(50).ToListAsync(ct); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 106: Split Query و جلوگیری از انفجار ضرب دکارتی در گزارش کارکنان#

در گزارش کارکنان اصل «بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking().AsSplitQuery()     .Include(x => x.Children).Include(x => x.Tags)     .Take(10).ToListAsync(ct);

نمونه 107: Split Query و جلوگیری از انفجار ضرب دکارتی در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. راهبرد نمونه این است که AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Invoices.AsNoTracking()     .Select(x => new { x.Id, ChildIds = x.Children.Select(c => c.Id), TagNames = x.Tags.Select(t => t.Name) })     .Take(20).ToListAsync(ct);

نمونه 108: Split Query و جلوگیری از انفجار ضرب دکارتی در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Split Query تکثیر داده را کم می‌کند اما چند Command اجرا می‌کند و هزینه تأخیر شبکه دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

options.UseSqlServer(connectionString, sql =>     sql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));

نمونه 109: Split Query و جلوگیری از انفجار ضرب دکارتی در صندوق پیام‌ها#

نمونه 109 برای صندوق پیام‌ها است. نشانه اولیه: دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. نسخه پیشنهادی AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Messages.AsSingleQuery()     .Include(x => x.Owner).Take(40).ToListAsync(ct);

نمونه 110: Split Query و جلوگیری از انفجار ضرب دکارتی در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var ids = await db.Events.OrderBy(x => x.Id).Select(x => x.Id).Take(50).ToListAsync(ct); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 111: Split Query و جلوگیری از انفجار ضرب دکارتی در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking().AsSplitQuery()     .Include(x => x.Children).Include(x => x.Tags)     .Take(10).ToListAsync(ct);

نمونه 112: Split Query و جلوگیری از انفجار ضرب دکارتی در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. راهبرد نمونه این است که AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Payments.AsNoTracking()     .Select(x => new { x.Id, ChildIds = x.Children.Select(c => c.Id), TagNames = x.Tags.Select(t => t.Name) })     .Take(20).ToListAsync(ct);

نمونه 113: Split Query و جلوگیری از انفجار ضرب دکارتی در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Split Query تکثیر داده را کم می‌کند اما چند Command اجرا می‌کند و هزینه تأخیر شبکه دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

options.UseSqlServer(connectionString, sql =>     sql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));

نمونه 114: Split Query و جلوگیری از انفجار ضرب دکارتی در مرور رخدادها#

نمونه 114 برای مرور رخدادها است. نشانه اولیه: دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. نسخه پیشنهادی AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.LogEntries.AsSingleQuery()     .Include(x => x.Owner).Take(40).ToListAsync(ct);

نمونه 115: Split Query و جلوگیری از انفجار ضرب دکارتی در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var ids = await db.AuditRecords.OrderBy(x => x.Id).Select(x => x.Id).Take(50).ToListAsync(ct); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 116: Split Query و جلوگیری از انفجار ضرب دکارتی در نشست‌های فعال#

در نشست‌های فعال اصل «بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking().AsSplitQuery()     .Include(x => x.Children).Include(x => x.Tags)     .Take(10).ToListAsync(ct);

نمونه 117: Split Query و جلوگیری از انفجار ضرب دکارتی در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. راهبرد نمونه این است که AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Courses.AsNoTracking()     .Select(x => new { x.Id, ChildIds = x.Children.Select(c => c.Id), TagNames = x.Tags.Select(t => t.Name) })     .Take(20).ToListAsync(ct);

نمونه 118: Split Query و جلوگیری از انفجار ضرب دکارتی در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Split Query تکثیر داده را کم می‌کند اما چند Command اجرا می‌کند و هزینه تأخیر شبکه دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: بارگذاری چند Collection هم‌سطح باید از نظر تکثیر ردیف تحلیل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

options.UseSqlServer(connectionString, sql =>     sql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));

نمونه 119: Split Query و جلوگیری از انفجار ضرب دکارتی در فهرست مقاله‌ها#

نمونه 119 برای فهرست مقاله‌ها است. نشانه اولیه: دو Include مجموعه‌ای باعث می‌شوند هر ردیف والد به حاصل‌ضرب تعداد فرزندان تبدیل شود. نسخه پیشنهادی AsSplitQuery، Projection یا چند Query هماهنگ را با توجه به Roundtrip و سازگاری داده ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Articles.AsSingleQuery()     .Include(x => x.Owner).Take(40).ToListAsync(ct);

نمونه 120: Split Query و جلوگیری از انفجار ضرب دکارتی در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف انتقالی، بایت شبکه، زمان SQL و Roundtrip را مقایسه کنید. در صفحه‌بندی و نسخه‌های قدیمی، Order یکتا و سازگاری میان Queryهای جدا را جدی بگیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var ids = await db.Notifications.OrderBy(x => x.Id).Select(x => x.Id).Take(50).ToListAsync(ct); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

فصل 7: حذف الگوی N+1 و کنترل Lazy Loading#

در فصل 7 تمرکز بر «حذف الگوی N+1 و کنترل Lazy Loading» است. اصل مرکزی این فصل چنین است: دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، N+1 معمولاً SQLهای کوتاه ولی بسیار پرتعداد تولید می‌کند و هزینه شبکه را غالب می‌سازد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 121: حذف الگوی N+1 و کنترل Lazy Loading در فهرست محصولات#

در فهرست محصولات اصل «دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking()     .Select(x => new { x.Id, x.Name, ActiveChildren = x.Children.Count(c => c.IsActive) })     .Take(10).ToListAsync(ct);

نمونه 122: حذف الگوی N+1 و کنترل Lazy Loading در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. راهبرد نمونه این است که داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var parents = await db.Orders.AsNoTracking().Take(20).ToListAsync(ct); var ids = parents.Select(x => x.Id).ToArray(); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 123: حذف الگوی N+1 و کنترل Lazy Loading در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا N+1 معمولاً SQLهای کوتاه ولی بسیار پرتعداد تولید می‌کند و هزینه شبکه را غالب می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Customers.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive))     .Take(30).ToListAsync(ct);

نمونه 124: حذف الگوی N+1 و کنترل Lazy Loading در مدیریت وبلاگ‌ها#

نمونه 124 برای مدیریت وبلاگ‌ها است. نشانه اولیه: یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. نسخه پیشنهادی داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Blogs.AsNoTracking()     .GroupJoin(db.Set<Child>(), x => x.Id, c => c.ParentId, (x, children) => new { x.Id, Count = children.Count() })     .Take(40).ToListAsync(ct);

نمونه 125: حذف الگوی N+1 و کنترل Lazy Loading در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// در پروژه‌های حساس به Roundtrip، Lazy Loading را فعال نکنید. // services.AddDbContext<AppDbContext>(o => o.UseLazyLoadingProxies()); // حذف services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

نمونه 126: حذف الگوی N+1 و کنترل Lazy Loading در گزارش کارکنان#

در گزارش کارکنان اصل «دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking()     .Select(x => new { x.Id, x.Name, ActiveChildren = x.Children.Count(c => c.IsActive) })     .Take(10).ToListAsync(ct);

نمونه 127: حذف الگوی N+1 و کنترل Lazy Loading در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. راهبرد نمونه این است که داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var parents = await db.Invoices.AsNoTracking().Take(20).ToListAsync(ct); var ids = parents.Select(x => x.Id).ToArray(); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 128: حذف الگوی N+1 و کنترل Lazy Loading در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا N+1 معمولاً SQLهای کوتاه ولی بسیار پرتعداد تولید می‌کند و هزینه شبکه را غالب می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Tickets.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive))     .Take(30).ToListAsync(ct);

نمونه 129: حذف الگوی N+1 و کنترل Lazy Loading در صندوق پیام‌ها#

نمونه 129 برای صندوق پیام‌ها است. نشانه اولیه: یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. نسخه پیشنهادی داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Messages.AsNoTracking()     .GroupJoin(db.Set<Child>(), x => x.Id, c => c.ParentId, (x, children) => new { x.Id, Count = children.Count() })     .Take(40).ToListAsync(ct);

نمونه 130: حذف الگوی N+1 و کنترل Lazy Loading در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// در پروژه‌های حساس به Roundtrip، Lazy Loading را فعال نکنید. // services.AddDbContext<AppDbContext>(o => o.UseLazyLoadingProxies()); // حذف services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

نمونه 131: حذف الگوی N+1 و کنترل Lazy Loading در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking()     .Select(x => new { x.Id, x.Name, ActiveChildren = x.Children.Count(c => c.IsActive) })     .Take(10).ToListAsync(ct);

نمونه 132: حذف الگوی N+1 و کنترل Lazy Loading در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. راهبرد نمونه این است که داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var parents = await db.Payments.AsNoTracking().Take(20).ToListAsync(ct); var ids = parents.Select(x => x.Id).ToArray(); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 133: حذف الگوی N+1 و کنترل Lazy Loading در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا N+1 معمولاً SQLهای کوتاه ولی بسیار پرتعداد تولید می‌کند و هزینه شبکه را غالب می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Shipments.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive))     .Take(30).ToListAsync(ct);

نمونه 134: حذف الگوی N+1 و کنترل Lazy Loading در مرور رخدادها#

نمونه 134 برای مرور رخدادها است. نشانه اولیه: یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. نسخه پیشنهادی داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.LogEntries.AsNoTracking()     .GroupJoin(db.Set<Child>(), x => x.Id, c => c.ParentId, (x, children) => new { x.Id, Count = children.Count() })     .Take(40).ToListAsync(ct);

نمونه 135: حذف الگوی N+1 و کنترل Lazy Loading در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// در پروژه‌های حساس به Roundtrip، Lazy Loading را فعال نکنید. // services.AddDbContext<AppDbContext>(o => o.UseLazyLoadingProxies()); // حذف services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

نمونه 136: حذف الگوی N+1 و کنترل Lazy Loading در نشست‌های فعال#

در نشست‌های فعال اصل «دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking()     .Select(x => new { x.Id, x.Name, ActiveChildren = x.Children.Count(c => c.IsActive) })     .Take(10).ToListAsync(ct);

نمونه 137: حذف الگوی N+1 و کنترل Lazy Loading در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. راهبرد نمونه این است که داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. SQL و پارامترها را ببینید و مقایسه را با تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var parents = await db.Courses.AsNoTracking().Take(20).ToListAsync(ct); var ids = parents.Select(x => x.Id).ToArray(); var children = await db.Set<Child>().Where(x => ids.Contains(x.ParentId)).ToListAsync(ct);

نمونه 138: حذف الگوی N+1 و کنترل Lazy Loading در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا N+1 معمولاً SQLهای کوتاه ولی بسیار پرتعداد تولید می‌کند و هزینه شبکه را غالب می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: دسترسی به Navigation در حلقه نباید برای هر ردیف یک Query مخفی ایجاد کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Enrollments.AsNoTracking()     .Include(x => x.Children.Where(c => c.IsActive))     .Take(30).ToListAsync(ct);

نمونه 139: حذف الگوی N+1 و کنترل Lazy Loading در فهرست مقاله‌ها#

نمونه 139 برای فهرست مقاله‌ها است. نشانه اولیه: یک Query فهرست و سپس ده‌ها یا صدها Query کوچک برای فرزندان اجرا می‌شود. نسخه پیشنهادی داده را به‌صورت Batch با Projection، Include هدفمند، GroupBy یا Query دوم بر اساس کلیدها بخوانید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var rows = await db.Articles.AsNoTracking()     .GroupJoin(db.Set<Child>(), x => x.Id, c => c.ParentId, (x, children) => new { x.Id, Count = children.Count() })     .Take(40).ToListAsync(ct);

نمونه 140: حذف الگوی N+1 و کنترل Lazy Loading در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Command در هر Request و توزیع زمانی آن‌ها را ثبت کنید. Lazy Loading را اگر فعال است در Serialization، حلقه و لاگ‌نویسی با حساسیت ویژه کنترل کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// در پروژه‌های حساس به Roundtrip، Lazy Loading را فعال نکنید. // services.AddDbContext<AppDbContext>(o => o.UseLazyLoadingProxies()); // حذف services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));

فصل 8: صفحه‌بندی Offset، Keyset و Cursor#

در فصل 8 تمرکز بر «صفحه‌بندی Offset، Keyset و Cursor» است. اصل مرکزی این فصل چنین است: برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، شرط روی ستون‌های مرتب‌شده می‌تواند Seek بسازد، در حالی‌که OFFSET معمولاً هزینه‌ای رو به رشد دارد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 141: صفحه‌بندی Offset، Keyset و Cursor در فهرست محصولات#

در فهرست محصولات اصل «برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var page = await db.Products.AsNoTracking()     .Where(x => x.Id > lastId)     .OrderBy(x => x.Id).Take(10).ToListAsync(ct);

نمونه 142: صفحه‌بندی Offset، Keyset و Cursor در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. راهبرد نمونه این است که برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var page = await db.Orders.AsNoTracking()     .Where(x => x.CreatedAt > lastDate || (x.CreatedAt == lastDate && x.Id > lastId))     .OrderBy(x => x.CreatedAt).ThenBy(x => x.Id).Take(20).ToListAsync(ct);

نمونه 143: صفحه‌بندی Offset، Keyset و Cursor در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا شرط روی ستون‌های مرتب‌شده می‌تواند Seek بسازد، در حالی‌که OFFSET معمولاً هزینه‌ای رو به رشد دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var firstPage = await db.Customers.AsNoTracking()     .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)     .Take(30).ToListAsync(ct);

نمونه 144: صفحه‌بندی Offset، Keyset و Cursor در مدیریت وبلاگ‌ها#

نمونه 144 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. نسخه پیشنهادی برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var previous = await db.Blogs.AsNoTracking()     .Where(x => x.Id < firstId).OrderByDescending(x => x.Id)     .Take(40).ToListAsync(ct);

نمونه 145: صفحه‌بندی Offset، Keyset و Cursor در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var page = await db.Posts.AsNoTracking()     .OrderBy(x => x.Id).Skip((pageNumber - 1) * 50).Take(50)     .ToListAsync(ct); // فقط برای عمق کم یا پرش مستقیم

نمونه 146: صفحه‌بندی Offset، Keyset و Cursor در گزارش کارکنان#

در گزارش کارکنان اصل «برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var page = await db.Employees.AsNoTracking()     .Where(x => x.Id > lastId)     .OrderBy(x => x.Id).Take(10).ToListAsync(ct);

نمونه 147: صفحه‌بندی Offset، Keyset و Cursor در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. راهبرد نمونه این است که برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var page = await db.Invoices.AsNoTracking()     .Where(x => x.CreatedAt > lastDate || (x.CreatedAt == lastDate && x.Id > lastId))     .OrderBy(x => x.CreatedAt).ThenBy(x => x.Id).Take(20).ToListAsync(ct);

نمونه 148: صفحه‌بندی Offset، Keyset و Cursor در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا شرط روی ستون‌های مرتب‌شده می‌تواند Seek بسازد، در حالی‌که OFFSET معمولاً هزینه‌ای رو به رشد دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var firstPage = await db.Tickets.AsNoTracking()     .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)     .Take(30).ToListAsync(ct);

نمونه 149: صفحه‌بندی Offset، Keyset و Cursor در صندوق پیام‌ها#

نمونه 149 برای صندوق پیام‌ها است. نشانه اولیه: Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. نسخه پیشنهادی برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var previous = await db.Messages.AsNoTracking()     .Where(x => x.Id < firstId).OrderByDescending(x => x.Id)     .Take(40).ToListAsync(ct);

نمونه 150: صفحه‌بندی Offset، Keyset و Cursor در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var page = await db.Events.AsNoTracking()     .OrderBy(x => x.Id).Skip((pageNumber - 1) * 50).Take(50)     .ToListAsync(ct); // فقط برای عمق کم یا پرش مستقیم

نمونه 151: صفحه‌بندی Offset، Keyset و Cursor در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var page = await db.Documents.AsNoTracking()     .Where(x => x.Id > lastId)     .OrderBy(x => x.Id).Take(10).ToListAsync(ct);

نمونه 152: صفحه‌بندی Offset، Keyset و Cursor در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. راهبرد نمونه این است که برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var page = await db.Payments.AsNoTracking()     .Where(x => x.CreatedAt > lastDate || (x.CreatedAt == lastDate && x.Id > lastId))     .OrderBy(x => x.CreatedAt).ThenBy(x => x.Id).Take(20).ToListAsync(ct);

نمونه 153: صفحه‌بندی Offset، Keyset و Cursor در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا شرط روی ستون‌های مرتب‌شده می‌تواند Seek بسازد، در حالی‌که OFFSET معمولاً هزینه‌ای رو به رشد دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var firstPage = await db.Shipments.AsNoTracking()     .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)     .Take(30).ToListAsync(ct);

نمونه 154: صفحه‌بندی Offset، Keyset و Cursor در مرور رخدادها#

نمونه 154 برای مرور رخدادها است. نشانه اولیه: Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. نسخه پیشنهادی برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var previous = await db.LogEntries.AsNoTracking()     .Where(x => x.Id < firstId).OrderByDescending(x => x.Id)     .Take(40).ToListAsync(ct);

نمونه 155: صفحه‌بندی Offset، Keyset و Cursor در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var page = await db.AuditRecords.AsNoTracking()     .OrderBy(x => x.Id).Skip((pageNumber - 1) * 50).Take(50)     .ToListAsync(ct); // فقط برای عمق کم یا پرش مستقیم

نمونه 156: صفحه‌بندی Offset، Keyset و Cursor در نشست‌های فعال#

در نشست‌های فعال اصل «برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var page = await db.Sessions.AsNoTracking()     .Where(x => x.Id > lastId)     .OrderBy(x => x.Id).Take(10).ToListAsync(ct);

نمونه 157: صفحه‌بندی Offset، Keyset و Cursor در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. راهبرد نمونه این است که برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var page = await db.Courses.AsNoTracking()     .Where(x => x.CreatedAt > lastDate || (x.CreatedAt == lastDate && x.Id > lastId))     .OrderBy(x => x.CreatedAt).ThenBy(x => x.Id).Take(20).ToListAsync(ct);

نمونه 158: صفحه‌بندی Offset، Keyset و Cursor در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا شرط روی ستون‌های مرتب‌شده می‌تواند Seek بسازد، در حالی‌که OFFSET معمولاً هزینه‌ای رو به رشد دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: برای پیمایش عمیق، آخرین کلید دیده‌شده بهتر از رد کردن هزاران ردیف است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var firstPage = await db.Enrollments.AsNoTracking()     .OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)     .Take(30).ToListAsync(ct);

نمونه 159: صفحه‌بندی Offset، Keyset و Cursor در فهرست مقاله‌ها#

نمونه 159 برای فهرست مقاله‌ها است. نشانه اولیه: Skip بزرگ باعث خواندن و Sort ردیف‌هایی می‌شود که در پاسخ نهایی وجود ندارند. نسخه پیشنهادی برای Next/Previous از Keyset با ترتیب کاملاً یکتا و Cursor پایدار استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var previous = await db.Articles.AsNoTracking()     .Where(x => x.Id < firstId).OrderByDescending(x => x.Id)     .Take(40).ToListAsync(ct);

نمونه 160: صفحه‌بندی Offset، Keyset و Cursor در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Logical Read، زمان Sort، ردیف‌های خوانده‌شده و ثبات پاسخ زیر تغییر هم‌زمان را بسنجید. Keyset برای رفتن مستقیم به شماره صفحه دلخواه مناسب نیست و ترتیب مرکب باید دقیق پیاده شود. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var page = await db.Notifications.AsNoTracking()     .OrderBy(x => x.Id).Skip((pageNumber - 1) * 50).Take(50)     .ToListAsync(ct); // فقط برای عمق کم یا پرش مستقیم

فصل 9: طراحی Index بر اساس Query واقعی#

در فصل 9 تمرکز بر «طراحی Index بر اساس Query واقعی» است. اصل مرکزی این فصل چنین است: Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، ترتیب ستون‌های کلید و پوشش ستون‌های خروجی می‌تواند Sort و Lookup را حذف کند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 161: طراحی Index بر اساس Query واقعی در فهرست محصولات#

در فهرست محصولات اصل «Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Product>().HasIndex(x => new { x.TenantId, x.CreatedAt });

نمونه 162: طراحی Index بر اساس Query واقعی در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. راهبرد نمونه این است که Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Order>().HasIndex(x => x.Name).HasDatabaseName("IX_Orders_Name");

نمونه 163: طراحی Index بر اساس Query واقعی در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا ترتیب ستون‌های کلید و پوشش ستون‌های خروجی می‌تواند Sort و Lookup را حذف کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Customer>().HasIndex(x => new { x.TenantId, x.IsActive }).HasFilter("[IsActive] = 1");

نمونه 164: طراحی Index بر اساس Query واقعی در مدیریت وبلاگ‌ها#

نمونه 164 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. نسخه پیشنهادی Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Blog>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IncludeProperties(x => new { x.Id, x.Name });

نمونه 165: طراحی Index بر اساس Query واقعی در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Post>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IsDescending(false, true);

نمونه 166: طراحی Index بر اساس Query واقعی در گزارش کارکنان#

در گزارش کارکنان اصل «Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Employee>().HasIndex(x => new { x.TenantId, x.CreatedAt });

نمونه 167: طراحی Index بر اساس Query واقعی در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. راهبرد نمونه این است که Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Invoice>().HasIndex(x => x.Name).HasDatabaseName("IX_Invoices_Name");

نمونه 168: طراحی Index بر اساس Query واقعی در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا ترتیب ستون‌های کلید و پوشش ستون‌های خروجی می‌تواند Sort و Lookup را حذف کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Ticket>().HasIndex(x => new { x.TenantId, x.IsActive }).HasFilter("[IsActive] = 1");

نمونه 169: طراحی Index بر اساس Query واقعی در صندوق پیام‌ها#

نمونه 169 برای صندوق پیام‌ها است. نشانه اولیه: Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. نسخه پیشنهادی Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Message>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IncludeProperties(x => new { x.Id, x.Name });

نمونه 170: طراحی Index بر اساس Query واقعی در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Event>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IsDescending(false, true);

نمونه 171: طراحی Index بر اساس Query واقعی در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Document>().HasIndex(x => new { x.TenantId, x.CreatedAt });

نمونه 172: طراحی Index بر اساس Query واقعی در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. راهبرد نمونه این است که Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Payment>().HasIndex(x => x.Name).HasDatabaseName("IX_Payments_Name");

نمونه 173: طراحی Index بر اساس Query واقعی در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا ترتیب ستون‌های کلید و پوشش ستون‌های خروجی می‌تواند Sort و Lookup را حذف کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Shipment>().HasIndex(x => new { x.TenantId, x.IsActive }).HasFilter("[IsActive] = 1");

نمونه 174: طراحی Index بر اساس Query واقعی در مرور رخدادها#

نمونه 174 برای مرور رخدادها است. نشانه اولیه: Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. نسخه پیشنهادی Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<LogEntry>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IncludeProperties(x => new { x.Id, x.Name });

نمونه 175: طراحی Index بر اساس Query واقعی در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<AuditRecord>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IsDescending(false, true);

نمونه 176: طراحی Index بر اساس Query واقعی در نشست‌های فعال#

در نشست‌های فعال اصل «Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Session>().HasIndex(x => new { x.TenantId, x.CreatedAt });

نمونه 177: طراحی Index بر اساس Query واقعی در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. راهبرد نمونه این است که Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Course>().HasIndex(x => x.Name).HasDatabaseName("IX_Courses_Name");

نمونه 178: طراحی Index بر اساس Query واقعی در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا ترتیب ستون‌های کلید و پوشش ستون‌های خروجی می‌تواند Sort و Lookup را حذف کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Index باید با Predicate، ترتیب، Selectivity و Order By مسیرهای پرتکرار هم‌راستا باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Enrollment>().HasIndex(x => new { x.TenantId, x.IsActive }).HasFilter("[IsActive] = 1");

نمونه 179: طراحی Index بر اساس Query واقعی در فهرست مقاله‌ها#

نمونه 179 برای فهرست مقاله‌ها است. نشانه اولیه: Indexهای زیاد و تصادفی سرعت نوشتن را کم می‌کنند اما Query اصلی هنوز Scan دارد. نسخه پیشنهادی Index مرکب، Filtered، Descending و Include را از روی Execution Plan و workload طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Article>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IncludeProperties(x => new { x.Id, x.Name });

نمونه 180: طراحی Index بر اساس Query واقعی در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Seek/Scan، Logical Read، Key Lookup، زمان نوشتن و اندازه Index را ثبت کنید. هر Index هزینه Insert، Update، فضای دیسک و نگهداری دارد؛ پیشنهاد خودکار را بدون آزمون نپذیرید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Notification>().HasIndex(x => new { x.TenantId, x.CreatedAt }).IsDescending(false, true);

فصل 10: Query قابل Seek، ترجمه LINQ و SARGability#

در فصل 10 تمرکز بر «Query قابل Seek، ترجمه LINQ و SARGability» است. اصل مرکزی این فصل چنین است: شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، عبارت قابل SARG به موتور اجازه می‌دهد محدوده Index را مستقیم پیدا کند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 181: Query قابل Seek، ترجمه LINQ و SARGability در فهرست محصولات#

در فهرست محصولات اصل «شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.Where(x => x.Name.StartsWith(prefix)).Take(10).ToListAsync(ct);

نمونه 182: Query قابل Seek، ترجمه LINQ و SARGability در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. راهبرد نمونه این است که بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. SQL و پارامترها را ببینید و مقایسه را با Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Orders.Where(x => x.CreatedAt >= from && x.CreatedAt < to).ToListAsync(ct);

نمونه 183: Query قابل Seek، ترجمه LINQ و SARGability در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا عبارت قابل SARG به موتور اجازه می‌دهد محدوده Index را مستقیم پیدا کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Customers.Where(x => EF.Functions.Like(x.Name, prefix + "%")).Take(30).ToListAsync(ct);

نمونه 184: Query قابل Seek، ترجمه LINQ و SARGability در مدیریت وبلاگ‌ها#

نمونه 184 برای مدیریت وبلاگ‌ها است. نشانه اولیه: تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. نسخه پیشنهادی بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Blog>().Property<string>("NormalizedName").HasComputedColumnSql("UPPER([Name])", stored: true); modelBuilder.Entity<Blog>().HasIndex("NormalizedName");

نمونه 185: Query قابل Seek، ترجمه LINQ و SARGability در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Posts.Where(x => x.TenantId == tenantId && x.Id >= minId && x.Id < maxId).ToListAsync(ct);

نمونه 186: Query قابل Seek، ترجمه LINQ و SARGability در گزارش کارکنان#

در گزارش کارکنان اصل «شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.Where(x => x.Name.StartsWith(prefix)).Take(10).ToListAsync(ct);

نمونه 187: Query قابل Seek، ترجمه LINQ و SARGability در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. راهبرد نمونه این است که بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. SQL و پارامترها را ببینید و مقایسه را با Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Invoices.Where(x => x.CreatedAt >= from && x.CreatedAt < to).ToListAsync(ct);

نمونه 188: Query قابل Seek، ترجمه LINQ و SARGability در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا عبارت قابل SARG به موتور اجازه می‌دهد محدوده Index را مستقیم پیدا کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Tickets.Where(x => EF.Functions.Like(x.Name, prefix + "%")).Take(30).ToListAsync(ct);

نمونه 189: Query قابل Seek، ترجمه LINQ و SARGability در صندوق پیام‌ها#

نمونه 189 برای صندوق پیام‌ها است. نشانه اولیه: تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. نسخه پیشنهادی بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Message>().Property<string>("NormalizedName").HasComputedColumnSql("UPPER([Name])", stored: true); modelBuilder.Entity<Message>().HasIndex("NormalizedName");

نمونه 190: Query قابل Seek، ترجمه LINQ و SARGability در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Events.Where(x => x.TenantId == tenantId && x.Id >= minId && x.Id < maxId).ToListAsync(ct);

نمونه 191: Query قابل Seek، ترجمه LINQ و SARGability در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.Where(x => x.Name.StartsWith(prefix)).Take(10).ToListAsync(ct);

نمونه 192: Query قابل Seek، ترجمه LINQ و SARGability در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. راهبرد نمونه این است که بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. SQL و پارامترها را ببینید و مقایسه را با Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Payments.Where(x => x.CreatedAt >= from && x.CreatedAt < to).ToListAsync(ct);

نمونه 193: Query قابل Seek، ترجمه LINQ و SARGability در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا عبارت قابل SARG به موتور اجازه می‌دهد محدوده Index را مستقیم پیدا کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Shipments.Where(x => EF.Functions.Like(x.Name, prefix + "%")).Take(30).ToListAsync(ct);

نمونه 194: Query قابل Seek، ترجمه LINQ و SARGability در مرور رخدادها#

نمونه 194 برای مرور رخدادها است. نشانه اولیه: تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. نسخه پیشنهادی بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<LogEntry>().Property<string>("NormalizedName").HasComputedColumnSql("UPPER([Name])", stored: true); modelBuilder.Entity<LogEntry>().HasIndex("NormalizedName");

نمونه 195: Query قابل Seek، ترجمه LINQ و SARGability در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.AuditRecords.Where(x => x.TenantId == tenantId && x.Id >= minId && x.Id < maxId).ToListAsync(ct);

نمونه 196: Query قابل Seek، ترجمه LINQ و SARGability در نشست‌های فعال#

در نشست‌های فعال اصل «شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.Where(x => x.Name.StartsWith(prefix)).Take(10).ToListAsync(ct);

نمونه 197: Query قابل Seek، ترجمه LINQ و SARGability در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. راهبرد نمونه این است که بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. SQL و پارامترها را ببینید و مقایسه را با Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var rows = await db.Courses.Where(x => x.CreatedAt >= from && x.CreatedAt < to).ToListAsync(ct);

نمونه 198: Query قابل Seek، ترجمه LINQ و SARGability در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا عبارت قابل SARG به موتور اجازه می‌دهد محدوده Index را مستقیم پیدا کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شرط باید تا حد ممکن ستون Indexشده را بدون تابع و تبدیل قابل جست‌وجو نگه دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Enrollments.Where(x => EF.Functions.Like(x.Name, prefix + "%")).Take(30).ToListAsync(ct);

نمونه 199: Query قابل Seek، ترجمه LINQ و SARGability در فهرست مقاله‌ها#

نمونه 199 برای فهرست مقاله‌ها است. نشانه اولیه: تابع روی ستون، تبدیل نوع، Contains عمومی یا محاسبه تاریخ باعث Scan می‌شود. نسخه پیشنهادی بازه‌ها، StartsWith، ستون محاسباتی پایدار و نوع داده هم‌سان را ترجیح دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Article>().Property<string>("NormalizedName").HasComputedColumnSql("UPPER([Name])", stored: true); modelBuilder.Entity<Article>().HasIndex("NormalizedName");

نمونه 200: Query قابل Seek، ترجمه LINQ و SARGability در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Execution Plan، Predicate باقی‌مانده، Logical Read و Warningهای تبدیل را بررسی کنید. رفتار Collation، منطقه زمانی، Null و Provider می‌تواند ترجمه و نتیجه را تغییر دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var rows = await db.Notifications.Where(x => x.TenantId == tenantId && x.Id >= minId && x.Id < maxId).ToListAsync(ct);

فصل 11: Query Cache، پارامتردهی و LINQ پویا#

در فصل 11 تمرکز بر «Query Cache، پارامتردهی و LINQ پویا» است. اصل مرکزی این فصل چنین است: شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، SQL پارامتری معمولاً Plan مشترک می‌سازد و از آلودگی Cache جلوگیری می‌کند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 201: Query Cache، پارامتردهی و LINQ پویا در فهرست محصولات#

در فهرست محصولات اصل «شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var wantedName = request.Name; var item = await db.Products.FirstOrDefaultAsync(x => x.Name == wantedName, ct);

نمونه 202: Query Cache، پارامتردهی و LINQ پویا در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. راهبرد نمونه این است که مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. SQL و پارامترها را ببینید و مقایسه را با Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

IQueryable<Order> query = db.Orders.AsNoTracking(); if (request.ActiveOnly) query = query.Where(x => x.IsActive); if (request.TenantId is int tenant) query = query.Where(x => x.TenantId == tenant);

نمونه 203: Query Cache، پارامتردهی و LINQ پویا در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL پارامتری معمولاً Plan مشترک می‌سازد و از آلودگی Cache جلوگیری می‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

Expression<Func<Customer, bool>> predicate = x => x.TenantId == tenantId; var rows = await db.Customers.Where(predicate).Take(30).ToListAsync(ct);

نمونه 204: Query Cache، پارامتردهی و LINQ پویا در مدیریت وبلاگ‌ها#

نمونه 204 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. نسخه پیشنهادی مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var minId = request.MinId; var query = db.Blogs.Where(x => x.Id >= minId).OrderBy(x => x.Id).Take(40); var rows = await query.ToListAsync(ct);

نمونه 205: Query Cache، پارامتردهی و LINQ پویا در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var active = request.Active; var count = await db.Posts.CountAsync(x => x.IsActive == active && x.TenantId == tenantId, ct);

نمونه 206: Query Cache، پارامتردهی و LINQ پویا در گزارش کارکنان#

در گزارش کارکنان اصل «شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var wantedName = request.Name; var item = await db.Employees.FirstOrDefaultAsync(x => x.Name == wantedName, ct);

نمونه 207: Query Cache، پارامتردهی و LINQ پویا در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. راهبرد نمونه این است که مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. SQL و پارامترها را ببینید و مقایسه را با Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

IQueryable<Invoice> query = db.Invoices.AsNoTracking(); if (request.ActiveOnly) query = query.Where(x => x.IsActive); if (request.TenantId is int tenant) query = query.Where(x => x.TenantId == tenant);

نمونه 208: Query Cache، پارامتردهی و LINQ پویا در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL پارامتری معمولاً Plan مشترک می‌سازد و از آلودگی Cache جلوگیری می‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

Expression<Func<Ticket, bool>> predicate = x => x.TenantId == tenantId; var rows = await db.Tickets.Where(predicate).Take(30).ToListAsync(ct);

نمونه 209: Query Cache، پارامتردهی و LINQ پویا در صندوق پیام‌ها#

نمونه 209 برای صندوق پیام‌ها است. نشانه اولیه: Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. نسخه پیشنهادی مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var minId = request.MinId; var query = db.Messages.Where(x => x.Id >= minId).OrderBy(x => x.Id).Take(40); var rows = await query.ToListAsync(ct);

نمونه 210: Query Cache، پارامتردهی و LINQ پویا در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var active = request.Active; var count = await db.Events.CountAsync(x => x.IsActive == active && x.TenantId == tenantId, ct);

نمونه 211: Query Cache، پارامتردهی و LINQ پویا در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var wantedName = request.Name; var item = await db.Documents.FirstOrDefaultAsync(x => x.Name == wantedName, ct);

نمونه 212: Query Cache، پارامتردهی و LINQ پویا در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. راهبرد نمونه این است که مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. SQL و پارامترها را ببینید و مقایسه را با Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

IQueryable<Payment> query = db.Payments.AsNoTracking(); if (request.ActiveOnly) query = query.Where(x => x.IsActive); if (request.TenantId is int tenant) query = query.Where(x => x.TenantId == tenant);

نمونه 213: Query Cache، پارامتردهی و LINQ پویا در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL پارامتری معمولاً Plan مشترک می‌سازد و از آلودگی Cache جلوگیری می‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

Expression<Func<Shipment, bool>> predicate = x => x.TenantId == tenantId; var rows = await db.Shipments.Where(predicate).Take(30).ToListAsync(ct);

نمونه 214: Query Cache، پارامتردهی و LINQ پویا در مرور رخدادها#

نمونه 214 برای مرور رخدادها است. نشانه اولیه: Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. نسخه پیشنهادی مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var minId = request.MinId; var query = db.LogEntries.Where(x => x.Id >= minId).OrderBy(x => x.Id).Take(40); var rows = await query.ToListAsync(ct);

نمونه 215: Query Cache، پارامتردهی و LINQ پویا در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var active = request.Active; var count = await db.AuditRecords.CountAsync(x => x.IsActive == active && x.TenantId == tenantId, ct);

نمونه 216: Query Cache، پارامتردهی و LINQ پویا در نشست‌های فعال#

در نشست‌های فعال اصل «شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var wantedName = request.Name; var item = await db.Sessions.FirstOrDefaultAsync(x => x.Name == wantedName, ct);

نمونه 217: Query Cache، پارامتردهی و LINQ پویا در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. راهبرد نمونه این است که مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. SQL و پارامترها را ببینید و مقایسه را با Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

IQueryable<Course> query = db.Courses.AsNoTracking(); if (request.ActiveOnly) query = query.Where(x => x.IsActive); if (request.TenantId is int tenant) query = query.Where(x => x.TenantId == tenant);

نمونه 218: Query Cache، پارامتردهی و LINQ پویا در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL پارامتری معمولاً Plan مشترک می‌سازد و از آلودگی Cache جلوگیری می‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل Expression Tree باید برای مقادیر مختلف پایدار بماند تا Cache داخلی و Plan Cache استفاده شوند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

Expression<Func<Enrollment, bool>> predicate = x => x.TenantId == tenantId; var rows = await db.Enrollments.Where(predicate).Take(30).ToListAsync(ct);

نمونه 219: Query Cache، پارامتردهی و LINQ پویا در فهرست مقاله‌ها#

نمونه 219 برای فهرست مقاله‌ها است. نشانه اولیه: Query پویا برای هر مقدار Constant جدید می‌سازد و نرخ Cache Hit پایین می‌ماند. نسخه پیشنهادی مقادیر را به‌صورت پارامتر Capture کنید و Predicateهای پویا را از بلوک‌های پایدار بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var minId = request.MinId; var query = db.Articles.Where(x => x.Id >= minId).OrderBy(x => x.Id).Take(40); var rows = await query.ToListAsync(ct);

نمونه 220: Query Cache، پارامتردهی و LINQ پویا در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Query Cache Hit Rate، تعداد Plan، زمان Compilation و مصرف CPU را بسنجید. پارامتردهی همیشه بهترین Plan را تضمین نمی‌کند؛ Parameter Sniffing و توزیع نامتوازن داده نیازمند اندازه‌گیری است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var active = request.Active; var count = await db.Notifications.CountAsync(x => x.IsActive == active && x.TenantId == tenantId, ct);

فصل 12: Compiled Query برای مسیرهای بسیار داغ#

در فصل 12 تمرکز بر «Compiled Query برای مسیرهای بسیار داغ» است. اصل مرکزی این فصل چنین است: Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Compiled Query مرحله بررسی Cache را دور می‌زند اما SQL و هزینه دیتابیس را جادویی بهبود نمی‌دهد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 221: Compiled Query برای مسیرهای بسیار داغ در فهرست محصولات#

در فهرست محصولات اصل «Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

private static readonly Func<AppDbContext, int, int, IAsyncEnumerable<Product>> ByTenant =     EF.CompileAsyncQuery((AppDbContext db, int tenantId, int take) =>         db.Products.AsNoTracking().Where(x => x.TenantId == tenantId).OrderBy(x => x.Id).Take(take));

نمونه 222: Compiled Query برای مسیرهای بسیار داغ در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. راهبرد نمونه این است که Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. SQL و پارامترها را ببینید و مقایسه را با Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

private static readonly Func<AppDbContext, int, Order?> ById =     EF.CompileQuery((AppDbContext db, int id) =>         db.Orders.AsNoTracking().FirstOrDefault(x => x.Id == id));

نمونه 223: Compiled Query برای مسیرهای بسیار داغ در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Compiled Query مرحله بررسی Cache را دور می‌زند اما SQL و هزینه دیتابیس را جادویی بهبود نمی‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await foreach (var item in ByTenant(db, tenantId, 30).WithCancellation(ct)) {     await writer.WriteAsync(item, ct); }

نمونه 224: Compiled Query برای مسیرهای بسیار داغ در مدیریت وبلاگ‌ها#

نمونه 224 برای مدیریت وبلاگ‌ها است. نشانه اولیه: تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. نسخه پیشنهادی Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

private static readonly Func<AppDbContext, int, Task<int>> ActiveCount =     EF.CompileAsyncQuery((AppDbContext db, int tenantId) =>         db.Blogs.Count(x => x.TenantId == tenantId && x.IsActive));

نمونه 225: Compiled Query برای مسیرهای بسیار داغ در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// فقط مسیر داغ و دارای شکل ثابت را Compile کنید. var value = ById(db, id); return value is null ? Results.NotFound() : Results.Ok(value);

نمونه 226: Compiled Query برای مسیرهای بسیار داغ در گزارش کارکنان#

در گزارش کارکنان اصل «Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

private static readonly Func<AppDbContext, int, int, IAsyncEnumerable<Employee>> ByTenant =     EF.CompileAsyncQuery((AppDbContext db, int tenantId, int take) =>         db.Employees.AsNoTracking().Where(x => x.TenantId == tenantId).OrderBy(x => x.Id).Take(take));

نمونه 227: Compiled Query برای مسیرهای بسیار داغ در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. راهبرد نمونه این است که Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. SQL و پارامترها را ببینید و مقایسه را با Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

private static readonly Func<AppDbContext, int, Invoice?> ById =     EF.CompileQuery((AppDbContext db, int id) =>         db.Invoices.AsNoTracking().FirstOrDefault(x => x.Id == id));

نمونه 228: Compiled Query برای مسیرهای بسیار داغ در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Compiled Query مرحله بررسی Cache را دور می‌زند اما SQL و هزینه دیتابیس را جادویی بهبود نمی‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await foreach (var item in ByTenant(db, tenantId, 30).WithCancellation(ct)) {     await writer.WriteAsync(item, ct); }

نمونه 229: Compiled Query برای مسیرهای بسیار داغ در صندوق پیام‌ها#

نمونه 229 برای صندوق پیام‌ها است. نشانه اولیه: تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. نسخه پیشنهادی Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

private static readonly Func<AppDbContext, int, Task<int>> ActiveCount =     EF.CompileAsyncQuery((AppDbContext db, int tenantId) =>         db.Messages.Count(x => x.TenantId == tenantId && x.IsActive));

نمونه 230: Compiled Query برای مسیرهای بسیار داغ در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// فقط مسیر داغ و دارای شکل ثابت را Compile کنید. var value = ById(db, id); return value is null ? Results.NotFound() : Results.Ok(value);

نمونه 231: Compiled Query برای مسیرهای بسیار داغ در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

private static readonly Func<AppDbContext, int, int, IAsyncEnumerable<Document>> ByTenant =     EF.CompileAsyncQuery((AppDbContext db, int tenantId, int take) =>         db.Documents.AsNoTracking().Where(x => x.TenantId == tenantId).OrderBy(x => x.Id).Take(take));

نمونه 232: Compiled Query برای مسیرهای بسیار داغ در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. راهبرد نمونه این است که Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. SQL و پارامترها را ببینید و مقایسه را با Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

private static readonly Func<AppDbContext, int, Payment?> ById =     EF.CompileQuery((AppDbContext db, int id) =>         db.Payments.AsNoTracking().FirstOrDefault(x => x.Id == id));

نمونه 233: Compiled Query برای مسیرهای بسیار داغ در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Compiled Query مرحله بررسی Cache را دور می‌زند اما SQL و هزینه دیتابیس را جادویی بهبود نمی‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await foreach (var item in ByTenant(db, tenantId, 30).WithCancellation(ct)) {     await writer.WriteAsync(item, ct); }

نمونه 234: Compiled Query برای مسیرهای بسیار داغ در مرور رخدادها#

نمونه 234 برای مرور رخدادها است. نشانه اولیه: تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. نسخه پیشنهادی Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

private static readonly Func<AppDbContext, int, Task<int>> ActiveCount =     EF.CompileAsyncQuery((AppDbContext db, int tenantId) =>         db.LogEntries.Count(x => x.TenantId == tenantId && x.IsActive));

نمونه 235: Compiled Query برای مسیرهای بسیار داغ در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// فقط مسیر داغ و دارای شکل ثابت را Compile کنید. var value = ById(db, id); return value is null ? Results.NotFound() : Results.Ok(value);

نمونه 236: Compiled Query برای مسیرهای بسیار داغ در نشست‌های فعال#

در نشست‌های فعال اصل «Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

private static readonly Func<AppDbContext, int, int, IAsyncEnumerable<Session>> ByTenant =     EF.CompileAsyncQuery((AppDbContext db, int tenantId, int take) =>         db.Sessions.AsNoTracking().Where(x => x.TenantId == tenantId).OrderBy(x => x.Id).Take(take));

نمونه 237: Compiled Query برای مسیرهای بسیار داغ در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. راهبرد نمونه این است که Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. SQL و پارامترها را ببینید و مقایسه را با Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

private static readonly Func<AppDbContext, int, Course?> ById =     EF.CompileQuery((AppDbContext db, int id) =>         db.Courses.AsNoTracking().FirstOrDefault(x => x.Id == id));

نمونه 238: Compiled Query برای مسیرهای بسیار داغ در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Compiled Query مرحله بررسی Cache را دور می‌زند اما SQL و هزینه دیتابیس را جادویی بهبود نمی‌دهد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Compiled Query فقط پس از اثبات هزینه ترجمه و Cache lookup در مسیر پرتکرار ارزش دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await foreach (var item in ByTenant(db, tenantId, 30).WithCancellation(ct)) {     await writer.WriteAsync(item, ct); }

نمونه 239: Compiled Query برای مسیرهای بسیار داغ در فهرست مقاله‌ها#

نمونه 239 برای فهرست مقاله‌ها است. نشانه اولیه: تیم پیش از اصلاح IO و Index، همه Queryها را Compile می‌کند و پیچیدگی نگهداری بالا می‌رود. نسخه پیشنهادی Queryهای ثابت، پرتکرار و دارای پارامتر Scalar را با EF.CompileQuery یا EF.CompileAsyncQuery بسازید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

private static readonly Func<AppDbContext, int, Task<int>> ActiveCount =     EF.CompileAsyncQuery((AppDbContext db, int tenantId) =>         db.Articles.Count(x => x.TenantId == tenantId && x.IsActive));

نمونه 240: Compiled Query برای مسیرهای بسیار داغ در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Latency درون‌فرایند، Allocation و Throughput را با Benchmark معتبر مقایسه کنید. برای مدل‌های متفاوت یک Context و پارامترهای پیچیده محدودیت وجود دارد؛ ابتدا مستندات نسخه را بررسی کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// فقط مسیر داغ و دارای شکل ثابت را Compile کنید. var value = ById(db, id); return value is null ? Results.NotFound() : Results.Ok(value);

فصل 13: DbContext Pooling و PooledDbContextFactory#

در فصل 13 تمرکز بر «DbContext Pooling و PooledDbContextFactory» است. اصل مرکزی این فصل چنین است: Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Pooling خود Context را بازاستفاده می‌کند و با Connection Pooling دیتابیس مفهوم جداگانه‌ای دارد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 241: DbContext Pooling و PooledDbContextFactory در فهرست محصولات#

در فهرست محصولات اصل «Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContextPool<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 256);

نمونه 242: DbContext Pooling و PooledDbContextFactory در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. راهبرد نمونه این است که AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

builder.Services.AddPooledDbContextFactory<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 128);

نمونه 243: DbContext Pooling و PooledDbContextFactory در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Pooling خود Context را بازاستفاده می‌کند و با Connection Pooling دیتابیس مفهوم جداگانه‌ای دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await using var db = await factory.CreateDbContextAsync(ct); var rows = await db.Customers.AsNoTracking().Where(x => x.TenantId == tenantId).Take(30).ToListAsync(ct);

نمونه 244: DbContext Pooling و PooledDbContextFactory در مدیریت وبلاگ‌ها#

نمونه 244 برای مدیریت وبلاگ‌ها است. نشانه اولیه: ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. نسخه پیشنهادی AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

public sealed class TenantDbContextFactory(IDbContextFactory<AppDbContext> pooled, ITenant tenant) {     public async ValueTask<AppDbContext> CreateAsync(CancellationToken ct)     {         var db = await pooled.CreateDbContextAsync(ct);         db.TenantId = tenant.Id;         return db;     } }

نمونه 245: DbContext Pooling و PooledDbContextFactory در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Pool اندازه Context و Pool اتصال دو تنظیم مستقل‌اند. builder.Services.AddDbContextPool<AppDbContext>(o =>     o.UseSqlServer(connectionString, sql => sql.CommandTimeout(20)), 128);

نمونه 246: DbContext Pooling و PooledDbContextFactory در گزارش کارکنان#

در گزارش کارکنان اصل «Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContextPool<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 256);

نمونه 247: DbContext Pooling و PooledDbContextFactory در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. راهبرد نمونه این است که AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

builder.Services.AddPooledDbContextFactory<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 128);

نمونه 248: DbContext Pooling و PooledDbContextFactory در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Pooling خود Context را بازاستفاده می‌کند و با Connection Pooling دیتابیس مفهوم جداگانه‌ای دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await using var db = await factory.CreateDbContextAsync(ct); var rows = await db.Tickets.AsNoTracking().Where(x => x.TenantId == tenantId).Take(30).ToListAsync(ct);

نمونه 249: DbContext Pooling و PooledDbContextFactory در صندوق پیام‌ها#

نمونه 249 برای صندوق پیام‌ها است. نشانه اولیه: ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. نسخه پیشنهادی AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

public sealed class TenantDbContextFactory(IDbContextFactory<AppDbContext> pooled, ITenant tenant) {     public async ValueTask<AppDbContext> CreateAsync(CancellationToken ct)     {         var db = await pooled.CreateDbContextAsync(ct);         db.TenantId = tenant.Id;         return db;     } }

نمونه 250: DbContext Pooling و PooledDbContextFactory در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Pool اندازه Context و Pool اتصال دو تنظیم مستقل‌اند. builder.Services.AddDbContextPool<AppDbContext>(o =>     o.UseSqlServer(connectionString, sql => sql.CommandTimeout(20)), 128);

نمونه 251: DbContext Pooling و PooledDbContextFactory در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContextPool<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 256);

نمونه 252: DbContext Pooling و PooledDbContextFactory در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. راهبرد نمونه این است که AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

builder.Services.AddPooledDbContextFactory<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 128);

نمونه 253: DbContext Pooling و PooledDbContextFactory در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Pooling خود Context را بازاستفاده می‌کند و با Connection Pooling دیتابیس مفهوم جداگانه‌ای دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await using var db = await factory.CreateDbContextAsync(ct); var rows = await db.Shipments.AsNoTracking().Where(x => x.TenantId == tenantId).Take(30).ToListAsync(ct);

نمونه 254: DbContext Pooling و PooledDbContextFactory در مرور رخدادها#

نمونه 254 برای مرور رخدادها است. نشانه اولیه: ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. نسخه پیشنهادی AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

public sealed class TenantDbContextFactory(IDbContextFactory<AppDbContext> pooled, ITenant tenant) {     public async ValueTask<AppDbContext> CreateAsync(CancellationToken ct)     {         var db = await pooled.CreateDbContextAsync(ct);         db.TenantId = tenant.Id;         return db;     } }

نمونه 255: DbContext Pooling و PooledDbContextFactory در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Pool اندازه Context و Pool اتصال دو تنظیم مستقل‌اند. builder.Services.AddDbContextPool<AppDbContext>(o =>     o.UseSqlServer(connectionString, sql => sql.CommandTimeout(20)), 128);

نمونه 256: DbContext Pooling و PooledDbContextFactory در نشست‌های فعال#

در نشست‌های فعال اصل «Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

builder.Services.AddDbContextPool<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 256);

نمونه 257: DbContext Pooling و PooledDbContextFactory در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. راهبرد نمونه این است که AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

builder.Services.AddPooledDbContextFactory<AppDbContext>(     options => options.UseSqlServer(connectionString),     poolSize: 128);

نمونه 258: DbContext Pooling و PooledDbContextFactory در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Pooling خود Context را بازاستفاده می‌کند و با Connection Pooling دیتابیس مفهوم جداگانه‌ای دارد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Context قابل Pool باید State درخواست قبلی را به درخواست بعدی نشت ندهد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await using var db = await factory.CreateDbContextAsync(ct); var rows = await db.Enrollments.AsNoTracking().Where(x => x.TenantId == tenantId).Take(30).ToListAsync(ct);

نمونه 259: DbContext Pooling و PooledDbContextFactory در فهرست مقاله‌ها#

نمونه 259 برای فهرست مقاله‌ها است. نشانه اولیه: ساخت مکرر Context در سرویس کم‌تأخیر هزینه ایجاد می‌کند یا TenantId اشتباه در Pool باقی می‌ماند. نسخه پیشنهادی AddDbContextPool یا PooledDbContextFactory را همراه با تزریق State امن و Resetپذیر استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

public sealed class TenantDbContextFactory(IDbContextFactory<AppDbContext> pooled, ITenant tenant) {     public async ValueTask<AppDbContext> CreateAsync(CancellationToken ct)     {         var db = await pooled.CreateDbContextAsync(ct);         db.TenantId = tenant.Id;         return db;     } }

نمونه 260: DbContext Pooling و PooledDbContextFactory در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Throughput، Allocation، زمان ساخت Context و نرخ اشباع Pool را اندازه بگیرید. OnConfiguring در Context Poolشده معمولاً یک‌بار اجرا می‌شود؛ State متغیر درخواست را آنجا قرار ندهید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Pool اندازه Context و Pool اتصال دو تنظیم مستقل‌اند. builder.Services.AddDbContextPool<AppDbContext>(o =>     o.UseSqlServer(connectionString, sql => sql.CommandTimeout(20)), 128);

فصل 14: طول عمر DbContext، Async و هم‌زمانی#

در فصل 14 تمرکز بر «طول عمر DbContext، Async و هم‌زمانی» است. اصل مرکزی این فصل چنین است: هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، DbContext برای عملیات موازی طراحی نشده و یک اتصال نیز معمولاً چند Reader مستقل را هم‌زمان اجرا نمی‌کند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 261: طول عمر DbContext، Async و هم‌زمانی در فهرست محصولات#

در فهرست محصولات اصل «هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await using var db = await factory.CreateDbContextAsync(ct); return await db.Products.AsNoTracking().Take(10).ToListAsync(ct);

نمونه 262: طول عمر DbContext، Async و هم‌زمانی در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. راهبرد نمونه این است که در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. SQL و پارامترها را ببینید و مقایسه را با زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var firstTask = LoadFirstAsync(factory, ct); var secondTask = LoadSecondAsync(factory, ct); await Task.WhenAll(firstTask, secondTask); // هر تابع Context جدا دارد

نمونه 263: طول عمر DbContext، Async و هم‌زمانی در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا DbContext برای عملیات موازی طراحی نشده و یک اتصال نیز معمولاً چند Reader مستقل را هم‌زمان اجرا نمی‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var row = await db.Customers.SingleAsync(x => x.Id == id, ct); row.Name = newName; await db.SaveChangesAsync(ct); // پیش از ادامه حتماً await

نمونه 264: طول عمر DbContext، Async و هم‌زمانی در مدیریت وبلاگ‌ها#

نمونه 264 برای مدیریت وبلاگ‌ها است. نشانه اولیه: یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. نسخه پیشنهادی در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString), ServiceLifetime.Scoped);

نمونه 265: طول عمر DbContext، Async و هم‌زمانی در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await Parallel.ForEachAsync(ids, ct, async (id, token) => {     await using var db = await factory.CreateDbContextAsync(token);     _ = await db.Posts.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id, token); });

نمونه 266: طول عمر DbContext، Async و هم‌زمانی در گزارش کارکنان#

در گزارش کارکنان اصل «هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await using var db = await factory.CreateDbContextAsync(ct); return await db.Employees.AsNoTracking().Take(10).ToListAsync(ct);

نمونه 267: طول عمر DbContext، Async و هم‌زمانی در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. راهبرد نمونه این است که در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. SQL و پارامترها را ببینید و مقایسه را با زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var firstTask = LoadFirstAsync(factory, ct); var secondTask = LoadSecondAsync(factory, ct); await Task.WhenAll(firstTask, secondTask); // هر تابع Context جدا دارد

نمونه 268: طول عمر DbContext، Async و هم‌زمانی در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا DbContext برای عملیات موازی طراحی نشده و یک اتصال نیز معمولاً چند Reader مستقل را هم‌زمان اجرا نمی‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var row = await db.Tickets.SingleAsync(x => x.Id == id, ct); row.Name = newName; await db.SaveChangesAsync(ct); // پیش از ادامه حتماً await

نمونه 269: طول عمر DbContext، Async و هم‌زمانی در صندوق پیام‌ها#

نمونه 269 برای صندوق پیام‌ها است. نشانه اولیه: یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. نسخه پیشنهادی در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString), ServiceLifetime.Scoped);

نمونه 270: طول عمر DbContext، Async و هم‌زمانی در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await Parallel.ForEachAsync(ids, ct, async (id, token) => {     await using var db = await factory.CreateDbContextAsync(token);     _ = await db.Events.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id, token); });

نمونه 271: طول عمر DbContext، Async و هم‌زمانی در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await using var db = await factory.CreateDbContextAsync(ct); return await db.Documents.AsNoTracking().Take(10).ToListAsync(ct);

نمونه 272: طول عمر DbContext، Async و هم‌زمانی در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. راهبرد نمونه این است که در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. SQL و پارامترها را ببینید و مقایسه را با زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var firstTask = LoadFirstAsync(factory, ct); var secondTask = LoadSecondAsync(factory, ct); await Task.WhenAll(firstTask, secondTask); // هر تابع Context جدا دارد

نمونه 273: طول عمر DbContext، Async و هم‌زمانی در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا DbContext برای عملیات موازی طراحی نشده و یک اتصال نیز معمولاً چند Reader مستقل را هم‌زمان اجرا نمی‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var row = await db.Shipments.SingleAsync(x => x.Id == id, ct); row.Name = newName; await db.SaveChangesAsync(ct); // پیش از ادامه حتماً await

نمونه 274: طول عمر DbContext، Async و هم‌زمانی در مرور رخدادها#

نمونه 274 برای مرور رخدادها است. نشانه اولیه: یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. نسخه پیشنهادی در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString), ServiceLifetime.Scoped);

نمونه 275: طول عمر DbContext، Async و هم‌زمانی در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await Parallel.ForEachAsync(ids, ct, async (id, token) => {     await using var db = await factory.CreateDbContextAsync(token);     _ = await db.AuditRecords.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id, token); });

نمونه 276: طول عمر DbContext، Async و هم‌زمانی در نشست‌های فعال#

در نشست‌های فعال اصل «هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await using var db = await factory.CreateDbContextAsync(ct); return await db.Sessions.AsNoTracking().Take(10).ToListAsync(ct);

نمونه 277: طول عمر DbContext، Async و هم‌زمانی در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. راهبرد نمونه این است که در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. SQL و پارامترها را ببینید و مقایسه را با زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var firstTask = LoadFirstAsync(factory, ct); var secondTask = LoadSecondAsync(factory, ct); await Task.WhenAll(firstTask, secondTask); // هر تابع Context جدا دارد

نمونه 278: طول عمر DbContext، Async و هم‌زمانی در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا DbContext برای عملیات موازی طراحی نشده و یک اتصال نیز معمولاً چند Reader مستقل را هم‌زمان اجرا نمی‌کند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هر DbContext یک Unit of Work کوتاه و غیرهم‌زمان است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var row = await db.Enrollments.SingleAsync(x => x.Id == id, ct); row.Name = newName; await db.SaveChangesAsync(ct); // پیش از ادامه حتماً await

نمونه 279: طول عمر DbContext، Async و هم‌زمانی در فهرست مقاله‌ها#

نمونه 279 برای فهرست مقاله‌ها است. نشانه اولیه: یک Context بین Threadها یا Taskهای موازی مشترک می‌شود و خطا یا State ناسازگار رخ می‌دهد. نسخه پیشنهادی در هر Scope یا عملیات مستقل Context جدا بسازید و تمام عملیات Async را فوراً await کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

builder.Services.AddDbContext<AppDbContext>(options =>     options.UseSqlServer(connectionString), ServiceLifetime.Scoped);

نمونه 280: طول عمر DbContext، Async و هم‌زمانی در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان نگهداری Context، تعداد Entry، اتصال‌های هم‌زمان و خطاهای concurrency را ثبت کنید. Context Singleton، fire-and-forget و نگه‌داشتن Entityهای tracked در Cache از خطاهای پرهزینه هستند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await Parallel.ForEachAsync(ids, ct, async (id, token) => {     await using var db = await factory.CreateDbContextAsync(token);     _ = await db.Notifications.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id, token); });

فصل 15: Streaming، Buffering و Cancellation#

در فصل 15 تمرکز بر «Streaming، Buffering و Cancellation» است. اصل مرکزی این فصل چنین است: حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Streaming ردیف‌ها را تدریجی می‌خواند ولی Query و Reader تا پایان مصرف فعال می‌مانند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 281: Streaming، Buffering و Cancellation در فهرست محصولات#

در فهرست محصولات اصل «حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await foreach (var row in db.Products.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct)) {     await sink.WriteAsync(row.Id, ct); }

نمونه 282: Streaming، Buffering و Cancellation در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. راهبرد نمونه این است که Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. SQL و پارامترها را ببینید و مقایسه را با Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Orders.AsNoTracking().Select(x => new { x.Id, x.Name }); await foreach (var row in query.AsAsyncEnumerable().WithCancellation(ct))     await writer.WriteAsync(row, ct);

نمونه 283: Streaming، Buffering و Cancellation در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Streaming ردیف‌ها را تدریجی می‌خواند ولی Query و Reader تا پایان مصرف فعال می‌مانند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

const int batchSize = 30; var lastId = 0; while (true) {     var batch = await db.Customers.AsNoTracking().Where(x => x.Id > lastId).OrderBy(x => x.Id).Take(batchSize).ToListAsync(ct);     if (batch.Count == 0) break;     lastId = batch[^1].Id; }

نمونه 284: Streaming، Buffering و Cancellation در مدیریت وبلاگ‌ها#

نمونه 284 برای مدیریت وبلاگ‌ها است. نشانه اولیه: ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. نسخه پیشنهادی Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var smallResult = await db.Blogs.AsNoTracking().Take(40).ToArrayAsync(ct); // Buffering آگاهانه

نمونه 285: Streaming، Buffering و Cancellation در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(10)); var rows = await db.Posts.AsNoTracking().Take(50).ToListAsync(timeout.Token);

نمونه 286: Streaming، Buffering و Cancellation در گزارش کارکنان#

در گزارش کارکنان اصل «حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await foreach (var row in db.Employees.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct)) {     await sink.WriteAsync(row.Id, ct); }

نمونه 287: Streaming، Buffering و Cancellation در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. راهبرد نمونه این است که Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. SQL و پارامترها را ببینید و مقایسه را با Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Invoices.AsNoTracking().Select(x => new { x.Id, x.Name }); await foreach (var row in query.AsAsyncEnumerable().WithCancellation(ct))     await writer.WriteAsync(row, ct);

نمونه 288: Streaming، Buffering و Cancellation در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Streaming ردیف‌ها را تدریجی می‌خواند ولی Query و Reader تا پایان مصرف فعال می‌مانند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

const int batchSize = 30; var lastId = 0; while (true) {     var batch = await db.Tickets.AsNoTracking().Where(x => x.Id > lastId).OrderBy(x => x.Id).Take(batchSize).ToListAsync(ct);     if (batch.Count == 0) break;     lastId = batch[^1].Id; }

نمونه 289: Streaming، Buffering و Cancellation در صندوق پیام‌ها#

نمونه 289 برای صندوق پیام‌ها است. نشانه اولیه: ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. نسخه پیشنهادی Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var smallResult = await db.Messages.AsNoTracking().Take(40).ToArrayAsync(ct); // Buffering آگاهانه

نمونه 290: Streaming، Buffering و Cancellation در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(10)); var rows = await db.Events.AsNoTracking().Take(50).ToListAsync(timeout.Token);

نمونه 291: Streaming، Buffering و Cancellation در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await foreach (var row in db.Documents.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct)) {     await sink.WriteAsync(row.Id, ct); }

نمونه 292: Streaming، Buffering و Cancellation در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. راهبرد نمونه این است که Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. SQL و پارامترها را ببینید و مقایسه را با Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Payments.AsNoTracking().Select(x => new { x.Id, x.Name }); await foreach (var row in query.AsAsyncEnumerable().WithCancellation(ct))     await writer.WriteAsync(row, ct);

نمونه 293: Streaming، Buffering و Cancellation در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Streaming ردیف‌ها را تدریجی می‌خواند ولی Query و Reader تا پایان مصرف فعال می‌مانند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

const int batchSize = 30; var lastId = 0; while (true) {     var batch = await db.Shipments.AsNoTracking().Where(x => x.Id > lastId).OrderBy(x => x.Id).Take(batchSize).ToListAsync(ct);     if (batch.Count == 0) break;     lastId = batch[^1].Id; }

نمونه 294: Streaming، Buffering و Cancellation در مرور رخدادها#

نمونه 294 برای مرور رخدادها است. نشانه اولیه: ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. نسخه پیشنهادی Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var smallResult = await db.LogEntries.AsNoTracking().Take(40).ToArrayAsync(ct); // Buffering آگاهانه

نمونه 295: Streaming، Buffering و Cancellation در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(10)); var rows = await db.AuditRecords.AsNoTracking().Take(50).ToListAsync(timeout.Token);

نمونه 296: Streaming، Buffering و Cancellation در نشست‌های فعال#

در نشست‌های فعال اصل «حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

await foreach (var row in db.Sessions.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct)) {     await sink.WriteAsync(row.Id, ct); }

نمونه 297: Streaming، Buffering و Cancellation در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. راهبرد نمونه این است که Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. SQL و پارامترها را ببینید و مقایسه را با Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var query = db.Courses.AsNoTracking().Select(x => new { x.Id, x.Name }); await foreach (var row in query.AsAsyncEnumerable().WithCancellation(ct))     await writer.WriteAsync(row, ct);

نمونه 298: Streaming، Buffering و Cancellation در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Streaming ردیف‌ها را تدریجی می‌خواند ولی Query و Reader تا پایان مصرف فعال می‌مانند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: حجم نتیجه باید تعیین کند داده یک‌جا در حافظه یا تدریجی مصرف شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

const int batchSize = 30; var lastId = 0; while (true) {     var batch = await db.Enrollments.AsNoTracking().Where(x => x.Id > lastId).OrderBy(x => x.Id).Take(batchSize).ToListAsync(ct);     if (batch.Count == 0) break;     lastId = batch[^1].Id; }

نمونه 299: Streaming، Buffering و Cancellation در فهرست مقاله‌ها#

نمونه 299 برای فهرست مقاله‌ها است. نشانه اولیه: ToListAsync روی میلیون‌ها ردیف حافظه را پر می‌کند یا Streaming طولانی اتصال را اشغال می‌کند. نسخه پیشنهادی Projection کوچک، AsAsyncEnumerable، CancellationToken و پردازش Batch را متعادل کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var smallResult = await db.Articles.AsNoTracking().Take(40).ToArrayAsync(ct); // Buffering آگاهانه

نمونه 300: Streaming، Buffering و Cancellation در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Peak Working Set، Allocation، مدت باز بودن Connection و سرعت مصرف‌کننده را بسنجید. در Retry strategy، Split Query یا لایه‌های میانی ممکن است Buffering داخلی رخ دهد؛ فقط به ظاهر API اتکا نکنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(10)); var rows = await db.Notifications.AsNoTracking().Take(50).ToListAsync(timeout.Token);

فصل 16: SaveChanges، Batching و ChangeTracker#

در فصل 16 تمرکز بر «SaveChanges، Batching و ChangeTracker» است. اصل مرکزی این فصل چنین است: هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، EF چند Statement را بر اساس Provider در Roundtripهای کمتر گروه می‌کند ولی هر Entity هنوز می‌تواند Statement جدا داشته باشد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 301: SaveChanges، Batching و ChangeTracker در فهرست محصولات#

در فهرست محصولات اصل «هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

db.Products.AddRange(items); await db.SaveChangesAsync(ct);

نمونه 302: SaveChanges، Batching و ChangeTracker در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. راهبرد نمونه این است که AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var old = db.ChangeTracker.AutoDetectChangesEnabled; try {     db.ChangeTracker.AutoDetectChangesEnabled = false;     db.AddRange(items);     db.ChangeTracker.DetectChanges();     await db.SaveChangesAsync(ct); } finally { db.ChangeTracker.AutoDetectChangesEnabled = old; }

نمونه 303: SaveChanges، Batching و ChangeTracker در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF چند Statement را بر اساس Provider در Roundtripهای کمتر گروه می‌کند ولی هر Entity هنوز می‌تواند Statement جدا داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

foreach (var batch in items.Chunk(30)) {     db.AddRange(batch);     await db.SaveChangesAsync(ct);     db.ChangeTracker.Clear(); }

نمونه 304: SaveChanges، Batching و ChangeTracker در مدیریت وبلاگ‌ها#

نمونه 304 برای مدیریت وبلاگ‌ها است. نشانه اولیه: حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. نسخه پیشنهادی AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.UseSqlServer(connectionString, sql =>     sql.MinBatchSize(4).MaxBatchSize(42)); // فقط پس از Benchmark

نمونه 305: SaveChanges، Batching و ChangeTracker در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var tracked = db.ChangeTracker.Entries<Post>().Count(); logger.LogDebug("Before SaveChanges: {Tracked} entries", tracked); await db.SaveChangesAsync(acceptAllChangesOnSuccess: true, ct);

نمونه 306: SaveChanges، Batching و ChangeTracker در گزارش کارکنان#

در گزارش کارکنان اصل «هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

db.Employees.AddRange(items); await db.SaveChangesAsync(ct);

نمونه 307: SaveChanges، Batching و ChangeTracker در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. راهبرد نمونه این است که AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var old = db.ChangeTracker.AutoDetectChangesEnabled; try {     db.ChangeTracker.AutoDetectChangesEnabled = false;     db.AddRange(items);     db.ChangeTracker.DetectChanges();     await db.SaveChangesAsync(ct); } finally { db.ChangeTracker.AutoDetectChangesEnabled = old; }

نمونه 308: SaveChanges، Batching و ChangeTracker در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF چند Statement را بر اساس Provider در Roundtripهای کمتر گروه می‌کند ولی هر Entity هنوز می‌تواند Statement جدا داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

foreach (var batch in items.Chunk(30)) {     db.AddRange(batch);     await db.SaveChangesAsync(ct);     db.ChangeTracker.Clear(); }

نمونه 309: SaveChanges، Batching و ChangeTracker در صندوق پیام‌ها#

نمونه 309 برای صندوق پیام‌ها است. نشانه اولیه: حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. نسخه پیشنهادی AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.UseSqlServer(connectionString, sql =>     sql.MinBatchSize(4).MaxBatchSize(42)); // فقط پس از Benchmark

نمونه 310: SaveChanges، Batching و ChangeTracker در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var tracked = db.ChangeTracker.Entries<Event>().Count(); logger.LogDebug("Before SaveChanges: {Tracked} entries", tracked); await db.SaveChangesAsync(acceptAllChangesOnSuccess: true, ct);

نمونه 311: SaveChanges، Batching و ChangeTracker در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

db.Documents.AddRange(items); await db.SaveChangesAsync(ct);

نمونه 312: SaveChanges، Batching و ChangeTracker در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. راهبرد نمونه این است که AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var old = db.ChangeTracker.AutoDetectChangesEnabled; try {     db.ChangeTracker.AutoDetectChangesEnabled = false;     db.AddRange(items);     db.ChangeTracker.DetectChanges();     await db.SaveChangesAsync(ct); } finally { db.ChangeTracker.AutoDetectChangesEnabled = old; }

نمونه 313: SaveChanges، Batching و ChangeTracker در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF چند Statement را بر اساس Provider در Roundtripهای کمتر گروه می‌کند ولی هر Entity هنوز می‌تواند Statement جدا داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

foreach (var batch in items.Chunk(30)) {     db.AddRange(batch);     await db.SaveChangesAsync(ct);     db.ChangeTracker.Clear(); }

نمونه 314: SaveChanges، Batching و ChangeTracker در مرور رخدادها#

نمونه 314 برای مرور رخدادها است. نشانه اولیه: حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. نسخه پیشنهادی AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.UseSqlServer(connectionString, sql =>     sql.MinBatchSize(4).MaxBatchSize(42)); // فقط پس از Benchmark

نمونه 315: SaveChanges، Batching و ChangeTracker در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var tracked = db.ChangeTracker.Entries<AuditRecord>().Count(); logger.LogDebug("Before SaveChanges: {Tracked} entries", tracked); await db.SaveChangesAsync(acceptAllChangesOnSuccess: true, ct);

نمونه 316: SaveChanges، Batching و ChangeTracker در نشست‌های فعال#

در نشست‌های فعال اصل «هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

db.Sessions.AddRange(items); await db.SaveChangesAsync(ct);

نمونه 317: SaveChanges، Batching و ChangeTracker در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. راهبرد نمونه این است که AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. SQL و پارامترها را ببینید و مقایسه را با تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var old = db.ChangeTracker.AutoDetectChangesEnabled; try {     db.ChangeTracker.AutoDetectChangesEnabled = false;     db.AddRange(items);     db.ChangeTracker.DetectChanges();     await db.SaveChangesAsync(ct); } finally { db.ChangeTracker.AutoDetectChangesEnabled = old; }

نمونه 318: SaveChanges، Batching و ChangeTracker در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF چند Statement را بر اساس Provider در Roundtripهای کمتر گروه می‌کند ولی هر Entity هنوز می‌تواند Statement جدا داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: هزینه DetectChanges و تعداد Statement باید متناسب با اندازه Unit of Work کنترل شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

foreach (var batch in items.Chunk(30)) {     db.AddRange(batch);     await db.SaveChangesAsync(ct);     db.ChangeTracker.Clear(); }

نمونه 319: SaveChanges، Batching و ChangeTracker در فهرست مقاله‌ها#

نمونه 319 برای فهرست مقاله‌ها است. نشانه اولیه: حلقه بزرگ برای هر ردیف SaveChanges می‌زند یا هزاران Entity tracked را تا پایان نگه می‌دارد. نسخه پیشنهادی AddRange، Batch منطقی، تنظیم آگاهانه AutoDetectChanges و SaveChanges در مرز درست را به کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

options.UseSqlServer(connectionString, sql =>     sql.MinBatchSize(4).MaxBatchSize(42)); // فقط پس از Benchmark

نمونه 320: SaveChanges، Batching و ChangeTracker در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد Batch، Statement، زمان DetectChanges، Allocation و Lock duration را ثبت کنید. خاموش کردن AutoDetectChanges بدون فراخوانی صریح و آزمون صحت، تغییرات را گم می‌کند. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var tracked = db.ChangeTracker.Entries<Notification>().Count(); logger.LogDebug("Before SaveChanges: {Tracked} entries", tracked); await db.SaveChangesAsync(acceptAllChangesOnSuccess: true, ct);

فصل 17: ExecuteUpdate و ExecuteDelete مجموعه‌ای#

در فصل 17 تمرکز بر «ExecuteUpdate و ExecuteDelete مجموعه‌ای» است. اصل مرکزی این فصل چنین است: عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، یک UPDATE یا DELETE مجموعه‌ای جای خواندن داده و صدها Statement را می‌گیرد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 321: ExecuteUpdate و ExecuteDelete مجموعه‌ای در فهرست محصولات#

در فهرست محصولات اصل «عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var affected = await db.Products.Where(x => x.TenantId == tenantId && x.IsActive)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.UpdatedAt, now), ct);

نمونه 322: ExecuteUpdate و ExecuteDelete مجموعه‌ای در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. راهبرد نمونه این است که از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Orders.Where(x => x.CreatedAt < cutoff)     .ExecuteDeleteAsync(ct);

نمونه 323: ExecuteUpdate و ExecuteDelete مجموعه‌ای در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا یک UPDATE یا DELETE مجموعه‌ای جای خواندن داده و صدها Statement را می‌گیرد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Customers.Where(x => x.Id == id)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName)                               .SetProperty(x => x.UpdatedAt, now), ct);

نمونه 324: ExecuteUpdate و ExecuteDelete مجموعه‌ای در مدیریت وبلاگ‌ها#

نمونه 324 برای مدیریت وبلاگ‌ها است. نشانه اولیه: برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. نسخه پیشنهادی از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var affected = await db.Blogs.Where(x => ids.Contains(x.Id))     .ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, false), ct);

نمونه 325: ExecuteUpdate و ExecuteDelete مجموعه‌ای در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

db.ChangeTracker.Clear(); var fresh = await db.Posts.AsNoTracking().SingleAsync(x => x.Id == id, ct); // بعد از عملیات مجموعه‌ای

نمونه 326: ExecuteUpdate و ExecuteDelete مجموعه‌ای در گزارش کارکنان#

در گزارش کارکنان اصل «عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var affected = await db.Employees.Where(x => x.TenantId == tenantId && x.IsActive)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.UpdatedAt, now), ct);

نمونه 327: ExecuteUpdate و ExecuteDelete مجموعه‌ای در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. راهبرد نمونه این است که از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Invoices.Where(x => x.CreatedAt < cutoff)     .ExecuteDeleteAsync(ct);

نمونه 328: ExecuteUpdate و ExecuteDelete مجموعه‌ای در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا یک UPDATE یا DELETE مجموعه‌ای جای خواندن داده و صدها Statement را می‌گیرد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Tickets.Where(x => x.Id == id)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName)                               .SetProperty(x => x.UpdatedAt, now), ct);

نمونه 329: ExecuteUpdate و ExecuteDelete مجموعه‌ای در صندوق پیام‌ها#

نمونه 329 برای صندوق پیام‌ها است. نشانه اولیه: برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. نسخه پیشنهادی از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var affected = await db.Messages.Where(x => ids.Contains(x.Id))     .ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, false), ct);

نمونه 330: ExecuteUpdate و ExecuteDelete مجموعه‌ای در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

db.ChangeTracker.Clear(); var fresh = await db.Events.AsNoTracking().SingleAsync(x => x.Id == id, ct); // بعد از عملیات مجموعه‌ای

نمونه 331: ExecuteUpdate و ExecuteDelete مجموعه‌ای در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var affected = await db.Documents.Where(x => x.TenantId == tenantId && x.IsActive)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.UpdatedAt, now), ct);

نمونه 332: ExecuteUpdate و ExecuteDelete مجموعه‌ای در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. راهبرد نمونه این است که از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Payments.Where(x => x.CreatedAt < cutoff)     .ExecuteDeleteAsync(ct);

نمونه 333: ExecuteUpdate و ExecuteDelete مجموعه‌ای در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا یک UPDATE یا DELETE مجموعه‌ای جای خواندن داده و صدها Statement را می‌گیرد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Shipments.Where(x => x.Id == id)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName)                               .SetProperty(x => x.UpdatedAt, now), ct);

نمونه 334: ExecuteUpdate و ExecuteDelete مجموعه‌ای در مرور رخدادها#

نمونه 334 برای مرور رخدادها است. نشانه اولیه: برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. نسخه پیشنهادی از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var affected = await db.LogEntries.Where(x => ids.Contains(x.Id))     .ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, false), ct);

نمونه 335: ExecuteUpdate و ExecuteDelete مجموعه‌ای در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

db.ChangeTracker.Clear(); var fresh = await db.AuditRecords.AsNoTracking().SingleAsync(x => x.Id == id, ct); // بعد از عملیات مجموعه‌ای

نمونه 336: ExecuteUpdate و ExecuteDelete مجموعه‌ای در نشست‌های فعال#

در نشست‌های فعال اصل «عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var affected = await db.Sessions.Where(x => x.TenantId == tenantId && x.IsActive)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.UpdatedAt, now), ct);

نمونه 337: ExecuteUpdate و ExecuteDelete مجموعه‌ای در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. راهبرد نمونه این است که از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Courses.Where(x => x.CreatedAt < cutoff)     .ExecuteDeleteAsync(ct);

نمونه 338: ExecuteUpdate و ExecuteDelete مجموعه‌ای در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا یک UPDATE یا DELETE مجموعه‌ای جای خواندن داده و صدها Statement را می‌گیرد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: عملیات مجموعه‌ای باید بدون Materialize و Tracking روی سرور اجرا شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Enrollments.Where(x => x.Id == id)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName)                               .SetProperty(x => x.UpdatedAt, now), ct);

نمونه 339: ExecuteUpdate و ExecuteDelete مجموعه‌ای در فهرست مقاله‌ها#

نمونه 339 برای فهرست مقاله‌ها است. نشانه اولیه: برای تغییر ساده همه ردیف‌ها ابتدا آن‌ها را خوانده و سپس تک‌تک Update می‌کنیم. نسخه پیشنهادی از ExecuteUpdateAsync و ExecuteDeleteAsync برای Predicate روشن و تغییر قابل بیان در SQL استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var affected = await db.Articles.Where(x => ids.Contains(x.Id))     .ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, false), ct);

نمونه 340: ExecuteUpdate و ExecuteDelete مجموعه‌ای در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: تعداد ردیف متاثر، Roundtrip، زمان Lock و Log growth را اندازه بگیرید. این APIها ChangeTracker را همگام نمی‌کنند؛ Entity tracked قدیمی و کنترل هم‌زمانی را مدیریت کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

db.ChangeTracker.Clear(); var fresh = await db.Notifications.AsNoTracking().SingleAsync(x => x.Id == id, ct); // بعد از عملیات مجموعه‌ای

فصل 18: Transaction، Retry و کنترل هم‌زمانی#

در فصل 18 تمرکز بر «Transaction، Retry و کنترل هم‌زمانی» است. اصل مرکزی این فصل چنین است: مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Isolation level و ترتیب دسترسی جداول روی Blocking و Deadlock اثر مستقیم دارند. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 341: Transaction، Retry و کنترل هم‌زمانی در فهرست محصولات#

در فهرست محصولات اصل «مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var strategy = db.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => {     await using var tx = await db.Database.BeginTransactionAsync(ct);     await ApplyChangesAsync(db, ct);     await tx.CommitAsync(ct); });

نمونه 342: Transaction، Retry و کنترل هم‌زمانی در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. راهبرد نمونه این است که ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. SQL و پارامترها را ببینید و مقایسه را با Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var row = await db.Orders.SingleAsync(x => x.Id == id, ct); row.Name = newName; try { await db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException) { await db.Entry(row).ReloadAsync(ct); throw; }

نمونه 343: Transaction، Retry و کنترل هم‌زمانی در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Isolation level و ترتیب دسترسی جداول روی Blocking و Deadlock اثر مستقیم دارند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Customer>().Property(x => x.RowVersion).IsRowVersion();

نمونه 344: Transaction، Retry و کنترل هم‌زمانی در مدیریت وبلاگ‌ها#

نمونه 344 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. نسخه پیشنهادی ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); await firstStep(db, ct); await secondStep(db, ct); await tx.CommitAsync(ct);

نمونه 345: Transaction، Retry و کنترل هم‌زمانی در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var affected = await db.Posts.Where(x => x.Id == id && x.RowVersion == version)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); if (affected == 0) throw new ConcurrencyException();

نمونه 346: Transaction، Retry و کنترل هم‌زمانی در گزارش کارکنان#

در گزارش کارکنان اصل «مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var strategy = db.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => {     await using var tx = await db.Database.BeginTransactionAsync(ct);     await ApplyChangesAsync(db, ct);     await tx.CommitAsync(ct); });

نمونه 347: Transaction، Retry و کنترل هم‌زمانی در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. راهبرد نمونه این است که ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. SQL و پارامترها را ببینید و مقایسه را با Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var row = await db.Invoices.SingleAsync(x => x.Id == id, ct); row.Name = newName; try { await db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException) { await db.Entry(row).ReloadAsync(ct); throw; }

نمونه 348: Transaction، Retry و کنترل هم‌زمانی در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Isolation level و ترتیب دسترسی جداول روی Blocking و Deadlock اثر مستقیم دارند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Ticket>().Property(x => x.RowVersion).IsRowVersion();

نمونه 349: Transaction، Retry و کنترل هم‌زمانی در صندوق پیام‌ها#

نمونه 349 برای صندوق پیام‌ها است. نشانه اولیه: Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. نسخه پیشنهادی ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); await firstStep(db, ct); await secondStep(db, ct); await tx.CommitAsync(ct);

نمونه 350: Transaction، Retry و کنترل هم‌زمانی در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var affected = await db.Events.Where(x => x.Id == id && x.RowVersion == version)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); if (affected == 0) throw new ConcurrencyException();

نمونه 351: Transaction، Retry و کنترل هم‌زمانی در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var strategy = db.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => {     await using var tx = await db.Database.BeginTransactionAsync(ct);     await ApplyChangesAsync(db, ct);     await tx.CommitAsync(ct); });

نمونه 352: Transaction، Retry و کنترل هم‌زمانی در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. راهبرد نمونه این است که ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. SQL و پارامترها را ببینید و مقایسه را با Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var row = await db.Payments.SingleAsync(x => x.Id == id, ct); row.Name = newName; try { await db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException) { await db.Entry(row).ReloadAsync(ct); throw; }

نمونه 353: Transaction، Retry و کنترل هم‌زمانی در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Isolation level و ترتیب دسترسی جداول روی Blocking و Deadlock اثر مستقیم دارند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Shipment>().Property(x => x.RowVersion).IsRowVersion();

نمونه 354: Transaction، Retry و کنترل هم‌زمانی در مرور رخدادها#

نمونه 354 برای مرور رخدادها است. نشانه اولیه: Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. نسخه پیشنهادی ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); await firstStep(db, ct); await secondStep(db, ct); await tx.CommitAsync(ct);

نمونه 355: Transaction، Retry و کنترل هم‌زمانی در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var affected = await db.AuditRecords.Where(x => x.Id == id && x.RowVersion == version)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); if (affected == 0) throw new ConcurrencyException();

نمونه 356: Transaction، Retry و کنترل هم‌زمانی در نشست‌های فعال#

در نشست‌های فعال اصل «مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var strategy = db.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => {     await using var tx = await db.Database.BeginTransactionAsync(ct);     await ApplyChangesAsync(db, ct);     await tx.CommitAsync(ct); });

نمونه 357: Transaction، Retry و کنترل هم‌زمانی در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. راهبرد نمونه این است که ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. SQL و پارامترها را ببینید و مقایسه را با Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var row = await db.Courses.SingleAsync(x => x.Id == id, ct); row.Name = newName; try { await db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException) { await db.Entry(row).ReloadAsync(ct); throw; }

نمونه 358: Transaction، Retry و کنترل هم‌زمانی در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Isolation level و ترتیب دسترسی جداول روی Blocking و Deadlock اثر مستقیم دارند. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: مرز Transaction باید کوتاه، قابل Retry و از نظر idempotency روشن باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.Entity<Enrollment>().Property(x => x.RowVersion).IsRowVersion();

نمونه 359: Transaction، Retry و کنترل هم‌زمانی در فهرست مقاله‌ها#

نمونه 359 برای فهرست مقاله‌ها است. نشانه اولیه: Transaction طولانی Lock نگه می‌دارد یا Retry کل عملیات را دوباره و ناسازگار اجرا می‌کند. نسخه پیشنهادی ExecutionStrategy، Transaction صریح و RowVersion را با ترتیب درست ترکیب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct); await firstStep(db, ct); await secondStep(db, ct); await tx.CommitAsync(ct);

نمونه 360: Transaction، Retry و کنترل هم‌زمانی در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Lock wait، deadlock، retry count، مدت Transaction و conflict rate را ثبت کنید. Retry بدون idempotency برای پرداخت، پیام و عملیات خارجی می‌تواند اثر تکراری بسازد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var affected = await db.Notifications.Where(x => x.Id == id && x.RowVersion == version)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); if (affected == 0) throw new ConcurrencyException();

فصل 19: SQL خام، Stored Procedure و Function Mapping#

در فصل 19 تمرکز بر «SQL خام، Stored Procedure و Function Mapping» است. اصل مرکزی این فصل چنین است: وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، SQL خام امکان استفاده مستقیم از قابلیت Provider را می‌دهد اما همچنان باید Projection و Index درست داشته باشد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 361: SQL خام، Stored Procedure و Function Mapping در فهرست محصولات#

در فهرست محصولات اصل «وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products     .FromSql($"SELECT * FROM dbo.Products WHERE TenantId = {tenantId}")     .AsNoTracking().ToListAsync(ct);

نمونه 362: SQL خام، Stored Procedure و Function Mapping در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. راهبرد نمونه این است که FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. SQL و پارامترها را ببینید و مقایسه را با Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Database.ExecuteSqlAsync(     $"UPDATE dbo.Orders SET UpdatedAt = {now} WHERE TenantId = {tenantId}", ct);

نمونه 363: SQL خام، Stored Procedure و Function Mapping در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL خام امکان استفاده مستقیم از قابلیت Provider را می‌دهد اما همچنان باید Projection و Index درست داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Customers.FromSql($"EXEC dbo.GetCustomersByTenant {tenantId}, {take}")     .AsNoTracking().ToListAsync(ct);

نمونه 364: SQL خام، Stored Procedure و Function Mapping در مدیریت وبلاگ‌ها#

نمونه 364 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. نسخه پیشنهادی FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.HasDbFunction(typeof(SearchFunctions).GetMethod(nameof(SearchFunctions.Score))!)     .HasName("ScoreBlog");

نمونه 365: SQL خام، Stored Procedure و Function Mapping در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Id", "Name", "CreatedAt" }; if (!allowed.Contains(sortColumn)) throw new ArgumentException(); // نام ستون فقط از Allowlist وارد SQL می‌شود.

نمونه 366: SQL خام، Stored Procedure و Function Mapping در گزارش کارکنان#

در گزارش کارکنان اصل «وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees     .FromSql($"SELECT * FROM dbo.Employees WHERE TenantId = {tenantId}")     .AsNoTracking().ToListAsync(ct);

نمونه 367: SQL خام، Stored Procedure و Function Mapping در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. راهبرد نمونه این است که FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. SQL و پارامترها را ببینید و مقایسه را با Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Database.ExecuteSqlAsync(     $"UPDATE dbo.Invoices SET UpdatedAt = {now} WHERE TenantId = {tenantId}", ct);

نمونه 368: SQL خام، Stored Procedure و Function Mapping در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL خام امکان استفاده مستقیم از قابلیت Provider را می‌دهد اما همچنان باید Projection و Index درست داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Tickets.FromSql($"EXEC dbo.GetTicketsByTenant {tenantId}, {take}")     .AsNoTracking().ToListAsync(ct);

نمونه 369: SQL خام، Stored Procedure و Function Mapping در صندوق پیام‌ها#

نمونه 369 برای صندوق پیام‌ها است. نشانه اولیه: Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. نسخه پیشنهادی FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.HasDbFunction(typeof(SearchFunctions).GetMethod(nameof(SearchFunctions.Score))!)     .HasName("ScoreMessage");

نمونه 370: SQL خام، Stored Procedure و Function Mapping در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Id", "Name", "CreatedAt" }; if (!allowed.Contains(sortColumn)) throw new ArgumentException(); // نام ستون فقط از Allowlist وارد SQL می‌شود.

نمونه 371: SQL خام، Stored Procedure و Function Mapping در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents     .FromSql($"SELECT * FROM dbo.Documents WHERE TenantId = {tenantId}")     .AsNoTracking().ToListAsync(ct);

نمونه 372: SQL خام، Stored Procedure و Function Mapping در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. راهبرد نمونه این است که FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. SQL و پارامترها را ببینید و مقایسه را با Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Database.ExecuteSqlAsync(     $"UPDATE dbo.Payments SET UpdatedAt = {now} WHERE TenantId = {tenantId}", ct);

نمونه 373: SQL خام، Stored Procedure و Function Mapping در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL خام امکان استفاده مستقیم از قابلیت Provider را می‌دهد اما همچنان باید Projection و Index درست داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Shipments.FromSql($"EXEC dbo.GetShipmentsByTenant {tenantId}, {take}")     .AsNoTracking().ToListAsync(ct);

نمونه 374: SQL خام، Stored Procedure و Function Mapping در مرور رخدادها#

نمونه 374 برای مرور رخدادها است. نشانه اولیه: Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. نسخه پیشنهادی FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.HasDbFunction(typeof(SearchFunctions).GetMethod(nameof(SearchFunctions.Score))!)     .HasName("ScoreLogEntry");

نمونه 375: SQL خام، Stored Procedure و Function Mapping در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Id", "Name", "CreatedAt" }; if (!allowed.Contains(sortColumn)) throw new ArgumentException(); // نام ستون فقط از Allowlist وارد SQL می‌شود.

نمونه 376: SQL خام، Stored Procedure و Function Mapping در نشست‌های فعال#

در نشست‌های فعال اصل «وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions     .FromSql($"SELECT * FROM dbo.Sessions WHERE TenantId = {tenantId}")     .AsNoTracking().ToListAsync(ct);

نمونه 377: SQL خام، Stored Procedure و Function Mapping در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. راهبرد نمونه این است که FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. SQL و پارامترها را ببینید و مقایسه را با Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var affected = await db.Database.ExecuteSqlAsync(     $"UPDATE dbo.Courses SET UpdatedAt = {now} WHERE TenantId = {tenantId}", ct);

نمونه 378: SQL خام، Stored Procedure و Function Mapping در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا SQL خام امکان استفاده مستقیم از قابلیت Provider را می‌دهد اما همچنان باید Projection و Index درست داشته باشد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: وقتی LINQ شکل مناسب SQL نمی‌سازد، خروج کنترل‌شده به SQL خام می‌تواند درست باشد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var rows = await db.Enrollments.FromSql($"EXEC dbo.GetEnrollmentsByTenant {tenantId}, {take}")     .AsNoTracking().ToListAsync(ct);

نمونه 379: SQL خام، Stored Procedure و Function Mapping در فهرست مقاله‌ها#

نمونه 379 برای فهرست مقاله‌ها است. نشانه اولیه: Query پیچیده با LINQ غیرقابل‌خواندن و Plan ضعیف باقی می‌ماند یا SQL خام با الحاق رشته ناامن نوشته می‌شود. نسخه پیشنهادی FromSql و ExecuteSql پارامتری، View، TVF یا Stored Procedure را پشت API محدود قرار دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.HasDbFunction(typeof(SearchFunctions).GetMethod(nameof(SearchFunctions.Score))!)     .HasName("ScoreArticle");

نمونه 380: SQL خام، Stored Procedure و Function Mapping در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، IO، زمان CPU، امنیت پارامتر و هزینه نگهداری را مقایسه کنید. نام ستون، جدول یا Order By را نمی‌توان مانند مقدار عادی پارامتری کرد؛ Allowlist لازم است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Id", "Name", "CreatedAt" }; if (!allowed.Contains(sortColumn)) throw new ArgumentException(); // نام ستون فقط از Allowlist وارد SQL می‌شود.

فصل 20: مدل‌سازی برای Performance و Compiled Model#

در فصل 20 تمرکز بر «مدل‌سازی برای Performance و Compiled Model» است. اصل مرکزی این فصل چنین است: شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، TPH غالباً Query ساده‌تری دارد؛ TPC برای نوع برگ خاص مناسب است و TPT معمولاً Join بیشتری می‌سازد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 381: مدل‌سازی برای Performance و Compiled Model در فهرست محصولات#

در فهرست محصولات اصل «شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Product>().UseTphMappingStrategy();

نمونه 382: مدل‌سازی برای Performance و Compiled Model در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. راهبرد نمونه این است که TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Order>().UseTpcMappingStrategy();

نمونه 383: مدل‌سازی برای Performance و Compiled Model در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا TPH غالباً Query ساده‌تری دارد؛ TPC برای نوع برگ خاص مناسب است و TPT معمولاً Join بیشتری می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

// تولید مدل Compileشده در خط فرمان: // dotnet ef dbcontext optimize --output-dir CompiledModels options.UseModel(AppDbContextModel.Instance);

نمونه 384: مدل‌سازی برای Performance و Compiled Model در مدیریت وبلاگ‌ها#

نمونه 384 برای مدیریت وبلاگ‌ها است. نشانه اولیه: مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. نسخه پیشنهادی TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Blog>().ComplexProperty(x => x.Address);

نمونه 385: مدل‌سازی برای Performance و Compiled Model در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Post>().Property(x => x.SearchName).HasComputedColumnSql("UPPER([Name])", stored: true);

نمونه 386: مدل‌سازی برای Performance و Compiled Model در گزارش کارکنان#

در گزارش کارکنان اصل «شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Employee>().UseTphMappingStrategy();

نمونه 387: مدل‌سازی برای Performance و Compiled Model در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. راهبرد نمونه این است که TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Invoice>().UseTpcMappingStrategy();

نمونه 388: مدل‌سازی برای Performance و Compiled Model در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا TPH غالباً Query ساده‌تری دارد؛ TPC برای نوع برگ خاص مناسب است و TPT معمولاً Join بیشتری می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

// تولید مدل Compileشده در خط فرمان: // dotnet ef dbcontext optimize --output-dir CompiledModels options.UseModel(AppDbContextModel.Instance);

نمونه 389: مدل‌سازی برای Performance و Compiled Model در صندوق پیام‌ها#

نمونه 389 برای صندوق پیام‌ها است. نشانه اولیه: مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. نسخه پیشنهادی TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Message>().ComplexProperty(x => x.Address);

نمونه 390: مدل‌سازی برای Performance و Compiled Model در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Event>().Property(x => x.SearchName).HasComputedColumnSql("UPPER([Name])", stored: true);

نمونه 391: مدل‌سازی برای Performance و Compiled Model در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Document>().UseTphMappingStrategy();

نمونه 392: مدل‌سازی برای Performance و Compiled Model در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. راهبرد نمونه این است که TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Payment>().UseTpcMappingStrategy();

نمونه 393: مدل‌سازی برای Performance و Compiled Model در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا TPH غالباً Query ساده‌تری دارد؛ TPC برای نوع برگ خاص مناسب است و TPT معمولاً Join بیشتری می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

// تولید مدل Compileشده در خط فرمان: // dotnet ef dbcontext optimize --output-dir CompiledModels options.UseModel(AppDbContextModel.Instance);

نمونه 394: مدل‌سازی برای Performance و Compiled Model در مرور رخدادها#

نمونه 394 برای مرور رخدادها است. نشانه اولیه: مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. نسخه پیشنهادی TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<LogEntry>().ComplexProperty(x => x.Address);

نمونه 395: مدل‌سازی برای Performance و Compiled Model در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<AuditRecord>().Property(x => x.SearchName).HasComputedColumnSql("UPPER([Name])", stored: true);

نمونه 396: مدل‌سازی برای Performance و Compiled Model در نشست‌های فعال#

در نشست‌های فعال اصل «شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Session>().UseTphMappingStrategy();

نمونه 397: مدل‌سازی برای Performance و Compiled Model در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. راهبرد نمونه این است که TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. SQL و پارامترها را ببینید و مقایسه را با زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

modelBuilder.Entity<Course>().UseTpcMappingStrategy();

نمونه 398: مدل‌سازی برای Performance و Compiled Model در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا TPH غالباً Query ساده‌تری دارد؛ TPC برای نوع برگ خاص مناسب است و TPT معمولاً Join بیشتری می‌سازد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: شکل مدل، وراثت و مرز Aggregate می‌تواند هزینه Query را از ابتدا تعیین کند. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

// تولید مدل Compileشده در خط فرمان: // dotnet ef dbcontext optimize --output-dir CompiledModels options.UseModel(AppDbContextModel.Instance);

نمونه 399: مدل‌سازی برای Performance و Compiled Model در فهرست مقاله‌ها#

نمونه 399 برای فهرست مقاله‌ها است. نشانه اولیه: مدل بسیار بزرگ Startup کند دارد یا TPT برای فهرست پرتکرار Joinهای فراوان تولید می‌کند. نسخه پیشنهادی TPH/TPC/TPT، Complex Type، denormalization و compiled model را با workload واقعی انتخاب کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Article>().ComplexProperty(x => x.Address);

نمونه 400: مدل‌سازی برای Performance و Compiled Model در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: زمان اولین Query، تعداد Join، اندازه مدل و زمان Migration/Startup را بسنجید. Compiled Model فقط برای مدل‌های بزرگ و Startup اثبات‌شده ارزش دارد و محدودیت‌های خود را دارد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

modelBuilder.Entity<Notification>().Property(x => x.SearchName).HasComputedColumnSql("UPPER([Name])", stored: true);

فصل 21: Caching در لایه برنامه و معماری خواندن#

در فصل 21 تمرکز بر «Caching در لایه برنامه و معماری خواندن» است. اصل مرکزی این فصل چنین است: Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Cache فشار دیتابیس را کم می‌کند اما Query بد را درمان نمی‌کند و هماهنگی چند نمونه برنامه مسئله جداست. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 401: Caching در لایه برنامه و معماری خواندن در فهرست محصولات#

در فهرست محصولات اصل «Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var key = $"tenant:{tenantId}:products:v{version}:1"; var rows = await cache.GetOrCreateAsync(key, async entry => {     entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);     return await db.Products.AsNoTracking().Take(10).ToArrayAsync(ct); });

نمونه 402: Caching در لایه برنامه و معماری خواندن در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. راهبرد نمونه این است که Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var dto = await db.Orders.AsNoTracking().Where(x => x.Id == id)     .Select(x => new OrderDto(x.Id, x.Name)).SingleOrDefaultAsync(ct); await distributedCache.SetAsync(cacheKey, Serialize(dto), options, ct);

نمونه 403: Caching در لایه برنامه و معماری خواندن در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Cache فشار دیتابیس را کم می‌کند اما Query بد را درمان نمی‌کند و هماهنگی چند نمونه برنامه مسئله جداست. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Customers.Where(x => x.Id == id).ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); await cache.RemoveAsync($"customers:{id}", ct);

نمونه 404: Caching در لایه برنامه و معماری خواندن در مدیریت وبلاگ‌ها#

نمونه 404 برای مدیریت وبلاگ‌ها است. نشانه اولیه: هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. نسخه پیشنهادی Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await stampedeLock.WaitAsync(ct); try {     if (!cache.TryGetValue(key, out value))         cache.Set(key, value = await loader(ct), ttl); } finally { stampedeLock.Release(); }

نمونه 405: Caching در لایه برنامه و معماری خواندن در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var key = $"tenant:{tenantId}:posts:page:{cursor}"; // Tenant جزئی از کلید است

نمونه 406: Caching در لایه برنامه و معماری خواندن در گزارش کارکنان#

در گزارش کارکنان اصل «Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var key = $"tenant:{tenantId}:employees:v{version}:6"; var rows = await cache.GetOrCreateAsync(key, async entry => {     entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);     return await db.Employees.AsNoTracking().Take(10).ToArrayAsync(ct); });

نمونه 407: Caching در لایه برنامه و معماری خواندن در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. راهبرد نمونه این است که Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var dto = await db.Invoices.AsNoTracking().Where(x => x.Id == id)     .Select(x => new InvoiceDto(x.Id, x.Name)).SingleOrDefaultAsync(ct); await distributedCache.SetAsync(cacheKey, Serialize(dto), options, ct);

نمونه 408: Caching در لایه برنامه و معماری خواندن در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Cache فشار دیتابیس را کم می‌کند اما Query بد را درمان نمی‌کند و هماهنگی چند نمونه برنامه مسئله جداست. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Tickets.Where(x => x.Id == id).ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); await cache.RemoveAsync($"tickets:{id}", ct);

نمونه 409: Caching در لایه برنامه و معماری خواندن در صندوق پیام‌ها#

نمونه 409 برای صندوق پیام‌ها است. نشانه اولیه: هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. نسخه پیشنهادی Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await stampedeLock.WaitAsync(ct); try {     if (!cache.TryGetValue(key, out value))         cache.Set(key, value = await loader(ct), ttl); } finally { stampedeLock.Release(); }

نمونه 410: Caching در لایه برنامه و معماری خواندن در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var key = $"tenant:{tenantId}:events:page:{cursor}"; // Tenant جزئی از کلید است

نمونه 411: Caching در لایه برنامه و معماری خواندن در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var key = $"tenant:{tenantId}:documents:v{version}:11"; var rows = await cache.GetOrCreateAsync(key, async entry => {     entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);     return await db.Documents.AsNoTracking().Take(10).ToArrayAsync(ct); });

نمونه 412: Caching در لایه برنامه و معماری خواندن در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. راهبرد نمونه این است که Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var dto = await db.Payments.AsNoTracking().Where(x => x.Id == id)     .Select(x => new PaymentDto(x.Id, x.Name)).SingleOrDefaultAsync(ct); await distributedCache.SetAsync(cacheKey, Serialize(dto), options, ct);

نمونه 413: Caching در لایه برنامه و معماری خواندن در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Cache فشار دیتابیس را کم می‌کند اما Query بد را درمان نمی‌کند و هماهنگی چند نمونه برنامه مسئله جداست. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Shipments.Where(x => x.Id == id).ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); await cache.RemoveAsync($"shipments:{id}", ct);

نمونه 414: Caching در لایه برنامه و معماری خواندن در مرور رخدادها#

نمونه 414 برای مرور رخدادها است. نشانه اولیه: هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. نسخه پیشنهادی Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await stampedeLock.WaitAsync(ct); try {     if (!cache.TryGetValue(key, out value))         cache.Set(key, value = await loader(ct), ttl); } finally { stampedeLock.Release(); }

نمونه 415: Caching در لایه برنامه و معماری خواندن در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var key = $"tenant:{tenantId}:auditrecords:page:{cursor}"; // Tenant جزئی از کلید است

نمونه 416: Caching در لایه برنامه و معماری خواندن در نشست‌های فعال#

در نشست‌های فعال اصل «Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var key = $"tenant:{tenantId}:sessions:v{version}:16"; var rows = await cache.GetOrCreateAsync(key, async entry => {     entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);     return await db.Sessions.AsNoTracking().Take(10).ToArrayAsync(ct); });

نمونه 417: Caching در لایه برنامه و معماری خواندن در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. راهبرد نمونه این است که Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. SQL و پارامترها را ببینید و مقایسه را با Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var dto = await db.Courses.AsNoTracking().Where(x => x.Id == id)     .Select(x => new CourseDto(x.Id, x.Name)).SingleOrDefaultAsync(ct); await distributedCache.SetAsync(cacheKey, Serialize(dto), options, ct);

نمونه 418: Caching در لایه برنامه و معماری خواندن در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Cache فشار دیتابیس را کم می‌کند اما Query بد را درمان نمی‌کند و هماهنگی چند نمونه برنامه مسئله جداست. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Cache باید هزینه Query داغ را کم کند بدون آن‌که صحت و تازگی مبهم شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await db.Enrollments.Where(x => x.Id == id).ExecuteUpdateAsync(s => s.SetProperty(x => x.Name, newName), ct); await cache.RemoveAsync($"enrollments:{id}", ct);

نمونه 419: Caching در لایه برنامه و معماری خواندن در فهرست مقاله‌ها#

نمونه 419 برای فهرست مقاله‌ها است. نشانه اولیه: هر درخواست همان داده مرجع را می‌خواند یا Cache بدون انقضا داده قدیمی تحویل می‌دهد. نسخه پیشنهادی Cache-Aside، کلید نسخه‌دار، TTL، invalidation رویدادی و جلوگیری از stampede را طراحی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

await stampedeLock.WaitAsync(ct); try {     if (!cache.TryGetValue(key, out value))         cache.Set(key, value = await loader(ct), ttl); } finally { stampedeLock.Release(); }

نمونه 420: Caching در لایه برنامه و معماری خواندن در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Hit ratio، miss latency، stale rate، اندازه Cache و Query saved را اندازه بگیرید. Entity tracked یا DbContext را Cache نکنید؛ DTO تغییرناپذیر و کلید شامل Tenant و نسخه امن‌تر است. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var key = $"tenant:{tenantId}:notifications:page:{cursor}"; // Tenant جزئی از کلید است

فصل 22: ASP.NET Core، Async و محدودکردن فشار#

در فصل 22 تمرکز بر «ASP.NET Core، Async و محدودکردن فشار» است. اصل مرکزی این فصل چنین است: Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، چند Query موازی ممکن است Latency یک Request را کم کند ولی بار کل و اتصال‌های هم‌زمان را بالا می‌برد. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 421: ASP.NET Core، Async و محدودکردن فشار در فهرست محصولات#

در فهرست محصولات اصل «Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

app.MapGet("/products", async (AppDbContext db, CancellationToken ct) =>     await db.Products.AsNoTracking().Select(x => new { x.Id, x.Name }).Take(10).ToListAsync(ct));

نمونه 422: ASP.NET Core، Async و محدودکردن فشار در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. راهبرد نمونه این است که Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); timeout.CancelAfter(TimeSpan.FromSeconds(5)); return await db.Orders.AsNoTracking().Take(20).ToListAsync(timeout.Token);

نمونه 423: ASP.NET Core، Async و محدودکردن فشار در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا چند Query موازی ممکن است Latency یک Request را کم کند ولی بار کل و اتصال‌های هم‌زمان را بالا می‌برد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await concurrencyGate.WaitAsync(ct); try { return await query(ct); } finally { concurrencyGate.Release(); }

نمونه 424: ASP.NET Core، Async و محدودکردن فشار در مدیریت وبلاگ‌ها#

نمونه 424 برای مدیریت وبلاگ‌ها است. نشانه اولیه: Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. نسخه پیشنهادی Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var a = LoadSummaryAsync(factory, ct); var b = LoadCountAsync(factory, ct); await Task.WhenAll(a, b); // هر عملیات Context مستقل دارد

نمونه 425: ASP.NET Core، Async و محدودکردن فشار در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

return Results.Stream(async stream => {     await foreach (var row in ReadPostsAsync(factory, ct))         await JsonSerializer.SerializeAsync(stream, row, cancellationToken: ct); });

نمونه 426: ASP.NET Core، Async و محدودکردن فشار در گزارش کارکنان#

در گزارش کارکنان اصل «Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

app.MapGet("/employees", async (AppDbContext db, CancellationToken ct) =>     await db.Employees.AsNoTracking().Select(x => new { x.Id, x.Name }).Take(10).ToListAsync(ct));

نمونه 427: ASP.NET Core، Async و محدودکردن فشار در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. راهبرد نمونه این است که Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); timeout.CancelAfter(TimeSpan.FromSeconds(5)); return await db.Invoices.AsNoTracking().Take(20).ToListAsync(timeout.Token);

نمونه 428: ASP.NET Core، Async و محدودکردن فشار در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا چند Query موازی ممکن است Latency یک Request را کم کند ولی بار کل و اتصال‌های هم‌زمان را بالا می‌برد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await concurrencyGate.WaitAsync(ct); try { return await query(ct); } finally { concurrencyGate.Release(); }

نمونه 429: ASP.NET Core، Async و محدودکردن فشار در صندوق پیام‌ها#

نمونه 429 برای صندوق پیام‌ها است. نشانه اولیه: Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. نسخه پیشنهادی Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var a = LoadSummaryAsync(factory, ct); var b = LoadCountAsync(factory, ct); await Task.WhenAll(a, b); // هر عملیات Context مستقل دارد

نمونه 430: ASP.NET Core، Async و محدودکردن فشار در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

return Results.Stream(async stream => {     await foreach (var row in ReadEventsAsync(factory, ct))         await JsonSerializer.SerializeAsync(stream, row, cancellationToken: ct); });

نمونه 431: ASP.NET Core، Async و محدودکردن فشار در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

app.MapGet("/documents", async (AppDbContext db, CancellationToken ct) =>     await db.Documents.AsNoTracking().Select(x => new { x.Id, x.Name }).Take(10).ToListAsync(ct));

نمونه 432: ASP.NET Core، Async و محدودکردن فشار در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. راهبرد نمونه این است که Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); timeout.CancelAfter(TimeSpan.FromSeconds(5)); return await db.Payments.AsNoTracking().Take(20).ToListAsync(timeout.Token);

نمونه 433: ASP.NET Core، Async و محدودکردن فشار در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا چند Query موازی ممکن است Latency یک Request را کم کند ولی بار کل و اتصال‌های هم‌زمان را بالا می‌برد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await concurrencyGate.WaitAsync(ct); try { return await query(ct); } finally { concurrencyGate.Release(); }

نمونه 434: ASP.NET Core، Async و محدودکردن فشار در مرور رخدادها#

نمونه 434 برای مرور رخدادها است. نشانه اولیه: Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. نسخه پیشنهادی Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var a = LoadSummaryAsync(factory, ct); var b = LoadCountAsync(factory, ct); await Task.WhenAll(a, b); // هر عملیات Context مستقل دارد

نمونه 435: ASP.NET Core، Async و محدودکردن فشار در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

return Results.Stream(async stream => {     await foreach (var row in ReadAuditRecordsAsync(factory, ct))         await JsonSerializer.SerializeAsync(stream, row, cancellationToken: ct); });

نمونه 436: ASP.NET Core، Async و محدودکردن فشار در نشست‌های فعال#

در نشست‌های فعال اصل «Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

app.MapGet("/sessions", async (AppDbContext db, CancellationToken ct) =>     await db.Sessions.AsNoTracking().Select(x => new { x.Id, x.Name }).Take(10).ToListAsync(ct));

نمونه 437: ASP.NET Core، Async و محدودکردن فشار در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. راهبرد نمونه این است که Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. SQL و پارامترها را ببینید و مقایسه را با ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); timeout.CancelAfter(TimeSpan.FromSeconds(5)); return await db.Courses.AsNoTracking().Take(20).ToListAsync(timeout.Token);

نمونه 438: ASP.NET Core، Async و محدودکردن فشار در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا چند Query موازی ممکن است Latency یک Request را کم کند ولی بار کل و اتصال‌های هم‌زمان را بالا می‌برد. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: Async ظرفیت Thread را آزاد می‌کند اما ظرفیت دیتابیس همچنان محدود است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

await concurrencyGate.WaitAsync(ct); try { return await query(ct); } finally { concurrencyGate.Release(); }

نمونه 439: ASP.NET Core، Async و محدودکردن فشار در فهرست مقاله‌ها#

نمونه 439 برای فهرست مقاله‌ها است. نشانه اولیه: Task.Run روی IO، fan-out بی‌حد یا Cancellation نادیده‌گرفته‌شده Pool اتصال را اشباع می‌کند. نسخه پیشنهادی Async سرتاسری، CancellationToken، محدودکننده هم‌زمانی و timeout بودجه‌بندی‌شده استفاده کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var a = LoadSummaryAsync(factory, ct); var b = LoadCountAsync(factory, ct); await Task.WhenAll(a, b); // هر عملیات Context مستقل دارد

نمونه 440: ASP.NET Core، Async و محدودکردن فشار در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: ThreadPool queue، Connection Pool wait، RPS، P95 و نرخ لغو را بسنجید. DbContext مشترک را در Task.WhenAll استفاده نکنید و Timeout را جایگزین Cancellation و ظرفیت‌سنجی ندانید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

return Results.Stream(async stream => {     await foreach (var row in ReadNotificationsAsync(factory, ct))         await JsonSerializer.SerializeAsync(stream, row, cancellationToken: ct); });

فصل 23: بهینه‌سازی C#، Allocation و GC در مسیر داده#

در فصل 23 تمرکز بر «بهینه‌سازی C#، Allocation و GC در مسیر داده» است. اصل مرکزی این فصل چنین است: کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، هزینه Materialization و Serialization در برنامه است و ممکن است پس از سریع شدن SQL به گلوگاه اصلی تبدیل شود. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 441: بهینه‌سازی C#، Allocation و GC در مسیر داده در فهرست محصولات#

در فهرست محصولات اصل «کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Products.AsNoTracking()     .Select(x => new ProductRow(x.Id, x.Name))     .ToArrayAsync(ct);

نمونه 442: بهینه‌سازی C#، Allocation و GC در مسیر داده در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. راهبرد نمونه این است که Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. SQL و پارامترها را ببینید و مقایسه را با Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var ids = await db.Orders.AsNoTracking().Where(x => x.IsActive)     .Select(x => x.Id).ToArrayAsync(ct); // Entity ساخته نمی‌شود

نمونه 443: بهینه‌سازی C#، Allocation و GC در مسیر داده در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا هزینه Materialization و Serialization در برنامه است و ممکن است پس از سریع شدن SQL به گلوگاه اصلی تبدیل شود. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024); try { await SerializeBatchAsync(buffer, ct); } finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }

نمونه 444: بهینه‌سازی C#، Allocation و GC در مسیر داده در مدیریت وبلاگ‌ها#

نمونه 444 برای مدیریت وبلاگ‌ها است. نشانه اولیه: LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. نسخه پیشنهادی Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var prefix = request.Prefix; // Closure پایدار و یک‌بار ساخته می‌شود var query = db.Blogs.Where(x => x.Name.StartsWith(prefix));

نمونه 445: بهینه‌سازی C#، Allocation و GC در مسیر داده در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var before = GC.GetTotalAllocatedBytes(precise: true); await ExecuteScenarioAsync(ct); var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;

نمونه 446: بهینه‌سازی C#، Allocation و GC در مسیر داده در گزارش کارکنان#

در گزارش کارکنان اصل «کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Employees.AsNoTracking()     .Select(x => new EmployeeRow(x.Id, x.Name))     .ToArrayAsync(ct);

نمونه 447: بهینه‌سازی C#، Allocation و GC در مسیر داده در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. راهبرد نمونه این است که Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. SQL و پارامترها را ببینید و مقایسه را با Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var ids = await db.Invoices.AsNoTracking().Where(x => x.IsActive)     .Select(x => x.Id).ToArrayAsync(ct); // Entity ساخته نمی‌شود

نمونه 448: بهینه‌سازی C#، Allocation و GC در مسیر داده در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا هزینه Materialization و Serialization در برنامه است و ممکن است پس از سریع شدن SQL به گلوگاه اصلی تبدیل شود. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024); try { await SerializeBatchAsync(buffer, ct); } finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }

نمونه 449: بهینه‌سازی C#، Allocation و GC در مسیر داده در صندوق پیام‌ها#

نمونه 449 برای صندوق پیام‌ها است. نشانه اولیه: LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. نسخه پیشنهادی Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var prefix = request.Prefix; // Closure پایدار و یک‌بار ساخته می‌شود var query = db.Messages.Where(x => x.Name.StartsWith(prefix));

نمونه 450: بهینه‌سازی C#، Allocation و GC در مسیر داده در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var before = GC.GetTotalAllocatedBytes(precise: true); await ExecuteScenarioAsync(ct); var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;

نمونه 451: بهینه‌سازی C#، Allocation و GC در مسیر داده در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Documents.AsNoTracking()     .Select(x => new DocumentRow(x.Id, x.Name))     .ToArrayAsync(ct);

نمونه 452: بهینه‌سازی C#، Allocation و GC در مسیر داده در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. راهبرد نمونه این است که Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. SQL و پارامترها را ببینید و مقایسه را با Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var ids = await db.Payments.AsNoTracking().Where(x => x.IsActive)     .Select(x => x.Id).ToArrayAsync(ct); // Entity ساخته نمی‌شود

نمونه 453: بهینه‌سازی C#، Allocation و GC در مسیر داده در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا هزینه Materialization و Serialization در برنامه است و ممکن است پس از سریع شدن SQL به گلوگاه اصلی تبدیل شود. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024); try { await SerializeBatchAsync(buffer, ct); } finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }

نمونه 454: بهینه‌سازی C#، Allocation و GC در مسیر داده در مرور رخدادها#

نمونه 454 برای مرور رخدادها است. نشانه اولیه: LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. نسخه پیشنهادی Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var prefix = request.Prefix; // Closure پایدار و یک‌بار ساخته می‌شود var query = db.LogEntries.Where(x => x.Name.StartsWith(prefix));

نمونه 455: بهینه‌سازی C#، Allocation و GC در مسیر داده در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var before = GC.GetTotalAllocatedBytes(precise: true); await ExecuteScenarioAsync(ct); var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;

نمونه 456: بهینه‌سازی C#، Allocation و GC در مسیر داده در نشست‌های فعال#

در نشست‌های فعال اصل «کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

var rows = await db.Sessions.AsNoTracking()     .Select(x => new SessionRow(x.Id, x.Name))     .ToArrayAsync(ct);

نمونه 457: بهینه‌سازی C#، Allocation و GC در مسیر داده در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. راهبرد نمونه این است که Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. SQL و پارامترها را ببینید و مقایسه را با Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var ids = await db.Courses.AsNoTracking().Where(x => x.IsActive)     .Select(x => x.Id).ToArrayAsync(ct); // Entity ساخته نمی‌شود

نمونه 458: بهینه‌سازی C#، Allocation و GC در مسیر داده در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا هزینه Materialization و Serialization در برنامه است و ممکن است پس از سریع شدن SQL به گلوگاه اصلی تبدیل شود. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: کاهش Entity و آبجکت موقت اغلب مهم‌تر از ترفندهای ریز CPU است. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024); try { await SerializeBatchAsync(buffer, ct); } finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }

نمونه 459: بهینه‌سازی C#، Allocation و GC در مسیر داده در فهرست مقاله‌ها#

نمونه 459 برای فهرست مقاله‌ها است. نشانه اولیه: LINQ زنجیره‌ای، Closure، رشته‌سازی و DTOهای حجیم در مسیر داغ Allocation زیاد می‌سازند. نسخه پیشنهادی Projection مستقیم، نوع سبک، ArrayPool در جای مناسب و حذف تبدیل‌های غیرضروری را با اندازه‌گیری انجام دهید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

var prefix = request.Prefix; // Closure پایدار و یک‌بار ساخته می‌شود var query = db.Articles.Where(x => x.Name.StartsWith(prefix));

نمونه 460: بهینه‌سازی C#، Allocation و GC در مسیر داده در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Allocated Bytes/op، Gen0/1/2، LOH، pause time و CPU را ثبت کنید. Poolکردن اشیای پیچیده یا نگه‌داشتن بافر بزرگ می‌تواند حافظه را بدتر کند؛ مالکیت و پاک‌سازی را روشن کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

var before = GC.GetTotalAllocatedBytes(precise: true); await ExecuteScenarioAsync(ct); var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;

فصل 24: Interceptor، Metrics، Benchmark و Load Test#

در فصل 24 تمرکز بر «Interceptor، Metrics، Benchmark و Load Test» است. اصل مرکزی این فصل چنین است: تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، Interceptor می‌تواند عملیات را تغییر دهد یا متوقف کند؛ برای لاگ ساده از logging معمولی استفاده کنید. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 461: Interceptor، Metrics، Benchmark و Load Test در فهرست محصولات#

در فهرست محصولات اصل «تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

public sealed class TimingInterceptor : DbCommandInterceptor {     public override InterceptionResult<DbDataReader> ReaderExecuting(         DbCommand command, CommandEventData data, InterceptionResult<DbDataReader> result)     {         Activity.Current?.SetTag("db.statement.length", command.CommandText.Length);         return result;     } }

نمونه 462: Interceptor، Metrics، Benchmark و Load Test در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. راهبرد نمونه این است که Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. SQL و پارامترها را ببینید و مقایسه را با P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.AddInterceptors(new TimingInterceptor());

نمونه 463: Interceptor، Metrics، Benchmark و Load Test در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Interceptor می‌تواند عملیات را تغییر دهد یا متوقف کند؛ برای لاگ ساده از logging معمولی استفاده کنید. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

private static readonly Meter Meter = new("MyApp.Data"); private static readonly Histogram<double> QueryMs = Meter.CreateHistogram<double>("ef.query.ms");

نمونه 464: Interceptor، Metrics، Benchmark و Load Test در مدیریت وبلاگ‌ها#

نمونه 464 برای مدیریت وبلاگ‌ها است. نشانه اولیه: یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. نسخه پیشنهادی Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

[MemoryDiagnoser] public class QueryBenchmarks {     [Benchmark]     public Task<int> CountActive() => db.Items.CountAsync(x => x.IsActive); }

نمونه 465: Interceptor، Metrics، Benchmark و Load Test در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Load test: warm-up، داده ثابت و محدودیت اتصال مشخص // dotnet-counters monitor --process-id <PID> System.Runtime Microsoft.EntityFrameworkCore

نمونه 466: Interceptor، Metrics، Benchmark و Load Test در گزارش کارکنان#

در گزارش کارکنان اصل «تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

public sealed class TimingInterceptor : DbCommandInterceptor {     public override InterceptionResult<DbDataReader> ReaderExecuting(         DbCommand command, CommandEventData data, InterceptionResult<DbDataReader> result)     {         Activity.Current?.SetTag("db.statement.length", command.CommandText.Length);         return result;     } }

نمونه 467: Interceptor، Metrics، Benchmark و Load Test در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. راهبرد نمونه این است که Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. SQL و پارامترها را ببینید و مقایسه را با P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.AddInterceptors(new TimingInterceptor());

نمونه 468: Interceptor، Metrics، Benchmark و Load Test در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Interceptor می‌تواند عملیات را تغییر دهد یا متوقف کند؛ برای لاگ ساده از logging معمولی استفاده کنید. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

private static readonly Meter Meter = new("MyApp.Data"); private static readonly Histogram<double> QueryMs = Meter.CreateHistogram<double>("ef.query.ms");

نمونه 469: Interceptor، Metrics، Benchmark و Load Test در صندوق پیام‌ها#

نمونه 469 برای صندوق پیام‌ها است. نشانه اولیه: یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. نسخه پیشنهادی Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

[MemoryDiagnoser] public class QueryBenchmarks {     [Benchmark]     public Task<int> CountActive() => db.Items.CountAsync(x => x.IsActive); }

نمونه 470: Interceptor، Metrics، Benchmark و Load Test در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Load test: warm-up، داده ثابت و محدودیت اتصال مشخص // dotnet-counters monitor --process-id <PID> System.Runtime Microsoft.EntityFrameworkCore

نمونه 471: Interceptor، Metrics، Benchmark و Load Test در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

public sealed class TimingInterceptor : DbCommandInterceptor {     public override InterceptionResult<DbDataReader> ReaderExecuting(         DbCommand command, CommandEventData data, InterceptionResult<DbDataReader> result)     {         Activity.Current?.SetTag("db.statement.length", command.CommandText.Length);         return result;     } }

نمونه 472: Interceptor، Metrics، Benchmark و Load Test در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. راهبرد نمونه این است که Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. SQL و پارامترها را ببینید و مقایسه را با P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.AddInterceptors(new TimingInterceptor());

نمونه 473: Interceptor، Metrics، Benchmark و Load Test در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Interceptor می‌تواند عملیات را تغییر دهد یا متوقف کند؛ برای لاگ ساده از logging معمولی استفاده کنید. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

private static readonly Meter Meter = new("MyApp.Data"); private static readonly Histogram<double> QueryMs = Meter.CreateHistogram<double>("ef.query.ms");

نمونه 474: Interceptor، Metrics، Benchmark و Load Test در مرور رخدادها#

نمونه 474 برای مرور رخدادها است. نشانه اولیه: یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. نسخه پیشنهادی Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

[MemoryDiagnoser] public class QueryBenchmarks {     [Benchmark]     public Task<int> CountActive() => db.Items.CountAsync(x => x.IsActive); }

نمونه 475: Interceptor، Metrics، Benchmark و Load Test در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Load test: warm-up، داده ثابت و محدودیت اتصال مشخص // dotnet-counters monitor --process-id <PID> System.Runtime Microsoft.EntityFrameworkCore

نمونه 476: Interceptor، Metrics، Benchmark و Load Test در نشست‌های فعال#

در نشست‌های فعال اصل «تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

public sealed class TimingInterceptor : DbCommandInterceptor {     public override InterceptionResult<DbDataReader> ReaderExecuting(         DbCommand command, CommandEventData data, InterceptionResult<DbDataReader> result)     {         Activity.Current?.SetTag("db.statement.length", command.CommandText.Length);         return result;     } }

نمونه 477: Interceptor، Metrics، Benchmark و Load Test در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. راهبرد نمونه این است که Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. SQL و پارامترها را ببینید و مقایسه را با P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

options.AddInterceptors(new TimingInterceptor());

نمونه 478: Interceptor، Metrics، Benchmark و Load Test در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا Interceptor می‌تواند عملیات را تغییر دهد یا متوقف کند؛ برای لاگ ساده از logging معمولی استفاده کنید. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: تشخیص پایدار به Telemetry کم‌هزینه و آزمایش قابل تکرار نیاز دارد. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

private static readonly Meter Meter = new("MyApp.Data"); private static readonly Histogram<double> QueryMs = Meter.CreateHistogram<double>("ef.query.ms");

نمونه 479: Interceptor، Metrics، Benchmark و Load Test در فهرست مقاله‌ها#

نمونه 479 برای فهرست مقاله‌ها است. نشانه اولیه: یک Stopwatch محلی یا لاگ تک‌نمونه به‌عنوان نتیجه قطعی ارائه می‌شود. نسخه پیشنهادی Interceptor برای اندازه‌گیری Command، Meter/Activity برای مشاهده و BenchmarkDotNet برای microbenchmark به‌کار ببرید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

[MemoryDiagnoser] public class QueryBenchmarks {     [Benchmark]     public Task<int> CountActive() => db.Items.CountAsync(x => x.IsActive); }

نمونه 480: Interceptor، Metrics، Benchmark و Load Test در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: P50/P95/P99، RPS، خطا، Query count، Allocation و منابع دیتابیس را هم‌زمان ببینید. Benchmark بدون warm-up، داده واقع‌نما و دیتابیس جدا نتیجه گمراه‌کننده می‌دهد. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

// Load test: warm-up، داده ثابت و محدودیت اتصال مشخص // dotnet-counters monitor --process-id <PID> System.Runtime Microsoft.EntityFrameworkCore

فصل 25: قابلیت‌های EF Core 10 با اثر عملکردی#

در فصل 25 تمرکز بر «قابلیت‌های EF Core 10 با اثر عملکردی» است. اصل مرکزی این فصل چنین است: ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود. این اصل کمک می‌کند بین تغییر ظاهراً سریع و بهبود قابل تکرار تفاوت بگذاریم. در پروژه واقعی لازم است ورودی، حجم داده، تعداد کاربر هم‌زمان، نوع Provider، منطقه استقرار برنامه و دیتابیس و وضعیت Cache ثبت شود. بدون این زمینه، مقایسه قبل و بعد ممکن است بیشتر بازتاب گرم شدن Cache یا تغییر بار لحظه‌ای باشد تا اثر کد.

نشانه رایج مشکل آن است که تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. برای تشخیص، یک Request مشخص را انتخاب کنید، شناسه هم‌بستگی به آن بدهید و مسیرش را از Endpoint تا SQL دنبال کنید. سپس خط مبنایی با داده واقع‌نما بسازید. اجرای اول ممکن است هزینه ساخت مدل، JIT، باز کردن Connection و گرم شدن Plan Cache را داشته باشد؛ بنابراین warm-up و چند تکرار ضروری است. در گزارش، میانه و صدک‌های بالا را جدا بنویسید و Outlier را بی‌دلیل حذف نکنید.

راهبرد پیشنهادی این فصل این است که JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. تغییر را کوچک نگه دارید تا علت اثر قابل انتساب باشد. اگر هم‌زمان Index، Projection، Pool و Cache را عوض کنید، معلوم نمی‌شود کدام بخش نتیجه داده و بازگشت از خطا دشوار می‌شود. ابتدا Query داغ را با TagWith علامت‌گذاری کنید، SQL و Plan را ذخیره کنید، یک تغییر اعمال کنید و همان سناریو را با همان داده و محدودیت اتصال دوباره اجرا کنید.

برای ارزیابی، Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. در سمت برنامه EventCounters، Meter، Activity، لاگ ساختاریافته و پروفایل حافظه مفید هستند. در سمت SQL Server از Actual Execution Plan، Query Store، STATISTICS IO/TIME، Wait Stats و شاخص‌های Lock استفاده کنید. هدف جمع‌کردن همه ابزارها نیست؛ هدف یافتن کمترین مجموعه شواهدی است که فرضیه را تأیید یا رد کند. Telemetry نیز باید کم‌هزینه، نمونه‌برداری‌شده و فاقد داده حساس باشد.

از دید دیتابیس، EF10 با SQL Server 2025 از نوع‌های JSON و Vector پشتیبانی می‌کند و Complex Type را گسترش داده است. عبارت LINQ فقط ورودی مترجم است و کیفیت نهایی را SQL، نوع پارامتر، Cardinality، Index و وضعیت آمار تعیین می‌کند. یک LINQ کوتاه می‌تواند SQL گران و یک LINQ طولانی می‌تواند SQL کاملاً مناسب بسازد. همیشه ستون‌های Select، Predicate، Join، Order By و تعداد Command را ببینید. اگر تخمین تعداد ردیف با مقدار واقعی فاصله زیاد دارد، آمار، توزیع داده و Parameter Sniffing را بررسی کنید.

نمونه‌های عملی#

نمونه 481: قابلیت‌های EF Core 10 با اثر عملکردی در فهرست محصولات#

در فهرست محصولات اصل «ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود» روی Product اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Product>().ComplexProperty(x => x.Details, b => b.ToJson());

نمونه 482: قابلیت‌های EF Core 10 با اثر عملکردی در گزارش سفارش‌ها#

این الگو برای سفارش زمانی مناسب است که تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. راهبرد نمونه این است که JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var nearest = await db.Orders.OrderBy(x =>     EF.Functions.VectorDistance("cosine", x.Embedding, queryVector))     .Take(20).ToListAsync(ct);

نمونه 483: قابلیت‌های EF Core 10 با اثر عملکردی در جست‌وجوی مشتریان#

Query موجودیت Customer یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF10 با SQL Server 2025 از نوع‌های JSON و Vector پشتیبانی می‌کند و Complex Type را گسترش داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.UseNamedDefaultConstraints();

نمونه 484: قابلیت‌های EF Core 10 با اثر عملکردی در مدیریت وبلاگ‌ها#

نمونه 484 برای مدیریت وبلاگ‌ها است. نشانه اولیه: تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. نسخه پیشنهادی JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Blog>()     .HasQueryFilter("TenantFilter", x => x.TenantId == tenantId)     .HasQueryFilter("ActiveFilter", x => x.IsActive);

نمونه 485: قابلیت‌های EF Core 10 با اثر عملکردی در فهرست نوشته‌ها#

این قطعه در سرویس مقاله هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await db.Posts.Where(x => x.TenantId == tenantId)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Details.Status, newStatus), ct);

نمونه 486: قابلیت‌های EF Core 10 با اثر عملکردی در گزارش کارکنان#

در گزارش کارکنان اصل «ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود» روی Employee اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Employee>().ComplexProperty(x => x.Details, b => b.ToJson());

نمونه 487: قابلیت‌های EF Core 10 با اثر عملکردی در فهرست صورتحساب‌ها#

این الگو برای صورتحساب زمانی مناسب است که تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. راهبرد نمونه این است که JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var nearest = await db.Invoices.OrderBy(x =>     EF.Functions.VectorDistance("cosine", x.Embedding, queryVector))     .Take(20).ToListAsync(ct);

نمونه 488: قابلیت‌های EF Core 10 با اثر عملکردی در صف تیکت‌ها#

Query موجودیت Ticket یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF10 با SQL Server 2025 از نوع‌های JSON و Vector پشتیبانی می‌کند و Complex Type را گسترش داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.UseNamedDefaultConstraints();

نمونه 489: قابلیت‌های EF Core 10 با اثر عملکردی در صندوق پیام‌ها#

نمونه 489 برای صندوق پیام‌ها است. نشانه اولیه: تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. نسخه پیشنهادی JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Message>()     .HasQueryFilter("TenantFilter", x => x.TenantId == tenantId)     .HasQueryFilter("ActiveFilter", x => x.IsActive);

نمونه 490: قابلیت‌های EF Core 10 با اثر عملکردی در رویدادهای آینده#

این قطعه در سرویس تقویم هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await db.Events.Where(x => x.TenantId == tenantId)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Details.Status, newStatus), ct);

نمونه 491: قابلیت‌های EF Core 10 با اثر عملکردی در جست‌وجوی اسناد#

در جست‌وجوی اسناد اصل «ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود» روی Document اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Document>().ComplexProperty(x => x.Details, b => b.ToJson());

نمونه 492: قابلیت‌های EF Core 10 با اثر عملکردی در گزارش پرداخت‌ها#

این الگو برای پرداخت زمانی مناسب است که تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. راهبرد نمونه این است که JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var nearest = await db.Payments.OrderBy(x =>     EF.Functions.VectorDistance("cosine", x.Embedding, queryVector))     .Take(20).ToListAsync(ct);

نمونه 493: قابلیت‌های EF Core 10 با اثر عملکردی در رهگیری مرسوله‌ها#

Query موجودیت Shipment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF10 با SQL Server 2025 از نوع‌های JSON و Vector پشتیبانی می‌کند و Complex Type را گسترش داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.UseNamedDefaultConstraints();

نمونه 494: قابلیت‌های EF Core 10 با اثر عملکردی در مرور رخدادها#

نمونه 494 برای مرور رخدادها است. نشانه اولیه: تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. نسخه پیشنهادی JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<LogEntry>()     .HasQueryFilter("TenantFilter", x => x.TenantId == tenantId)     .HasQueryFilter("ActiveFilter", x => x.IsActive);

نمونه 495: قابلیت‌های EF Core 10 با اثر عملکردی در گزارش ممیزی#

این قطعه در سرویس ممیزی هزینه اجرای SQL را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await db.AuditRecords.Where(x => x.TenantId == tenantId)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Details.Status, newStatus), ct);

نمونه 496: قابلیت‌های EF Core 10 با اثر عملکردی در نشست‌های فعال#

در نشست‌های فعال اصل «ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود» روی Session اجرا می‌شود. SQL، تعداد ردیف و P95 را قبل و بعد با داده یکسان ثبت کنید. تعداد Command و Allocation را نیز بسنجید. اگر Plan هنوز Scan یا Sort دارد، Index و Predicate را جدا بررسی کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید خط مبنا باید زمان صدک بالا، تعداد فرمان و خواندن منطقی را ثبت کند.

modelBuilder.Entity<Session>().ComplexProperty(x => x.Details, b => b.ToJson());

نمونه 497: قابلیت‌های EF Core 10 با اثر عملکردی در فهرست دوره‌ها#

این الگو برای آموزش زمانی مناسب است که تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. راهبرد نمونه این است که JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. SQL و پارامترها را ببینید و مقایسه را با Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید انجام دهید. آزمایش باید Cardinality نزدیک محیط تولید داشته باشد. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید نتیجه بدون سنجش حافظه، طرح اجرا و نرخ خطا قابل پذیرش نیست.

var nearest = await db.Courses.OrderBy(x =>     EF.Functions.VectorDistance("cosine", x.Embedding, queryVector))     .Take(20).ToListAsync(ct);

نمونه 498: قابلیت‌های EF Core 10 با اثر عملکردی در گزارش ثبت‌نام‌ها#

Query موجودیت Enrollment یک مسیر داغ فرض شده است. تغییر را زیر بار کنترل‌شده بسنجید، زیرا EF10 با SQL Server 2025 از نوع‌های JSON و Vector پشتیبانی می‌کند و Complex Type را گسترش داده است. اجرای سرد و گرم را جدا و Roundtrip، Logical Read، Allocation و P99 را با هم ثبت کنید. اصل تصمیم: ویژگی جدید فقط وقتی مفید است که Plan، سازگاری Provider و هزینه مهاجرت آن سنجیده شود. متن نهایی پرس‌وجو، پارامترها و تعداد رفت‌وبرگشت‌ها را برای مقایسه نگه دارید.

modelBuilder.UseNamedDefaultConstraints();

نمونه 499: قابلیت‌های EF Core 10 با اثر عملکردی در فهرست مقاله‌ها#

نمونه 499 برای فهرست مقاله‌ها است. نشانه اولیه: تیم با دیدن API جدید بدون بررسی نسخه SQL Server یا الگوی داده آن را فعال می‌کند. نسخه پیشنهادی JSON نوع‌دار، Vector Search، Complex Type، Query Filter نام‌دار و ترجمه‌های جدید را هدفمند ارزیابی کنید. DbContext باید کوتاه‌عمر باشد و Query داده اضافی نیاورد. سپس Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. پیش از کپی، SQL نهایی مدل خود را بررسی کنید. اثر تغییر باید زیر بار کنترل‌شده و داده نزدیک تولید دوباره مشاهده شود.

modelBuilder.Entity<Article>()     .HasQueryFilter("TenantFilter", x => x.TenantId == tenantId)     .HasQueryFilter("ActiveFilter", x => x.IsActive);

نمونه 500: قابلیت‌های EF Core 10 با اثر عملکردی در اعلان‌های کاربر#

این قطعه در سرویس اعلان هزینه به‌روزرسانی را هدف می‌گیرد. برای ترتیب، Null، Tenant و تغییر هم‌زمان تست صحت بنویسید و Benchmark قبل و بعد را نگه دارید. معیارها: Plan، اندازه ذخیره‌سازی، زمان جست‌وجو، کیفیت نتیجه و هزینه Migration را ثبت کنید. EF10 به .NET 10 نیاز دارد؛ Breaking Change و سطح سازگاری دیتابیس را پیش از ارتقا مرور کنید. در نتیجه مبهم Query را با TagWith در Query Store جدا کنید. کاهش میانگین همراه با رشد صدک نودونه یا خطا یک شکست سیستمی است.

await db.Notifications.Where(x => x.TenantId == tenantId)     .ExecuteUpdateAsync(s => s.SetProperty(x => x.Details.Status, newStatus), ct);

چک‌لیست نهایی Performance Review#

آیا Endpoint کند با Trace و TagWith به SQL مشخص متصل شده است؟

آیا خط مبنا شامل P50، P95، P99، نرخ خطا، Query count و Allocation است؟

آیا Query فقط ستون‌ها و ردیف‌های موردنیاز را برمی‌گرداند؟

آیا Query خواندنی به Tracking نیاز واقعی دارد؟

آیا Include مجموعه‌ای، N+1 یا انفجار ضرب دکارتی وجود دارد؟

آیا ترتیب صفحه‌بندی یکتا و برای عمق زیاد از Keyset استفاده شده است؟

آیا Index از Predicate و Order By اصلی پشتیبانی می‌کند و هزینه نوشتن سنجیده شده است؟

آیا Expression Tree و SQL برای مقادیر مختلف شکل پایدار و پارامتری دارند؟

آیا SaveChanges داخل حلقه، Tracking طولانی یا Update ردیف‌به‌ردیف قابل حذف است؟

آیا DbContext بین Taskها مشترک نشده و CancellationToken تا دیتابیس منتقل شده است؟

آیا Pool Context و Pool اتصال جداگانه اندازه‌گیری و تنظیم شده‌اند؟

آیا Transaction کوتاه، Retry idempotent و کنترل هم‌زمانی روشن است؟

آیا Cache کلید Tenant، TTL، invalidation و جلوگیری از stampede دارد؟

آیا Benchmark warm-up، حجم داده و محدودیت اتصال مشابه تولید دارد؟

آیا پس از استقرار داشبورد، هشدار و برنامه بازگشت وجود دارد؟

 

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

حرف 500 حداکثر

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620