آموزش جامع جدولهای بهینهشده برای حافظه (Memory-Optimized) در SQL Server
مقدمه
جدولهای بهینهشده برای حافظه (Memory-Optimized) یکی از ابزارهای مهم طراحی پردازش میانی در Microsoft SQL Server است. مسئله اصلی صرفاً نوشتن یک دستور نیست؛ باید بدانیم داده کجا نگهداری میشود، چه کسی آن را میبیند، Optimizer چه اطلاعاتی برای برآورد دارد و پاکسازی در چه زمانی رخ میدهد.
جدول Memory-Optimized بخشی از In-Memory OLTP است و ردیفها و ایندکسهای خود را با ساختارهای بدون Page در حافظه مدیریت میکند. کنترل همزمانی خوشبینانه و نسخهبندی ردیف جای قفلگذاری سنتی را میگیرد. جدول میتواند SCHEMA_AND_DATA برای دوام کامل یا SCHEMA_ONLY برای نگهداری صرفاً ساختار پس از بازیابی باشد؛ حالت دوم در برخی سناریوها جایگزین مرحلهبندی tempdb میشود.
در این راهنما از مثالهای کوچک آغاز میکنیم و سپس به دامنه، NULL، تراکنش، ایندکس، خطاهای رایج و سناریوهای Performance میرسیم. تمام Queryها برای آزمایش در محیط کنترلشده نوشته شدهاند و مثالهای دارای پیشنیاز، آن پیشنیاز را صریح اعلام میکنند.
برای مقایسه این ساختار با گزینههای دیگر، راهنمای جامع اشیای موقت در SQL Server را نیز مطالعه کنید.
تعریف و معماری جدول بهینهشده برای حافظه
جدول Memory-Optimized بخشی از In-Memory OLTP است و ردیفها و ایندکسهای خود را با ساختارهای بدون Page در حافظه مدیریت میکند. کنترل همزمانی خوشبینانه و نسخهبندی ردیف جای قفلگذاری سنتی را میگیرد. جدول میتواند SCHEMA_AND_DATA برای دوام کامل یا SCHEMA_ONLY برای نگهداری صرفاً ساختار پس از بازیابی باشد؛ حالت دوم در برخی سناریوها جایگزین مرحلهبندی tempdb میشود.
انتخاب صحیح زمانی رخ میدهد که سه محور را همزمان ببینیم: تعداد و توزیع ردیفها، مرز دسترسی نشست یا Batch، و تعداد دفعات خواندن و نوشتن. یک ساختار ساده در حجم کم ممکن است بهترین باشد، اما همان ساختار در گزارش چندمیلیونی میتواند به برآورد Cardinality نامناسب یا فشار منابع منجر شود.
همچنین باید تفاوت میان عمر منطقی داده و عمر فیزیکی شیء را درک کرد. SQL Server بخشی از عملیات را Cache میکند و این موضوع به معنای دائمی شدن داده نیست. قرارداد برنامه باید بر رفتار مستند دامنه و تراکنش تکیه کند، نه بر مشاهده اتفاقی یک اجرای آزمایشی.
Syntax استاندارد
CREATE TABLE dbo.FastStage
(
SessionID INT NOT NULL,
ItemID INT NOT NULL,
Payload NVARCHAR(200) NULL,
INDEX IX_FastStage HASH(SessionID,ItemID) WITH(BUCKET_COUNT=1024)
)
WITH(MEMORY_OPTIMIZED=ON,DURABILITY=SCHEMA_ONLY);
پارامترها و اجزای تعریف
- دیتابیس SQL Server باید FILEGROUP از نوع MEMORY_OPTIMIZED_DATA داشته باشد؛ Azure SQL Database زیرساخت را مدیریت میکند.
- DURABILITY تعیین میکند فقط Schema یا Schema و Data در بازیابی باقی بماند.
- هر جدول بهینهشده برای حافظه به حداقل یک ایندکس Hash یا Nonclustered نیاز دارد.
- BUCKET_COUNT ایندکس Hash باید با تعداد کلیدهای یکتای مورد انتظار متناسب انتخاب و سپس پایش شود.
نوع خروجی و نحوه مصرف
CREATE TABLE یک شیء دائمی در metadata دیتابیس ایجاد میکند که اجرای آن از موتور In-Memory OLTP استفاده میکند. در حالت SCHEMA_ONLY داده پس از Restart یا بازیابی پایدار نمیماند، اما خود جدول باقی است و باید هنگام استقرار ساخته شود، نه برای هر فراخوانی.
چه زمانی جدول بهینهشده برای حافظه را انتخاب کنیم؟
کاربردهای مناسب شامل جایگزینی Staging پرتکرار و پررقابت tempdb با جدول SCHEMA_ONLY و جداسازی SessionID، پردازش تراکنشی بسیار همزمان با کاهش Lock و Latch، Table Type بهینهشده برای حافظه و TVPهای پرترافیک، نگهداری داده داغ با SLA سخت پس از برآورد دقیق حافظه و سازگاری ویژگیها است. با این حال، مناسب بودن از نام قابلیت نتیجه نمیشود؛ باید Query مصرفکننده و تعداد اجرای همزمان را نیز تحلیل کرد.
- جایگزینی Staging پرتکرار و پررقابت tempdb با جدول SCHEMA_ONLY و جداسازی SessionID
- پردازش تراکنشی بسیار همزمان با کاهش Lock و Latch
- Table Type بهینهشده برای حافظه و TVPهای پرترافیک
- نگهداری داده داغ با SLA سخت پس از برآورد دقیق حافظه و سازگاری ویژگیها
برای تصمیمگیری، یک نمونه با داده واقعی بسازید، Baseline ثبت کنید و گزینه رقیب را با همان ورودی اجرا کنید. تعداد Logical Read، CPU، Duration و اختلاف Estimated Rows با Actual Rows را مقایسه کنید. اگر SLA همزمانی مهم است، تست بار چندنشستی نیز اجباری است.
مثالهای عملی مستقل
ده سناریوی زیر جنبههای متفاوت جدول بهینهشده برای حافظه را پوشش میدهند. هر مثال هدف، Query، خروجی مورد انتظار و نکته اجرایی مستقل دارد.
مثال 1: بررسی آمادگی دیتابیس
پیش از ساخت، وجود FILEGROUP مخصوص In-Memory OLTP را کنترل میکنیم.
SELECT CASE WHEN EXISTS(SELECT 1 FROM sys.filegroups WHERE type='FX') THEN N'آماده' ELSE N'نیازمند MEMORY_OPTIMIZED_DATA' END AS InMemoryState;
| InMemoryState |
|---|
| آماده یا نیازمند MEMORY_OPTIMIZED_DATA |
نکته کاربردی: این بررسی فقط زیرساخت را میسنجد؛ ظرفیت حافظه، Edition و سازگاری اشیا نیز باید ارزیابی شوند.
مثال 2: ساخت جدول SCHEMA_ONLY
یک Stage سریع با ایندکس Nonclustered ایجاد میکنیم؛ اجرای آن به FILEGROUP آماده نیاز دارد.
IF OBJECT_ID(N'dbo.FastStage',N'U') IS NULL
EXEC(N'CREATE TABLE dbo.FastStage(SessionID INT NOT NULL,ItemID INT NOT NULL,Payload NVARCHAR(100) NULL,INDEX IX_FastStage NONCLUSTERED(SessionID,ItemID)) WITH(MEMORY_OPTIMIZED=ON,DURABILITY=SCHEMA_ONLY);');
SELECT name,durability_desc FROM sys.tables WHERE object_id=OBJECT_ID(N'dbo.FastStage');
| name | durability_desc |
|---|
| FastStage | SCHEMA_ONLY |
نکته کاربردی: ساخت یکباره در Deployment انجام میشود؛ CREATE و DROP برای هر درخواست الگوی ضدکارایی است.
مثال 3: درج و خواندن داده نشست
داده را با SessionID جدا میکنیم تا جدول مشترک نقش Stage چندمصرفکننده را داشته باشد.
IF OBJECT_ID(N'dbo.FastStage',N'U') IS NULL THROW 51000,N'ابتدا FastStage را ایجاد کنید.',1;
DELETE FROM dbo.FastStage WHERE SessionID=@@SPID;
INSERT INTO dbo.FastStage(SessionID,ItemID,Payload) VALUES(@@SPID,1,N'A'),(@@SPID,2,N'B');
SELECT ItemID,Payload FROM dbo.FastStage WHERE SessionID=@@SPID ORDER BY ItemID;
نکته کاربردی: جداسازی منطقی را با پاکسازی، امنیت و کلید مناسب کامل کنید؛ @@SPID پس از قطع اتصال در آینده قابل استفاده مجدد است.
مثال 4: ساخت Table Type حافظهای
نوع جدول یکبار ساخته میشود و سپس متغیرهای آن بدون tempdb استفاده میشوند.
IF TYPE_ID(N'dbo.FastIDList') IS NULL
EXEC(N'CREATE TYPE dbo.FastIDList AS TABLE(ID INT NOT NULL PRIMARY KEY NONCLUSTERED) WITH(MEMORY_OPTIMIZED=ON);');
DECLARE @IDs dbo.FastIDList;
INSERT INTO @IDs VALUES(1),(2);
SELECT COUNT(*) AS RowCount FROM @IDs;
نکته کاربردی: نوع حافظهای باید حداقل یک ایندکس داشته باشد و برای TVPهای پرتکرار گزینه قابل آزمایش است.
مثال 5: ایندکس Hash و Bucket Count
یک جدول نمونه برای جستوجوی برابری با Hash Index تعریف میکنیم.
IF OBJECT_ID(N'dbo.FastLookup',N'U') IS NULL
EXEC(N'CREATE TABLE dbo.FastLookup(ID INT NOT NULL,ValueText NVARCHAR(50),INDEX IX_FastLookup HASH(ID) WITH(BUCKET_COUNT=2048)) WITH(MEMORY_OPTIMIZED=ON,DURABILITY=SCHEMA_ONLY);');
SELECT name,type_desc FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.FastLookup');
| name | type_desc |
|---|
| IX_FastLookup | NONCLUSTERED HASH |
نکته کاربردی: Bucket Count را بر اساس کلید یکتا پیشبینی کنید و پس از بار واقعی برخوردها را اندازه بگیرید.
مثال 6: ایندکس Nonclustered برای بازه
برای خواندن Range از ایندکس مرتب Nonclustered استفاده میکنیم.
IF OBJECT_ID(N'dbo.FastRange',N'U') IS NULL
EXEC(N'CREATE TABLE dbo.FastRange(ID INT NOT NULL,EventTime DATETIME2(0) NOT NULL,INDEX IX_FastRange NONCLUSTERED(EventTime)) WITH(MEMORY_OPTIMIZED=ON,DURABILITY=SCHEMA_ONLY);');
SELECT name,type_desc FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.FastRange');
| name | type_desc |
|---|
| IX_FastRange | NONCLUSTERED |
نکته کاربردی: Hash Index ترتیب ندارد؛ برای BETWEEN، ORDER BY و Range Scan ایندکس Nonclustered را ارزیابی کنید.
مثال 7: ستون Nullable
وجود ستون اختیاری را در metadata یک جدول حافظهای بررسی میکنیم.
IF OBJECT_ID(N'dbo.FastStage',N'U') IS NULL THROW 51001,N'FastStage موجود نیست.',1;
SELECT name,is_nullable FROM sys.columns WHERE object_id=OBJECT_ID(N'dbo.FastStage') ORDER BY column_id;
| name | is_nullable |
|---|
| SessionID | 0 |
| ItemID | 0 |
| Payload | 1 |
نکته کاربردی: پشتیبانی نوع داده و محدودیتها در نسخههای جدید گستردهتر شده، اما مهاجرت باید علیه نسخه مقصد تست شود.
مثال 8: پاکسازی هر اجرای سازمانی
ردیفهای همان نشست را پس از پایان پردازش حذف میکنیم.
IF OBJECT_ID(N'dbo.FastStage',N'U') IS NULL THROW 51002,N'FastStage موجود نیست.',1;
DELETE FROM dbo.FastStage WHERE SessionID=@@SPID;
SELECT @@ROWCOUNT AS DeletedRows;
| DeletedRows |
|---|
| تعداد ردیفهای همان نشست |
نکته کاربردی: SCHEMA_ONLY داده را Log نمیکند، ولی حذف منطقی برای کنترل حافظه و جلوگیری از خواندن داده کهنه لازم است.
مثال 9: شناسایی طراحی اشتباه زمان اجرا
فهرست جدولهای حافظهای و زمان ساخت را برای بازبینی استقرار استخراج میکنیم.
SELECT name,create_date,durability_desc FROM sys.tables WHERE is_memory_optimized=1 ORDER BY create_date DESC;
| name | create_date | durability_desc |
|---|
| نام جدول | زمان ساخت | SCHEMA_ONLY یا SCHEMA_AND_DATA |
نکته کاربردی: اگر Create Date دائماً با اجرای برنامه تغییر کند، احتمالاً DDL به اشتباه در مسیر درخواست قرار گرفته است.
مثال 10: پایش مصرف حافظه
حافظه جدولها و ایندکسهای In-Memory را از DMV تجمیع میکنیم.
SELECT OBJECT_NAME(object_id) AS ObjectName,
SUM(memory_allocated_for_table_kb+memory_allocated_for_indexes_kb) AS AllocatedKB,
SUM(memory_used_by_table_kb+memory_used_by_indexes_kb) AS UsedKB
FROM sys.dm_db_xtp_table_memory_stats
GROUP BY object_id ORDER BY UsedKB DESC;
| ObjectName | AllocatedKB | UsedKB |
|---|
| هر جدول حافظهای | حافظه تخصیصیافته | حافظه مصرفشده |
نکته کاربردی: پایش روند و Alert ظرفیت از خود عدد لحظهای مهمتر است؛ نسخههای ردیف و بار اوج را نیز لحاظ کنید.
خطاهای رایج و روش اصلاح
خطای 1
ساخت و حذف Memory-Optimized Table در مسیر آنلاین هزینه کامپایل دارد و روی Recovery و Replica نیز اثر میگذارد؛ آن را هنگام Deployment بسازید. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 2
انتخاب BUCKET_COUNT بسیار کوچک زنجیره برخورد طولانی و مصرف CPU ایجاد میکند؛ بسیار بزرگ نیز حافظه را هدر میدهد. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 3
SCHEMA_ONLY به معنای پایداری داده نیست و استفاده از آن برای رکورد تجاری حیاتی خطر از دست رفتن داده دارد. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 4
انتقال مستقیم طراحی Disk-Based بدون بررسی نوع داده، Constraint، Trigger و الگوی تراکنش ممکن است با محدودیت سازگاری روبهرو شود. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
ملاحظات کارایی و بهینهسازی
سود اصلی در حذف Page latch، کاهش Lock و برای SCHEMA_ONLY حذف Log I/O داده است؛ هر بارکاری از این گلوگاهها رنج نمیبرد.
حافظه مورد نیاز جدول، نسخههای ردیف و ایندکسها را برآورد و با DMVهای XTP پایش کنید؛ فشار حافظه میتواند عملیات را متوقف کند.
Hash Index برای جستوجوی برابری مناسب است؛ Range Scan و مرتبسازی معمولاً با Nonclustered Index بهترند.
Garbage Collection نسخهها، تراکنشهای طولانی و Conflictهای خوشبینانه را در تست همزمانی واقعی بررسی کنید.
مقایسه باید Throughput، CPU، P95/P99 Latency، Log Bytes و Tempdb Waitها را همزمان بسنجد، نه فقط زمان یک Query.
هیچ Hint یا نوع شیئی درمان همگانی نیست. Baseline را نگه دارید تا پس از تغییر بتوانید رگرسیون را تشخیص دهید. برای Queryهای حساس، Query Store و Actual Plan امکان مقایسه پایدارتر را فراهم میکنند و Wait Statistics نشان میدهد مشکل واقعاً CPU، I/O، قفل یا تخصیص است.
بهترین روشها
- Schema جدول بهینهشده برای حافظه را باریک و صریح تعریف کنید و از ستونهای بلااستفاده دوری کنید.
- طول عمر، مالک ایجاد و مسئول پاکسازی را در طراحی و مستندات مشخص کنید.
- ایندکس را از روی Predicate، Join و Order By واقعی طراحی کنید، نه از روی حدس.
- داده حساس را متناسب با دامنه دید و مجوزها محافظت کنید.
- مسیر موفق، خطا، Rollback و اجرای همزمان را در تست خودکار پوشش دهید.
- پس از Upgrade یا تغییر Compatibility Level آزمون Performance را تکرار کنید.
- کد DDL و Query را در Source Control نگه دارید و تغییر را با برنامه بازگشت منتشر کنید.
سؤالات متداول
1. جدول بهینهشده برای حافظه دقیقاً چیست و چه مسئلهای را حل میکند؟
جدول Memory-Optimized بخشی از In-Memory OLTP است و ردیفها و ایندکسهای خود را با ساختارهای بدون Page در حافظه مدیریت میکند. کنترل همزمانی خوشبینانه و نسخهبندی ردیف جای قفلگذاری سنتی را میگیرد. جدول میتواند SCHEMA_AND_DATA برای دوام کامل یا SCHEMA_ONLY برای نگهداری صرفاً ساختار پس از بازیابی باشد؛ حالت دوم در برخی سناریوها جایگزین مرحلهبندی tempdb میشود. انتخاب آن باید از نیاز واقعی دامنه، حجم داده و نحوه مصرف شروع شود.
2. برای شروع کار با جدول بهینهشده برای حافظه چه مراحلی لازم است؟
ابتدا Schema و طول عمر داده را مشخص کنید، سپس نمونه Syntax مقاله را در دیتابیس آزمایشی اجرا کنید و با داده شبیه تولید، صحت و طرح اجرا را بسنجید.
3. آیا استفاده از جدول بهینهشده برای حافظه هزینه توسعه گزارش را کاهش میدهد؟
اگر ساختار برای چند مرحله پردازش مناسب باشد، کد سادهتر و عیبیابی سریعتر میشود. در پروژه تجاری بهتر است هزینه tempdb، نگهداری و همزمانی نیز در برآورد مشاوره لحاظ شود.
4. چه زمانی سرمایهگذاری روی بهینهسازی جدول بهینهشده برای حافظه توجیه تجاری دارد؟
وقتی زمان پاسخ، مصرف CPU یا انتظار کاربران روی درآمد و SLA اثر دارد، ثبت Baseline و آزمایش کنترلشده میتواند ارزش تغییر را روشن کند؛ بهینهسازی بدون عدد قابل دفاع نیست.
5. جدول بهینهشده برای حافظه چه تفاوتی با سایر اشیای موقت دارد؟
تفاوت اصلی در دامنه دید، طول عمر، آمار، ایندکس، مدل ذخیرهسازی و هزینه همزمانی است. جدول مقایسه مقاله مادر انتخاب میان #Table، ##Table، @Table و In-Memory را خلاصه میکند.
6. آیا میتوان طراحی جدول بهینهشده برای حافظه را برای پروژه سازمانی سفارش داد؟
بله؛ در یک خدمت حرفهای، الگوی Query، حجم، Execution Plan، Wait Statistics و محدودیت نسخه بررسی میشود و اسکریپت مهاجرت و آزمون بازگشت نیز تحویل میگردد.
7. رایجترین خطای جدول بهینهشده برای حافظه چیست؟
ساخت و حذف Memory-Optimized Table در مسیر آنلاین هزینه کامپایل دارد و روی Recovery و Replica نیز اثر میگذارد؛ آن را هنگام Deployment بسازید. برای جلوگیری، مسیر خطا را مانند مسیر موفقیت در تست خودکار پوشش دهید.
8. چگونه Performance جدول بهینهشده برای حافظه را اندازه بگیریم؟
STATISTICS IO، STATISTICS TIME، Actual Execution Plan، مصرف tempdb یا XTP و زمان صدکی را قبل و بعد ثبت کنید. چند اجرای همزمان از تست تککاربره معتبرتر است.
9. بهترین روش استفاده از جدول بهینهشده برای حافظه چیست؟
Schema باریک، نام روشن، مالکیت طول عمر، پاکسازی مشخص و ایندکس مبتنی بر Query واقعی اصول پایهاند. تصمیم نهایی باید با داده تولیدی و طرح اجرای واقعی تأیید شود.
10. جدول بهینهشده برای حافظه با کدام نسخههای SQL Server سازگار است؟
اصل قابلیت در نسخههای پشتیبانیشده SQL Server موجود است، اما جزئیات بهبودهای Optimizer و In-Memory با نسخه و Compatibility Level فرق دارد. مستندات نسخه مقصد و آزمون رگرسیون را معیار قرار دهید.
سؤالات مصاحبه تخصصی
پرسش 1: دامنه و طول عمر جدول بهینهشده برای حافظه را توضیح دهید و یک مورد استفاده مناسب نام ببرید.
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
پرسش 2: Optimizer برای جدول بهینهشده برای حافظه چه اطلاعاتی در اختیار دارد و این موضوع چگونه بر Join اثر میگذارد؟
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
پرسش 3: اگر تعداد ردیفها صد برابر شود، چه شاخصهایی را پیش از تغییر طراحی بررسی میکنید؟
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
پرسش 4: نقش Index و هزینه نگهداری آن در این ساختار چیست؟
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
پرسش 5: چگونه Race Condition، پاکسازی و مسیر Rollback را آزمایش میکنید؟
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
پرسش 6: برای مهاجرت به گزینه دیگر چه Baseline و معیار پذیرشی تعریف میکنید؟
پاسخ حرفهای باید علاوه بر تعریف، یک Trade-off و روش اندازهگیری ارائه کند. نام بردن از Actual Plan، IO، CPU، دامنه و آزمون همزمانی نشان میدهد داوطلب قابلیت را در پروژه واقعی فهمیده است.
چکلیست نهایی
- هدف داده میانی و طول عمر آن مشخص است.
- حجم معمول، اوج و تعداد نشست همزمان اندازهگیری شده است.
- Schema و نوع داده بیش از نیاز بزرگ نیست.
- Predicate و Joinهای اصلی ایندکس مناسب دارند.
- Estimated Rows و Actual Rows مقایسه شدهاند.
- مسیر NULL، خطا و Rollback تست شده است.
- پاکسازی و مالکیت شیء روشن است.
- Baseline و برنامه بازگشت ثبت شده است.
جمعبندی
جدولهای بهینهشده برای حافظه (Memory-Optimized) زمانی ارزشمند است که ویژگیهای واقعی آن با مسئله هماهنگ باشد. دامنه، حجم، آمار، ایندکس و همزمانی را یک تصمیم واحد ببینید و به یک آزمایش سریع تکنشستی اکتفا نکنید.
از مثالهای این مقاله بهعنوان نقطه شروع استفاده کنید، سپس داده و Query پروژه خود را جایگزین کنید. نتیجه درست باید هم از نظر صحت کسبوکار و هم از نظر معیارهای Performance قابل دفاع باشد.
برای مرور همه گزینهها و انتخاب ساختار مناسب، به مقاله مادر اشیای موقت در SQL Server بازگردید.