Развитие языка ABAP в последнее время тесно связано с таким понятием как Code-To-Data, когда все расчёты принято выполнять на СУБД, а результатами пользоваться уже на сервере приложений. Основная причина такого перехода — развитие собственной СУБД HANA и более глубокая интеграция с её возможностями непосредственно в языке. Соответственно OpenSQL (или как сейчас принято называть ABAP SQL) постоянно расширяется новыми конструкциями языка, давая разработчикам все больше возможностей для переноса вычислений на СУБД.
Начиная с релиза ABAP 7.51 в языке стала доступна конструкция WITH позволяющая создавать подзапросы объединённые в рамках одного SQL выражения и использовать табличные результаты этих подзапросов для формирования общего результата. В какой-то степени конструкция WITH является более удобной альтернативой использованию глобальных временных таблиц и более быстрым вариантом нежели использование FOR ALL ENTRIES или нескольких последовательных запросов.
Далее на простом примере разберём как это работает.
Синтаксис
Общий синтаксис WITH выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
WITH +cte1[( name1, name2, ... )] AS ( SELECT subquery_clauses [UNION ...] ), [hierarchy] [associations][, +cte2[( name1, name2, ... )] AS ( SELECT subquery_clauses [UNION ...] ), [hierarchy] [associations], ... ] SELECT mainquery_clauses [UNION ...] INTO|APPENDING target [UP TO ...] [OFFSET ...] [abap_options]. ... [ENDWITH]. |
Выражение состоит из одного или более именнованного общего табличного выражения (Common table expression, далее CTE) и основного запроса, использующего одно или несколько общих выражений.
Символ «+» в начале каждого имени CTE является ключевым символом по аналогии с @ у ABAP переменных в SQL выражениях. После имени CTE опционально в скобках можно указать альтернативные имена для каждого выбранного в рамках CTE поля.
Каждый оператор WITH должен завершаться основным запросом (SELECT..), который использует по крайней мере один из своих CTE, и каждый CTE должен использоваться по крайней мере в одном последующем запросе. CTE не может использовать сам себя себя в качестве источника данных. WITH может использоваться как отдельный оператор или как дополнение в OPEN CURSOR.
ENDWITH — является аналогом ENDSELECT в случае если вы не выполняете запись во внутреннюю таблицу, а открываете цикл WITH, который необходимо закрыть с помощью ENDWITH. В отличие от SELECT..ENDSELECT нельзя использовать вложенные циклы.
Конструкция WITH пропускает буферизацию таблиц.
Примеры
Рассмотрим первый пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
DATA lv_carrid TYPE spfli-carrid VALUE 'LH'. cl_demo_input=>request( CHANGING field = lv_carrid ). lv_carrid = to_upper( lv_carrid ). WITH +connections AS ( SELECT spfli~carrid, carrname, connid, cityfrom, cityto FROM spfli INNER JOIN scarr ON scarr~carrid = spfli~carrid WHERE spfli~carrid = @lv_carrid ), +sum_seats AS ( SELECT carrid, connid, SUM( seatsocc ) AS sum_seats FROM sflight WHERE carrid = @lv_carrid GROUP BY carrid, connid ), +result( name, connection, departure, arrival, occupied ) AS ( SELECT carrname, c~connid, cityfrom, cityto, sum_seats FROM +connections AS c INNER JOIN +sum_seats AS s ON c~carrid = s~carrid AND c~connid = s~connid ) SELECT * FROM +result ORDER BY name, connection INTO TABLE @DATA(lt_result). cl_demo_output=>display( lt_result ). |
С помощью WITH мы создаём три подзапроса:
- connections — где выбираем все записи из таблицы с рейсами SPFLI для выбранной авиакомпании;
- sum_seats — где для авиакомпании и рейса считаем сумму занятых в полёте мест;
- result — где создаём объединение двух предыдущих выборок и переименовываем поля для читаемости;
Финальный SELECT содержит уже конечную выборку из результатов полученных в подзапросах.
Для наглядности результаты промежуточных подзапросов и финального result:
Рассмотрим еще один пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
WITH +cities AS ( SELECT cityfrom AS city FROM spfli WHERE carrid = @carrid UNION DISTINCT SELECT cityto AS city FROM spfli WHERE carrid = @carrid ) SELECT * FROM sgeocity WHERE city IN ( SELECT city FROM +cities ) INTO TABLE @DATA(result). |
Тут мы создали CTE cities как объединение двух подзапросов для поиска уникальных городов в таблице с рейсами из полей «город отправки» и «город прилёта».
Как видно из кода, основной SELECT не обязательно должен использовать CTE в качестве основного источника данных, в данном примере он используется как подзапрос. Важно лишь то, что каждый объявленный CTE должен быть использован.
Конструкцию WITH так же удобно использовать для формирования итоговых строк:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
DATA carrid TYPE sflight-carrid VALUE 'AA'. cl_demo_input=>request( CHANGING field = carrid ). WITH +total AS ( SELECT carrid, connid, CAST( '00000000' AS DATS ) AS fldate, SUM( seatsocc ) AS seatsocc FROM sflight WHERE carrid = @( to_upper( carrid ) ) GROUP BY carrid, connid ) SELECT ' ' AS total, carrid, connid, fldate, seatsocc FROM sflight WHERE carrid = @( to_upper( carrid ) ) UNION SELECT 'X' AS total, carrid, connid, fldate, seatsocc FROM +total ORDER BY carrid, connid, total, fldate, seatsocc INTO TABLE @DATA(result). cl_demo_output=>display( result ). |
Динамическое использование
Как и при использовании оператора SELECT, допускается использование динамических конструкций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
DATA carrid TYPE spfli-carrid VALUE 'LH'. cl_demo_input=>request( CHANGING field = carrid ). DATA: sel_sub1 TYPE string VALUE `cityfrom AS city`, sel_sub2 TYPE string VALUE `cityto AS city`, frm_sub TYPE string VALUE `spfli`, whr_sub TYPE string VALUE `carrid = @carrid`, sel_main TYPE string VALUE `*`, frm_main TYPE string VALUE `sgeocity`, whr_main TYPE string VALUE `city IN ( SELECT city FROM +cities )`. WITH +cities AS ( SELECT (sel_sub1) FROM (frm_sub) WHERE (whr_sub) UNION DISTINCT SELECT (sel_sub1) FROM (frm_sub) WHERE (whr_sub) ) SELECT (sel_main) FROM (frm_main) WHERE (whr_main) INTO TABLE NEW @DATA(result). ASSIGN result->* TO FIELD-SYMBOL(<fs>). cl_demo_output=>display( <fs> ). |
Ограничения СУБД
При использовании конструкции WITH следует учитывать тот факт, что не все СУБД поддерживают полный перечень её возможностей. Так в подзапросах ограничение число записей и сортировка может быть недоступна на вашей СУБД, о чём вас предупредит расширенная проверка кода.
Производительность
Учитывая что выражение WITH целиком выполняется на уровне СУБД, скорость его работы будет выше нежели последовательное исполнение нескольких запросов или использование FOR ALL ENTRIES (даже на HANA с FDA), убедиться в этом можете запустив следующий пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
TABLES: spfli. SELECT-OPTIONS s_carrid FOR spfli-carrid. DATA: lv_start_time TYPE timestampl, lv_end_time TYPE timestampl, lv_diff TYPE timestampl. GET TIME STAMP FIELD lv_start_time. WITH +connections AS ( SELECT spfli~carrid, carrname, connid, cityfrom, cityto FROM spfli INNER JOIN scarr ON scarr~carrid = spfli~carrid WHERE spfli~carrid IN @s_carrid ) SELECT * FROM sflight INNER JOIN +connections AS c ON c~carrid = sflight~carrid AND c~connid = sflight~connid INTO TABLE @DATA(lt_result). GET TIME STAMP FIELD lv_end_time. lv_diff = lv_end_time - lv_start_time. WRITE: /(50) 'WITH Speed: ', lv_diff. GET TIME STAMP FIELD lv_start_time. SELECT spfli~carrid, carrname, connid, cityfrom, cityto FROM spfli INNER JOIN scarr ON scarr~carrid = spfli~carrid INTO TABLE @DATA(lt_connections) WHERE spfli~carrid IN @s_carrid. SELECT * FROM sflight FOR ALL ENTRIES IN @lt_connections WHERE sflight~carrid = @lt_connections-carrid AND sflight~connid = @lt_connections-connid INTO TABLE @DATA(lt_result_2). GET TIME STAMP FIELD lv_end_time. lv_diff = lv_end_time - lv_start_time. WRITE: /(50) 'FAE Speed: ', lv_diff. |
Результат: