11 May 2011

MSSql: Использование оператора APPLY в TSql

SQL
Недавно, реализуя некоторый код доступа к данным, я столкнулся с задачей выбора последних N записей для каждой сущности. Пользователь kuda78 подсказал вместо многоэтажной выборки использовать метод SelectMany.
Исследуя, какой SQL код создает LinqToSQL, я натолкнулся на интересный SQL оператор APPLY.


Как гласит MSDN эта команда выполняет следующее:
http://technet.microsoft.com/en-us/library/ms175156.aspx
Оператор APPLY позволяет вызывать возвращающую табличное значение функцию для каждой строки, возвращаемой внешним табличным выражением запроса. Возвращающая табличное значение функция выступает в роли правого входа, а внешнее табличное выражение — в роли левого входа. Правый вход оценивается для каждой строки из левого входа, а созданные строки объединяются для конечного вывода. Список столбцов, созданных оператором APPLY, является набором столбцов в левом входе, за которым следует список столбцов, возвращенный правым входом.

Как оказалось APPLY очень хорошо подходит к решению поставленной задачи.

Давайте рассмотрим на примере:
Задача: Выбрать 10 последних заказов для каждого заказчика.

Пускай имеем следующую простую структуру БД:

CREATE TABLE Customer
(
	CustomerID INT PRIMARY KEY,
	CustomerName NVARCHAR(30) NOT NULL
)

CREATE TABLE Nomenclature
(
	NomenclatureID INT PRIMARY KEY,
	NomenclatureName NVARCHAR(30) NOT NULL,
	Price MONEY  NOT NULL
)

CREATE TABLE Deal
(
	DealID INT IDENTITY(1, 1) PRIMARY KEY,
	CustomerID INT NOT NULL,
	NomenclatureID INT NOT NULL,
	[Count] DECIMAL(8,2) NOT NULL,
	DealDate DATETIME NOT NULL
)


Теперь нам надо выбрать 10 последних заказов для каждого заказчика. Раньше мы пользовались следующим подходом: сначала для каждого заказчика выбирал дату, начиная с которой у него было 10 заказов, а потом выбирал все заказы с этой даты.

SELECT
	d.DealDate,
	c.CustomerName,
	n.NomenclatureName,
	n.Price,
	d.Count
FROM
	Customer c JOIN Deal d ON
		d.CustomerID = c.CustomerID
	JOIN (SELECT  c.CustomerID,
		(SELECT MIN(lastDeals.DealDate) FROM (SELECT TOP 10 d1.DealDate FROM Deal d1 WHERE
 d1.CustomerID = c.CustomerID ORDER BY d1.DealDate DESC) LastDeals) LastDealDate
		FROM Customer c) ld ON
		ld.CustomerID = c.CustomerID
	JOIN Nomenclature n ON
		n.NomenclatureID = d.NomenclatureID
WHERE 
	d.DealDate >= ld.LastDealDate 
ORDER BY c.CustomerName, d.DealDate DESC

* Для простоты я специально сделал допущение, что 2 заказа в один и тот же момент времени быть не могут.

С использованием APPLY SQL код приобрел большую читаемость:

SELECT 
	d.DealDate,
	c.CustomerName,
	n.NomenclatureName,
	n.Price,
	d.Count
FROM
	Customer c 
	OUTER APPLY (SELECT TOP 10 d1.* FROM Deal d1 Where d1.CustomerID = c.CustomerID ORDER BY d1.DealDate DESC) d
	INNER JOIN Nomenclature n ON
		n.NomenclatureID = d.NomenclatureID
ORDER BY c.CustomerName, d.DealDate DESC

План и время выполнения запроса при наличии всех необходимых индексов также вселяют уверенность, что эта функция еще неоднократно нам пригодится.

Файл создания бд с индексами: CreateDB.txt
Файл с SQL запросами: Queries.txt
Tags:mssqlоператорыAPPLYtsql
Hubs: SQL
+14
38.2k 28
Comments 16
Popular right now
Старший SQL-разработчик / Senior SQL Developer
from 80,000 to 180,000 ₽iCertaintyСамараRemote job
Старший full-stack разработчик .NET + MS SQL
from 200,000 to 200,000 ₽КАУСМосква
Разработчик PL/SQL (Oracle)
from 140,000 ₽Ренессанс ЖизньRemote job
Специалист технической поддержки
from 50,000 to 70,000 ₽IT Smart FinanceНовосибирск