۱۲۰ سوال آزمون استخدامی T-SQL و SQL Server با جواب کامل؛ از Junior تا Senior
افشین رفوآ
1405/03/31
۱۲۰ سوال آزمون استخدامی T-SQL و SQL Server با جواب کامل؛ از Junior تا Senior
اگه داری برای آزمون استخدامی SQL Server، مصاحبه T-SQL، موقعیت شغلی SQL Developer، Database Developer، SQL Server Specialist یا حتی مسیر DBA آماده میشی، این مقاله دقیقاً برای تو نوشته شده.
توی مصاحبههای SQL Server معمولاً قرار نیست فقط ازت بپرسن JOIN چیه یا فرق DELETE و TRUNCATE چیه. اینها سؤالهای پایهاند، اما مصاحبهگر حرفهای دنبال چیز مهمتریه: میخواد بفهمه واقعاً با SQL Server کار کردی یا فقط چند تا دستور رو حفظی بلدی.
برای همین، سؤالهای جدی SQL Server معمولاً از چند بخش مختلف میان؛ از مفاهیم پایه و کوئرینویسی عملی گرفته تا Stored Procedure، Window Function، Performance Tuning، Execution Plan، Index، Transaction، Deadlock، Backup، Security و سناریوهایی که واقعاً توی محیط Production اتفاق میافتن.
توی این مقاله، ۱۲۰ سؤال مهم استخدامی T-SQL و SQL Server رو همراه با جوابهای کامل و آموزشی بررسی میکنیم. هدف فقط این نیست که یک جواب کوتاه حفظ کنی و بری سر جلسه مصاحبه. هر جواب طوری توضیح داده شده که بفهمی چرا اون سؤال مهمه، کجای کار به درد پروژه واقعی میخوره و مصاحبهگر دقیقاً میخواد چه چیزی رو از جوابت تشخیص بده.
بخش اول: سوالات پایه SQL Server برای سطح Junior
۱. تفاوت دقیق TRUNCATE و DELETE در نحوه استفاده از Transaction Log چیه؟
```دستور DELETE رکوردها رو دونهدونه حذف میکنه. یعنی SQL Server برای هر رکوردی که حذف میشه، اطلاعات لازم رو داخل Transaction Log ثبت میکنه تا اگه لازم شد، عملیات قابل Rollback باشه. به همین خاطر، وقتی روی یک جدول بزرگ DELETE اجرا میکنی، ممکنه حجم زیادی Log تولید بشه و عملیات هم کندتر پیش بره.
اما TRUNCATE رکوردها رو سطر به سطر حذف نمیکنه. بهجاش Pageهای داده رو آزاد میکنه. برای همین معمولاً خیلی سریعتر از DELETE اجرا میشه و Log کمتری هم تولید میکنه.
نکته مهم اینجاست که برخلاف چیزی که خیلیها اشتباه میگن، TRUNCATE در SQL Server اگه داخل Transaction اجرا بشه، قابل Rollback هست.
چند تفاوت مهم دیگه بین DELETE و TRUNCATE:
- DELETE میتونه شرط WHERE داشته باشه، اما TRUNCATE نمیتونه.
- DELETE باعث اجرای Triggerهای مربوط به Delete میشه، ولی TRUNCATE این Triggerها رو اجرا نمیکنه.
- TRUNCATE معمولاً مقدار IDENTITY رو Reset میکنه، اما DELETE این کار رو انجام نمیده.
نکته طلایی مصاحبه: اگه کسی بگه «TRUNCATE قابل Rollback نیست»، جوابش دقیق نیست. جواب حرفهای اینه که TRUNCATE کملاگتره، Pageها رو آزاد میکنه، Trigger رو فعال نمیکنه، WHERE نداره و اگه داخل Transaction اجرا بشه، قابل Rollback هست.
۲. تفاوت INNER JOIN با LEFT JOIN چیه؟
```INNER JOIN فقط رکوردهایی رو برمیگردونه که در هر دو جدول، شرط اتصال رو داشته باشن. یعنی اگه یک رکورد توی جدول اول باشه، اما رکورد متناظرش توی جدول دوم وجود نداشته باشه، توی خروجی نمایش داده نمیشه.
اما LEFT JOIN همه رکوردهای جدول سمت چپ رو برمیگردونه؛ حتی اگه توی جدول سمت راست هیچ رکورد متناظری براشون وجود نداشته باشه. در این حالت، ستونهای جدول سمت راست با مقدار NULL نمایش داده میشن.
فرض کن دو جدول Customers و Orders داریم. این کوئری فقط مشتریهایی رو نشون میده که سفارش دارن:
SELECT c.CustomerID, c.Name, o.OrderID
```
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID;
```
اما این یکی همه مشتریها رو نشون میده؛ حتی اونهایی که هیچ سفارشی ثبت نکردن:
SELECT c.CustomerID, c.Name, o.OrderID
```
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID;
```
نکته مهم: برای پیدا کردن رکوردهایی که در جدول اصلی وجود دارن ولی در جدول دوم نیستن، معمولاً از LEFT JOIN همراه با شرطی مثل WHERE RightTable.ID IS NULL استفاده میکنیم.
۳. تفاوت VARCHAR و NVARCHAR چیه و چه اثری روی فضای ذخیرهسازی دارن؟
```VARCHAR برای ذخیره رشتههای غیر Unicode استفاده میشه. یعنی بیشتر برای متنهای انگلیسی یا دادههایی مناسبه که نیاز جدی به پشتیبانی چندزبانه ندارن.
NVARCHAR برای ذخیره دادههای Unicode استفاده میشه. اگه قراره داده فارسی، عربی، چینی، روسی یا هر نوع متن چندزبانه ذخیره کنی، معمولاً انتخاب امنتر NVARCHAR هست.
از نظر فضای ذخیرهسازی، NVARCHAR معمولاً فضای بیشتری نسبت به VARCHAR مصرف میکنه. بهصورت سنتی میگیم هر کاراکتر NVARCHAR دو بایت فضا میگیره، اما در نسخهها و Collationهای جدیدتر، مخصوصاً وقتی بحث UTF-8 مطرح میشه، موضوع میتونه به تنظیمات Collation هم وابسته باشه.
برای یک سیستم فارسیزبان، ستونهایی مثل نام، نام خانوادگی، آدرس و توضیحات بهتره NVARCHAR باشن:
Name NVARCHAR(100)
اما اگه یک ستون فقط قراره کد انگلیسی، ایمیل، شناسه خارجی یا یک مقدار فنی رو نگه داره، ممکنه VARCHAR کاملاً کافی باشه.
نکته مصاحبهای: جواب «همیشه NVARCHAR استفاده کن» جواب حرفهای نیست. جواب دقیق اینه که انتخاب بین VARCHAR و NVARCHAR باید بر اساس زبان داده، Collation، حجم جدول، Index، I/O و نیاز واقعی سیستم انجام بشه.
۴. تفاوت اصلی WHERE و HAVING در فیلتر کردن دادهها چیه؟
```WHERE قبل از گروهبندی دادهها اعمال میشه. یعنی اول رکوردهای خام جدول رو فیلتر میکنه و بعد نتیجه وارد مرحله GROUP BY میشه.
اما HAVING بعد از GROUP BY اجرا میشه و برای فیلتر کردن گروهها استفاده میشه، نه رکوردهای خام.
مثلاً اگه بخوایم فقط سفارشهای سال ۲۰۲۶ وارد محاسبه بشن، باید از WHERE استفاده کنیم:
SELECT CustomerID, COUNT(*) AS OrderCount
```
FROM Orders
WHERE OrderDate >= '20260101'
AND OrderDate < '20270101'
GROUP BY CustomerID;
```
اما اگه بخوایم مشتریهایی رو پیدا کنیم که بیشتر از ۵ سفارش داشتن، باید از HAVING استفاده کنیم:
SELECT CustomerID, COUNT(*) AS OrderCount
```
FROM Orders
GROUP BY CustomerID
HAVING COUNT(*) > 5;
```
نکته خیلی مهم: توابع تجمیعی مثل COUNT، SUM، AVG، MAX و MIN معمولاً در HAVING استفاده میشن، نه در WHERE.
۵. توابع COALESCE و ISNULL چه تفاوتی در تعیین Data Type خروجی دارن؟
```هر دو تابع برای جایگزین کردن مقدار NULL استفاده میشن، اما رفتارشان دقیقاً یکی نیست.
ISNULL مخصوص SQL Server هست و فقط دو آرگومان میگیره:
SELECT ISNULL(NULL, 'Unknown');
اما COALESCE استانداردتره، میتونه چند مقدار بگیره و اولین مقدار غیر NULL رو برگردونه:
SELECT COALESCE(NULL, NULL, 'Unknown', 'Other');
تفاوت مهم این دو تابع در نوع داده خروجیه. ISNULL معمولاً نوع داده آرگومان اول رو مبنا قرار میده، اما COALESCE بر اساس قوانین Data Type Precedence در SQL Server نوع داده خروجی رو مشخص میکنه.
مثلاً در این نمونه، ممکنه خروجی به طول آرگومان اول محدود بشه:
SELECT ISNULL(CAST(NULL AS VARCHAR(5)), 'LongText');
نکته مصاحبهای: کاندیدای حرفهای باید بدونه که تفاوت COALESCE و ISNULL فقط در تعداد آرگومانها نیست. این دو از نظر Data Type، استاندارد بودن و رفتار داخل Expressionها هم تفاوت دارن.
۶. تفاوت UNION و UNION ALL چیه و چرا UNION ALL از نظر Performance بهتره؟
```UNION خروجی دو Query رو با هم ترکیب میکنه و رکوردهای تکراری رو حذف میکنه.
UNION ALL هم خروجیها رو ترکیب میکنه، اما رکوردهای تکراری رو حذف نمیکنه و همه چیز رو همونطور که هست برمیگردونه.
مثلاً این کوئری CustomerIDها رو بدون تکرار برمیگردونه:
SELECT CustomerID FROM Orders2025
```
UNION
SELECT CustomerID FROM Orders2026;
```
اما این یکی همه رکوردها رو با تکرار برمیگردونه:
SELECT CustomerID FROM Orders2025
```
UNION ALL
SELECT CustomerID FROM Orders2026;
```
دلیل سریعتر بودن UNION ALL اینه که UNION برای حذف رکوردهای تکراری معمولاً نیاز به Sort یا Hash Aggregate داره. همین موضوع باعث مصرف بیشتر CPU، Memory و گاهی TempDB میشه.
اما UNION ALL فقط خروجی Queryها رو پشت سر هم میچسبونه و وارد دردسر حذف Duplicate نمیشه.
نکته مهم: اگه مطمئنی دادهها تکراری نیستن، یا تکراری بودن خروجی برات مهم نیست، UNION ALL انتخاب بهتریه.
۷. Primary Key چه تفاوتی با Unique Key در برخورد با مقدار NULL داره؟
```Primary Key کلید اصلی جدوله. یعنی هر رکورد باید با اون بهصورت یکتا شناسایی بشه. مقدار Primary Key نمیتونه NULL باشه، چون رکورد بدون شناسه مشخص، عملاً قابل اتکا نیست.
Unique Key هم یکتا بودن مقدار رو تضمین میکنه، اما در SQL Server میتونه مقدار NULL داشته باشه. البته در یک Unique Constraint معمولی، معمولاً فقط یک مقدار NULL مجازه، چون SQL Server اون رو هم مثل یک مقدار خاص در نظر میگیره.
چند تفاوت مهم بین Primary Key و Unique Key:
- در هر جدول فقط یک Primary Key میتونیم داشته باشیم، اما چند Unique Constraint میتونیم تعریف کنیم.
- Primary Key بهصورت پیشفرض NOT NULL هست.
- Primary Key معمولاً برای ساخت رابطه با Foreign Key استفاده میشه.
- Unique Key بیشتر برای جلوگیری از تکرار مقدار در ستونهایی مثل کد ملی، ایمیل، شماره پرسنلی یا کد محصول استفاده میشه.
نکته مصاحبهای: جواب خوب فقط این نیست که بگیم «هر دو Unique هستن». باید به NULL، تعداد مجاز در جدول، نقش در طراحی رابطه و استفاده در Foreign Key هم اشاره بشه.
۸. آیا میتونیم در شرط WHERE از توابع تجمیعی مثل SUM یا MAX استفاده کنیم؟
```بهصورت مستقیم، نه. چون WHERE قبل از GROUP BY و قبل از محاسبه توابع تجمیعی اجرا میشه. یعنی وقتی SQL Server داره WHERE رو بررسی میکنه، هنوز SUM، COUNT، AVG و بقیه Aggregate Functionها محاسبه نشدن.
این یک اشتباه رایجه:
SELECT CustomerID, COUNT(*) AS OrderCount
```
FROM Orders
WHERE COUNT(*) > 5
GROUP BY CustomerID;
```
این کوئری خطا میده، چون COUNT هنوز در مرحله WHERE قابل استفاده نیست.
روش درست اینه که از HAVING استفاده کنیم:
SELECT CustomerID, COUNT(*) AS OrderCount
```
FROM Orders
GROUP BY CustomerID
HAVING COUNT(*) > 5;
```
جمعبندی ساده: اگه میخوای روی داده خام فیلتر بزنی، از WHERE استفاده کن. اگه میخوای روی نتیجه گروهبندی فیلتر بزنی، برو سراغ HAVING.
۹. تابع NULLIF چه کاربردی داره و چطور جلوی خطای Divide By Zero رو میگیره؟
```تابع NULLIF دو مقدار رو با هم مقایسه میکنه. اگه برابر باشن، NULL برمیگردونه. اگه برابر نباشن، مقدار اول رو برمیگردونه.
ساختار کلی تابع اینه:
NULLIF(value1, value2)
یکی از کاربردهای خیلی مهم NULLIF، جلوگیری از خطای تقسیم بر صفره.
مثلاً این کوئری رو در نظر بگیر:
SELECT TotalSales / TotalOrders AS AvgOrderAmount
```
FROM SalesSummary;
```
اگه مقدار TotalOrders صفر باشه، SQL Server خطای Divide By Zero میده.
روش بهتر اینه:
SELECT TotalSales / NULLIF(TotalOrders, 0) AS AvgOrderAmount
```
FROM SalesSummary;
```
در این حالت، اگه TotalOrders صفر باشه، عبارت NULLIF(TotalOrders, 0) مقدار NULL برمیگردونه. نتیجه تقسیم هم NULL میشه، اما گزارش یا Query با خطای تقسیم بر صفر متوقف نمیشه.
نکته مهم: این تکنیک توی گزارشهای مالی، داشبوردهای مدیریتی و Queryهای تحلیلی خیلی کاربردیه، چون جلوی خراب شدن خروجی بهخاطر یک مقدار صفر رو میگیره.
۱۰. دستور MERGE در چه سناریوهایی استفاده میشه؟
```دستور MERGE برای ترکیب عملیات Insert، Update و گاهی Delete استفاده میشه. یعنی میتونی یک جدول Source رو با یک جدول Target مقایسه کنی و بر اساس نتیجه، تصمیم بگیری چه رکوردهایی Insert بشن، چه رکوردهایی Update بشن و در بعضی سناریوها چه رکوردهایی حذف بشن.
MERGE معمولاً در سناریوهایی مثل Synchronization، Data Warehouse، ETL و Upsert استفاده میشه.
مثلاً فرض کن یک جدول Stage داری و میخوای اطلاعاتش رو با جدول اصلی هماهنگ کنی:
- اگه رکورد از قبل وجود داشت، Update بشه.
- اگه رکورد وجود نداشت، Insert بشه.
- در بعضی سناریوها، اگه رکورد در Source نبود، از Target حذف بشه.
نمونه ساده MERGE:
MERGE Customers AS target
```
USING StagingCustomers AS source
ON target.CustomerID = source.CustomerID
WHEN MATCHED THEN
UPDATE SET target.Name = source.Name
WHEN NOT MATCHED BY TARGET THEN
INSERT (CustomerID, Name)
VALUES (source.CustomerID, source.Name);
```
نکته خیلی مهم: MERGE روی کاغذ دستور جذابیه، چون چند عملیات رو یکجا انجام میده. اما در SQL Server باید با احتیاط ازش استفاده کرد. در سیستمهای حساس، بعضی تیمها بهخاطر پیچیدگی، ریسکهای همزمانی و رفتارهای غیرمنتظره، ترجیح میدن عملیات Insert و Update رو جداگانه، شفافتر و کنترلشدهتر بنویسن.
۱۱. تفاوت CAST و CONVERT چیه و CONVERT چه امکانات اضافهای برای فرمت تاریخ داره؟
```هر دو دستور CAST و CONVERT برای تبدیل نوع داده استفاده میشن. مثلاً وقتی میخوای یک مقدار متنی رو به تاریخ تبدیل کنی، یا یک عدد رو به رشته تبدیل کنی، سراغ اینها میری.
CAST استاندارد SQL هست و فقط کار اصلی تبدیل نوع داده رو انجام میده:
SELECT CAST('2026-01-01' AS DATE);
اما CONVERT مخصوص SQL Server هست و یک امکان اضافه مهم داره: میتونی برای بعضی تبدیلها، مخصوصاً تاریخ و زمان، Style مشخص کنی.
SELECT CONVERT(VARCHAR(10), GETDATE(), 120);
مثلاً Style 112 خروجی تاریخ رو به شکل YYYYMMDD برمیگردونه:
SELECT CONVERT(CHAR(8), GETDATE(), 112);
نکته مصاحبهای: اگه قابلحمل بودن کد بین دیتابیسهای مختلف برات مهمه، CAST انتخاب استانداردتریه. اما اگه داخل SQL Server به فرمت خاص تاریخ نیاز داری، CONVERT کاربردیتره.
۱۲. تفاوت CHAR و VARCHAR چیه و چه زمانی CHAR انتخاب بهتریه؟
```CHAR طول ثابت داره. یعنی اگه یک ستون رو CHAR(10) تعریف کنی و فقط مقدار ABC داخلش بریزی، SQL Server باقی فضا رو تا طول ۱۰ با Space پر میکنه.
اما VARCHAR طول متغیر داره. یعنی فقط به اندازه مقدار واقعی فضا میگیره، بهاضافه مقدار کمی Overhead برای مدیریت طول داده.
CHAR زمانی انتخاب خوبیه که مقدارها همیشه طول ثابت داشته باشن. مثلاً:
- کد کشور دوحرفی
- کد وضعیت ثابت
- کدهایی که از قبل میدونی همیشه طول مشخصی دارن
مثلاً برای کد کشور:
CountryCode CHAR(2)
اما برای چیزهایی مثل نام، آدرس، ایمیل، توضیحات و متنهایی که طولشون متغیره، معمولاً VARCHAR یا NVARCHAR انتخاب بهتریه.
نکته مهم: استفاده اشتباه از CHAR برای متنهای متغیر باعث هدر رفتن فضای ذخیرهسازی میشه و در جدولهای بزرگ میتونه I/O رو هم بالا ببره.
۱۳. کاربرد SCOPE_IDENTITY()، @@IDENTITY و IDENT_CURRENT چیه و چه فرقی با هم دارن؟
```هر سه برای گرفتن مقدار Identity استفاده میشن، اما تفاوتشون خیلی مهمه و توی پروژه واقعی میتونه جلوی یک باگ جدی رو بگیره.
تابع SCOPE_IDENTITY() آخرین مقدار Identity تولیدشده در همان Scope و همان Session رو برمیگردونه. این معمولاً امنترین گزینه برای گرفتن ID رکوردیه که همین الان Insert کردی.
INSERT INTO Customers(Name) VALUES (N'علی');
```
SELECT SCOPE_IDENTITY();
```
اما @@IDENTITY آخرین Identity تولیدشده در همان Session رو برمیگردونه، حتی اگه اون Identity توسط یک Trigger و در یک جدول دیگه ساخته شده باشه. برای همین ممکنه عددی رو برگردونه که اصلاً ID رکورد اصلی تو نیست.
تابع IDENT_CURRENT('TableName') آخرین Identity تولیدشده برای یک جدول خاص رو برمیگردونه، بدون توجه به اینکه این مقدار در Session تو ساخته شده یا Session یک کاربر دیگه.
یعنی در محیط چندکاربره، IDENT_CURRENT ممکنه مقداری رو نشون بده که مربوط به Insert یک کاربر دیگه است، نه کاری که تو همین الان انجام دادی.
نکته طلایی: در بیشتر سناریوهای برنامهنویسی، وقتی بعد از Insert میخوای ID رکورد تازه ثبتشده رو بگیری، انتخاب درست SCOPE_IDENTITY() هست.
۱۴. دستور LIKE با Wildcardهای % و _ و [] چطور کار میکنه؟
```LIKE برای جستجوی الگو داخل رشتهها استفاده میشه. یعنی وقتی دنبال یک متن دقیق نیستی و میخوای الگوی خاصی رو پیدا کنی، از LIKE کمک میگیری.
- % یعنی هر تعداد کاراکتر؛ حتی صفر کاراکتر.
- _ یعنی دقیقاً یک کاراکتر.
- [] یعنی یکی از کاراکترهای داخل براکت.
مثلاً این کوئری نامهایی رو پیدا میکنه که با «علی» شروع میشن:
SELECT * FROM Customers
```
WHERE Name LIKE N'علی%';
```
این یکی یعنی مقدار با A شروع بشه، بعد دقیقاً یک کاراکتر داشته باشه، بعد به 1 برسه:
SELECT * FROM Customers
```
WHERE Code LIKE 'A_1';
```
و این کوئری کدهایی رو پیدا میکنه که با A یا B یا C شروع میشن:
SELECT * FROM Products
```
WHERE Code LIKE '[ABC]%';
```
نکته Performance: اگه الگو با % شروع بشه، مثل WHERE Name LIKE N'%علی'، معمولاً Index Seek اتفاق نمیافته و کوئری میتونه کند بشه. چون SQL Server نمیتونه از ابتدای مقدار برای جستجوی سریع استفاده کنه.
۱۵. استفاده از IN چه فرقی با چند OR پشت سر هم داره؟
```از نظر منطقی، IN و چند OR پشت سر هم معمولاً نتیجه یکسانی میدن.
مثلاً این شرط:
WHERE Status IN ('A', 'B', 'C')
معمولاً معادل این شرطه:
WHERE Status = 'A'
```
OR Status = 'B'
OR Status = 'C'
```
اما IN خواناتر، کوتاهتر و تمیزتره؛ مخصوصاً وقتی تعداد مقدارها زیاد میشه. از نظر Performance هم SQL Server Optimizer در خیلی از موارد این دو حالت رو به شکل مشابهی تحلیل میکنه.
پس در بسیاری از سناریوها، تفاوت اصلی بیشتر در خوانایی و نگهداری کده، نه الزاماً سرعت اجرا.
نکته مهم: اگه لیست مقدارها خیلی بزرگه، بهتره بهجای یک IN طولانی، از جدول موقت، Table-Valued Parameter یا Join با یک جدول کمکی استفاده کنی. این کار هم تمیزتره، هم در سناریوهای جدی قابلکنترلتره.
۱۶. در دستور BETWEEN، آیا مقدار ابتدا و انتهای بازه هم داخل خروجی حساب میشن؟
```بله. BETWEEN در SQL Server شامل مقدار ابتدا و انتهای بازه هم میشه. یعنی اصطلاحاً Inclusive هست.
این دو شرط از نظر منطقی معادل هم هستن:
WHERE Amount BETWEEN 100 AND 200
WHERE Amount >= 100 AND Amount <= 200
اما وقتی پای تاریخ وسط میاد، باید خیلی دقت کنی. اینجا خیلیها اشتباه میکنن.
مثلاً این شرط در نگاه اول درست به نظر میرسه:
WHERE OrderDate BETWEEN '2026-01-01' AND '2026-01-31'
اما اگه OrderDate شامل زمان هم باشه، این شرط عملاً فقط تا 2026-01-31 00:00:00 رو پوشش میده. یعنی سفارشهای روز ۳۱ ژانویه بعد از نیمهشب ممکنه از خروجی حذف بشن.
روش حرفهایتر برای بازه تاریخی اینه:
WHERE OrderDate >= '20260101'
```
AND OrderDate < '20260201'
```
نکته مهم: برای تاریخها، معمولاً بهتره بهجای BETWEEN از شرط شروعِ بسته و پایانِ باز استفاده کنی. یعنی از تاریخ شروع به بعد، ولی کوچکتر از روز بعد از تاریخ پایان.
۱۷. قابلیت WITH TIES در کنار TOP چه کاری انجام میده؟
```WITH TIES باعث میشه اگه چند رکورد در رتبه آخر مقدار مساوی داشته باشن، همه اونها در خروجی نمایش داده بشن.
مثلاً این کوئری رو ببین:
SELECT TOP (3) WITH TIES *
```
FROM Employees
ORDER BY Salary DESC;
```
اینجا هدف اینه که ۳ حقوق بالاتر رو بگیریم. اما اگه نفر سوم، چهارم و پنجم حقوق یکسانی داشته باشن، SQL Server همه اونها رو برمیگردونه، نه فقط سه رکورد اول رو.
دلیلش هم واضحه: وقتی چند نفر با نفر سوم حقوق مساوی دارن، حذف کردنشون از نظر رتبهبندی منصفانه نیست.
نکته مهم: برای استفاده از WITH TIES باید ORDER BY داشته باشی، چون SQL Server باید بدونه رتبهبندی رو بر اساس چه ستونی انجام بده.
کاربرد رایج این قابلیت وقتیه که میخوای مثلاً «۳ حقوق برتر» یا «۱۰ فروشنده برتر» رو بگیری، اما نمیخوای کسانی که با نفر آخر مقدار مساوی دارن، اشتباهی از خروجی حذف بشن.
```۱۸. قابلیت ON DELETE CASCADE در Foreign Key چه رفتاری ایجاد میکنه؟
```وقتی روی یک Foreign Key گزینه ON DELETE CASCADE تعریف میکنی، با حذف رکورد والد، رکوردهای فرزند مرتبط هم بهصورت خودکار حذف میشن.
مثلاً فرض کن جدول Customers و Orders داری. اگه ON DELETE CASCADE فعال باشه و یک مشتری حذف بشه، سفارشهای مربوط به اون مشتری هم خودکار حذف میشن.
ALTER TABLE Orders
```
ADD CONSTRAINT FK_Orders_Customers
FOREIGN KEY (CustomerID)
REFERENCES Customers(CustomerID)
ON DELETE CASCADE;
```
این قابلیت در بعضی مدلها میتونه مفید باشه، چون باعث میشه دادههای وابسته یتیم نشن. اما در محیط واقعی، مخصوصاً Production، باید با احتیاط خیلی زیاد ازش استفاده کرد.
نکته خیلی مهم: ON DELETE CASCADE میتونه خطرناک هم باشه. اگه اشتباهی یک رکورد والد حذف بشه، ممکنه هزاران یا حتی میلیونها رکورد فرزند هم خودکار حذف بشن.
نکته مصاحبهای: جواب حرفهای فقط این نیست که بگی «برای حذف خودکار رکوردهای وابسته است». باید به ریسک حذف زنجیرهای، طراحی درست رابطهها، Audit، Backup و محدودیتهای محیط واقعی هم اشاره کنی.
۱۹. تفاوت DROP و DELETE از نظر ساختار جدول چیه؟
```DELETE فقط دادههای داخل جدول رو حذف میکنه. یعنی خود جدول، ستونها، Indexها، Constraintها و Permissionها باقی میمونن.
مثلاً این دستور فقط رکوردهای جدول Orders رو حذف میکنه:
DELETE FROM Orders;
بعد از اجرای این دستور، جدول Orders هنوز وجود داره، فقط خالی شده.
اما DROP کل Object رو حذف میکنه. یعنی اگه جدول رو Drop کنی، خود جدول، دادهها، ساختار، Indexها، Constraintها و وابستگیهای مربوط به اون از بین میرن.
DROP TABLE Orders;
بعد از اجرای این دستور، دیگه خود جدول Orders هم وجود نداره.
نکته مهم: DROP در محیط Production بسیار حساسه و نباید بدون کنترل دسترسی، Change Management، بررسی وابستگیها و Backup مناسب اجرا بشه.
۲۰. مزیت استفاده از فرمت تاریخ YYYYMMDD در SQL Server چیه؟
```فرمت YYYYMMDD مثل 20260131 یکی از امنترین فرمتها برای نوشتن تاریخ در SQL Serverه، چون نسبت به تنظیمات زبان و DateFormat در Session کمتر دچار ابهام میشه.
مثلاً این تاریخ میتونه مبهم باشه:
'01/02/2026'
این یعنی اول فوریه؟ یا دوم ژانویه؟ جوابش بستگی به تنظیمات زبان و فرمت تاریخ در Session داره.
اما این یکی خیلی واضحتر و امنتره:
'20260201'
در این حالت، منظور تاریخ ۱ فوریه ۲۰۲۶ است و احتمال برداشت اشتباه خیلی کمتر میشه.
نکته مهم: در Queryهای جدی، مخصوصاً سیستمهای چندزبانه یا سرورهایی که تنظیمات زبان و تاریخ متفاوت دارن، از فرمتهای غیرمبهم مثل YYYYMMDD استفاده کن. این کار جلوی خیلی از باگهای عجیب و سختپیدا رو میگیره.
بخش دوم: سوالات Mid-Level برای توسعه T-SQL
۲۱. مفهوم CTE چیه و چه مزیتی نسبت به Subqueryهای تو در تو داره؟
```CTE مخفف Common Table Expression هست. به زبان ساده، CTE یک ساختار موقت و منطقیه که فقط داخل همون Statement اعتبار داره و با کلمه کلیدی WITH تعریف میشه.
مثلاً این کوئری رو ببین:
WITH CustomerOrders AS
```
(
SELECT CustomerID, COUNT(*) AS OrderCount
FROM Orders
GROUP BY CustomerID
)
SELECT *
FROM CustomerOrders
WHERE OrderCount > 5;
```
مزیت اصلی CTE خوانایی کده. بهجای اینکه چند تا Subquery تودرتو و سختخوان بنویسی، میتونی Query رو مرحلهبهمرحله جلو ببری. این کار مخصوصاً وقتی Query بزرگ میشه، خیلی کمک میکنه که هم خودت بهتر بفهمیش، هم نفر بعدی که قراره کدت رو نگهداری کنه اذیت نشه.
نکته دقیق: CTE الزاماً داخل حافظه ذخیره نمیشه و الزاماً Performance رو بهتر نمیکنه. SQL Server اون رو داخل Execution Plan تحلیل میکنه. اگه نیاز به استفاده چندباره، Index، یا پردازش چندمرحلهای سنگین داری، ممکنه Temp Table انتخاب مناسبتری باشه.
۲۲. تفاوت Local Temp Table با # و Global Temp Table با ## چیه؟
```Local Temp Table با یک # ساخته میشه و فقط در همون Session قابل دسترسیه. یعنی همون Connection یا Session که جدول رو ساخته، میتونه ازش استفاده کنه.
CREATE TABLE #TempOrders
```
(
OrderID INT,
TotalAmount DECIMAL(18,2)
);
```
وقتی Session بسته بشه، این جدول موقت هم حذف میشه.
اما Global Temp Table با دو تا ## ساخته میشه و برای Sessionهای دیگه هم قابل مشاهده است؛ البته تا وقتی که Session سازنده بسته بشه و بقیه Sessionهایی هم که دارن ازش استفاده میکنن، کارشون تموم بشه.
CREATE TABLE ##GlobalTemp
```
(
ID INT
);
```
نکته مهم: در بیشتر سناریوهای عادی، از #TempTable استفاده میکنیم. استفاده از ##TempTable باید با احتیاط باشه، چون ممکنه بین کاربرها، Jobها یا Processهای مختلف تداخل ایجاد کنه.
۲۳. Table Variable و Temp Table از نظر Transaction و Performance چه تفاوتهایی دارن؟
```Temp Table در tempdb ساخته میشه و کاملاً در رفتار Transaction شرکت میکنه. یعنی اگه داخل یک Transaction روی اون Insert انجام بدی و بعد Rollback کنی، تغییرات دادهای اون هم Rollback میشه.
Table Variable هم معمولاً در tempdb مدیریت میشه، اما رفتارش با Temp Table دقیقاً یکی نیست. تعریف خود Table Variable مثل یک آبجکت عادی داخل Transaction Rollback نمیشه، و در عمل بحث مهمتر در مصاحبه معمولاً فقط Transaction نیست؛ بیشتر بحث Performance، Statistics و Cardinality Estimation مطرحه.
تفاوتهای کاربردی این دو تا اینهاست:
- Temp Table معمولاً برای دادههای بیشتر و Queryهای پیچیدهتر انتخاب بهتریه.
- Temp Table میتونه Statistics بهتری داشته باشه و Optimizer معمولاً تخمین دقیقتری از حجم داده میزنه.
- روی Temp Table میتونی Indexهای کاربردیتری تعریف کنی.
- Table Variable بیشتر برای دادههای کم و پردازشهای ساده مناسبه.
- در نسخههای جدید SQL Server رفتار Table Variable بهتر شده، اما هنوز نباید کورکورانه ازش استفاده کرد.
نکته مصاحبهای: جواب قوی اینه که انتخاب بین #Temp و @Table به حجم داده، نیاز به Index، Cardinality Estimation، Recompile، پیچیدگی Query و الگوی استفاده بستگی داره. جواب «همیشه Temp Table بهتره» یا «همیشه Table Variable سبکتره» جواب دقیق و حرفهای نیست.
۲۴. ساختار TRY...CATCH چطور کار میکنه و ERROR_MESSAGE() و ERROR_LINE() کجا به درد میخورن؟
```TRY...CATCH برای مدیریت خطا در T-SQL استفاده میشه. یعنی دستورات اصلی رو داخل بخش TRY مینویسی و اگه خطایی رخ بده، کنترل برنامه میره داخل بخش CATCH.
ساختار کلیش این شکلیه:
BEGIN TRY
-- دستورات اصلی
```
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage,
ERROR_LINE() AS ErrorLine;
END CATCH;
```
اگر داخل بخش TRY خطایی رخ بده، SQL Server اجرای عادی رو متوقف میکنه و وارد CATCH میشه. اونجا میتونی اطلاعات خطا رو بخونی، Log ثبت کنی، Transaction رو Rollback کنی یا خطا رو دوباره پرتاب کنی.
چند تابع مهم برای خواندن جزئیات خطا:
- ERROR_MESSAGE() متن خطا رو برمیگردونه.
- ERROR_LINE() شماره خطی رو میده که خطا در اون رخ داده.
- ERROR_NUMBER() شماره خطای SQL Server رو برمیگردونه.
- ERROR_SEVERITY() شدت خطا رو مشخص میکنه.
- ERROR_PROCEDURE() اسم Procedure یا Trigger مربوط به خطا رو برمیگردونه.
در کار واقعی، TRY...CATCH معمولاً کنار Transaction استفاده میشه:
BEGIN TRY
BEGIN TRAN;
-- عملیات اصلی
COMMIT TRAN;
```
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
```
THROW;
```
END CATCH;
```
نکته طلایی: در نسخههای جدیدتر SQL Server، برای پرتاب مجدد خطا معمولاً THROW تمیزتر و استانداردتر از RAISERROR حساب میشه. مخصوصاً وقتی میخوای خطای اصلی با جزئیات درست منتقل بشه.
۲۵. تفاوت Stored Procedure و User Defined Function چیه؟
```Stored Procedure بیشتر برای اجرای یک فرآیند یا عملیات استفاده میشه. یعنی میتونه Insert، Update، Delete، Transaction، Dynamic SQL، Error Handling و حتی چند Result Set داشته باشه.
اما Function معمولاً برای محاسبه و برگردوندن مقدار استفاده میشه. Function میتونه Scalar باشه، یعنی یک مقدار برگردونه، یا Table-Valued باشه، یعنی خروجی جدولی بده. ولی در کل محدودیتهای بیشتری نسبت به Stored Procedure داره.
تفاوتهای مهم این دو:
- Procedure رو معمولاً با EXEC اجرا میکنیم.
- Function رو میتونیم داخل SELECT استفاده کنیم.
- Procedure میتونه چندین عملیات DML انجام بده.
- Function محدودتره و نباید اثر جانبی جدی روی دیتابیس داشته باشه.
- Function باید مقدار برگردونه.
نمونه استفاده از Function:
SELECT dbo.CalculateTax(TotalAmount)
```
FROM Orders;
```
نمونه اجرای Procedure:
EXEC dbo.CreateOrder @CustomerID = 1, @Amount = 500000;
نکته Performance: Scalar UDFها در نسخههای قدیمی SQL Server میتونستن خیلی کند باشن، مخصوصاً وقتی روی میلیونها رکورد اجرا میشدن. از SQL Server 2019 به بعد، قابلیت Scalar UDF Inlining در بعضی سناریوها شرایط رو بهتر کرده، اما باز هم نباید بدون بررسی Execution Plan و حجم داده از Scalar Function استفاده کرد.
۲۶. چرا Inline Table-Valued Function معمولاً بهتر از Multi-Statement Table-Valued Functionه؟
```Inline Table-Valued Function یا Inline TVF رو میتونی شبیه یک View پارامتردار در نظر بگیری. داخلش فقط یک Query برمیگرده و Optimizer میتونه محتوای اون Query رو ببیند و برای کل عملیات Execution Plan بهتری بسازه.
نمونه Inline TVF:
CREATE FUNCTION dbo.GetCustomerOrders(@CustomerID INT)
```
RETURNS TABLE
AS
RETURN
(
SELECT *
FROM Orders
WHERE CustomerID = @CustomerID
);
```
اما Multi-Statement TVF معمولاً داخل یک Table Variable داده میریزه و بعد خروجی رو برمیگردونه. در نسخههای قدیمیتر SQL Server، Optimizer تخمین ضعیفی از تعداد رکوردهای خروجی این نوع Function داشت و همین موضوع میتونست باعث Planهای بد بشه.
نکته مهم: اگه Function فقط قراره یک Query رو برگردونه، Inline TVF معمولاً انتخاب بهتریه. Multi-Statement TVF فقط وقتی منطقیه که واقعاً منطق چندمرحلهای لازم داشته باشی و نتونی کار رو با یک Query تمیز حل کنی.
۲۷. Window Function چیه و چه فرقی با GROUP BY سنتی داره؟
```Window Functionها روی مجموعهای از ردیفهای مرتبط محاسبه انجام میدن، اما برخلاف GROUP BY، ردیفهای اصلی رو در هم ادغام نمیکنن. یعنی هم جزئیات هر ردیف رو داری، هم میتونی کنار هر ردیف یک محاسبه تحلیلی ببینی.
مثلاً این کوئری رو ببین:
SELECT
OrderID,
CustomerID,
TotalAmount,
SUM(TotalAmount) OVER(PARTITION BY CustomerID) AS CustomerTotal
```
FROM Orders;
```
اینجا همچنان هر سفارش رو جداگانه میبینی، اما مجموع خریدهای همون مشتری هم کنار هر ردیف نمایش داده میشه.
اما اگر از GROUP BY استفاده کنی:
SELECT CustomerID, SUM(TotalAmount)
```
FROM Orders
GROUP BY CustomerID;
```
خروجی دیگه در سطح سفارش نیست؛ خلاصه میشه در سطح CustomerID. یعنی جزئیات تکتک سفارشها رو از دست میدی.
نکته مصاحبهای: کلید فهم Window Functionها اینه: محاسبه تحلیلی انجام میدن، اما جزئیات ردیفها رو حفظ میکنن. همین جمله رو اگه درست توضیح بدی، مصاحبهگر میفهمه فقط Syntax حفظ نکردی.
۲۸. تفاوت ROW_NUMBER()، RANK() و DENSE_RANK() در رتبهبندی رکوردها چیه؟
```هر سه تابع برای رتبهبندی رکوردها استفاده میشن، اما وقتی مقدارهای مساوی داشته باشیم، رفتارشان فرق میکنه.
ROW_NUMBER() به هر رکورد یک شماره یکتا میده، حتی اگر مقدارهای رتبهبندی مساوی باشن.
RANK() به مقدارهای مساوی رتبه یکسان میده، اما بعد از تساوی، رتبه بعدی رو با فاصله حساب میکنه.
DENSE_RANK() هم به مقدارهای مساوی رتبه یکسان میده، اما فاصله ایجاد نمیکنه.
مثلاً فرض کن حقوقها اینها باشن:
1000, 900, 900, 800
خروجی رتبهبندی این شکلی میشه:
- ROW_NUMBER: 1, 2, 3, 4
- RANK: 1, 2, 2, 4
- DENSE_RANK: 1, 2, 2, 3
نکته مهم: برای پیدا کردن دومین حقوق متمایز، معمولاً DENSE_RANK() بهتر از ROW_NUMBER() جواب میده، چون با مقدارهای مساوی درستتر برخورد میکنه.
۲۹. دستورات LEAD و LAG چه کاربردی برای دسترسی به رکوردهای قبلی و بعدی دارن؟
```LAG مقدار رکورد قبلی رو برمیگردونه و LEAD مقدار رکورد بعدی رو. این دو تابع وقتی خیلی به درد میخورن که بخوای یک ردیف رو با ردیف قبل یا بعد خودش مقایسه کنی.
مثال:
SELECT
CustomerID,
OrderDate,
TotalAmount,
LAG(TotalAmount) OVER(PARTITION BY CustomerID ORDER BY OrderDate) AS PreviousAmount,
LEAD(TotalAmount) OVER(PARTITION BY CustomerID ORDER BY OrderDate) AS NextAmount
```
FROM Orders;
```
اینجا برای هر سفارش، مبلغ سفارش قبلی و سفارش بعدی هم کنار همون ردیف نمایش داده میشه.
چند کاربرد رایج LEAD و LAG:
- مقایسه فروش ماه جاری با ماه قبل
- محاسبه فاصله زمانی بین دو ورود کاربر
- تحلیل روند قیمت
- پیدا کردن تغییرات وضعیت
- بررسی رفتار مشتری در سفارشهای پشت سر هم
نکته مصاحبهای: قبل از LEAD و LAG، معمولاً مجبور بودیم از Self Join استفاده کنیم. Window Functionها این کار رو خیلی تمیزتر، کوتاهتر و خواناتر کردن.
۳۰. تفاوت CROSS APPLY و OUTER APPLY چیه و چه زمانی بهجای JOIN ازشون استفاده میکنیم؟
```APPLY زمانی کاربرد داره که بخوای برای هر ردیف از جدول سمت چپ، یک Query یا Table-Valued Function رو اجرا کنی. یعنی سمت راست Query میتونه به مقدارهای ردیف سمت چپ وابسته باشه.
CROSS APPLY فقط ردیفهایی رو برمیگردونه که سمت راست براشون خروجی داشته باشه. از این نظر شبیه INNER JOIN رفتار میکنه.
OUTER APPLY همه ردیفهای سمت چپ رو نگه میداره، حتی اگه سمت راست هیچ خروجیای نداشته باشه. از این نظر شبیه LEFT JOIN هست.
یک مثال کاربردی: گرفتن آخرین سفارش هر مشتری با OUTER APPLY:
SELECT c.CustomerID, c.Name, o.OrderID, o.OrderDate
```
FROM Customers c
OUTER APPLY
(
SELECT TOP (1) *
FROM Orders o
WHERE o.CustomerID = c.CustomerID
ORDER BY o.OrderDate DESC, o.OrderID DESC
) o;
```
در این مثال، برای هر مشتری، یک Query جدا اجرا میشه که آخرین سفارش همون مشتری رو پیدا میکنه. اگر مشتری سفارشی نداشته باشه، چون از OUTER APPLY استفاده کردیم، خود مشتری همچنان در خروجی باقی میمونه.
نکته مهم: APPLY برای سناریوهایی مثل Top N per Group، اجرای TVF پارامتردار، گرفتن آخرین رکورد مرتبط، یا Queryهایی که به ردیف بیرونی وابستهاند خیلی کاربردیه. اینجا APPLY معمولاً خواناتر و طبیعیتر از JOINهای پیچیده درمیاد.
۳۱. جدولهای سیستمی inserted و deleted در Triggerها چه دادههایی رو نگه میدارن؟
```در Triggerهای DML، یعنی Triggerهایی که روی INSERT، UPDATE و DELETE اجرا میشن، SQL Server دو جدول منطقی موقت به نامهای inserted و deleted در اختیار ما میذاره.
این جدولها واقعی و دائمی نیستن؛ فقط موقع اجرای Trigger وجود دارن و کمک میکنن بفهمیم قبل و بعد از تغییر، چه دادههایی داشتیم.
- در عملیات INSERT، رکوردهای جدید داخل inserted قرار میگیرن.
- در عملیات DELETE، رکوردهای حذفشده داخل deleted قرار میگیرن.
- در عملیات UPDATE، مقدار قبلی داخل deleted و مقدار جدید داخل inserted قرار میگیره.
یک نمونه ساده:
CREATE TRIGGER trg_Orders_Update
```
ON Orders
AFTER UPDATE
AS
BEGIN
SELECT *
FROM inserted;
```
SELECT *
FROM deleted;
```
END;
```
در این مثال، وقتی رکوردی در جدول Orders آپدیت بشه، میتونیم مقدار جدید رو از inserted و مقدار قبلی رو از deleted ببینیم.
نکته خیلی مهم: Trigger باید Set-Based نوشته بشه، نه Row-Based. یعنی نباید فرض کنی فقط یک رکورد تغییر کرده. یک UPDATE میتونه همزمان هزاران ردیف رو تغییر بده، پس کد Trigger باید برای چند رکورد هم درست کار کنه.
۳۲. تفاوت EXISTS و IN در برخورد با NULL داخل Subquery چیه؟
```IN یک لیست از مقدارها رو بررسی میکنه. یعنی میگه مقدار ستون موردنظر، داخل خروجی Subquery هست یا نه. اما وقتی پای NULL وسط میاد، مخصوصاً در NOT IN، ممکنه نتیجهای بگیری که اصلاً انتظارش رو نداری.
این کوئری رو ببین:
SELECT *
```
FROM Customers
WHERE CustomerID NOT IN (SELECT CustomerID FROM Orders);
```
اگر خروجی Subquery حتی یک مقدار NULL داشته باشه، شرط میتونه به UNKNOWN تبدیل بشه و در نتیجه هیچ رکوردی برنگرده. این یکی از باگهای کلاسیک SQL در پروژههای واقعیه.
روش امنتر معمولاً استفاده از NOT EXISTS هست:
SELECT *
```
FROM Customers c
WHERE NOT EXISTS
(
SELECT 1
FROM Orders o
WHERE o.CustomerID = c.CustomerID
);
```
EXISTS دنبال این نیست که مقدار دقیقاً داخل یک لیست هست یا نه؛ فقط بررسی میکنه آیا رکورد متناظر وجود داره یا نه. برای همین در سناریوهای وجود و عدم وجود، معمولاً خواناتر و امنتره.
نکته مصاحبهای: اگر کاندیدا خطر NOT IN با NULL رو بدونه، یعنی فقط Syntax حفظ نکرده و احتمالاً با Queryهای واقعی و دردسرهای واقعی SQL کار کرده.
۳۳. تفاوت خروجی COUNT(*) و COUNT(ColumnName) چیه؟
```COUNT(*) تعداد کل ردیفها رو میشماره؛ فرقی هم نمیکنه ستونها NULL باشن یا نباشن.
اما COUNT(ColumnName) فقط ردیفهایی رو میشماره که اون ستون خاص مقدار NULL نداشته باشه.
مثال:
SELECT
COUNT(*) AS TotalRows,
COUNT(Email) AS RowsWithEmail
```
FROM Customers;
```
فرض کن ۱۰۰ مشتری داریم، ولی فقط ۸۰ نفر ایمیل ثبت کردن. در این حالت خروجی میتونه این شکلی باشه:
TotalRows = 100
```
RowsWithEmail = 80
```
نکته مهم: برای شمارش کل رکوردها معمولاً COUNT(*) انتخاب درستتریه. اما اگر میخوای تعداد مقدارهای موجود در یک ستون خاص رو بدونی، باید از COUNT(ColumnName) استفاده کنی.
۳۴. دستورات PIVOT و UNPIVOT چطور دادهها رو بین سطر و ستون جابهجا میکنن؟
```PIVOT دادهها رو از حالت سطری به حالت ستونی تبدیل میکنه. مثلاً میتونی فروش سالهای مختلف رو از چند ردیف جدا، به چند ستون جدا تبدیل کنی.
UNPIVOT برعکس این کار رو انجام میده؛ یعنی ستونها رو به ردیف تبدیل میکنه.
یک مثال ساده از PIVOT:
SELECT *
```
FROM
(
SELECT CustomerID, YEAR(OrderDate) AS OrderYear, TotalAmount
FROM Orders
) src
PIVOT
(
SUM(TotalAmount)
FOR OrderYear IN ([2024], [2025], [2026])
) p;
```
اینجا فروش هر مشتری بر اساس سال، به ستونهای جداگانه تبدیل میشه.
کاربردهای رایج PIVOT و UNPIVOT:
- گزارشهای مدیریتی
- گزارش فروش ماهانه یا سالانه
- آمادهسازی داده برای ابزارهای BI
- تبدیل ساختار داده برای گزارشگیری راحتتر
نکته مهم: در خیلی از پروژهها، PIVOT ثابت کافی نیست؛ چون ستونها از قبل مشخص نیستن. مثلاً سالها، ماهها یا دستهبندیها ممکنه داینامیک باشن. در این حالت معمولاً به Dynamic Pivot نیاز داریم.
۳۵. توابع SUBSTRING، CHARINDEX و PATINDEX چطور برای کار با متنها با هم ترکیب میشن؟
```این سه تابع برای دستکاری و جستجو داخل متنها خیلی کاربرد دارن.
- SUBSTRING بخشی از یک رشته رو جدا میکنه.
- CHARINDEX محل قرار گرفتن یک عبارت مشخص رو داخل رشته پیدا میکنه.
- PATINDEX شبیه CHARINDEX هست، ولی از Pattern و Wildcard هم پشتیبانی میکنه.
مثلاً اگر بخوای بخش قبل از @ رو از ایمیل جدا کنی، میتونی این کار رو انجام بدی:
SELECT
Email,
SUBSTRING(Email, 1, CHARINDEX('@', Email) - 1) AS UserNamePart
```
FROM Customers
WHERE Email IS NOT NULL
AND CHARINDEX('@', Email) > 0;
```
در این کوئری، CHARINDEX جای @ رو پیدا میکنه و SUBSTRING بخش قبل از اون رو جدا میکنه.
نمونه PATINDEX:
SELECT PATINDEX('%[0-9]%', 'ABC123');
این کوئری محل اولین عدد داخل رشته رو پیدا میکنه.
نکته مهم: موقع کار با متنها باید مراقب دادههای نامعتبر باشی. اگر CHARINDEX مقدار صفر برگردونه و همون رو مستقیم داخل SUBSTRING استفاده کنی، ممکنه خطا بگیری. برای همین بهتره قبلش شرط اعتبارسنجی بذاری.
۳۶. توابع DATEADD، DATEDIFF و EOMONTH چه کاربردی در محاسبات مالی و ماهانه دارن؟
```این سه تابع برای کار با تاریخ در SQL Server خیلی مهمن، مخصوصاً وقتی بحث گزارشهای مالی، گزارشهای ماهانه، سررسیدها و محاسبات دورهای مطرحه.
- DATEADD به یک تاریخ، مقدار مشخصی اضافه یا از اون کم میکنه.
- DATEDIFF اختلاف بین دو تاریخ رو بر اساس یک واحد مشخص حساب میکنه.
- EOMONTH آخرین روز ماه رو برمیگردونه.
چند مثال ساده:
SELECT DATEADD(DAY, 7, GETDATE());
این دستور ۷ روز به تاریخ امروز اضافه میکنه.
SELECT DATEDIFF(DAY, '20260101', GETDATE());
این یکی تعداد روزهای بین دو تاریخ رو برمیگردونه.
SELECT EOMONTH(GETDATE());
این دستور آخرین روز ماه جاری رو میده.
چند کاربرد واقعی در سیستمهای مالی:
- محاسبه فروش ماهانه
- محاسبه تاریخ سررسید
- ساخت گزارش پایان ماه
- محاسبه مانده دوره
- گزارشگیری بر اساس بازههای زمانی دقیق
نکته مهم: برای فیلتر ماهانه، بهتره بهجای MONTH(OrderDate) = 1 از بازه تاریخ استفاده کنی. چون استفاده از تابع روی ستون میتونه جلوی استفاده درست از Index رو بگیره.
WHERE OrderDate >= '20260101'
```
AND OrderDate < '20260201'
۳۷. در Queryهایی که از NOT IN با Subquery استفاده میکنن، وجود یک NULL چه دردسری درست میکنه؟
```اگر Subquery داخل NOT IN حتی یک مقدار NULL برگردونه، شرط میتونه برای همه رکوردها UNKNOWN بشه و نتیجه نهایی خالی برگرده.
این کوئری در ظاهر ساده است، اما میتونه خطرناک باشه:
SELECT *
```
FROM Customers
WHERE CustomerID NOT IN
(
SELECT CustomerID
FROM Orders
);
```
اگر Orders.CustomerID حتی یک مقدار NULL داشته باشه، ممکنه خروجی Query هیچ مشتریای نباشه؛ حتی اگر واقعاً مشتریهایی وجود داشته باشن که سفارش ندارن.
راهحل حرفهایتر معمولاً NOT EXISTS هست:
SELECT *
```
FROM Customers c
WHERE NOT EXISTS
(
SELECT 1
FROM Orders o
WHERE o.CustomerID = c.CustomerID
);
```
اگر مجبور باشی از NOT IN استفاده کنی، حداقل باید NULLها رو از خروجی Subquery حذف کنی:
WHERE CustomerID NOT IN
```
(
SELECT CustomerID
FROM Orders
WHERE CustomerID IS NOT NULL
)
```
نکته طلایی: برای Anti Join در SQL Server، معمولاً NOT EXISTS انتخاب امنتر و حرفهایتریه. چون از دام NULL در NOT IN دور میمونه و رفتار قابلاعتمادتری میده.
۳۸. کلاز OUTPUT در دستورات INSERT، UPDATE و DELETE چه امکاناتی به برنامهنویس میده؟
```کلاز OUTPUT بهت اجازه میده رکوردهایی رو که موقع INSERT، UPDATE یا DELETE تغییر کردن، همون لحظه ببینی یا حتی داخل یک جدول دیگه ذخیره کنی.
مثلاً در INSERT میتونی مقدارهای تازه Insertشده رو بگیری:
INSERT INTO Customers(Name)
```
OUTPUT inserted.CustomerID, inserted.Name
VALUES (N'رضا');
```
در UPDATE هم میتونی مقدار قبل و بعد از تغییر رو همزمان داشته باشی:
UPDATE Orders
```
SET TotalAmount = TotalAmount * 1.1
OUTPUT deleted.TotalAmount AS OldAmount,
inserted.TotalAmount AS NewAmount
WHERE OrderDate >= '20260101';
```
کاربردهای رایج OUTPUT:
- ثبت Audit Log
- گرفتن IDهای Insertشده
- ثبت مقدارهای قبل و بعد از Update
- انتقال دادههای حذفشده به جدول آرشیو
- ردیابی تغییرات در عملیاتهای Set-Based
نکته مهم: OUTPUT در عملیاتهای Set-Based خیلی ارزشمنده، چون بدون Loop و بدون Query اضافه میتونی تغییرات انجامشده رو ثبت یا بررسی کنی.
۳۹. Sequence چیه و چه تفاوتی با Identity داره؟
```Identity یک ویژگی برای ستون داخل یک جدوله. یعنی روی یک ستون تعریف میکنی تا SQL Server بهصورت خودکار برای هر رکورد جدید یک عدد تولید کنه.
اما Sequence یک Object مستقل داخل دیتابیسه. یعنی به یک جدول خاص وابسته نیست و میتونی ازش برای چند جدول یا چند عملیات مختلف استفاده کنی.
نمونه ساخت Sequence:
CREATE SEQUENCE dbo.OrderNumberSeq
START WITH 1000
INCREMENT BY 1;
```
SELECT NEXT VALUE FOR dbo.OrderNumberSeq;
```
تفاوتهای مهم Identity و Sequence:
- Identity به یک ستون در یک جدول وابسته است.
- Sequence مستقل از جدول ساخته میشه.
- Sequence میتونه قبل از INSERT مقدار تولید کنه.
- Sequence میتونه بین چند جدول مشترک استفاده بشه.
- Sequence قابلیت Cache داره.
نکته مهم: Sequence برای سناریوهایی مناسبه که شمارهگذاری مستقل از جدول میخوای، یا میخوای یک الگوی شمارهگذاری مشترک بین چند Entity داشته باشی.
۴۰. تفاوت Schema با Database چیه و چرا جدولها رو گروهبندی میکنیم؟
```Database ظرف اصلی دادههاست. داخل یک Database میتونی چند Schema داشته باشی. Schema یک لایه منطقی برای گروهبندی Objectهایی مثل Table، View، Stored Procedure و Function هست.
مثلاً این نامها رو ببین:
Sales.Orders
```
HR.Employees
Finance.Payments
```
در اینجا Sales، HR و Finance اسم Schema هستن و Orders، Employees و Payments اسم Objectهای داخل اون Schemaها.
مزیتهای استفاده درست از Schema:
- سازماندهی بهتر Objectهای دیتابیس
- مدیریت راحتتر Permissionها
- جلوگیری از شلوغی و بینظمی دیتابیس
- تفکیک دامنههای کاری مثل فروش، منابع انسانی و مالی
- خواناتر شدن ساختار دیتابیس در پروژههای بزرگ
نکته مصاحبهای: کاندیدای حرفهای باید بدونه dbo فقط یک Schema پیشفرضه، نه اینکه الزاماً بهترین انتخاب برای همه جدولها باشه. در پروژههای جدی، Schema میتونه بخشی از طراحی تمیز دیتابیس باشه.
۴۱. کلاز GROUPING SETS چه مزیتی نسبت به چند GROUP BY با UNION ALL داره؟
```GROUPING SETS بهت اجازه میده چند سطح مختلف از گروهبندی رو داخل یک Query بنویسی. یعنی بهجای اینکه چند Query جدا با GROUP BY بنویسی و بعد با UNION ALL به هم بچسبونی، همه گروهبندیها رو تمیزتر و متمرکزتر داخل یک دستور مینویسی.
مثلاً فرض کن میخوای فروش رو هم به تفکیک مشتری و سال ببینی، هم به تفکیک مشتری، هم مجموع کل:
SELECT
CustomerID,
YEAR(OrderDate) AS OrderYear,
SUM(TotalAmount) AS TotalSales
```
FROM Orders
GROUP BY GROUPING SETS
(
(CustomerID, YEAR(OrderDate)),
(CustomerID),
()
);
```
بدون GROUPING SETS، برای همین گزارش باید چند Query جدا بنویسی و خروجیها رو با UNION ALL ترکیب کنی. این کار هم کد رو طولانیتر میکنه، هم نگهداری و تغییر دادنش سختتر میشه.
نکته مهم: برای گزارشهای مدیریتی چندسطحی، GROUPING SETS، ROLLUP و CUBE ابزارهای خیلی قدرتمندی هستن. مخصوصاً وقتی میخوای جمعهای جزئی و جمع کل رو در یک خروجی داشته باشی.
۴۲. توابع CHOOSE و IIF چه کمکی به سادهتر شدن شرطهای CASE WHEN میکنن؟
```تابع IIF رو میتونی نسخه کوتاهتر و سادهتر CASE WHEN برای شرطهای ساده در نظر بگیری.
SELECT IIF(TotalAmount > 1000000, N'High', N'Normal')
```
FROM Orders;
```
در این مثال، اگر مبلغ سفارش بیشتر از یک میلیون باشه، مقدار High برمیگرده؛ در غیر این صورت Normal.
تابع CHOOSE هم بر اساس یک عدد ورودی، یکی از مقدارهای لیست رو انتخاب میکنه:
SELECT CHOOSE(2, N'Low', N'Medium', N'High');
خروجی این دستور میشه:
Medium
اما نکته مهم اینه که CASE WHEN همچنان گزینه قدرتمندتر، استانداردتر و مناسبتر برای شرطهای پیچیده است.
نکته مصاحبهای: استفاده از IIF برای خوانایی در شرطهای ساده خوبه، اما نباید فکر کنیم جایگزین کامل CASE در همه سناریوهاست. برای شرطهای چندمرحلهای و منطق جدیتر، CASE معمولاً انتخاب بهتریه.
۴۳. تابع STRING_AGG چطور کار FOR XML PATH رو سادهتر کرده؟
```قبل از اینکه STRING_AGG به SQL Server اضافه بشه، برای چسباندن چند مقدار رشتهای کنار هم معمولاً از روشهای پیچیدهتری مثل FOR XML PATH استفاده میکردیم. این روش کار میکرد، اما هم خوانایی کمتری داشت، هم برای خیلی از برنامهنویسها گیجکننده بود.
STRING_AGG این کار رو خیلی سادهتر کرده. مثلاً این Query همه OrderIDهای هر مشتری رو داخل یک رشته، با جداکننده کاما، نمایش میده:
SELECT
CustomerID,
STRING_AGG(CAST(OrderID AS VARCHAR(20)), ',') AS OrderIDs
```
FROM Orders
GROUP BY CustomerID;
```
یعنی بهجای اینکه برای هر مشتری چند ردیف سفارش ببینی، میتونی شناسه سفارشهاش رو به شکل یک متن تجمیعشده داشته باشی.
در نسخههای جدیدتر SQL Server، حتی میتونی با WITHIN GROUP ترتیب خروجی رو هم مشخص کنی:
STRING_AGG(Name, ',') WITHIN GROUP (ORDER BY Name)
نکته مصاحبهای: اگر کسی فقط FOR XML PATH بلد باشه بد نیست، چون روش قدیمی و رایجی بوده. اما آشنایی با STRING_AGG نشون میده با قابلیتهای جدیدتر SQL Server هم کار کرده و هنوز با روشهای قدیمی گیر نکرده.
۴۴. Table Partitioning چیه و چه زمانی سراغش میریم؟
```Partitioning یعنی یک جدول بزرگ رو از نظر منطقی و فیزیکی به بخشهای کوچکتر تقسیم کنیم. این تقسیم معمولاً بر اساس یک ستون انجام میشه؛ مثلاً تاریخ سفارش، سال مالی یا ماه ثبت داده.
فرض کن یک جدول Orders داری که صدها میلیون رکورد داخلشه. میتونی این جدول رو بر اساس سال یا ماه Partition کنی تا مدیریت دادهها راحتتر بشه.
چند مزیت مهم Partitioning:
- مدیریت بهتر دادههای حجیم
- آرشیو راحتتر دادههای قدیمی
- حذف یا انتقال سریعتر دادهها با Partition Switching
- بهبود بعضی Queryها، به شرطی که فیلتر روی ستون Partition انجام بشه
- کنترل بهتر روی نگهداری و مدیریت دادههای تاریخی
اما باید حواست باشه: Partitioning جادو نیست. اگر Queryها بد نوشته شده باشن، Index مناسب وجود نداشته باشه، یا فیلترها از ستون Partition درست استفاده نکنن، ممکنه Partitioning کمکی به Performance نکنه.
نکته مهم: Partitioning بیشتر یک ابزار برای مدیریت دادههای بزرگه، نه یک درمان فوری برای همه مشکلات Performance. اگر کسی فقط برای سریعتر شدن هر Query سراغ Partitioning بره، احتمالاً مسئله رو درست نفهمیده.
۴۵. کاربرد PARTITION BY در Window Functionها چیه؟
```PARTITION BY در Window Functionها دادهها رو به گروههای منطقی تقسیم میکنه، اما برخلاف GROUP BY خروجی رو خلاصه نمیکنه و ردیفها رو از بین نمیبره.
مثلاً این Query برای هر مشتری، سفارشها رو جداگانه شمارهگذاری میکنه:
SELECT
OrderID,
CustomerID,
TotalAmount,
ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderDate DESC) AS RowNum
```
FROM Orders;
```
در اینجا شمارهگذاری برای هر CustomerID از اول شروع میشه. یعنی هر مشتری Partition جداگانه خودش رو داره.
چند کاربرد رایج PARTITION BY:
- پیدا کردن آخرین سفارش هر مشتری
- رتبهبندی کارمندان داخل هر دپارتمان
- محاسبه Running Total برای هر حساب
- مقایسه سفارش فعلی با سفارش قبلی همان مشتری
- محاسبه مجموع، میانگین یا رتبه بدون حذف جزئیات ردیفها
نکته مصاحبهای: فرق اصلی PARTITION BY با GROUP BY اینه که PARTITION BY فقط محدوده محاسبه رو مشخص میکنه، اما ردیفها رو حذف یا خلاصه نمیکنه. همین تفاوت برای فهم Window Functionها خیلی کلیدیه.
بخش سوم: سوالات عملی کوئرینویسی T-SQL
در این بخش فرض میکنیم چند جدول ساده و ذهنی داریم تا سؤالها قابل فهمتر باشن:
Customers(CustomerID, Name, RegistrationDate)
Orders(OrderID, CustomerID, OrderDate, TotalAmount)
Employees(EmployeeID, Name, ManagerID, Salary)
۴۶. با CTE و ROW_NUMBER() چطور رکوردهای تکراری رو حذف کنیم و فقط یک نسخه نگه داریم؟
```فرض کنیم در جدول Customers چند رکورد تکراری بر اساس Name داریم و میخوایم از هر نام فقط یک رکورد باقی بمونه.
WITH Duplicates AS
```
(
SELECT
CustomerID,
Name,
ROW_NUMBER() OVER
(
PARTITION BY Name
ORDER BY CustomerID
) AS rn
FROM Customers
)
DELETE FROM Duplicates
WHERE rn > 1;
```
منطق این Query اینه:
- PARTITION BY Name مشتریهای همنام رو داخل یک گروه قرار میده.
- ROW_NUMBER() به هر رکورد داخل هر گروه یک شماره میده.
- رکورد شماره ۱ نگه داشته میشه.
- رکوردهایی که rn آنها بزرگتر از ۱ است، حذف میشن.
اما در کار واقعی، قبل از اجرای DELETE حتماً اول نسخه SELECT بگیر:
WITH Duplicates AS
```
(
SELECT
CustomerID,
Name,
ROW_NUMBER() OVER(PARTITION BY Name ORDER BY CustomerID) AS rn
FROM Customers
)
SELECT *
FROM Duplicates
WHERE rn > 1;
```
نکته خیلی مهم: در محیط Production بدون دیدن خروجی SELECT، هیچوقت DELETE واقعی اجرا نکن. حذف داده شوخی نیست؛ مخصوصاً وقتی شرط حذف بر اساس تشخیص رکورد تکراریه.
۴۷. چطور آخرین سفارش ثبتشده هر مشتری رو برگردونیم؟
```یکی از روشهای حرفهای برای این کار استفاده از ROW_NUMBER() هست. برای هر مشتری سفارشها رو بر اساس تاریخ نزولی مرتب میکنیم و بعد فقط ردیف شماره ۱ رو برمیگردونیم.
WITH RankedOrders AS
```
(
SELECT
OrderID,
CustomerID,
OrderDate,
TotalAmount,
ROW_NUMBER() OVER
(
PARTITION BY CustomerID
ORDER BY OrderDate DESC, OrderID DESC
) AS rn
FROM Orders
)
SELECT
OrderID,
CustomerID,
OrderDate,
TotalAmount
FROM RankedOrders
WHERE rn = 1;
```
چرا کنار OrderDate از OrderID DESC هم استفاده کردیم؟ چون ممکنه یک مشتری در یک روز یا حتی در یک زمان مشابه چند سفارش داشته باشه. با اضافه کردن OrderID DESC خروجی قطعیتر و قابلاعتمادتر میشه.
یک روش دیگر هم استفاده از OUTER APPLY هست:
SELECT
c.CustomerID,
c.Name,
o.OrderID,
o.OrderDate,
o.TotalAmount
```
FROM Customers c
OUTER APPLY
(
SELECT TOP (1)
OrderID,
OrderDate,
TotalAmount
FROM Orders o
WHERE o.CustomerID = c.CustomerID
ORDER BY o.OrderDate DESC, o.OrderID DESC
) o;
```
نکته مصاحبهای: هر دو روش خوبن. ROW_NUMBER برای تحلیل مجموعهای عالیه. OUTER APPLY برای گرفتن Top 1 per row خیلی خوانا و طبیعی درمیاد.
۴۸. چطور دومین حقوق بالاتر جدول کارمندان رو پیدا کنیم؟
```اول باید مشخص کنیم منظور از «دومین حقوق» چیه. آیا دومین ردیفه؟ یا دومین حقوق متمایز؟ در مصاحبه بهتره همین سؤال رو از مصاحبهگر بپرسی، چون جواب این دو حالت ممکنه متفاوت باشه.
اگر منظور دومین حقوق متمایز باشه، روش خوب استفاده از DENSE_RANK() هست:
WITH SalaryRanks AS
```
(
SELECT
Salary,
DENSE_RANK() OVER(ORDER BY Salary DESC) AS SalaryRank
FROM Employees
)
SELECT DISTINCT Salary
FROM SalaryRanks
WHERE SalaryRank = 2;
```
چرا DENSE_RANK بهتره؟ چون اگر چند نفر بالاترین حقوق یکسان داشته باشن، باز هم دومین مقدار متمایز حقوق رو درست پیدا میکنه.
روش دیگر با OFFSET/FETCH:
SELECT DISTINCT Salary
```
FROM Employees
ORDER BY Salary DESC
OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY;
```
این روش فقط وقتی درست جواب میده که اول DISTINCT اعمال شده باشه؛ وگرنه ممکنه دومین ردیف رو برگردونه، نه دومین حقوق متمایز.
نکته طلایی: در مصاحبه حتماً بپرس «منظورتون دومین ردیفه یا دومین حقوق متمایز؟» همین سؤال ساده نشان میده تو مسئله رو دقیق میفهمی، نه اینکه فقط دنبال نوشتن یک Query حفظی باشی.
۴۹. چطور مجموع فروش تجمعی رو به تفکیک ماههای سال محاسبه کنیم؟
```برای محاسبه فروش تجمعی ماهانه، بهتره کار رو دو مرحلهای انجام بدیم: اول فروش هر ماه رو حساب کنیم، بعد روی خروجی ماهانه Running Total بگیریم.
WITH MonthlySales AS
```
(
SELECT
DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1) AS SalesMonth,
SUM(TotalAmount) AS MonthlyTotal
FROM Orders
GROUP BY DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1)
)
SELECT
SalesMonth,
MonthlyTotal,
SUM(MonthlyTotal) OVER
(
ORDER BY SalesMonth
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS RunningTotal
FROM MonthlySales
ORDER BY SalesMonth;
```
توضیح Query:
- CTE اول فروش هر ماه رو میسازه.
- Window Function دوم مجموع تجمعی رو محاسبه میکنه.
- عبارت ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW یعنی از ابتدای دادهها تا ردیف فعلی جمع بزن.
نکته مهم: برای گزارشهای مالی، بهتره اول تاریخ رو به ابتدای ماه Normalize کنیم. این کار باعث میشه گروهبندی تمیزتر، خروجی قابلاعتمادتر و گزارش قابل نگهداریتر باشه.
۵۰. با LEFT JOIN چطور کاربرانی رو پیدا کنیم که تا حالا خریدی نکردن؟
```برای پیدا کردن مشتریهایی که هیچ سفارشی ندارن، میتونیم از LEFT JOIN استفاده کنیم. جدول Customers رو سمت چپ میذاریم تا همه مشتریها حفظ بشن، بعد مشتریهایی رو جدا میکنیم که در جدول Orders رکورد متناظر ندارن.
SELECT
c.CustomerID,
c.Name,
c.RegistrationDate
```
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderID IS NULL;
```
منطق این Query ساده است:
- LEFT JOIN همه مشتریها رو نگه میداره.
- اگر سفارشی برای مشتری وجود نداشته باشه، ستونهای Orders مقدار NULL میگیرن.
- با WHERE o.OrderID IS NULL فقط مشتریهای بدون سفارش رو جدا میکنیم.
روش دیگر استفاده از NOT EXISTS هست:
SELECT
c.CustomerID,
c.Name,
c.RegistrationDate
```
FROM Customers c
WHERE NOT EXISTS
(
SELECT 1
FROM Orders o
WHERE o.CustomerID = c.CustomerID
);
```
نکته مصاحبهای: هر دو روش رایجن. در خیلی از سناریوها NOT EXISTS خواناتر و در برابر مشکل NULL امنتره، مخصوصاً وقتی هدف پیدا کردن رکوردهایی باشه که در جدول دوم وجود ندارن.