آموزش تابع ROW_NUMBER در SQL Server و شمارهگذاری حرفهای رکوردها
آموزش تابع ROW_NUMBER در SQL Server و شمارهگذاری حرفهای رکوردها
مقدمه
ROW_NUMBER یکی از توابع رتبهبندی و پنجرهای SQL Server است. این تابع برای هر ردیف نتیجه یک شماره ترتیبی یکتا تولید میکند. شمارهگذاری از عدد یک آغاز میشود و ترتیب آن بر اساس ORDER BY داخل عبارت OVER تعیین میگردد. ROW_NUMBER در صفحهبندی نتایج، انتخاب اولین یا آخرین رکورد هر گروه، حذف دادههای تکراری، تهیه گزارش و ساخت شماره ردیف کاربرد زیادی دارد.
تفاوت مهم ROW_NUMBER با ستون Identity این است که Identity یک مقدار ذخیرهشده در جدول است، اما ROW_NUMBER هنگام اجرای کوئری محاسبه میشود و بخشی از داده دائمی نیست. بنابراین با تغییر فیلتر یا ترتیب، شماره ردیفها نیز تغییر میکنند.
تعریف و نحو ROW_NUMBER
ROW_NUMBER() OVER
(
[ PARTITION BY partition_expression ]
ORDER BY order_expression [ ASC | DESC ]
)
پارامترها
- PARTITION BY اختیاری است و نتیجه را به گروههای مستقل تقسیم میکند.
- ORDER BY اجباری است و ترتیب اختصاص شماره را مشخص میکند.
- هر گروه در PARTITION BY شمارهگذاری را دوباره از یک آغاز میکند.
نوع خروجی
نوع خروجی ROW_NUMBER برابر bigint است. این ویژگی باعث میشود شمارهگذاری مجموعههای بسیار بزرگ نیز از محدوده int عبور نکند.
مثال پایه
DECLARE @Employees TABLE
(
EmployeeID int,
FullName nvarchar(100),
Salary decimal(18,2)
);
INSERT INTO @Employees VALUES
(10, N'مریم احمدی', 42000000),
(11, N'علی کریمی', 51000000),
(12, N'رضا محمدی', 47000000);
SELECT
ROW_NUMBER() OVER (ORDER BY Salary DESC) AS RowNo,
EmployeeID,
FullName,
Salary
FROM @Employees;
| RowNo | EmployeeID | FullName | Salary |
| 1 | 11 | علی کریمی | 51000000 |
| 2 | 12 | رضا محمدی | 47000000 |
| 3 | 10 | مریم احمدی | 42000000 |
استفاده از PARTITION BY
DECLARE @Sales TABLE
(
SaleID int,
SellerName nvarchar(100),
SaleYear int,
Amount decimal(18,2)
);
INSERT INTO @Sales VALUES
(1, N'علی', 1404, 10000000),
(2, N'مریم', 1404, 18000000),
(3, N'حسن', 1405, 15000000),
(4, N'زهرا', 1405, 21000000);
SELECT
SaleYear,
SellerName,
Amount,
ROW_NUMBER() OVER
(
PARTITION BY SaleYear
ORDER BY Amount DESC
) AS RowInYear
FROM @Sales;
برای هر سال شمارهگذاری از یک شروع میشود. این الگو برای انتخاب بهترین فروشنده هر سال یا آخرین سفارش هر مشتری بسیار مفید است.
انتخاب جدیدترین رکورد هر گروه
WITH RankedNews AS
(
SELECT
NewsID,
NewsGroupID,
NewsTitle,
PublishedDate,
ROW_NUMBER() OVER
(
PARTITION BY NewsGroupID
ORDER BY PublishedDate DESC, NewsID DESC
) AS RowNo
FROM dbo.tblNewsContent
)
SELECT NewsID, NewsGroupID, NewsTitle, PublishedDate
FROM RankedNews
WHERE RowNo = 1;
استفاده از NewsID به عنوان معیار دوم، نتیجه را در زمان برابر بودن PublishedDate قطعی میکند. اگر ORDER BY یکتا نباشد، ترتیب ردیفهای همارزش ممکن است در اجراهای مختلف تغییر کند.
صفحهبندی با ROW_NUMBER
DECLARE @PageNumber int = 3;
DECLARE @PageSize int = 20;
WITH NumberedRows AS
(
SELECT
NewsID,
NewsTitle,
PublishedDate,
ROW_NUMBER() OVER
(
ORDER BY PublishedDate DESC, NewsID DESC
) AS RowNo
FROM dbo.tblNewsContent
WHERE ArchiveState = 0
)
SELECT NewsID, NewsTitle, PublishedDate
FROM NumberedRows
WHERE RowNo BETWEEN
((@PageNumber - 1) * @PageSize) + 1
AND (@PageNumber * @PageSize)
ORDER BY RowNo;
در نسخههای جدید SQL Server استفاده از OFFSET و FETCH نیز رایج است، اما ROW_NUMBER زمانی مفید است که علاوه بر صفحهبندی به شماره ردیف یا منطق پیچیدهتر نیاز دارید.
حذف دادههای تکراری با حفظ یک ردیف
WITH DuplicateRows AS
(
SELECT
NewsTagID,
NewsID,
TagDescription,
ROW_NUMBER() OVER
(
PARTITION BY NewsID, TagDescription
ORDER BY NewsTagID
) AS RowNo
FROM dbo.tblTags
)
DELETE FROM DuplicateRows
WHERE RowNo > 1;
قبل از اجرای DELETE، همان CTE را با SELECT بررسی کنید و از پایگاه داده نسخه پشتیبان داشته باشید. در جدول نمونه شما کلید اصلی ترکیبی از NewsID و TagDescription است و اصولاً اجازه درج تکراری نمیدهد، اما این الگو برای جدولهای بدون محدودیت یکتا کاربردی است.
مقایسه ROW_NUMBER، RANK و DENSE_RANK
| تابع | رفتار در مقدارهای مساوی | پرش شماره |
| ROW_NUMBER | به هر ردیف شماره متفاوت میدهد | ندارد |
| RANK | رتبه برابر میدهد | دارد |
| DENSE_RANK | رتبه برابر میدهد | ندارد |
SELECT
FullName,
Salary,
ROW_NUMBER() OVER (ORDER BY Salary DESC) AS RowNumberValue,
RANK() OVER (ORDER BY Salary DESC) AS RankValue,
DENSE_RANK() OVER (ORDER BY Salary DESC) AS DenseRankValue
FROM @Employees;
خطاها و اشتباهات رایج
- استفاده از ROW_NUMBER بدون ORDER BY که از نظر نحو مجاز نیست.
- تصور اینکه ORDER BY بیرونی جای ORDER BY داخل OVER را میگیرد.
- استفاده از ترتیب غیر یکتا و انتظار نتیجه کاملاً ثابت.
- حذف داده تکراری بدون بررسی اولیه ردیفهای RowNo بزرگتر از یک.
- فیلتر کردن مستقیم روی نام مستعار RowNo در همان سطح SELECT؛ معمولاً باید CTE یا زیرکوئری استفاده شود.
نکات کارایی
توابع پنجرهای اغلب به مرتبسازی نیاز دارند. وجود ایندکسی که ستونهای PARTITION BY و ORDER BY را پوشش دهد میتواند هزینه Sort را کاهش دهد. برای مثال، در انتخاب آخرین خبر هر گروه، ایندکس روی NewsGroupID و PublishedDate و NewsID میتواند مفید باشد. با این حال طراحی ایندکس باید بر اساس حجم جدول، انتخابپذیری و سایر عملیات نوشتن انجام شود. طرح اجرا را بررسی کنید و به Sort، Window Spool، Memory Grant و Spill به tempdb توجه داشته باشید.
بهترین روشها
- در ORDER BY یک ستون یکتا به عنوان معیار نهایی اضافه کنید.
- فیلترهای اصلی را پیش از اجرای تابع پنجرهای اعمال کنید.
- برای صفحهبندی روی مجموعه بسیار بزرگ، روش Keyset Pagination را نیز بررسی کنید.
- پیش از DELETE مبتنی بر ROW_NUMBER، نتیجه را با SELECT کنترل کنید.
- ایندکس را بر اساس PARTITION BY و ORDER BY طراحی و آزمایش کنید.
سؤالات متداول و مصاحبه
آیا ROW_NUMBER مقدار دائمی ایجاد میکند؟
خیر. شماره در زمان اجرای کوئری ساخته میشود و در جدول ذخیره نمیگردد.
آیا دو ردیف میتوانند ROW_NUMBER برابر داشته باشند؟
در یک مجموعه یا پارتیشن خیر. هر ردیف شماره یکتا میگیرد.
چه زمانی RANK بهتر است؟
زمانی که ردیفهای همامتیاز باید رتبه یکسان بگیرند، مانند رتبهبندی مسابقه.
چکلیست و جمعبندی
- ORDER BY قطعی و ترجیحاً یکتا است.
- PARTITION BY دقیقاً مطابق گروهبندی کسبوکار انتخاب شده است.
- فیلترها پیش از محاسبه اعمال شدهاند.
- ایندکس و طرح اجرا بررسی شدهاند.
- عملیات حذف ابتدا با SELECT آزمایش شده است.
ROW_NUMBER ابزاری قدرتمند برای شمارهگذاری و تحلیل ترتیبی دادهها است. با ترکیب صحیح OVER، PARTITION BY و ORDER BY میتوان بسیاری از مسائل پیچیده گزارشگیری، صفحهبندی و پاکسازی داده را با کوئری خوانا و قابل نگهداری حل کرد.