بازگشت N سطر برتر در SQL Server: راهنمای جامع APPLY و ROW_NUMBER برای بهینهسازی پرسوجوها
بازیابی N سطر برتر از یک جدول، یک وظیفه رایج و حیاتی در SQL Server است که بهینهسازی آن میتواند عملکرد کلی سیستم را بهبود بخشد. روشهای مختلفی برای انجام این کار وجود دارد، از جمله استفاده از عبارت TOP
، تابع ROW_NUMBER()
یا اپراتور APPLY
. این مقاله به بررسی عمیق و کاربردی نحوه استفاده از اپراتور APPLY
و تابع پنجرهای ROW_NUMBER()
برای بازگرداندن سطرهای برتر از یک جدول در سناریوهای مختلف میپردازد تا به شما در نوشتن پرسوجوهای بهینهتر کمک کند.
برای شروع و نمایش مثالهای عملی، یک پایگاه داده و جدول نمونه ایجاد میکنیم. اسکریپت SQL زیر یک پایگاه داده به نام TestDB
، یک جدول به نام Products
با ستونهای ProductID
, ProductName
, CategoryID
و Price
ایجاد کرده و آن را با دادههای نمونه پر میکند. این ساختار داده به ما امکان میدهد تا سناریوهای مختلف بازیابی سطرها را با سهولت آزمایش کنیم.
USE master;
GO
IF DB_ID('TestDB') IS NOT NULL
DROP DATABASE TestDB;
GO
CREATE DATABASE TestDB;
GO
USE TestDB;
GO
CREATE TABLE Products (
ProductID INT IDENTITY(1,1) PRIMARY KEY,
ProductName NVARCHAR(100),
CategoryID INT,
Price DECIMAL(10, 2)
);
GO
INSERT INTO Products (ProductName, CategoryID, Price) VALUES
('Laptop', 1, 1200.00),
('Desktop', 1, 1500.00),
('Monitor', 1, 300.00),
('Keyboard', 2, 75.00),
('Mouse', 2, 50.00),
('Printer', 2, 200.00),
('Smartphone', 3, 800.00),
('Tablet', 3, 500.00),
('Smartwatch', 3, 250.00),
('Headphones', 2, 120.00),
('Webcam', 2, 60.00),
('External SSD', 1, 180.00),
('Speaker', 3, 150.00),
('Router', 1, 90.00);
GO
اپراتور APPLY
برای فراخوانی یک تابع با مقادیر جدولی (table-valued function
) برای هر سطر بازگردانده شده توسط یک عبارت جدول خارجی استفاده میشود. این اپراتور عملکردی مشابه JOIN
دارد، اما مزیت اصلی آن زمانی آشکار میشود که سمت راست اپراتور (table-valued function
) به ستونهایی از سمت چپ (outer table expression
) وابسته باشد. دو نوع اصلی از APPLY
وجود دارد که هر کدام کاربردهای خاص خود را در بازیابی دادهها دارند.
CROSS APPLY
CROSS APPLY
تنها سطرهایی را از عبارت جدول خارجی بازمیگرداند که یک مجموعه نتیجه غیرخالی از تابع با مقادیر جدولی تولید میکنند. این اپراتور در عملکرد و نتیجهدهی بسیار شبیه به یک INNER JOIN
است، اما انعطافپذیری بیشتری را در کار با توابع جدولی فراهم میکند.
فرض کنید میخواهیم 2 محصول گرانقیمت برتر در هر دسته را پیدا کنیم. میتوانیم این کار را با استفاده از CROSS APPLY
و یک زیرپرسوجو که برای هر CategoryID
به صورت جداگانه اجرا میشود، به سادگی انجام دهیم.
SELECT
p.CategoryID,
p.ProductName,
p.Price
FROM
(SELECT DISTINCT CategoryID FROM Products) AS c
CROSS APPLY
(SELECT TOP 2 ProductName, Price
FROM Products
WHERE CategoryID = c.CategoryID
ORDER BY Price DESC) AS p;
GO
OUTER APPLY
OUTER APPLY
تمام سطرها را از عبارت جدول خارجی بازمیگرداند، صرف نظر از اینکه تابع با مقادیر جدولی نتیجهای تولید کند یا خیر. اگر تابع با مقادیر جدولی برای یک سطر خاص نتیجهای تولید نکند، ستونهای بازگردانده شده از آن تابع NULL
خواهند بود. این رفتار شبیه به یک LEFT OUTER JOIN
است.
بیایید مثال قبلی را تغییر دهیم تا دستهها را حتی اگر 2 محصول برتر نداشته باشند، یا اگر یک دسته کمتر از 2 محصول داشته باشد، نمایش دهد. برای سادگی، ما فرض میکنیم که ممکن است دستههایی وجود داشته باشند که هیچ محصولی ندارند (اگرچه جدول Products
ما در حال حاضر چنین دستههایی ندارد). این مثال به خوبی رفتار OUTER APPLY
را نشان میدهد.
SELECT
c.CategoryID,
p.ProductName,
p.Price
FROM
(SELECT DISTINCT CategoryID FROM Products) AS c
OUTER APPLY
(SELECT TOP 2 ProductName, Price
FROM Products
WHERE CategoryID = c.CategoryID
ORDER BY Price DESC) AS p;
GO
تابع ROW_NUMBER()
یک تابع پنجرهای (window function
) قدرتمند است که یک عدد ترتیبی منحصر به فرد را به هر سطر در یک پارتیشن خاص از مجموعه نتایج اختصاص میدهد. شمارهگذاری از 1 برای اولین سطر در هر پارتیشن آغاز میشود و برای استفاده از آن، حتماً باید یک عبارت ORDER BY
در داخل عبارت OVER()
آن مشخص شود تا ترتیب شمارهگذاری سطرها به درستی تعیین گردد.
برای یافتن N محصول برتر در هر دسته با استفاده از ROW_NUMBER()
، ابتدا دادهها را بر اساس CategoryID
پارتیشنبندی میکنیم و سپس بر اساس Price
به ترتیب نزولی مرتب میکنیم. پس از اختصاص شماره سطر با استفاده از RowNum
، میتوانیم با یک شرط ساده فیلتر کنیم تا تنها سطرهایی را نگه داریم که شماره سطر آنها کمتر یا مساوی N مورد نظر ما باشد.
WITH RankedProducts AS (
SELECT
ProductID,
ProductName,
CategoryID,
Price,
ROW_NUMBER() OVER (PARTITION BY CategoryID ORDER BY Price DESC) AS RowNum
FROM
Products
)
SELECT
ProductID,
ProductName,
CategoryID,
Price
FROM
RankedProducts
WHERE
RowNum <= 2; -- Get top 2 products per category
GO
ملاحظات عملکردی و بهینهسازی
هر دو اپراتور APPLY
و تابع ROW_NUMBER()
ابزارهای قدرتمندی برای بازیابی N سطر برتر در هر گروه در SQL Server هستند. با این حال، عملکرد آنها میتواند بر اساس فاکتورهای متعددی مانند حجم داده، استراتژی ایندکسگذاری و پیچیدگی کلی پرسوجو به طور قابل توجهی متفاوت باشد.
اپراتور APPLY
: این اپراتور اغلب زمانی که زیرپرسوجو یا تابع با مقادیر جدولی بسیار انتخابی باشد (یعنی تعداد کمی سطر را برگرداند) عملکرد خوبی از خود نشان میدهد. با این حال، اگر پرسوجوی بیرونی تعداد زیادی سطر را بازگرداند و پرسوجوی داخلی پیچیده باشد یا به درستی بهینه نشده باشد، میتواند کارایی کمتری داشته باشد. بهینهساز SQL Server ممکن است یک nested loop join
را انتخاب کند که در آن پرسوجوی داخلی برای هر سطر از پرسوجوی بیرونی اجرا میشود. ایندکسگذاری مناسب روی ستونهای کلیدی مانند CategoryID
و Price
برای بهینهسازی عملکرد این روش بسیار حیاتی است.
تابع ROW_NUMBER()
: به طور کلی، استفاده از ROW_NUMBER()
در ترکیب با یک CTE
(Common Table Expression) یا یک زیرپرسوجو، به ویژه با مجموعههای داده بزرگ، عملکرد خوبی از خود نشان میدهد. موتور پایگاه داده میتواند دادهها را یک بار به طور کارآمد مرتب و پارتیشنبندی کند. با این حال، استفاده از یک عبارت ORDER BY
بزرگ روی ستون ایندکسنشده در داخل عبارت OVER()
میتواند منجر به عملیات مرتبسازی سنگین و مصرف بالای منابع شود که بر عملکرد کلی تأثیر منفی میگذارد. بنابراین، اطمینان از ایندکسگذاری صحیح ستونهایی که در عبارتهای PARTITION BY
و ORDER BY
استفاده میشوند، برای دستیابی به بهترین کارایی ضروری است.