آموزش جامع جدولهای موقت محلی (#Table) در SQL Server
مقدمه
جدولهای موقت محلی (#Table) یکی از ابزارهای مهم طراحی پردازش میانی در Microsoft SQL Server است. مسئله اصلی صرفاً نوشتن یک دستور نیست؛ باید بدانیم داده کجا نگهداری میشود، چه کسی آن را میبیند، Optimizer چه اطلاعاتی برای برآورد دارد و پاکسازی در چه زمانی رخ میدهد.
جدول موقت محلی با پیشوند # ساخته میشود، در tempdb قرار میگیرد و نام منطقی آن فقط در نشست سازنده و دامنههای تو در توی همان نشست قابل مشاهده است. SQL Server برای جداسازی همنامها یک پسوند داخلی به نام فیزیکی اضافه میکند. این ساختار مانند جدول عادی ستون، قید، آمار و ایندکس دارد و برای مجموعهدادههای میانی با اندازه متغیر انتخابی انعطافپذیر است.
در این راهنما از مثالهای کوچک آغاز میکنیم و سپس به دامنه، NULL، تراکنش، ایندکس، خطاهای رایج و سناریوهای Performance میرسیم. تمام Queryها برای آزمایش در محیط کنترلشده نوشته شدهاند و مثالهای دارای پیشنیاز، آن پیشنیاز را صریح اعلام میکنند.
برای مقایسه این ساختار با گزینههای دیگر، راهنمای جامع اشیای موقت در SQL Server را نیز مطالعه کنید.
تعریف و معماری جدول موقت محلی
جدول موقت محلی با پیشوند # ساخته میشود، در tempdb قرار میگیرد و نام منطقی آن فقط در نشست سازنده و دامنههای تو در توی همان نشست قابل مشاهده است. SQL Server برای جداسازی همنامها یک پسوند داخلی به نام فیزیکی اضافه میکند. این ساختار مانند جدول عادی ستون، قید، آمار و ایندکس دارد و برای مجموعهدادههای میانی با اندازه متغیر انتخابی انعطافپذیر است.
انتخاب صحیح زمانی رخ میدهد که سه محور را همزمان ببینیم: تعداد و توزیع ردیفها، مرز دسترسی نشست یا Batch، و تعداد دفعات خواندن و نوشتن. یک ساختار ساده در حجم کم ممکن است بهترین باشد، اما همان ساختار در گزارش چندمیلیونی میتواند به برآورد Cardinality نامناسب یا فشار منابع منجر شود.
همچنین باید تفاوت میان عمر منطقی داده و عمر فیزیکی شیء را درک کرد. SQL Server بخشی از عملیات را Cache میکند و این موضوع به معنای دائمی شدن داده نیست. قرارداد برنامه باید بر رفتار مستند دامنه و تراکنش تکیه کند، نه بر مشاهده اتفاقی یک اجرای آزمایشی.
Syntax استاندارد
DROP TABLE IF EXISTS #WorkItems;
CREATE TABLE #WorkItems
(
WorkItemID INT NOT NULL PRIMARY KEY,
Title NVARCHAR(100) NOT NULL,
CreatedAt DATETIME2(0) NOT NULL
);
پارامترها و اجزای تعریف
- نام جدول باید با یک علامت # آغاز شود و بهتر است هدف آن را روشن بیان کند.
- نوع و طول ستونها را مطابق داده واقعی انتخاب کنید تا مصرف tempdb و حافظه اضافه نشود.
- قیدهای PRIMARY KEY، UNIQUE و CHECK در صورت نیاز هنگام ساخت قابل تعریف هستند.
- پس از CREATE میتوان با CREATE INDEX ایندکسهای تکمیلی ساخت و از آمار خودکار بهره برد.
نوع خروجی و نحوه مصرف
جدول موقت محلی یک مقدار اسکالر برنمیگرداند؛ یک شیء جدولی رابطهای ایجاد میکند که تا پایان دامنه یا حذف صریح در دسترس است. نوع هر ستون دقیقاً از تعریف CREATE TABLE یا خروجی SELECT INTO به دست میآید.
چه زمانی جدول موقت محلی را انتخاب کنیم؟
کاربردهای مناسب شامل مرحلهبندی نتایج بزرگ میان چند Query و جلوگیری از محاسبه چندباره، ساخت ایندکس روی خروجی میانی برای Join و فیلترهای پرتکرار، انتقال داده میان Stored Procedure والد و رویههای تو در تو، شکستن یک گزارش پیچیده به مراحل قابل مشاهده، تستپذیر و قابل تنظیم است. با این حال، مناسب بودن از نام قابلیت نتیجه نمیشود؛ باید Query مصرفکننده و تعداد اجرای همزمان را نیز تحلیل کرد.
- مرحلهبندی نتایج بزرگ میان چند Query و جلوگیری از محاسبه چندباره
- ساخت ایندکس روی خروجی میانی برای Join و فیلترهای پرتکرار
- انتقال داده میان Stored Procedure والد و رویههای تو در تو
- شکستن یک گزارش پیچیده به مراحل قابل مشاهده، تستپذیر و قابل تنظیم
برای تصمیمگیری، یک نمونه با داده واقعی بسازید، Baseline ثبت کنید و گزینه رقیب را با همان ورودی اجرا کنید. تعداد Logical Read، CPU، Duration و اختلاف Estimated Rows با Actual Rows را مقایسه کنید. اگر SLA همزمانی مهم است، تست بار چندنشستی نیز اجباری است.
مثالهای عملی مستقل
ده سناریوی زیر جنبههای متفاوت جدول موقت محلی را پوشش میدهند. هر مثال هدف، Query، خروجی مورد انتظار و نکته اجرایی مستقل دارد.
مثال 1: ساخت و خواندن پایه
یک فهرست کوچک سفارش در جدول موقت ایجاد میکنیم و آن را مرتب میخوانیم.
DROP TABLE IF EXISTS #Orders;
CREATE TABLE #Orders (OrderID INT PRIMARY KEY, Amount DECIMAL(12,2));
INSERT INTO #Orders (OrderID, Amount) VALUES (1, 125000.00), (2, 98000.00);
SELECT OrderID, Amount FROM #Orders ORDER BY OrderID;
| OrderID | Amount |
|---|
| 1 | 125000.00 |
| 2 | 98000.00 |
نکته کاربردی: حذف شرطی در ابتدای نمونه باعث میشود اجرای دوباره در همان نشست خطای وجود شیء ندهد.
مثال 2: ساخت سریع با SELECT INTO
از دادههای نمونه فقط سفارشهای باز را بدون تعریف دستی ستونها مرحلهبندی میکنیم.
DROP TABLE IF EXISTS #OpenOrders;
WITH SourceData AS
(
SELECT * FROM (VALUES (1,N'باز'),(2,N'بسته'),(3,N'باز')) v(OrderID,StatusName)
)
SELECT OrderID, StatusName INTO #OpenOrders FROM SourceData WHERE StatusName = N'باز';
SELECT COUNT(*) AS OpenCount FROM #OpenOrders;
نکته کاربردی: SELECT INTO برای نمونهسازی سریع مناسب است؛ در کد پایدار نوع و طول ستونهای حاصل را بازبینی کنید.
مثال 3: تجمیع فروش
فروشهای روزانه را ابتدا ذخیره و سپس مجموع هر فروشنده را محاسبه میکنیم.
DROP TABLE IF EXISTS #Sales;
CREATE TABLE #Sales (SellerID INT, Amount DECIMAL(12,2));
INSERT INTO #Sales VALUES (10,500.00),(10,250.00),(20,900.00);
SELECT SellerID, SUM(Amount) AS TotalAmount FROM #Sales GROUP BY SellerID ORDER BY SellerID;
| SellerID | TotalAmount |
|---|
| 10 | 750.00 |
| 20 | 900.00 |
نکته کاربردی: مرحلهبندی زمانی ارزش دارد که همان داده پاکسازیشده در چند تجمیع یا Join دوباره استفاده شود.
مثال 4: فیلتر بازهای در WHERE
رویدادهای هفت روز نخست تیر را با شرط نیمهباز انتخاب میکنیم.
DROP TABLE IF EXISTS #Events;
CREATE TABLE #Events (EventID INT, EventDate DATE);
INSERT INTO #Events VALUES (1,'2026-06-22'),(2,'2026-06-28'),(3,'2026-07-01');
SELECT EventID, EventDate FROM #Events
WHERE EventDate >= '2026-06-22' AND EventDate < '2026-06-29' ORDER BY EventID;
| EventID | EventDate |
|---|
| 1 | 2026-06-22 |
| 2 | 2026-06-28 |
نکته کاربردی: شرط نیمهباز از خطاهای مربوط به بخش زمان جلوگیری میکند و برای Seek ایندکس مناسب است.
مثال 5: ترکیب با ROW_NUMBER
برای هر مشتری آخرین رخداد را از جدول موقت استخراج میکنیم.
DROP TABLE IF EXISTS #History;
CREATE TABLE #History (CustomerID INT, EventID INT, EventTime DATETIME2(0));
INSERT INTO #History VALUES (1,11,'2026-07-01T10:00:00'),(1,12,'2026-07-02T09:00:00'),(2,21,'2026-07-01T08:00:00');
WITH Ranked AS
(SELECT *, ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY EventTime DESC) AS rn FROM #History)
SELECT CustomerID, EventID FROM Ranked WHERE rn=1 ORDER BY CustomerID;
نکته کاربردی: ایندکس روی CustomerID و EventTime برای مجموعههای بزرگ میتواند مرتبسازی را ارزانتر کند.
مثال 6: مدیریت NULL
مقادیر تخفیف نامشخص را نگه میداریم و در گزارش با COALESCE به صفر تبدیل میکنیم.
DROP TABLE IF EXISTS #Discounts;
CREATE TABLE #Discounts (ProductID INT, DiscountAmount DECIMAL(10,2) NULL);
INSERT INTO #Discounts VALUES (1,NULL),(2,25.50);
SELECT ProductID, COALESCE(DiscountAmount,0) AS EffectiveDiscount FROM #Discounts ORDER BY ProductID;
| ProductID | EffectiveDiscount |
|---|
| 1 | 0.00 |
| 2 | 25.50 |
نکته کاربردی: تبدیل NULL را در مرز گزارش انجام دهید؛ صفر و مقدار نامشخص همیشه معنای تجاری یکسان ندارند.
مثال 7: قید یکتا و Identity
برای دادههای ورودی یک کلید داخلی و کد تجاری یکتا تعریف میکنیم.
DROP TABLE IF EXISTS #InputRows;
CREATE TABLE #InputRows (RowID INT IDENTITY(1,1) PRIMARY KEY, Code NVARCHAR(20) UNIQUE);
INSERT INTO #InputRows (Code) VALUES (N'A-10'),(N'B-20');
SELECT RowID, Code FROM #InputRows ORDER BY RowID;
نکته کاربردی: قید یکتا خطا را زود آشکار میکند و از تکثیر خاموش داده در مراحل بعدی جلوگیری میکند.
مثال 8: دسترسی از Dynamic SQL
یک جدول موقت در دامنه بیرونی میسازیم و در sp_executesql تو در تو میخوانیم.
DROP TABLE IF EXISTS #ScopeDemo;
CREATE TABLE #ScopeDemo (ID INT);
INSERT INTO #ScopeDemo VALUES (7),(8);
EXEC sys.sp_executesql N'SELECT COUNT(*) AS RowCount FROM #ScopeDemo;';
نکته کاربردی: Batch تو در تو جدول موقت محلی نشست والد را میبیند؛ جدولی که داخل Batch ساخته شود پس از پایان همان دامنه قابل اتکا نیست.
مثال 9: اصلاح بررسی اشتباه وجود
روش درست شناسایی جدول موقت را با OBJECT_ID نمایش میدهیم.
DROP TABLE IF EXISTS #CheckMe;
CREATE TABLE #CheckMe (ID INT);
SELECT CASE WHEN OBJECT_ID('tempdb..#CheckMe') IS NOT NULL THEN N'موجود' ELSE N'ناموجود' END AS ObjectState;
نکته کاربردی: نوشتن OBJECT_ID(N'#CheckMe') بدون tempdb ممکن است برنامه پاکسازی را دچار رفتار اشتباه کند.
مثال 10: ایندکس برای Join کارا
روی کلید جستوجوی داده میانی ایندکس میسازیم و تنها ستونهای لازم را برمیگردانیم.
DROP TABLE IF EXISTS #Keys;
CREATE TABLE #Keys (CustomerID INT NOT NULL, RequestedAt DATETIME2(0) NOT NULL);
INSERT INTO #Keys VALUES (2,'2026-07-20T09:00:00'),(1,'2026-07-20T08:00:00');
CREATE UNIQUE CLUSTERED INDEX CX_Keys ON #Keys(CustomerID);
SELECT CustomerID, RequestedAt FROM #Keys WHERE CustomerID=2;
| CustomerID | RequestedAt |
|---|
| 2 | 2026-07-20 09:00:00 |
نکته کاربردی: ایندکس را پس از بارگذاری انبوه یا پیش از آن فقط با اندازهگیری انتخاب کنید؛ نقطه سربهسر به حجم و الگوی خواندن بستگی دارد.
خطاهای رایج و روش اصلاح
خطای 1
بررسی وجود با OBJECT_ID باید نام tempdb..#Table را به کار ببرد؛ بررسی نام در دیتابیس جاری نتیجه قابل اعتماد نمیدهد. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 2
استفاده افراطی از SELECT * هم اندازه ردیف را بالا میبرد و هم قرارداد داده میانی را شکننده میکند. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 3
فیلترکردن ستون جدول دائمی با تبدیل یا تابع، حتی پس از Join با #Table، میتواند ایندکس اصلی را غیرقابل جستوجو کند. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
خطای 4
ساخت و حذف مکرر جدولهای موقت در بار همزمان بالا میتواند فشار تخصیص و metadata در tempdb را زیاد کند. راه اصلاح این است که فرض را به یک آزمون قابل تکرار تبدیل کنید و رفتار را در نسخه و Compatibility Level مقصد بسنجید.
ملاحظات کارایی و بهینهسازی
برای داده اندک ابتدا سادگی را بسنجید، اما برای برآوردهای نامطمئن و Joinهای جدی، آمار جدول موقت معمولاً به بهینهساز کمک میکند.
ایندکس را بر اساس مسیر واقعی خواندن بسازید؛ ایندکس اضافی هزینه INSERT و فضای tempdb را افزایش میدهد.
فقط ستونهای لازم را ذخیره کنید و نوع داده را بیش از نیاز بزرگ نگیرید. NVARCHAR(MAX) در یک مرحلهبندی معمولی غالباً علامت طراحی ضعیف است.
Actual Execution Plan، STATISTICS IO و STATISTICS TIME را قبل و بعد از تغییر مقایسه کنید؛ سرعت یک اجرای گرم معیار کافی نیست.
در SQL Serverهای جدید بهبودهای tempdb و caching مفیدند، ولی طراحی فایلها، ظرفیت دیسک و الگوی همزمانی همچنان باید پایش شود.
هیچ 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. جدول موقت محلی دقیقاً چیست و چه مسئلهای را حل میکند؟
جدول موقت محلی با پیشوند # ساخته میشود، در tempdb قرار میگیرد و نام منطقی آن فقط در نشست سازنده و دامنههای تو در توی همان نشست قابل مشاهده است. SQL Server برای جداسازی همنامها یک پسوند داخلی به نام فیزیکی اضافه میکند. این ساختار مانند جدول عادی ستون، قید، آمار و ایندکس دارد و برای مجموعهدادههای میانی با اندازه متغیر انتخابی انعطافپذیر است. انتخاب آن باید از نیاز واقعی دامنه، حجم داده و نحوه مصرف شروع شود.
2. برای شروع کار با جدول موقت محلی چه مراحلی لازم است؟
ابتدا Schema و طول عمر داده را مشخص کنید، سپس نمونه Syntax مقاله را در دیتابیس آزمایشی اجرا کنید و با داده شبیه تولید، صحت و طرح اجرا را بسنجید.
3. آیا استفاده از جدول موقت محلی هزینه توسعه گزارش را کاهش میدهد؟
اگر ساختار برای چند مرحله پردازش مناسب باشد، کد سادهتر و عیبیابی سریعتر میشود. در پروژه تجاری بهتر است هزینه tempdb، نگهداری و همزمانی نیز در برآورد مشاوره لحاظ شود.
4. چه زمانی سرمایهگذاری روی بهینهسازی جدول موقت محلی توجیه تجاری دارد؟
وقتی زمان پاسخ، مصرف CPU یا انتظار کاربران روی درآمد و SLA اثر دارد، ثبت Baseline و آزمایش کنترلشده میتواند ارزش تغییر را روشن کند؛ بهینهسازی بدون عدد قابل دفاع نیست.
5. جدول موقت محلی چه تفاوتی با سایر اشیای موقت دارد؟
تفاوت اصلی در دامنه دید، طول عمر، آمار، ایندکس، مدل ذخیرهسازی و هزینه همزمانی است. جدول مقایسه مقاله مادر انتخاب میان #Table، ##Table، @Table و In-Memory را خلاصه میکند.
6. آیا میتوان طراحی جدول موقت محلی را برای پروژه سازمانی سفارش داد؟
بله؛ در یک خدمت حرفهای، الگوی Query، حجم، Execution Plan، Wait Statistics و محدودیت نسخه بررسی میشود و اسکریپت مهاجرت و آزمون بازگشت نیز تحویل میگردد.
7. رایجترین خطای جدول موقت محلی چیست؟
بررسی وجود با OBJECT_ID باید نام tempdb..#Table را به کار ببرد؛ بررسی نام در دیتابیس جاری نتیجه قابل اعتماد نمیدهد. برای جلوگیری، مسیر خطا را مانند مسیر موفقیت در تست خودکار پوشش دهید.
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 و برنامه بازگشت ثبت شده است.
جمعبندی
جدولهای موقت محلی (#Table) زمانی ارزشمند است که ویژگیهای واقعی آن با مسئله هماهنگ باشد. دامنه، حجم، آمار، ایندکس و همزمانی را یک تصمیم واحد ببینید و به یک آزمایش سریع تکنشستی اکتفا نکنید.
از مثالهای این مقاله بهعنوان نقطه شروع استفاده کنید، سپس داده و Query پروژه خود را جایگزین کنید. نتیجه درست باید هم از نظر صحت کسبوکار و هم از نظر معیارهای Performance قابل دفاع باشد.
برای مرور همه گزینهها و انتخاب ساختار مناسب، به مقاله مادر اشیای موقت در SQL Server بازگردید.