Оператор CROSS APPLY в T-SQL. Cross apply t sql
SQL.RU | Чудесный оператор CROSS APPLY
Сегодня я хочу рассказать более подробно об операторе APPLY, а конкретнее о его типе CROSS APPLY. Этот оператор появился впервые в SQL Server 2005, но к сожалению многие так и не научились им пользоваться, возможно это из-за того, что в BOL (SQL Server Books Online) этот оператор плохо описан и имеет очень "сухие" примеры его использования. В этой статье я покажу несколько интересных демонстраций, где этот оператор может пригодиться.
Основная фича оператора заключается в том, что APPLY позволяет вызывать табличную функцию для каждой строки, возвращаемой внешним табличным выражением запроса. Именно этот пример есть в BOL. Оператор CROSS APPLY возвращает только строки из внешней таблицы, которые создает результирующий набор из возвращающего табличное значение функции. Оператор OUTER APPLY возвращает и строки, которые формируют результирующий набор, и строки, которые этого не делают, со значениями NULL в столбцах, созданных возвращающей табличное значение функцией.
Для демонстрации некоторых фич оператора APPLY, создадим тестовую БД и пару таблиц:
use master go --Создаю тестовую БД, --для демонстрации возможностей оператора CrossApply if db_id ( 'CrossApplyDemo' ) is not null drop database CrossApplyDemo go create database CrossApplyDemo go use CrossApplyDemo go --Создаю тестовую таблицу стран if object_id ( 'dbo.Countries', 'U' ) is not null drop table dbo.Countries go create table dbo.Countries ( CountryID int, Country nvarchar(255) ) go --Добавим 5 стран, используя синтаксис SQL Server 2008 insert into dbo.Countries ( CountryID, Country ) values ( 1, N'Россия' ), ( 2, N'США' ), ( 3, N'Германия' ) , ( 4, N'Франция' ), ( 5, N'Италия' ), ( 6, N'Испания' ) go --Создаю тестовую таблицу городов if object_id ( 'dbo.Cities', 'U' ) is not null drop table dbo.Cities go create table dbo.Cities ( CityID int, CountryID int, City nvarchar(255) ) go --Добавим несколько городов insert into dbo.Cities ( CityID, CountryID, City ) values ( 1, 1, N'Москва' ), ( 2, 1, N'Санкт-Петербург' ), ( 3, 1, N'Екатеринбург' ) , ( 4, 1, N'Новосибирс' ), ( 5, 1, N'Самара' ), ( 6, 2, N'Чикаго' ) , ( 7, 2, N'Вашингтон' ), ( 8, 2, N'Атланта' ), ( 9, 3, N'Берлин' ) , ( 10, 3, N'Мюнхен' ), ( 11, 3, N'Гамбург' ), ( 12, 3, N'Бремен' ) , ( 13, 4, N'Париж' ), ( 14, 4, N'Лион' ), ( 15, 5, N'Милан' ) go Основное назначение оператора - это работа с табличными функциями. Создадим функцию, которая возвращает список городов по входному параметру @CountyID: --Табличная функция create function dbo.GetCities( @CountyID int ) returns table as return ( select CityID, City from dbo.Cities where CountryID = @CountyID ) go Результат вызова функции представлен ниже: select * from dbo.GetCities (1) ------------------------------ --Результат: ------------------------------ --CityID City ------------- --------------------- --1 Москва --2 Санкт-Петербург --3 Екатеринбург --4 Новосибирс --5 Самара А теперь с помощью оператора APPLY я выведу список городов для каждой страны из таблицы Countries select * from dbo.Countries c cross apply dbo.GetCities ( c.CountryID ) ap ------------------------------ --Результат: ------------------------------ --CountryID Country CityID City ------------- --------------- ----------- --------------- --1 Россия 1 Москва --1 Россия 2 Санкт-Петербург --1 Россия 3 Екатеринбург --1 Россия 4 Новосибирс --1 Россия 5 Самара --2 США 6 Чикаго --2 США 7 Вашингтон --2 США 8 Атланта --3 Германия 9 Берлин --3 Германия 10 Мюнхен --3 Германия 11 Гамбург --3 Германия 12 Бремен --4 Франция 13 Париж --4 Франция 14 Лион --5 Италия 15 Милан Очень удобно, но подобный пример описан в БОЛ, а я покажу, как можно ещё использовать оператор CROSS APPLY. Часто бывает задача вывести несколько первых значений из группы. Например, как вывести по 3 города для каждой страны, отсортированных по алфавиту!? С помощью оператора APPLY это сделать достаточно легко: select * from dbo.Countries c cross apply ( select top 3 City from dbo.Cities where CountryID = c.CountryID order by City ) ap ------------------------------ --Результат: ------------------------------ --CountryID Country City ------------- --------------- --------------- --1 Россия Екатеринбург --1 Россия Москва --1 Россия Новосибирс --2 США Атланта --2 США Вашингтон --2 США Чикаго --3 Германия Берлин --3 Германия Бремен --3 Германия Гамбург --4 Франция Лион --4 Франция Париж --5 Италия Милан Теперь попробуем ещё более усложнить наш запрос. Выведем первую букву каждого из 3х городов каждой страны и общее количество этих букв среди ВСЕХ городов текущей страны: select * from dbo.Countries c cross apply ( select top 3 City from dbo.Cities where CountryID = c.CountryID order by City ) ap cross apply ( select l 'Letter', sum (cl) 'LetterCount' from (select left( ap.City, 1 ) l, len( City ) - len ( replace ( City, left( ap.City, 1 ) ,'' ) ) cl from dbo.Cities where CountryID = c.CountryID ) t group by l ) apLetters ------------------------------ --Результат: ------------------------------ --CountryID Country City Letter LetterCount ------------- --------------- --------------- ------ ----------- --1 Россия Екатеринбург Е 4 --1 Россия Москва М 2 --1 Россия Новосибирс Н 3 --2 США Атланта А 5 --2 США Вашингтон В 1 --2 США Чикаго Ч 1 --3 Германия Берлин Б 3 --3 Германия Бремен Б 3 --3 Германия Гамбург Г 2 --4 Франция Лион Л 1 --4 Франция Париж П 1 --5 Италия Милан М 1www.t-sql.ru
Оператор CROSS APPLY в T-SQL | Info-Comp.ru
Сейчас давайте рассмотрим достаточно полезный, в некоторых случаях, оператор CROSS APPLY языка T-SQL, затронем немного теории и, конечно же, рассмотрим примеры использования.
Мы с Вами оператор CROSS APPLY как в справочнике T-SQL, так и в материале про основы программирования на T-SQL не рассматривали. Честно говоря, и в официальной документации данный оператор описан кратко, а он действительно полезный, поэтому сегодня я предлагаю рассмотреть возможности CROSS APPLY, в том числе и те, которые не описаны в документации.
CROSS APPLY
CROSS APPLY – это тип оператора APPLY, который позволяет вызывать табличную функцию для каждой строки внешнего табличного выражения. Вместо табличной функции можно также использовать и подзапрос. Есть еще и другой тип OUTER APPLY, он в отличие от CROSS APPLY возвращает и строки, которые формируют результирующий набор и те, которые этого не делают, т.е. со значениями NULL в столбцах.
Примечание! Примеры ниже будут рассмотрены в MS SQL Server 2008 R2.
Примеры использования CROSS APPLY
Для начала давайте определимся с исходными данными, допустим, у нас есть таблица товаров (Products) и таблица продаж (Sales) вот с такими данными:
--ProductName – Наименование товара SELECT * FROM Products --ProductID – Идентификатор товара --DT – Дата продажи SELECT * FROM SalesА также у нас есть функция (FT_GET_Sales), которая просто выводит список продаж по идентификатору товара.
CREATE FUNCTION dbo.FT_GET_Sales(@ProductID INT) RETURNS TABLE AS RETURN ( SELECT S.ID, S.DT, P.ProductName FROM Sales S LEFT JOIN Products P ON S.ProductID = P.ID WHERE S.ProductID = @ProductID )Пример ее работы:
SELECT Sales.DT, Sales.ProductName FROM dbo.FT_GET_Sales(3) AS SalesПример использования CROSS APPLY с табличной функцией
Теперь допустим, нам необходимо получить продажи не только одного товара, но и других, для этого мы можем использовать оператор CROSS APPLY
SELECT P.ProductName, Sales.DT FROM Products P CROSS APPLY dbo.FT_GET_Sales(P.ID) AS SalesГде как Вы понимаете, табличная функция FT_GET_Sales была вызвана для каждой строки таблицы Products.
Пример использования CROSS APPLY с подзапросом
Как уже было сказано выше, CROSS APPLY можно использовать и с подзапросом, для примера давайте допустим, что нам нужно получить последнюю дату продажи каждого товара.
SELECT Products.ProductName, Sales.DT FROM Products CROSS APPLY ( SELECT TOP 1 P.DT, P.ProductName FROM dbo.FT_GET_Sales(Products.ID) AS P ORDER BY P.DT DESC )AS SalesКак видим, после CROSS APPLY у нас идет подзапрос.
Пример использования OUTER APPLY
Если Вы заметили, у нас в исходных данных есть позиция «Клавиатура», по которой не было продаж. И в случае возникновения необходимости получить последнюю дату продажи включая товары, по которым не было продаж, т.е. выводить NULL значения, чтобы мы видели, какие товары не продавались вообще, тип CROSS APPLY можно заменить на OUTER APPLY.
Пример:
SELECT Products.ProductName, Sales.DT FROM Products OUTER APPLY ( SELECT TOP 1 P.DT, P.ProductName FROM dbo.FT_GET_Sales(Products.ID) AS P ORDER BY P.DT DESC )AS SalesТеперь видно, что некоторые товары продавались и последняя дата их продажи такая то, а некоторые товары не продавались, т.е. у них отсутствует дата продажи.
Заметка! Начинающим программистам рекомендую почитать мою книгу «Путь программиста T-SQL», в ней я подробно, с большим количеством примеров, рассказываю про другие полезные возможности языка Transact-SQL.
На этом все, удачи!
Похожие статьи:
info-comp.ru
performance - T-SQL CROSS APPLY с GROUP BY
Интересное поведение. Строго говоря, ваш запрос неверен - если у вас нет заказов для Мэри, но вы хотите обновить ее запись, вы должны использовать outer apply вместо cross. Кроме того, было бы лучше обработать этот сценарий "без записей" с обертками isnull() в части set.
Прямо сейчас значения в строке Мэри не переписаны нулями - они остались без изменений, потому что apply ничего не возвращает для нее. Вы можете увидеть это, изменив инициализацию таблицы следующим образом:
insert into @cust values (01, 'Fred', -1, -1, -1) insert into @cust values (02, 'Mary', -1, -1, -1) insert into @cust values (03, 'Karl', -1, -1, -1)С group by на месте, строка Мэри не получает нулей, она все еще имеет все эти -1. Это точно такое же поведение, когда вы пытаетесь присвоить значение скалярной переменной с запросом, который не возвращает строк, после этого переменная по-прежнему будет сохранять свое предыдущее значение. Это документированная и хорошо известная функция.
Сказав это, мне все же очень интересно (по крайней мере, мне), почему комментирование group by так сильно меняет поведение. Мы можем сузить его, посмотрев результаты подзапроса apply, например:
select MAX(AMOUNT) MaxOrder, SUM(AMOUNT) TotalAmountSpent, COUNT(OID) OrderCount from @order o where o.CUSTID = 2; select MAX(AMOUNT) MaxOrder, SUM(AMOUNT) TotalAmountSpent, COUNT(OID) OrderCount from @order o where o.CUSTID = 2 group by o.CUSTID;Как представляется, задание критериев группировки работает как дополнительный фильтр. Вероятно, это то, как агрегация реализована в SQL Server.
EDIT: после некоторого поиска я обнаружил, что Oracle работает точно так же. Кажется, это стандартное поведение. Кроме того, здесь обсуждается этот эффект: Count Возврат пустой, а не 0
Короче говоря, group by отфильтровывает группы, которых нет, поэтому, когда вы указываете клиента без продаж, вы ничего не получаете. Однако без группировки такой стадии фильтрации нет, поэтому вы получаете агрегаты для всей таблицы - null для max и sum и ноль для count. И в вашем конкретном примере group by фактически не нужен, потому что все возвращенные столбцы являются агрегатами (что довольно редко).
qaru.site