آموزش کامل DISTINCT در SQL Server
مقدمه
کلیدواژه DISTINCT یکی از مباحث کلیدی Microsoft SQL Server در حوزه یکتاسازی خروجی است. یادگیری درست آن فقط به حفظ Syntax محدود نمیشود؛ باید ترتیب منطقی پردازش، نوع داده، رفتار NULL، Unicode، Collation، اثر ایندکس، شکل Execution Plan، سطح دسترسی و شرایط اجرای همزمان نیز در نظر گرفته شود. در این مقاله موضوع DISTINCT از سطح مقدماتی آغاز میشود و تا نکات حرفهای، خطاهای رایج، سناریوهای واقعی و روشهای جایگزین ادامه پیدا میکند.
DISTINCT ردیفهای تکراری Result Set را بر اساس ترکیب تمام ستونها و عبارتهای فهرست SELECT حذف میکند. این کلیدواژه داده جدول را تغییر نمیدهد و نباید برای پنهان کردن Join اشتباه استفاده شود.
| ویژگی | مقدار |
|---|
| نام انگلیسی | DISTINCT |
| دستهبندی | یکتاسازی خروجی |
| سطح | مقدماتی تا متوسط |
| نسخههای پشتیبانیشده | همه نسخهها |
| زمان مطالعه | حدود ۱۴ تا ۲۰ دقیقه |
| پیشنیاز | آشنایی با جدول، ستون، نوع داده و اصول پایه T-SQL |
تعریف و نحوه عملکرد
DISTINCT در T-SQL بخشی از چرخه پردازش Query یا تغییر داده است. SQL Server ابتدا متن Statement را Parse و Bind میکند، سپس بهینهساز بر اساس آمار، ایندکسها، Constraintها و تخمین Cardinality یک Plan انتخاب میکند. نتیجه واقعی میتواند با حجم داده، مقدار پارامتر، نسخه موتور و وضعیت آمار تغییر کند؛ بنابراین کدی که روی چند ردیف سریع است الزاماً در Production سریع یا حتی از نظر همزمانی ایمن نیست.
در استفاده از کلیدواژه DISTINCT باید Grain داده و هدف کسبوکار روشن باشد. Grain مشخص میکند هر ردیف خروجی یا هر ردیف تحت اثر چه معنایی دارد. وقتی این تعریف مبهم باشد، امکان تولید تکرار، حذف یا تغییر بیش از انتظار، شمارش اشتباه و Plan پرهزینه افزایش مییابد.
زمان استفاده
- وقتی نیاز مستقیم کسبوکار با قابلیت DISTINCT همراستا است.
- وقتی نوع داده، تعداد ردیف و ترتیب یا محدوده عملیات از قبل مشخص شده است.
- وقتی نتیجه با Query کنترلی، Constraint یا شمارش ردیف قابل اعتبارسنجی است.
- وقتی اثر آن بر Log، Lock، TempDB و ایندکسها با حجم واقعی آزمایش شده است.
زمان عدم استفاده
- وقتی هدف فقط با یک میانبر ظاهری حل میشود ولی علت اصلی طراحی یا Join اشتباه باقی میماند.
- وقتی ورودی کاربر بدون پارامترسازی یا اعتبارسنجی وارد Statement میشود.
- وقتی عملیات تغییر داده بدون Transaction، Backup یا راه برگشت اجرا میشود.
- وقتی نسخه SQL Server مقصد یا محدودیتهای Schema بررسی نشده است.
Syntax، پارامترها و نوع خروجی
نحو عمومی
SELECT DISTINCT column1, column2
FROM dbo.TableName
WHERE predicate;
SELECT COUNT(DISTINCT column1)
FROM dbo.TableName;
اجزای اصلی
| جزء | توضیح |
|---|
| DISTINCT | یکتاسازی کل خروجی SELECT |
| ستونها | همه ستونها در کلید یکتایی شرکت میکنند |
| COUNT(DISTINCT) | شمارش مقادیر یکتای غیر NULL |
| WHERE | کاهش ورودی پیش از یکتاسازی |
نوع خروجی و اثر Statement
نوع خروجی DISTINCT به ستونها و عبارتهای استفادهشده وابسته است. در Statementهای خواندنی یک Result Set ساخته میشود و در عملیات DML معمولاً @@ROWCOUNT تعداد ردیفهای تحت اثر را گزارش میکند. هرجا OUTPUT پشتیبانی شود، میتوان کلید یا مقدار قبل و بعد را به شکل کنترلشده دریافت کرد.
در محاسبات عددی باید Precision، Scale و احتمال Overflow بررسی شود. در متن، طول nvarchar و Collation اهمیت دارد. در تاریخ، نوع date یا datetime2 و مرزهای بازه باید صریح باشند. تبدیل ضمنی ممکن است هم نتیجه را تغییر دهد و هم باعث از دست رفتن Index Seek شود.
مثالهای عملی و نتیجه نمونه
شهرهای یکتا
SELECT DISTINCT City
FROM dbo.Customers
WHERE City IS NOT NULL
ORDER BY City;
هر شهر فقط یک بار دیده میشود.
ترکیب دو ستون
SELECT DISTINCT City, CustomerType
FROM dbo.Customers
ORDER BY City, CustomerType;
یکتایی روی ترکیب شهر و نوع مشتری است.
| City | CustomerType |
|---|
| تهران | حقیقی |
| تهران | حقوقی |
شمارش محصول یکتا
SELECT COUNT(DISTINCT ProductID) AS DistinctProducts
FROM dbo.OrderDetails;
NULL شمرده نمیشود.
رفتار با NULL، انواع داده، Unicode و Collation
رفتار با NULL
چند NULL در SELECT DISTINCT یک مقدار یکتا محسوب میشوند، ولی COUNT(DISTINCT column) آنها را نمیشمارد.
NULL به معنای صفر یا رشته خالی نیست و منطق سهحالته SQL را ایجاد میکند. برای جلوگیری از نتیجه غیرمنتظره، دادههای NULL، مقدار خالی و مقدار واقعی را در تستهای جداگانه پوشش دهید. در محاسبات تجمعی نیز تفاوت COUNT(*) و COUNT(column) و رفتار توابع نسبت به NULL اهمیت دارد.
انواع داده
ستون عریض یا nvarchar(max) میتواند Sort یا Hash سنگین ایجاد کند.
نوع پارامتر بهتر است دقیقاً با نوع و طول ستون هماهنگ باشد. پارامتر nvarchar(4000) برای ستونی با طول بسیار کوتاه یا مقایسه int با متن میتواند تخمین Cardinality و استفاده از ایندکس را خراب کند. CAST و CONVERT را روی مقدار ورودی انجام دهید، نه بدون ضرورت روی ستون ایندکسشده.
Unicode و Collation
برابری متن بر اساس Collation است؛ حساسیت به حروف و Accent نتیجه یکتایی را تغییر میدهد.
برای ثابت فارسی از N استفاده کنید. اختلاف Collation میان Databaseها، TempDB و ستونهای متنی ممکن است خطا یا Sort و Convert اضافه ایجاد کند. COLLATE در سطح Query راهحل موقت است و باید اثر آن بر Seek و ترتیب ایندکس اندازهگیری شود.
خطاهای رایج، محدودیتها و Edge Caseها
- استفاده برای پنهان کردن Join غلط
- افزودن کلید یکتا به SELECT
- فرض مرتب شدن خروجی
- نادیده گرفتن TempDB Spill
- جایگزین کردن Constraint با DISTINCT
Edge Caseهای مهم DISTINCT شامل جدول خالی، مقدار NULL، کلید تکراری، چند ردیف همرتبه، مقدار بسیار بزرگ، اجرای همزمان و تغییر Schema است. هرکدام میتواند نتیجهای متفاوت از تست ساده ایجاد کند. تست باید هم روی داده مرزی و هم روی حجم نزدیک Production انجام شود.
برای Statementهای تغییردهنده، Trigger، Foreign Key، Cascade، Indexed View، Temporal Table و Replication نیز ممکن است رفتار یا محدودیت اضافه ایجاد کنند. برای Queryهای خواندنی، Row-Level Security، View و Permission میتوانند ردیف قابل مشاهده را تغییر دهند.
Performance Considerations، Index و Execution Plan
- پیش از DISTINCT فیلتر کنید.
- فقط ستون لازم را انتخاب کنید.
- Sort/Hash و Spill را در Plan ببینید.
- برای یکتایی دائمی Unique Constraint بسازید.
نکات مربوط به Index
ایندکس مناسب برای DISTINCT باید بر اساس Predicate، Join، ترتیب Sort و ستونهای خروجی طراحی شود. افزودن هر ایندکس هزینه نگهداری INSERT، UPDATE و DELETE را افزایش میدهد؛ بنابراین ایندکس صرفاً برای زیبا شدن یک Plan ساخته نمیشود. Logical Reads، تعداد Lookup، اندازه Index و فراوانی اجرای Query باید همزمان سنجیده شوند.
Execution Plan
در Actual Execution Plan به تفاوت Estimated Rows و Actual Rows، نوع Scan یا Seek، Sort، Hash Match، Nested Loops، Spool، Memory Grant و هشدار Spill یا Implicit Conversion توجه کنید. اختلاف بزرگ تخمین و واقعیت میتواند از آمار قدیمی، پارامتر حساس، Predicate همبسته یا داده Skew شده ناشی شود.
SET STATISTICS IO و SET STATISTICS TIME را در محیط تست فعال کنید و Query Store را برای مقایسه Planها و Regression به کار ببرید. زمان یک اجرای گرم بهتنهایی معیار کافی نیست؛ CPU، I/O، TempDB، Blocking، Deadlock و پایداری Plan نیز باید اندازهگیری شوند.
Best Practices و نکات امنیتی
- نام Schema و ستونها را صریح بنویسید.
- ورودی برنامه را با sp_executesql یا پارامتر واقعی ارسال کنید.
- برای DML ابتدا SELECT پیشنمایش با همان WHERE اجرا کنید.
- تعداد ردیف مورد انتظار را با @@ROWCOUNT یا OUTPUT کنترل کنید.
- در عملیات حساس از SET XACT_ABORT ON، TRY/CATCH و Transaction استفاده کنید.
- کد را با داده NULL، تکراری، مرزی و حجم بالا آزمایش کنید.
امنیت
DISTINCT ابزار کنترل دسترسی نیست و داده حساس را امن نمیکند.
اصل کمترین سطح دسترسی را رعایت کنید. نام جدول، ستون، جهت Sort و سایر Identifierها با پارامتر عادی جایگزین نمیشوند؛ آنها باید از فهرست سفید انتخاب و در Dynamic SQL با QUOTENAME محافظت شوند. داده حساس را فقط در صورت نیاز برگردانید و Audit عملیات تغییردهنده را نگه دارید.
مقایسه با روشهای مشابه
| روش | تفاوت اصلی | کاربرد مناسب |
|---|
| DISTINCT | حذف تکرار خروجی | فهرست یکتا |
| GROUP BY | گروهبندی و Aggregate | گزارش |
| ROW_NUMBER | انتخاب ردیف مشخص هر گروه | پاکسازی تکرار |
روشهای جایگزین
GROUP BY بدون Aggregate خروجی مشابه میدهد، اما برای انتخاب یک رکورد از هر گروه ROW_NUMBER روشنتر است.
دستورات و توابع مرتبط
GROUP BY، COUNT، ROW_NUMBER، UNIQUE INDEX و UNION
سناریوی واقعی و طراحی Production
در یک سامانه فروش یا تولید، DISTINCT ممکن است روی میلیونها ردیف و همزمان با تراکنشهای دیگر اجرا شود. طراحی Production باید محدوده عملیات، Isolation Level، Lock Duration، ظرفیت Transaction Log، سیاست Retry و زمانبندی Job را مشخص کند. اجرای درست فقط به موفق شدن Statement محدود نیست؛ نتیجه باید قابل Audit و قابل تکرار باشد.
در ETL بهتر است داده ابتدا به جدول Stage وارد شود، تعداد و کیفیت آن کنترل گردد و سپس به مقصد منتقل شود. در گزارشگیری نیز Snapshot زمانی یا مرز ثابت لازم است تا کاربر هنگام صفحهبندی یا محاسبه شاخص با جابهجایی غیرمنتظره داده روبهرو نشود.
برای عملیات انبوه، Batch Size را با آزمون تعیین کنید. Batch بسیار کوچک overhead زیادی دارد و Batch بسیار بزرگ Log، Lock و زمان Rollback را افزایش میدهد. مانیتورینگ باید تعداد ردیف، مدت اجرا، خطا، Deadlock و مصرف منابع را ثبت کند.
نسخههای پشتیبانیشده، تاریخچه و منابع
DISTINCT طبق این مقاله برای همه نسخهها در نظر گرفته شده است. بااینحال برخی Optionها یا رفتارهای بهینهساز به Compatibility Level و Cumulative Update وابستهاند. قبل از استفاده در سامانه حیاتی، Syntax را در نسخه مقصد و با همان Compatibility Level آزمایش کنید.
برای مرجع دقیق، صفحه رسمی Microsoft Learn مربوط به DISTINCT Transact-SQL، مستندات Query Processing Architecture، Index Design Guide، Transaction Locking Guide و Release Notes نسخه مقصد را مطالعه کنید.
ترفندهای حرفهای و Cheat Sheet
- قبل از اجرای DISTINCT تعداد ردیف مورد انتظار را یادداشت کنید.
- Actual Plan را با حداقل دو مقدار پارامتر متفاوت مقایسه کنید.
- در کد فارسی ثابتهای متنی را با N بنویسید.
- هشدارهای Plan را نادیده نگیرید؛ بهویژه Spill و Convert.
- برای تغییر داده، نتیجه کنترل و Audit پس از Commit داشته باشید.
-- Cheat Sheet: DISTINCT
-- 1) هدف، Grain و تعداد ردیف مورد انتظار را مشخص کنید.
-- 2) نوع داده، NULL، Unicode و Collation را کنترل کنید.
-- 3) Predicate و Index را با Actual Plan بسنجید.
-- 4) عملیات حساس را داخل TRY/CATCH و Transaction اجرا کنید.
-- 5) @@ROWCOUNT، OUTPUT یا SELECT کنترلی را بررسی کنید.
تمرین و Quiz
تمرین
ترکیب یکتای CustomerID و Status سفارشهای یک سال اخیر را استخراج کنید.
Quiz
مهمترین کنترل قبل از استفاده از DISTINCT در محیط Production چیست؟
پاسخ: تعریف دقیق محدوده و تعداد ردیف، هماهنگی نوع داده، بررسی Execution Plan و داشتن روش اعتبارسنجی و بازگشت.
سؤالات مصاحبه
- DISTINCT در ترتیب منطقی پردازش SQL Server چه جایگاهی دارد؟
- رفتار DISTINCT با NULL و نوع داده ناسازگار چیست؟
- کدام Operatorهای Execution Plan برای DISTINCT رایجاند؟
- چه زمانی جایگزین DISTINCT انتخاب بهتری است؟
- چگونه امنیت و همزمانی DISTINCT را در Production کنترل میکنید؟
سؤالات متداول
آیا DISTINCT همیشه از ایندکس استفاده میکند؟
خیر. انتخاب Scan یا Seek به Predicate، Selectivity، آمار، پوشش ستونها و هزینه تخمینی بستگی دارد. وجود ایندکس بهتنهایی تضمین استفاده نیست.
چرا نتیجه یا سرعت DISTINCT بین دو اجرا فرق میکند؟
تغییر داده، پارامتر، Cache Plan، آمار، فشار همزمانی، Memory Grant و Compatibility Level میتوانند Plan یا زمان اجرا را تغییر دهند.
چگونه DISTINCT را امنتر اجرا کنیم؟
پارامترسازی، حداقل Permission، Transaction مناسب، کنترل تعداد ردیف، Audit و آزمایش در محیط مشابه Production مهمترین اقدامات هستند.
چکلیست نهایی
- Syntax DISTINCT با نسخه مقصد سازگار است.
- همه News/Business Keyها و محدوده ردیف کنترل شدهاند.
- NULL، رشته خالی، Unicode و Collation تست شدهاند.
- نوع داده پارامتر با ستون هماهنگ است.
- Actual Plan، Logical Reads و TempDB بررسی شدهاند.
- ورودی پارامتری و Permission حداقلی است.
- برای DML راه Rollback و Audit وجود دارد.
- خروجی با Query کنترلی اعتبارسنجی میشود.
جمعبندی
کلیدواژه DISTINCT زمانی قابل اعتماد است که Syntax، معنای منطقی و هزینه فیزیکی آن همزمان فهمیده شود. برای DISTINCT فقط رسیدن به نتیجه ظاهراً درست کافی نیست؛ نتیجه باید در برابر NULL، تکرار، تغییر حجم، اجرای همزمان و تغییر Plan پایدار باشد.
کد خوانا، ستونهای صریح، Predicate دقیق، نوع داده هماهنگ، Index سنجیده، پارامترسازی و کنترل پس از اجرا مهمترین اصولاند. با رعایت این موارد میتوان مقاله و نمونه کد را مستقیماً در CMS آموزشی منتشر کرد و همان الگوها را با احتیاط در پروژه واقعی به کار برد.