Две темы, которым посвящается эта лекция, касаются сравнительно новых возможностей оператора SELECT языка SQL, впервые появившихся в
В аналитических приложениях обычно требуются не детальные данные, непосредственно хранящиеся в базе данных, а некоторые их обобщения, агрегаты. Например, аналитика интересует не GROUP BY и вызовы GROUP BY в общем случае являются "трудными" для СУБД, поскольку для группирования таблицы, вообще говоря, требуется
В системах баз данных, специально спроектированных в расчете на аналитические приложения, проблему обычно решают за счет явного избыточного хранения
Однако далеко не каждое предприятие может позволить себе одновременно поддерживать оперативную базу данных для работы обычных приложений оперативной обработки транзакций (GROUP BY и вызовами
Разработчики GROUP BY, обеспечивающих требуемые суммарные данные. В этой лекции мы обсудим наиболее важные, с нашей точки зрения, конструкции языка SQL, облегчающие формулировку, выполнение и использование результатов и и новую , позволяющую правильно трактовать результаты
Традиционно язык SQL никогда не обладал возможностью формулировки WHERE результат раздела FROM должен быть полностью вычислен.
Однако разработчикам приложений часто приходится решать задачи, для которых недостаточно традиционных средств формулировки запросов языка SQL: например, нахождение маршрута движения между двумя заданными географическими точками, определения общего набора комплектующих для сбора некоторого агрегата и т.д. Компании-производители SQL-ориентированных СУБД пытались удовлетворять такие потребности за счет
Следует отметить и некоторое давление на SQL-сообщество со стороны сообщества логических систем баз данных. На основе языка
Компромиссное (не слишком красивое) решение для введения рекурсии в SQL было найдено на основе введения в выражение запроса. Только в этом разделе допускается как
В этом курсе мы не будем подробно обсуждать возможности языка SQL, предназначенные для поддержки содержит следующий набор строк (покажем содержимое только тех столбцов, которые потребуются в примерах, причем для простоты будем считать, что в столбце EMP_DATE содержится не полная дата, а только год рождения служащего):
EMP_NO |
DEPT_NO |
EMP_BDATE |
EMP_SAL |
|---|---|---|---|
2440 |
1 |
1950 |
15000.00 |
2441 |
1 |
1950 |
16000.00 |
2442 |
1 |
1960 |
14000.00 |
2443 |
1 |
1960 |
19000.00 |
2444 |
2 |
1950 |
17000.00 |
2445 |
2 |
1950 |
16000.00 |
2446 |
2 |
1960 |
14000.00 |
2447 |
2 |
1960 |
20000.00 |
2448 |
3 |
1950 |
18000.00 |
2449 |
3 |
1950 |
13000.00 |
2450 |
3 |
1960 |
21000.00 |
2451 |
3 |
1960 |
22000.00 |
Представим себе, что для проведения анализа требуется узнать максимальный размер зарплаты на всем предприятии, максимальный размер зарплаты в каждом отделе и максимальный размер зарплаты служащих каждой возрастной категории каждого отдела. Если пользоваться стандартными средствами языка SQL, обсуждавшимися ранее в предложенном курсе, то для получения этих данных потребуется три запроса:
SELECT MAX (EMP_SAL) AS MAX_ENT_SAL
FROM EMP;
SELECT DEPT_NO, MAX (EMP_SAL) AS MAX_DEP_SAL
FROM EMP
GROUP BY DEPT_NO;
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL)
AS MAX_DEP_BDATE_SAL
FROM EMP
GROUP BY DEPT_NO, EMP_BDATE;
При выполнении запросов будут получены следующие результирующие таблицы:
MAX_ENT_SAL |
|---|
22000.00 |
DEPT_NO |
MAX_DEP_SAL |
|---|---|
1 |
19000.00 |
2 |
20000.00 |
3 |
22000.00 |
DEPT_NO |
EMP_BDATE |
MAX_DEP_BDATE_SAL |
|---|---|---|
1 |
1950 |
16000.00 |
1 |
1960 |
19000.00 |
2 |
1950 |
17000.00 |
2 |
1960 |
20000.00 |
3 |
1950 |
18000.00 |
3 |
1960 |
22000.00 |
Эти же результаты можно получить при выполнении единственного запроса, если в его формулировке использовать специальный вид группировки (пример 16.1):
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL) AS MAX_SAL
FROM EMP
GROUP BY ROLLUP (DEPT_NO, EMP_BDATE);
Сначала покажем, как будет выглядеть результирующая таблица этого запроса, а потом приведем развернутое пояснение действия новой конструкции. В результате выполнения запроса будет получена таблица, показанная на рис.16.1.
Как видно, в столбце MAX_SAL первой , EMP_BDATE .
DEPT_NO и EMP_BDATE в этой строке содержат MAX_SAL не привязано к каким-либо отделу и возрастной категории. В столбце MAX_SAL следующих трех строк находятся максимальные значения зарплаты служащих отделов с номерами 1, 2 и 3 соответственно, что показывают значения столбца DEPT_NO. Столбец EMP_BDATE в этих строках содержит MAX_SAL не привязано к какой-либо возрастной категории. Наконец, в столбце MAX_SAL в последних шести строках содержатся максимальные значения зарплаты служащих каждой возрастной категории каждого отдела, что показывают значения столбцов DEPT_NO и EMP_BDATE, которые теперь содержат соответствующий номер отдела и год рождения служащих.
(рис 16.1) Результат запроса с разделом GROUP BY ROLLUPВ общем случае пусть раздел группировки запроса имеет вид ( cname1, cname2, ... , cnamen ), где cnamei ( i = 1, 2, ... , n ) - имя столбца таблицы-результата раздела FROM запроса. Пусть в списке выборки используются вызовы AGG1, AGG2, ... , AGGm над значениями столбцов, не входящих в список группировки, а также имена столбцов cname1, cname2, ... , cnamen. Тогда запрос выполняется следующим образом. Первая строка результата (первый набор строк результирующей таблицы) производится таким образом, как если бы в запросе вообще отсутствовал раздел GROUP BY, т.е. агрегатные функции AGG1, AGG2, ... , AGGm вычисляются над значениями всех строк таблицы. Значением столбцов cname1, cname2, ... , cnamen в этой строке является NULL. ( i+1 )-й набор строк результата формируется так, как если бы раздел группировки запроса имел вид GROUP BY ( cname1, cname2, ... , cnamei ) ( 1<=i<n ). Во всех этих строках значением столбцов cname(i+1), ... , cnamen является NULL. Наконец, ( n+1 )-й набор строк результата формируется так, как если бы раздел группировки запроса имел вид GROUP BY ( cname1, cname2, ... , cnamen ).
Может показаться, что запросы, содержащие , настолько сложны, что их выполнение будет занимать чрезмерно большое время. Это ощущение является ложным. В действительности, при выполнении запросов с обычной группировкой вида GROUP BY cname1, cname2, ... , cnamen, как правило, последовательно выполняется сортировка строк таблицы-результата раздела FROM в соответствии со значениями столбца cname1, затем - в соответствии со значениями столбца cname2 и т. д., и в заключение - сортировка в соответствии со значениями столбца cnamen. Во время выполнения каждой сортировки можно заодно вычислять значения , лишь незначительно отличается от стоимости выполнения запроса с обычной группировкой.
Обсудим теперь один более тонкий вопрос. Как говорилось в лекции 12, DEPT_NO и EMP_BDATE таблицы допускает появление в этих столбцах могло бы иметь, например, следующий вид:
EMP_NO |
DEPT_NO |
EMP_BDATE |
EMP_SAL |
|---|---|---|---|
2440 |
1 |
1950 |
15000.00 |
2441 |
1 |
1950 |
16000.00 |
2442 |
1 |
1960 |
14000.00 |
2443 |
1 |
1960 |
19000.00 |
2452 |
1 |
NULL |
15000.00 |
2453 |
1 |
NULL |
17000.00 |
2444 |
2 |
1950 |
17000.00 |
2445 |
2 |
1950 |
16000.00 |
2446 |
2 |
1960 |
14000.00 |
2447 |
2 |
1960 |
20000.00 |
2448 |
3 |
1950 |
18000.00 |
2449 |
3 |
1950 |
13000.00 |
2450 |
3 |
1960 |
21000.00 |
2451 |
3 |
1960 |
22000.00 |
2454 |
NULL |
1950 |
13000.00 |
2455 |
NULL |
1950 |
14000.00 |
2456 |
NULL |
NULL |
19000.00 |
Тогда результат запроса из примера 16.1 имел бы следующий
(рис 16.2) Результат запроса с разделом GROUP BY ROLLUP к таблице с неопределенными значениями столбцов группировкиОчевидно, что, просматривая строки таблицы, показанной на рис.16.2, невозможно установить, в какой из первых трех строк DEPT_NO и EMP_BDATE означает то, что эта строка является сводной для всего предприятия, а не то, что она является сводной для всех служащих с неизвестными номером отдела и годом рождения или просто для всех служащих с неизвестным номером отдела. Аналогичным образом невозможно понять, какая строка в следующей далее паре строк является сводной для всех служащих отдела номер 1, а не для всех служащих отдела номер 1 с неизвестным годом рождения.
Для того чтобы всегда можно было разобраться в результатах запросов, включающих , в язык SQL была введена специальная . , и принимает целое значение 1 в тех строках результирующей таблицы, в которых соответствующий столбец имеет значение NULL по той причине, что строка является сводной для более обобщенной группы. В противном случае принимает значение 0
Уточним формулировку запроса из примера 16.1 (пример 16.1a):
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL) AS MAX_SAL,
GROUPING (DEPT_NO) AS GDN, GROUPING (EMP_BDATE)
AS GEB
FROM EMP
GROUP BY ROLLUP (DEPT_NO, EMP_BDATE);
Результирующая таблица для этого запроса будет иметь следующий вид:
(рис 16.3) Результат запроса с разделом GROUP BY ROLLUP и вызовами агрегатной функции GROUPING к таблице с неопределенными значениями столбцов группировкиАнализируя значения столбцов GDN и GEB в строках таблицы, показанной на рис.16.4, можно убедиться, что значение столбца MAX_SAL в первой строке является максимальным значением зарплаты всех служащих предприятия, во второй строке - максимальным значением зарплаты служащих с неизвестными номером отдела и годом рождения, а в третьей строке - максимальным значением зарплаты всех служащих с неизвестным номером отдела. В следующих трех строках значения столбца MAX_SAL являются максимальными значениями зарплаты служащих с неизвестным годом рождения из отделов с номерами 1, 2 и 3 соответственно. Как видно, значения столбцов GDN и GEB являются своего рода индикаторами, указывающими на природу основных значений строки.
Наконец, заметим, что, в отличие от запросов с традиционной группировкой, результат запроса, содержащего , зависит от порядка столбцов в списке группировки. При выполнении запроса происходит движение по этому списку слева направо с повышением уровня детальности результирующих данных. Существует еще одна разновидность запроса с группировкой, основанная на использовании .
Пусть раздел группировки запроса имеет вид ( cname1, cname2, ... , cnamen ), где cnamei ( i = 1, 2, ... , n ) - имя столбца таблицы-результата раздела FROM запроса. Обозначим через SGBC множество {cname1, cname2, ... , cnamen}. Пусть Si является произвольным подмножеством SGBC, т.е. Si представляет собой пустое множество или имеет вид {cnamei1, cnamei2, ... , cnameim}, где m<=n, и каждое имя столбца cnameij совпадает с одним и только одним именем столбца из списка столбцов . Очевидно, что у множества SGBC существует 2n подмножеств различных вида Si. Тогда по определению результат этого запроса совпадает с объединением результатов 2n запросов с теми же разделами SELECT, FROM и WHERE, что и у запроса с , и с разделом группировки вида GROUP BY Si, причем во всех строках результата частичного запроса значением любого столбца cnamej такого, что cnamej ∈ SGBC и cnamej ∉ Si, является NULL. Запрос с разделом группировки вида GROUP BY S, где S - пустое множество, трактуется как запрос без раздела GROUP BY. Вот пример запроса, содержащего .
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL)AS MAX_SAL, GROUPING (DEPT_NO) AS GDN, GROUPING (EMP_BDATE) AS GEB FROM EMP GROUP BY CUBE (DEPT_NO, EMP_BDATE);
Результирующая таблица для этого запроса будет иметь следующий вид:
(рис 16.4) Результат запроса с разделом GROUP BY CUBE и вызовами агрегатной функции GROUPING к таблице с неопределенными значениями столбцов группировкиКак видно, результат запроса из примера 16.2 совсем немного отличается от результата запроса из примера 16.1a. Добавились две последние строки, показывающие максимальные значения зарплаты всех служащих предприятия, родившихся в 1950-м и 1960-м гг. соответственно.
Наш пример может навести на мысль, что и в общем случае запросы, содержащие , не слишком отличаются от запросов с , и выполнение этих запросов тоже не слишком различается. Однако это совсем не так. Запрос, содержащий , действительно вырождается в объединение результатов 2n запросов с обычным разделом GROUP BY. Соответственно, сложность выполнения такого запроса несравненно выше сложности выполнения похожего запроса с . В нашем примере все получилось так просто только по той причине, что в запросе имеются всего два столбца группировки.
Начнем этот раздел с нескольких определений, касающихся понятий, которые связаны с рекурсией. Эти понятия имеют общий характер, но в приведенных ниже определениях и комментариях к ним (там, где это уместно) подчеркивается контекст SQL.
(рис 16.5) Пример дереваПри Корень-Потомок1-Потомок2-Потомок3-П1.1-П1.2-П1.3-П2.1-П2.3-П3.1-П3.2-П3.3.
Корень-Потомок1-П1.1-П1.2-П1.3-Потомок2-П2.1-П2.2-П2.3-Потомок3-П3.1-П3.2-П3.3.
(рис 16.6) Пример графа с циклом
(рис 16.7) Графовый пример непрямой рекурсииFROM и/или в
Монотонность. {1, 2, ... , n, ...} является монотонной. В SQL:1999 свойство монотонности поддерживается в том смысле, что число строк результата
A и B связаны отношением A прямо или косвенно вызывает B, и B прямо или косвенно вызывает A A вызывает элемент B через элемент C, а элемент B вызывает элемент A через элемент D ).

(рис 16.9) Графовый пример нелинейной рекурсии(рис 16.8) Графовый пример взаимной рекурсииEXCEPT и , спецификация , условие NOT EXISTS и т.д. В стандарте SQL не запрещается использование
В предыдущих лекциях мы уже говорили о разновидности спецификации ссылки на таблицу с использованием раздела . Однако мы умышленно отложили обсуждение рекурсивных возможностей. Полный синтаксис раздела выглядит следующим образом:
with_clause ::= WITH [ RECURSIVE ]
with_element_comma_list
with_element ::= query_name [ (column_name_list) ]
AS ( query_expression ) [ search_or_cycle_clause ]
search_or_cycle_clause ::= search_clause
| cycle_clause
| search_clause cycle_clause
search_clause ::= SEARCH recursive_search_order SET
sequence_column_name
recursive_search_order ::= DEPTH FIRST BY
order_item_commalist
| BREADTH FIRST BY order_item_commalist
cycle_clause ::= CYCLE cycle_column_name_comma_list
SET cycle_mark_column_name TO value_expression
DEFAULT value_expression
USING path_column_name
Для иллюстрации возможностей и пояснения смысла конструкций и воспользуемся классическим примером "разборки деталей" (в данном случае мы будем разбирать автомобиль). Предположим, что данные о конструктивных элементах автомобиля хранятся в таблице , определенной следующим образом:
CREATE TABLE CAR (CONTAINING_PART VARCHAR (10),
CONTAINED_PART VARCHAR (10),
NUMBER_OF_PARTS INTEGER,
PART_COST DECIMAL (6,2));
У автомобиля имеется один конструктивный элемент верхнего уровня - полностью собранный автомобиль. Этот элемент не является составной частью какого-либо другого элемента, и для его строки значением столбца CONTAINING_PART является текстовая строка длины 0. В любой другой строке таблицы , соответствующей некоторому неатомарному конструктивному элементу e, столбец CONTAINING_PART содержит идентификационный номер элемента e1, в который входит элемент e, столбец NUMBER_OF_PARTS - число экземпляров элемента e, входящих в e1, а столбец CONTAINED_PART - идентификационный номер самого элемента e. В любой строке таблицы , соответствующей некоторому CONTAINED_PART является строка длины 0, а в столбце PART_COST сохраняется цена атомарного конструктивного элемента (для неатомарных элементов значение этого столбца равно нулю).
Предположим, что нам требуется разобрать автомобиль, начиная с элемента самого верхнего уровня, и для каждого конструктивного элемента получить его номер, общее число используемых экземпляров этого элемента, а также, если элемент является атомарным, общую стоимость используемых экземпляров. Вот возможная формулировка запроса (пример 16.3):
WITH RECURSIVE PARTS (PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINED_PART, 1, 0.00 (a)
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINED_PART, CAR.NUMBER_OF_PARTS,
CAR.NUMBER_OF_PARTS * CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
SELECT PART_NUMBER, SUM(NUMBER_OF PARTS),
SUM(COST) (b)
FROM PARTS
GROUP BY PART_NUMBER;
Этот запрос будет выполняться следующим образом. При вычислении раздела FROM основного запроса ( b ) начнется выполнение a ), определенного в разделе . На первом шаге рекурсии будет выполнена часть данного выражения, предшествующая операции и образующая PARTS, в котором, в нашем случае, появится единственная строка, соответствующая автомобилю целиком. На следующем шаге к таблице PARTS будут добавлены строки, соответствующие конструктивным элементам второго уровня (для автомобиля это, по-видимому, двигатель, колеса, , в результирующей таблице могут появляться строки-дубликаты. Наличие строки-дубликата вида <part_no, number, cost> означает, что элемент с номером part_no входит в одном и том же числе экземпляров в несколько конструктивных элементов более высокого уровня.
В приведенном выше примере не определялся порядок, в котором строки добавляются к . При указании требования с требованием обхода иерархии элементов автомобиля
WITH RECURSIVE PARTS (ASSEMBLY, PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINING_PART, CONTAINED_PART, 1, 0.00
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINING_PART, CAR.CONTAINED_PART,
CAR.NUMBER_OF_PARTS, CAR.NUMBER_OF_PARTS *
CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
SEARCH BREADTH FIRST
BY CONTAINING_PART, CONTAINED_PART
SET ORDER_COLUMN
SELECT PART_NUMBER, NUMBER_OF PARTS, COST
FROM PARTS
ORDER BY ORDER_COLUMN;
В списке столбцов сортировки раздела должны указываться имена столбцов WITH. Поскольку в данном случае мы хотим, чтобы в результате сначала появлялись все конструктивные элементы одного уровня ( CONTAINING_PART ), а затем все их подэлементы ( CONTAINED_PART ), в список выборки PARTS добавлен столбец CONTAINING_PART, который не используется нигде, кроме раздела . В разделе SET к результирующей таблице ORDER_COLUMN. Название соответствует природе столбца, потому что при выполнении с указанием столбца, определенного в разделе SET.
Наконец, обсудим, для чего нужен раздел . Дело в том, что иногда сами данные, хранимые в таблицах базы данных, могут иметь циклическую природу. Представим себе, например, компанию, в которой существует совет директоров, являющийся высшим органом управления компанией. Обычным случаем является тот, когда по крайней мере один из членов совета директоров является простым служащим этой же компании (например, он может входить в совет директоров как представитель профсоюза). Назовем данного члена совета директоров EMP_DIR. Как член совета директоров, EMP_DIR "управляет" деятельностью президента компании. С другой стороны, как служащий компании, EMP_DIR находится в прямом или косвенном подчинении у президента компании. Такое положение может привести к зацикливанию выполнения обеспечивает некоторую возможность распознавать подобные ситуации. Если у пользователя имеется полная уверенность в отсутствии циклов в данных, к которым адресуется не требуется.
Подход к распознаванию зацикленных запросов, принятый в SQL, состоит в том, что распознаются данные, которые уже участвовали ранее в формировании результата при добавлении к результату строк, удовлетворяющих условию запроса, такие строки помечаются указанным значением, которое означает, что эти строки уже вошли в результат. При попытке добавления к результату каждой новой строки проверяется, не находится ли она уже в результате, т. е. не помечена ли она этим указанным в разделе значением. Если это действительно так, то считается, что имеет место цикл, и дальнейшее выполнение
Обсудим все это более формально. Для удобства воспроизведем еще раз синтаксис раздела .
cycle_clause ::= CYCLE cycle_column_name_comma_list
SET cycle_mark_column_name TO value_expression_1
DEFAULT value_expression_2
USING path_column_name
В списке cycle_column_name_comma_list указываются имена одного или нескольких столбцов, которые используются для идентификации новых строк результата на основе строк, уже входящих в результат. Например, в примерах 16.3 и 16.4 столбец CONTAINED_PART связывает конструктивный элемент автомобиля с входящими в его состав подэлементами (через значения их столбцов CONTAINING_PART ). Раздел SET приводит к образованию нового столбца результирующей таблицы. Для строк, которые попадают в результат первый раз, в столбец cycle_mark_column_name заносится value_expression_2. В повторно заносимых строках значение столбца - value_expression_1. Типом данных этого столбца является value_expression_1 и value_expression_2 разумно использовать константы ' 0 ' и ' 1 ' или ' Y ' и ' N '.
Раздел USING приводит к образованию еще одного дополнительного столбца результата с именем path_column_name. ARRAY, причем кардинальность этого типа предполагается достаточно большой, чтобы сохранить информацию обо всех строках, попавших в результат. Элементы массива имеют "строчный тип" ( row type ), содержащий столько столбцов, сколько их указано в списке раздела . Каждый элемент массива соответствует строке результата, и в его столбцах содержится копия значений соответствующих столбцов этой строки. Вот пример запроса, содержащего раздел (пример 16.5):
WITH RECURSIVE PARTS (PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINED_PART, 1, 0.00
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINED_PART, CAR.NUMBER_OF_PARTS,
CAR.NUMBER_OF_PARTS * CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
CYRCLE CONTAINED_PART
SET CYCLEMARK TO 'Y' DEFAULT 'N'
USING CYRCLEPATH
SELECT PART_NUMBER, SUM(NUMBER_OF PARTS), SUM(COST)
FROM PARTS
ORDER BY PART_NUMBER;
Имена столбцов CYCLEMARK и CYRCLEPATH выбраны произвольным образом - требуется только, чтобы имена этих столбцов отличались от имен столбцов CYRCLEPATH. При попытке добавления к результату новой строки на основе текущего содержимого столбца CYRCLEPATH проверяется, не содержится ли она уже в результате. Если не содержится, то данные об этой строке добавляются к столбцу CYRCLEPATH (к массиву добавляется новый элемент), в столбец CYCLEMARK этой строки заносится значение ' N ', и строка добавляется к результату. Иначе в столбец CYCLEMARK соответствующей строки результата заносится значение ' Y ', означающее, что от этой строки начинается цикл.
CREATE RECURSIVE VIEW table_name [ column_name_comma_list ] AS query_expression
Хотя для того, чтобы ); наличие избыточного ключевого в определении WITH CHECK OPTION не может присутствовать в определении и WITH CHECK OPTION ).
В заключение этого раздела могу сказать, что лично мне механизм рекурсии, предлагаемый в стандарте SQL, представляется громоздким и ограниченным. Кроме того, насколько мне известно, компании, поставляющие SQL-ориентированные СУБД, не спешат внедрять в свои продукты средства рекурсии в соответствии со стандартом SQL:1999 (или, по крайней мере, не слишком их афишируют).
Если вернуться к синтаксическим определениям разд. "Общие синтакические правила построения скалярных выражений" лекции 13, то можно убедиться, что в последних четырех лекциях мы рассмотрели все варианты организации оператора SELECT языка SQL (за исключением конструкций collection_derived_table и ONLY ( table_or_query_name ), относящихся к объектным расширениям языка SQL).
Для общего понимания языка на модельном уровне более важными являются предыдущие три лекции. Данная лекция включена в курс, скорее, с целью общего ознакомления читателей с новыми возможностями оператора выборки, чем с целью их подробного описания. С большой вероятностью средства формулировки аналитических и
Две темы, которым посвящается эта лекция, касаются сравнительно новых возможностей оператора SELECT языка SQL, впервые появившихся в
В аналитических приложениях обычно требуются не детальные данные, непосредственно хранящиеся в базе данных, а некоторые их обобщения, агрегаты. Например, аналитика интересует не GROUP BY и вызовы GROUP BY в общем случае являются "трудными" для СУБД, поскольку для группирования таблицы, вообще говоря, требуется
В системах баз данных, специально спроектированных в расчете на аналитические приложения, проблему обычно решают за счет явного избыточного хранения
Однако далеко не каждое предприятие может позволить себе одновременно поддерживать оперативную базу данных для работы обычных приложений оперативной обработки транзакций (GROUP BY и вызовами
Разработчики GROUP BY, обеспечивающих требуемые суммарные данные. В этой лекции мы обсудим наиболее важные, с нашей точки зрения, конструкции языка SQL, облегчающие формулировку, выполнение и использование результатов и и новую , позволяющую правильно трактовать результаты
Традиционно язык SQL никогда не обладал возможностью формулировки WHERE результат раздела FROM должен быть полностью вычислен.
Однако разработчикам приложений часто приходится решать задачи, для которых недостаточно традиционных средств формулировки запросов языка SQL: например, нахождение маршрута движения между двумя заданными географическими точками, определения общего набора комплектующих для сбора некоторого агрегата и т.д. Компании-производители SQL-ориентированных СУБД пытались удовлетворять такие потребности за счет
Следует отметить и некоторое давление на SQL-сообщество со стороны сообщества логических систем баз данных. На основе языка
Компромиссное (не слишком красивое) решение для введения рекурсии в SQL было найдено на основе введения в выражение запроса. Только в этом разделе допускается как
В этом курсе мы не будем подробно обсуждать возможности языка SQL, предназначенные для поддержки содержит следующий набор строк (покажем содержимое только тех столбцов, которые потребуются в примерах, причем для простоты будем считать, что в столбце EMP_DATE содержится не полная дата, а только год рождения служащего):
EMP_NO |
DEPT_NO |
EMP_BDATE |
EMP_SAL |
|---|---|---|---|
2440 |
1 |
1950 |
15000.00 |
2441 |
1 |
1950 |
16000.00 |
2442 |
1 |
1960 |
14000.00 |
2443 |
1 |
1960 |
19000.00 |
2444 |
2 |
1950 |
17000.00 |
2445 |
2 |
1950 |
16000.00 |
2446 |
2 |
1960 |
14000.00 |
2447 |
2 |
1960 |
20000.00 |
2448 |
3 |
1950 |
18000.00 |
2449 |
3 |
1950 |
13000.00 |
2450 |
3 |
1960 |
21000.00 |
2451 |
3 |
1960 |
22000.00 |
Представим себе, что для проведения анализа требуется узнать максимальный размер зарплаты на всем предприятии, максимальный размер зарплаты в каждом отделе и максимальный размер зарплаты служащих каждой возрастной категории каждого отдела. Если пользоваться стандартными средствами языка SQL, обсуждавшимися ранее в предложенном курсе, то для получения этих данных потребуется три запроса:
SELECT MAX (EMP_SAL) AS MAX_ENT_SAL
FROM EMP;
SELECT DEPT_NO, MAX (EMP_SAL) AS MAX_DEP_SAL
FROM EMP
GROUP BY DEPT_NO;
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL)
AS MAX_DEP_BDATE_SAL
FROM EMP
GROUP BY DEPT_NO, EMP_BDATE;
При выполнении запросов будут получены следующие результирующие таблицы:
MAX_ENT_SAL |
|---|
22000.00 |
DEPT_NO |
MAX_DEP_SAL |
|---|---|
1 |
19000.00 |
2 |
20000.00 |
3 |
22000.00 |
DEPT_NO |
EMP_BDATE |
MAX_DEP_BDATE_SAL |
|---|---|---|
1 |
1950 |
16000.00 |
1 |
1960 |
19000.00 |
2 |
1950 |
17000.00 |
2 |
1960 |
20000.00 |
3 |
1950 |
18000.00 |
3 |
1960 |
22000.00 |
Эти же результаты можно получить при выполнении единственного запроса, если в его формулировке использовать специальный вид группировки (пример 16.1):
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL) AS MAX_SAL
FROM EMP
GROUP BY ROLLUP (DEPT_NO, EMP_BDATE);
Сначала покажем, как будет выглядеть результирующая таблица этого запроса, а потом приведем развернутое пояснение действия новой конструкции. В результате выполнения запроса будет получена таблица, показанная на рис.16.1.
Как видно, в столбце MAX_SAL первой , EMP_BDATE .
DEPT_NO и EMP_BDATE в этой строке содержат MAX_SAL не привязано к каким-либо отделу и возрастной категории. В столбце MAX_SAL следующих трех строк находятся максимальные значения зарплаты служащих отделов с номерами 1, 2 и 3 соответственно, что показывают значения столбца DEPT_NO. Столбец EMP_BDATE в этих строках содержит MAX_SAL не привязано к какой-либо возрастной категории. Наконец, в столбце MAX_SAL в последних шести строках содержатся максимальные значения зарплаты служащих каждой возрастной категории каждого отдела, что показывают значения столбцов DEPT_NO и EMP_BDATE, которые теперь содержат соответствующий номер отдела и год рождения служащих.
(рис 16.1) Результат запроса с разделом GROUP BY ROLLUPВ общем случае пусть раздел группировки запроса имеет вид ( cname1, cname2, ... , cnamen ), где cnamei ( i = 1, 2, ... , n ) - имя столбца таблицы-результата раздела FROM запроса. Пусть в списке выборки используются вызовы AGG1, AGG2, ... , AGGm над значениями столбцов, не входящих в список группировки, а также имена столбцов cname1, cname2, ... , cnamen. Тогда запрос выполняется следующим образом. Первая строка результата (первый набор строк результирующей таблицы) производится таким образом, как если бы в запросе вообще отсутствовал раздел GROUP BY, т.е. агрегатные функции AGG1, AGG2, ... , AGGm вычисляются над значениями всех строк таблицы. Значением столбцов cname1, cname2, ... , cnamen в этой строке является NULL. ( i+1 )-й набор строк результата формируется так, как если бы раздел группировки запроса имел вид GROUP BY ( cname1, cname2, ... , cnamei ) ( 1<=i<n ). Во всех этих строках значением столбцов cname(i+1), ... , cnamen является NULL. Наконец, ( n+1 )-й набор строк результата формируется так, как если бы раздел группировки запроса имел вид GROUP BY ( cname1, cname2, ... , cnamen ).
Может показаться, что запросы, содержащие , настолько сложны, что их выполнение будет занимать чрезмерно большое время. Это ощущение является ложным. В действительности, при выполнении запросов с обычной группировкой вида GROUP BY cname1, cname2, ... , cnamen, как правило, последовательно выполняется сортировка строк таблицы-результата раздела FROM в соответствии со значениями столбца cname1, затем - в соответствии со значениями столбца cname2 и т. д., и в заключение - сортировка в соответствии со значениями столбца cnamen. Во время выполнения каждой сортировки можно заодно вычислять значения , лишь незначительно отличается от стоимости выполнения запроса с обычной группировкой.
Обсудим теперь один более тонкий вопрос. Как говорилось в лекции 12, DEPT_NO и EMP_BDATE таблицы допускает появление в этих столбцах могло бы иметь, например, следующий вид:
EMP_NO |
DEPT_NO |
EMP_BDATE |
EMP_SAL |
|---|---|---|---|
2440 |
1 |
1950 |
15000.00 |
2441 |
1 |
1950 |
16000.00 |
2442 |
1 |
1960 |
14000.00 |
2443 |
1 |
1960 |
19000.00 |
2452 |
1 |
NULL |
15000.00 |
2453 |
1 |
NULL |
17000.00 |
2444 |
2 |
1950 |
17000.00 |
2445 |
2 |
1950 |
16000.00 |
2446 |
2 |
1960 |
14000.00 |
2447 |
2 |
1960 |
20000.00 |
2448 |
3 |
1950 |
18000.00 |
2449 |
3 |
1950 |
13000.00 |
2450 |
3 |
1960 |
21000.00 |
2451 |
3 |
1960 |
22000.00 |
2454 |
NULL |
1950 |
13000.00 |
2455 |
NULL |
1950 |
14000.00 |
2456 |
NULL |
NULL |
19000.00 |
Тогда результат запроса из примера 16.1 имел бы следующий
(рис 16.2) Результат запроса с разделом GROUP BY ROLLUP к таблице с неопределенными значениями столбцов группировкиОчевидно, что, просматривая строки таблицы, показанной на рис.16.2, невозможно установить, в какой из первых трех строк DEPT_NO и EMP_BDATE означает то, что эта строка является сводной для всего предприятия, а не то, что она является сводной для всех служащих с неизвестными номером отдела и годом рождения или просто для всех служащих с неизвестным номером отдела. Аналогичным образом невозможно понять, какая строка в следующей далее паре строк является сводной для всех служащих отдела номер 1, а не для всех служащих отдела номер 1 с неизвестным годом рождения.
Для того чтобы всегда можно было разобраться в результатах запросов, включающих , в язык SQL была введена специальная . , и принимает целое значение 1 в тех строках результирующей таблицы, в которых соответствующий столбец имеет значение NULL по той причине, что строка является сводной для более обобщенной группы. В противном случае принимает значение 0
Уточним формулировку запроса из примера 16.1 (пример 16.1a):
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL) AS MAX_SAL,
GROUPING (DEPT_NO) AS GDN, GROUPING (EMP_BDATE)
AS GEB
FROM EMP
GROUP BY ROLLUP (DEPT_NO, EMP_BDATE);
Результирующая таблица для этого запроса будет иметь следующий вид:
(рис 16.3) Результат запроса с разделом GROUP BY ROLLUP и вызовами агрегатной функции GROUPING к таблице с неопределенными значениями столбцов группировкиАнализируя значения столбцов GDN и GEB в строках таблицы, показанной на рис.16.4, можно убедиться, что значение столбца MAX_SAL в первой строке является максимальным значением зарплаты всех служащих предприятия, во второй строке - максимальным значением зарплаты служащих с неизвестными номером отдела и годом рождения, а в третьей строке - максимальным значением зарплаты всех служащих с неизвестным номером отдела. В следующих трех строках значения столбца MAX_SAL являются максимальными значениями зарплаты служащих с неизвестным годом рождения из отделов с номерами 1, 2 и 3 соответственно. Как видно, значения столбцов GDN и GEB являются своего рода индикаторами, указывающими на природу основных значений строки.
Наконец, заметим, что, в отличие от запросов с традиционной группировкой, результат запроса, содержащего , зависит от порядка столбцов в списке группировки. При выполнении запроса происходит движение по этому списку слева направо с повышением уровня детальности результирующих данных. Существует еще одна разновидность запроса с группировкой, основанная на использовании .
Пусть раздел группировки запроса имеет вид ( cname1, cname2, ... , cnamen ), где cnamei ( i = 1, 2, ... , n ) - имя столбца таблицы-результата раздела FROM запроса. Обозначим через SGBC множество {cname1, cname2, ... , cnamen}. Пусть Si является произвольным подмножеством SGBC, т.е. Si представляет собой пустое множество или имеет вид {cnamei1, cnamei2, ... , cnameim}, где m<=n, и каждое имя столбца cnameij совпадает с одним и только одним именем столбца из списка столбцов . Очевидно, что у множества SGBC существует 2n подмножеств различных вида Si. Тогда по определению результат этого запроса совпадает с объединением результатов 2n запросов с теми же разделами SELECT, FROM и WHERE, что и у запроса с , и с разделом группировки вида GROUP BY Si, причем во всех строках результата частичного запроса значением любого столбца cnamej такого, что cnamej ∈ SGBC и cnamej ∉ Si, является NULL. Запрос с разделом группировки вида GROUP BY S, где S - пустое множество, трактуется как запрос без раздела GROUP BY. Вот пример запроса, содержащего .
SELECT DEPT_NO, EMP_BDATE, MAX (EMP_SAL)AS MAX_SAL, GROUPING (DEPT_NO) AS GDN, GROUPING (EMP_BDATE) AS GEB FROM EMP GROUP BY CUBE (DEPT_NO, EMP_BDATE);
Результирующая таблица для этого запроса будет иметь следующий вид:
(рис 16.4) Результат запроса с разделом GROUP BY CUBE и вызовами агрегатной функции GROUPING к таблице с неопределенными значениями столбцов группировкиКак видно, результат запроса из примера 16.2 совсем немного отличается от результата запроса из примера 16.1a. Добавились две последние строки, показывающие максимальные значения зарплаты всех служащих предприятия, родившихся в 1950-м и 1960-м гг. соответственно.
Наш пример может навести на мысль, что и в общем случае запросы, содержащие , не слишком отличаются от запросов с , и выполнение этих запросов тоже не слишком различается. Однако это совсем не так. Запрос, содержащий , действительно вырождается в объединение результатов 2n запросов с обычным разделом GROUP BY. Соответственно, сложность выполнения такого запроса несравненно выше сложности выполнения похожего запроса с . В нашем примере все получилось так просто только по той причине, что в запросе имеются всего два столбца группировки.
Начнем этот раздел с нескольких определений, касающихся понятий, которые связаны с рекурсией. Эти понятия имеют общий характер, но в приведенных ниже определениях и комментариях к ним (там, где это уместно) подчеркивается контекст SQL.
(рис 16.5) Пример дереваПри Корень-Потомок1-Потомок2-Потомок3-П1.1-П1.2-П1.3-П2.1-П2.3-П3.1-П3.2-П3.3.
Корень-Потомок1-П1.1-П1.2-П1.3-Потомок2-П2.1-П2.2-П2.3-Потомок3-П3.1-П3.2-П3.3.
(рис 16.6) Пример графа с циклом
(рис 16.7) Графовый пример непрямой рекурсииFROM и/или в
Монотонность. {1, 2, ... , n, ...} является монотонной. В SQL:1999 свойство монотонности поддерживается в том смысле, что число строк результата
A и B связаны отношением A прямо или косвенно вызывает B, и B прямо или косвенно вызывает A A вызывает элемент B через элемент C, а элемент B вызывает элемент A через элемент D ).

(рис 16.9) Графовый пример нелинейной рекурсии(рис 16.8) Графовый пример взаимной рекурсииEXCEPT и , спецификация , условие NOT EXISTS и т.д. В стандарте SQL не запрещается использование
В предыдущих лекциях мы уже говорили о разновидности спецификации ссылки на таблицу с использованием раздела . Однако мы умышленно отложили обсуждение рекурсивных возможностей. Полный синтаксис раздела выглядит следующим образом:
with_clause ::= WITH [ RECURSIVE ]
with_element_comma_list
with_element ::= query_name [ (column_name_list) ]
AS ( query_expression ) [ search_or_cycle_clause ]
search_or_cycle_clause ::= search_clause
| cycle_clause
| search_clause cycle_clause
search_clause ::= SEARCH recursive_search_order SET
sequence_column_name
recursive_search_order ::= DEPTH FIRST BY
order_item_commalist
| BREADTH FIRST BY order_item_commalist
cycle_clause ::= CYCLE cycle_column_name_comma_list
SET cycle_mark_column_name TO value_expression
DEFAULT value_expression
USING path_column_name
Для иллюстрации возможностей и пояснения смысла конструкций и воспользуемся классическим примером "разборки деталей" (в данном случае мы будем разбирать автомобиль). Предположим, что данные о конструктивных элементах автомобиля хранятся в таблице , определенной следующим образом:
CREATE TABLE CAR (CONTAINING_PART VARCHAR (10),
CONTAINED_PART VARCHAR (10),
NUMBER_OF_PARTS INTEGER,
PART_COST DECIMAL (6,2));
У автомобиля имеется один конструктивный элемент верхнего уровня - полностью собранный автомобиль. Этот элемент не является составной частью какого-либо другого элемента, и для его строки значением столбца CONTAINING_PART является текстовая строка длины 0. В любой другой строке таблицы , соответствующей некоторому неатомарному конструктивному элементу e, столбец CONTAINING_PART содержит идентификационный номер элемента e1, в который входит элемент e, столбец NUMBER_OF_PARTS - число экземпляров элемента e, входящих в e1, а столбец CONTAINED_PART - идентификационный номер самого элемента e. В любой строке таблицы , соответствующей некоторому CONTAINED_PART является строка длины 0, а в столбце PART_COST сохраняется цена атомарного конструктивного элемента (для неатомарных элементов значение этого столбца равно нулю).
Предположим, что нам требуется разобрать автомобиль, начиная с элемента самого верхнего уровня, и для каждого конструктивного элемента получить его номер, общее число используемых экземпляров этого элемента, а также, если элемент является атомарным, общую стоимость используемых экземпляров. Вот возможная формулировка запроса (пример 16.3):
WITH RECURSIVE PARTS (PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINED_PART, 1, 0.00 (a)
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINED_PART, CAR.NUMBER_OF_PARTS,
CAR.NUMBER_OF_PARTS * CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
SELECT PART_NUMBER, SUM(NUMBER_OF PARTS),
SUM(COST) (b)
FROM PARTS
GROUP BY PART_NUMBER;
Этот запрос будет выполняться следующим образом. При вычислении раздела FROM основного запроса ( b ) начнется выполнение a ), определенного в разделе . На первом шаге рекурсии будет выполнена часть данного выражения, предшествующая операции и образующая PARTS, в котором, в нашем случае, появится единственная строка, соответствующая автомобилю целиком. На следующем шаге к таблице PARTS будут добавлены строки, соответствующие конструктивным элементам второго уровня (для автомобиля это, по-видимому, двигатель, колеса, , в результирующей таблице могут появляться строки-дубликаты. Наличие строки-дубликата вида <part_no, number, cost> означает, что элемент с номером part_no входит в одном и том же числе экземпляров в несколько конструктивных элементов более высокого уровня.
В приведенном выше примере не определялся порядок, в котором строки добавляются к . При указании требования с требованием обхода иерархии элементов автомобиля
WITH RECURSIVE PARTS (ASSEMBLY, PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINING_PART, CONTAINED_PART, 1, 0.00
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINING_PART, CAR.CONTAINED_PART,
CAR.NUMBER_OF_PARTS, CAR.NUMBER_OF_PARTS *
CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
SEARCH BREADTH FIRST
BY CONTAINING_PART, CONTAINED_PART
SET ORDER_COLUMN
SELECT PART_NUMBER, NUMBER_OF PARTS, COST
FROM PARTS
ORDER BY ORDER_COLUMN;
В списке столбцов сортировки раздела должны указываться имена столбцов WITH. Поскольку в данном случае мы хотим, чтобы в результате сначала появлялись все конструктивные элементы одного уровня ( CONTAINING_PART ), а затем все их подэлементы ( CONTAINED_PART ), в список выборки PARTS добавлен столбец CONTAINING_PART, который не используется нигде, кроме раздела . В разделе SET к результирующей таблице ORDER_COLUMN. Название соответствует природе столбца, потому что при выполнении с указанием столбца, определенного в разделе SET.
Наконец, обсудим, для чего нужен раздел . Дело в том, что иногда сами данные, хранимые в таблицах базы данных, могут иметь циклическую природу. Представим себе, например, компанию, в которой существует совет директоров, являющийся высшим органом управления компанией. Обычным случаем является тот, когда по крайней мере один из членов совета директоров является простым служащим этой же компании (например, он может входить в совет директоров как представитель профсоюза). Назовем данного члена совета директоров EMP_DIR. Как член совета директоров, EMP_DIR "управляет" деятельностью президента компании. С другой стороны, как служащий компании, EMP_DIR находится в прямом или косвенном подчинении у президента компании. Такое положение может привести к зацикливанию выполнения обеспечивает некоторую возможность распознавать подобные ситуации. Если у пользователя имеется полная уверенность в отсутствии циклов в данных, к которым адресуется не требуется.
Подход к распознаванию зацикленных запросов, принятый в SQL, состоит в том, что распознаются данные, которые уже участвовали ранее в формировании результата при добавлении к результату строк, удовлетворяющих условию запроса, такие строки помечаются указанным значением, которое означает, что эти строки уже вошли в результат. При попытке добавления к результату каждой новой строки проверяется, не находится ли она уже в результате, т. е. не помечена ли она этим указанным в разделе значением. Если это действительно так, то считается, что имеет место цикл, и дальнейшее выполнение
Обсудим все это более формально. Для удобства воспроизведем еще раз синтаксис раздела .
cycle_clause ::= CYCLE cycle_column_name_comma_list
SET cycle_mark_column_name TO value_expression_1
DEFAULT value_expression_2
USING path_column_name
В списке cycle_column_name_comma_list указываются имена одного или нескольких столбцов, которые используются для идентификации новых строк результата на основе строк, уже входящих в результат. Например, в примерах 16.3 и 16.4 столбец CONTAINED_PART связывает конструктивный элемент автомобиля с входящими в его состав подэлементами (через значения их столбцов CONTAINING_PART ). Раздел SET приводит к образованию нового столбца результирующей таблицы. Для строк, которые попадают в результат первый раз, в столбец cycle_mark_column_name заносится value_expression_2. В повторно заносимых строках значение столбца - value_expression_1. Типом данных этого столбца является value_expression_1 и value_expression_2 разумно использовать константы ' 0 ' и ' 1 ' или ' Y ' и ' N '.
Раздел USING приводит к образованию еще одного дополнительного столбца результата с именем path_column_name. ARRAY, причем кардинальность этого типа предполагается достаточно большой, чтобы сохранить информацию обо всех строках, попавших в результат. Элементы массива имеют "строчный тип" ( row type ), содержащий столько столбцов, сколько их указано в списке раздела . Каждый элемент массива соответствует строке результата, и в его столбцах содержится копия значений соответствующих столбцов этой строки. Вот пример запроса, содержащего раздел (пример 16.5):
WITH RECURSIVE PARTS (PART_NUMBER,
NUMBER_OF_PARTS, COST) AS
(SELECT CONTAINED_PART, 1, 0.00
FROM CAR
WHERE CONTAINING_PART = ''
UNION ALL
SELECT CAR.CONTAINED_PART, CAR.NUMBER_OF_PARTS,
CAR.NUMBER_OF_PARTS * CAR.PART_COST
FROM CAR, PARTS
WHERE PARTS.PART_NUMBER = CAR.CONTAINING_PART)
CYRCLE CONTAINED_PART
SET CYCLEMARK TO 'Y' DEFAULT 'N'
USING CYRCLEPATH
SELECT PART_NUMBER, SUM(NUMBER_OF PARTS), SUM(COST)
FROM PARTS
ORDER BY PART_NUMBER;
Имена столбцов CYCLEMARK и CYRCLEPATH выбраны произвольным образом - требуется только, чтобы имена этих столбцов отличались от имен столбцов CYRCLEPATH. При попытке добавления к результату новой строки на основе текущего содержимого столбца CYRCLEPATH проверяется, не содержится ли она уже в результате. Если не содержится, то данные об этой строке добавляются к столбцу CYRCLEPATH (к массиву добавляется новый элемент), в столбец CYCLEMARK этой строки заносится значение ' N ', и строка добавляется к результату. Иначе в столбец CYCLEMARK соответствующей строки результата заносится значение ' Y ', означающее, что от этой строки начинается цикл.
CREATE RECURSIVE VIEW table_name [ column_name_comma_list ] AS query_expression
Хотя для того, чтобы ); наличие избыточного ключевого в определении WITH CHECK OPTION не может присутствовать в определении и WITH CHECK OPTION ).
В заключение этого раздела могу сказать, что лично мне механизм рекурсии, предлагаемый в стандарте SQL, представляется громоздким и ограниченным. Кроме того, насколько мне известно, компании, поставляющие SQL-ориентированные СУБД, не спешат внедрять в свои продукты средства рекурсии в соответствии со стандартом SQL:1999 (или, по крайней мере, не слишком их афишируют).
Если вернуться к синтаксическим определениям разд. "Общие синтакические правила построения скалярных выражений" лекции 13, то можно убедиться, что в последних четырех лекциях мы рассмотрели все варианты организации оператора SELECT языка SQL (за исключением конструкций collection_derived_table и ONLY ( table_or_query_name ), относящихся к объектным расширениям языка SQL).
Для общего понимания языка на модельном уровне более важными являются предыдущие три лекции. Данная лекция включена в курс, скорее, с целью общего ознакомления читателей с новыми возможностями оператора выборки, чем с целью их подробного описания. С большой вероятностью средства формулировки аналитических и
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.