Табличные выражения

7.2. Табличные выражения

Табличное выражение предоставляет некую таблицу. Табличное выражение содержит предложение FROM, за которым необязательно следуют предложения WHERE, GROUP BY и HAVING. Простое табличное выражение просто указывает на какую-либо таблицу на диске, так называемую базовую таблицу, но для модификации и комбинирования базовых таблиц различными способами могут быть использованы более сложные выражения.

Необязательные предложения WHERE, GROUP BY и HAVING в табличном выражении, задают конвеер последовательных преобразований, выполняемых над таблицей, полученной из предложения FROM. Все эти преобразования создают виртуальную таблицу, которая предоставляет строки, которые затем передаются в список выборки для отбора строк на выходе запроса.

7.2.1. Предложение FROM

FROM Clause создаёт таблицу из одной или более других таблиц, которые задаются списком разделённых запятыми ссылок на таблицы:

FROM ссылка_на_таблицу [, ссылка_на_таблицу [, ...]]

Ссылка на таблицу может быть именем таблицы (возможно с указанием схемы) или производной таблицей, такой как подзапрос, соединием таблиц, или сложной комбинацией из них. Если в списке предложения FROM указано более одной ссылки на таблицу, они просто соединяются (см. ниже) в форму промежуточной виртуальной таблицы, которая может затем быть преобразована с помощью предложений WHERE, GROUP BY, и HAVING, и в конце-концов стать результатом всего табличного выражения.

Когда ссылка на таблицу является родительской таблицей в иерархии наследования, данная ссылка на таблицу выдаёт строки не только этой таблицы, но и всех её таблиц-потомков, если перед именем таблицы не указано ключевое слово ONLY. Однако, ссылка на такую таблицу выдаёт только колонки, которые есть в этой таблице; любые другие колонки в таблицах-потомках игнорируются.

7.2.1.1. Соединённые таблицы

Соединённая таблица — это таблица, производная от двух других (реальных или в свою очередь производных) таблиц, полученая в соответствии с правилами определённого типа соединения. Доступные типы соединения: inner (внутреннее), outer (внешнее) и cross (перекрёсное).

Типы соединений

Перекрёстное соединение (cross join)
T1 CROSS JOIN T2

Для каждой комбинации строк из T1 и T2 (декартово произведение) соединённая таблица будет содержать строки, состоящие из всех колонок таблицы T1, за которым следуют все колонки из таблицы T2. Если таблицы имеют соответственно N и M строк, соединённая таблица будет иметь N * M строк.

FROM T1 CROSS JOIN T2 эквивалентно FROM T1, T2. Также это эквивалентно FROM T1 INNER JOIN T2 ON TRUE (см. ниже).

Квалифицированные соединения
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON логическое_выражение
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( список_колонок_для_соединения )
T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2

Слова INNER и OUTER являются необязательными во всех формах. INNER применяется по умолчанию; LEFT, RIGHT и FULL неявно указывают на внешнее (outer) соединение.

Условие соединения задаётся в предложениях ON или USING, или неявно, с помощью слова NATURAL. Условие соединения определяет какие строки из двух исходных таблиц будут считаться "совпавшими", как ниже описывается в деталях.

Предложение ON является наиболее обычным способом задать условие соединения: в нём указывается логическое выражение того же типа, что используется в предложении WHERE. Пара строк из T1 и T2 считается совпавшей, если для неё, выражение, заданное в ON принимает значение истина.

USING имеет более краткую форму: в ней указывается разделённый запятыми список имён колонок, которые должны быть общими в соединяемых таблицах и условия соединения, задавая равенство каждой из этих пар колонок. Таким образом, вывод JOIN USING будет состоять из одной колонки для каждой, являющейся равной, пары входных колонок, за которыми следует остальные колонки из каждой таблицы. Так, USING (a, b, c) эквивалентно ON (t1.a = t2.a AND t1.b = t2.b AND t1.c = t2.c) с тем исключением, что если ON используется там, где в результате будут две колонки a, b и c, то USING там, где будет только одна из них (и они будут выданы первыми, если используется SELECT *).

Наконец, NATURAL — это краткая форма USING: она формирует список USING, содержащий все имена колонок, которые есть в обеих входных таблицах. Как и с USING, эти колонки появляются в выходной таблице только один раз. Если общих колонок нет, NATURAL ведёт себя как CROSS JOIN.

Возможные типы квалифицированных соединений:

INNER JOIN

Для каждой строки R1 из таблицы T1, соединённая таблица будет иметь строку для каждой строки в T2, которая удовлетворяет условию соединения с R1.

LEFT OUTER JOIN

Сперва выполняется INNER JOIN. Затем, для каждой строки в T1, которая не удовлетворяет условию соединения с любой строкой в T2, добавляется соединённая строка с значениями NULL в колонках T2. Таким образом, соединённая таблица всегда имеет по крайней мере одну строку для каждой строки из T1.

RIGHT OUTER JOIN

Сперва выполняется INNER JOIN. Затем, для каждой строки в T2, которая не удовлетворяет условию соединения с любой строкой в T1, добавляется соединенная строка с значениями NULL в колонках T1. Этот тип соединения является обратным по отношению к LEFT JOIN: результирующая таблица всегда будет иметь хотя бы одну строку для каждой строки из T2.

FULL OUTER JOIN

Сперва выполняется INNER JOIN. Затем, для каждой строки в T1, которая не удовлетворяет условию соединения с любой строкой в T2, добавляется соединенная строка с значениями NULL в колонках T2. Также, для каждой строки T2, которая не удовлетворяет условию соединения с любой строкой в T1, добавляется соединённая строка с значениями NULL в колонках T1.

Соединения всех типов могуть быть сцеплены вместе или скомпанованы: одна или обе таблицы T1 и T2 могут быть соединяющимися таблицами. Для управления порядком выполнения соединений, вокруг JOIN могут использоваться круглые скобки. В отсутствие скобок, предложения JOIN компануются слева направо.

Чтобы проиллюстрировать всё вышеизложенное, предположим, что у нас есть две таблицы: t1:

 num | name
-----+------
   1 | a
   2 | b
   3 | c

и t2:

 num | value
-----+-------
   1 | xxx
   3 | yyy
   5 | zzz

далее мы выполним разные виды соединений:

=> SELECT * FROM t1 CROSS JOIN t2;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   1 | a    |   3 | yyy
   1 | a    |   5 | zzz
   2 | b    |   1 | xxx
   2 | b    |   3 | yyy
   2 | b    |   5 | zzz
   3 | c    |   1 | xxx
   3 | c    |   3 | yyy
   3 | c    |   5 | zzz
(9 rows)

=> SELECT * FROM t1 INNER JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   3 | c    |   3 | yyy
(2 rows)

=> SELECT * FROM t1 INNER JOIN t2 USING (num);
 num | name | value
-----+------+-------
   1 | a    | xxx
   3 | c    | yyy
(2 rows)

=> SELECT * FROM t1 NATURAL INNER JOIN t2;
 num | name | value
-----+------+-------
   1 | a    | xxx
   3 | c    | yyy
(2 rows)

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |   3 | yyy
(3 rows)

=> SELECT * FROM t1 LEFT JOIN t2 USING (num);
 num | name | value
-----+------+-------
   1 | a    | xxx
   2 | b    |
   3 | c    | yyy
(3 rows)

=> SELECT * FROM t1 RIGHT JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   3 | c    |   3 | yyy
     |      |   5 | zzz
(3 rows)

=> SELECT * FROM t1 FULL JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |   3 | yyy
     |      |   5 | zzz
(4 rows)

Условие соединения, которое задаётся в ON, также может содержать условия, которые напрямую не относятся к соединению. Это может оказаться полезным для некоторых запросов, но нуждается в осторожном использовании. Например:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num AND t2.value = 'xxx';
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |     |
(3 rows)

Обратите внимание, что если поместить ограничение в предложение WHERE, то вы получите другой результат:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num WHERE t2.value = 'xxx';
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
(1 row)

Это потому, что ограничение помещённое в предложение ON обрабатывается перед соединением, в то время как ограничение, помещённое в предложение WHERE обрабатывается после соединения.

7.2.1.2. Псевдонимы таблиц и колонок

В сложных ссылках на таблицы, для дальнейших ссылок в остальной части запроса, могут быть использованы временные имена. Они называются псевдонимами таблиц.

Чтобы создать псевдоним таблицы, пишите

FROM ссылка_на_таблицу AS псевдоним

или

FROM ссылка_на_таблицу псевдоним

Ключевое слово AS является необязательным. Псевдоним может быть любым идентификатором.

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

SELECT * FROM some_very_long_table_name s JOIN another_fairly_long_name a ON s.id = a.num;

Псевдоним даёт новое имя ссылающееся на ту же таблицу в текущем запросе — поэтому где-либо ещё в текущем запросе не разрешается ссылаться на таблицу по первоначальному имени. Таким образом, вот это неправильно:

SELECT * FROM my_table AS m WHERE my_table.a > 5;    -- ошибка

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

SELECT * FROM people AS mother JOIN people AS child ON mother.id = child.mother_id;

Кроме того, псевдноним также требуется, если ссылка на таблицу является подзапросом (см. Section 7.2.1.3).

Скобки используются для того, чтобы избежать путаницы. В следующем примере, первый оператор назначает псевдоним b для второй таблицы my_table, а второй оператор назначает псевдоним результату соединения:

SELECT * FROM my_table AS a CROSS JOIN my_table AS b ...
SELECT * FROM (my_table AS a CROSS JOIN my_table) AS b ...

Другая форма псевдонимов таблиц даёт временные имена колонкам таблицы, точно также как и для самой таблицы:

FROM ссылка_на_таблицу [AS] псевдоним ( колонка1 [, колонка2 [, ...]] )

Вначале, указываются псевдонимы колонок, затем обычые колонки таблицы, оставшиеся колонки не переименовываются. Такой синтаксис особенно полезен для подзапросов соединений таблиц с самими собой.

Когда к результатам предложения JOIN применяется псевдоним, то этот псевдоним скрывает первоначальные имена внутри JOIN. Например:

SELECT a.* FROM my_table AS a JOIN your_table AS b ON ...

является правильным SQL запросом, но:

SELECT a.* FROM (my_table AS a JOIN your_table AS b ON ...) AS c

неправильный запрос: псевдоним таблицы a не виден за пределами псевдонима c.

7.2.1.3. Подзапросы

Подзапросы, предоставляющие на выходе таблицу, должны быть заключены в круглые скобки и им должен быть назначен псевдоним таблицы. (См. Section 7.2.1.2.) Например:

FROM (SELECT * FROM table1) AS alias_name

Этот пример эквивалентен FROM table1 AS alias_name. Когда подзапрос выполняет группировку или агрегирование, возникают более интересные случаи, которые не могут быть представлены как простое соединение таблиц.

Подзапрос также можте быть списком VALUES:

FROM (VALUES ('anne', 'smith'), ('bob', 'jones'), ('joe', 'blow'))
     AS names(first, last)

И снова требуется псевдоним таблицы. Назначение имён псевдонимов для колонок списка VALUES необзяталеьно, но является хорошим тоном. Подробности см. в Section 7.7.

7.2.1.4. Табличные функции

Табличные функции — это функции, которые возвращают список колонок либо базовых типов данных (скалярные типы), либо составных типов данных (колонки таблиц). Они используются в предложении FROM как обычные таблицы, представления или подзапросы. Колонки, возвращаемые табличными функциями могут быть включены в предложения SELECT, JOIN или WHERE точно таким же образом как и колонки таблицы, представления или подзапроса.

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

В предложении FROM табличная функция может получить псевдоним, но также может остаться без псевдонима. Если функция используется в предложении FROM без псевдонима, то в качестве имени таблицы-результата используется имя этой функции.

Несколько примеров:

CREATE TABLE foo (fooid int, foosubid int, fooname text);

CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;

SELECT * FROM getfoo(1) AS t1;

SELECT * FROM foo
    WHERE foosubid IN (
                        SELECT foosubid
                        FROM getfoo(foo.fooid) z
                        WHERE z.fooid = foo.fooid
                      );

CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1);

SELECT * FROM vw_getfoo;

В некоторых случаях, полезно определить табличные функции, которые могут возвращать различные списки колонок, в зависимости от того как они вызываются. Чтобы реализовать такое поведение, табличная функция может быть объявлена как возвращающая псевдотип record. Когда такая функция используется в запросе, ожидаемая структура строки должна быть задана в самом запросе, так что система может знать как обработать и спланировать запрос. Рассмотрим пример:

SELECT *
    FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc')
      AS t1(proname name, prosrc text)
    WHERE proname LIKE 'bytea%';

Функция dblink (часть (модуля dblink) выполняет удалённый запрос. Она объявляется как возвращающая record и, таким образом, может быть использована для любых типов запроса. Фактический список колонок должен быть указан при вызове запроса, так что он известен обработчику запроса, например * должно быть развёрнуто в список.

7.2.2. Предолжение WHERE

Синтаксис WHERE Clause следующий:

WHERE условие_поиска

где условие поиска является любым значением выражения (см. Section 4.2), которое возвращает значение логического типа boolean.

После того как выполнена обработка предложения FROM, каждая строка, полученной в итоге производной вируальной таблицы, проверяется согласно условию поиска. Если результатом этого условия является истина, то строка оставляется в виртуальной таблице, в противном случае (например, если результатом является ложь или NULL), строка отбрасывается. Условие поиска обычно использует по крайней мере одну колонку из таблицы, полученой в предложении FROM; этого не требуется, но в противном случае предложение WHERE будет фактически бесполезным.

Note: Условие соединения INNER JOIN модет быть написано либо в предложении WHERE, либо в предложении JOIN. Например, эти табличные выражения эквивалентны:

FROM a, b WHERE a.id = b.id AND b.val > 5

и:

FROM a INNER JOIN b ON (a.id = b.id) WHERE b.val > 5

или возможно даже:

FROM a NATURAL JOIN b WHERE b.val > 5

Какой из вышеперечисленных запросов использовать вам, в основном вопрос стиля. Синтаксис JOIN в предложении FROM, предположительно, не будет решением, переносимым на другие SQL СУБД, даже если они работают по стандарту SQL. Для OUTER JOIN в этом случае выбора нет: такие соединения должны выполняться в предложении FROM. Предложение ON/USING в OUTER JOIN не эквивалентно условию в WHERE, потому что результаты запроса будут разными.

Вот несколько примеров предложения WHERE:

SELECT ... FROM fdt WHERE c1 > 5

SELECT ... FROM fdt WHERE c1 IN (1, 2, 3)

SELECT ... FROM fdt WHERE c1 IN (SELECT c1 FROM t2)

SELECT ... FROM fdt WHERE c1 IN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10)

SELECT ... FROM fdt WHERE c1 BETWEEN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10) AND 100

SELECT ... FROM fdt WHERE EXISTS (SELECT c1 FROM t2 WHERE c2 > fdt.c1)

fdt — это таблица, полученная в предложении FROM. Строки, которые не попадают под условие поиска в выражении WHERE, удаляются из fdt. Обратите внимание, используйте скалярные позапросы как выражения значений. Как и другие запросы, подзапросы могут возвращать сложные табличные выражения. Также обратите внимание, как fdt используется в подзапросах. Полное имя c1 такое как fdt.c1 необходимо только если c1 также является именем колонки во входной таблице подзапроса. Но полное имя колонки обеспечивает однозначность, даже когда она не требуется. Этот пример показывает как пространство имён колонок внешнего запроса расширяет пространаство имён колонок во внутренних запросах.

7.2.3. Предложения GROUP BY и HAVING

После прохода через фильтр WHERE, полученная входная таблица может быть сгруппирована, используя предложение GROUP BY, а также из неё могут удалены строки, используя предложение HAVING.

SELECT список_выборки
    FROM ...
    [WHERE ...]
    GROUP BY ссылка_на_группирумую_колонку [, ссылка_на_группирумую_колонку]...

GROUP BY Clause используется для группировки вместе тех строк таблицы, которые имеют те же самые значения во всех перечисленных колонках. Порядок, в котором перечисляются колоки значения не имеет. Эффект группировки состоит в комбинировании каждого списка строк, имеющих общие значения колонок в одну сгруппированную строку, которая представляет все строки в данной группе. Это осуществляется исключением избыточности в выводе и/или подсчётом агрегатов, которые применяются к группам. Например:

=> SELECT * FROM test1;
 x | y
---+---
 a | 3
 c | 2
 b | 5
 a | 1
(4 rows)

=> SELECT x FROM test1 GROUP BY x;
 x
---
 a
 b
 c
(3 rows)

Во втором запросе, мы не можем написать SELECT * FROM test1 GROUP BY x, потому что в нём нет ни одного значения в колонке y, которое может быть ассоциировано с каждой группой. Группируемые колонки могут быть перечислены в списке выбора, в случае, если они имеют хотя бы одно значение в каждой группе.

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

=> SELECT x, sum(y) FROM test1 GROUP BY x;
 x | sum
---+-----
 a |   4
 b |   5
 c |   2
(3 rows)

Здесь sum является агрегатной функцией, которая подсчитывает одно значение во всей группе. Больше информации о доступных агрегатных функциях можно найти в Section 9.18.

Tip: Группировка без агрегирующих выражений эффективно подсчитывает список отдельных значений в колонке. Это же может быть достигнуто, используя предложение DISTINCT (см. Section 7.3.3).

Здесь другой пример: он подсчитывает полные продажи для каждого продукта (вместо подсчёта полных продаж всех продуктов):

SELECT product_id, p.name, (sum(s.units) * p.price) AS sales
    FROM products p LEFT JOIN sales s USING (product_id)
    GROUP BY product_id, p.name, p.price;

В этом примере, колонки product_id, p.name и p.price должны быть в предложении GROUP BY, поскольку в запросе они указываются в списке выборки (но см. ниже). Колонки s.units нет в списке GROUP BY, поскольку она используется только в агрегирующем выражении (sum(...)), которое представляет собой продажи продукта. Для каждого продукта, запрос возвращает суммарную строку о всех продажах этого продукта.

Если таблица products сделана так, что product_id является первичным ключём, то его должно быть достаточно для группировки по product_id в данном выше примере, так как name и price должны быть функционально зависимы от product_id и таким образом не должно быть двусмысленности в отношенни того какие name и price будут соответствовать каждой группе product_id.

В строгом SQL, GROUP BY может группировать только по колонкам исходной таблицы, но PostgreSQL расширяет поведение группировки, позволяя GROUP BY группировать по колонкам в списке выбора. Также позволяется группировка по значениям вместо имён колонок.

Если таблица была сгруппирована с помощью предложения GROUP BY, но интерес представляют только определённые группы, можно использовать предложение HAVING, которое очень похоже на предложение WHERE, но только для удаления групп из результата. Синтаксис такой:

SELECT список_выбора FROM ... [WHERE ...] GROUP BY ... HAVING логическое_выражение

Выражения в предложении HAVING могут использовать как сгруппированные выражения, так и несгруппированные выражения (которые необходимо обработать агрегатной функцией).

Ghbvth:

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING sum(y) > 3;
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING x < 'c';
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

Ещё один, более реалистичный пример:

SELECT product_id, p.name, (sum(s.units) * (p.price - p.cost)) AS profit
    FROM products p LEFT JOIN sales s USING (product_id)
    WHERE s.date > CURRENT_DATE - INTERVAL '4 weeks'
    GROUP BY product_id, p.name, p.price, p.cost
    HAVING sum(p.price * s.units) > 5000;

В этом примере, предложение WHERE выбирает строки с колонками, которые не группируются (выражение истинно только для продаж за последние четыре недели), в то время как предложение HAVING ограничивает вывод сгруппированных результатов только для случаев, когда объём продаж превышает 5000. Обратите внимание, что агрегатные выражения могут разными во всех частях запроса.

Если запрос содержит вызовы агрегатный функций, но не содержит предложение GROUP BY, группировка всё-равно будет осуществлена: результатом будет одна сгруппированная строка (или не будет никаких строк, если данная сгруппированная строка будет исключена предложением HAVING). Тоже самое будет, если запрос содержит предложение HAVING, даже без каких-либо вызовов агрегатных функций GROUP BY.

7.2.4. Обработка оконных функций

Если запрос содержит какие-либо оконные функции (см. Section 3.5, Section 9.19 и Section 4.2.8), то эти функции выполняются после выполнения всех предложений группировки, агрегирования и HAVING. Таким образом, если запрос использует какие-либо агрегаты, GROUP BY или HAVING, то строки, которые будут предоставлены оконным функциям будут уже сгруппированными, а не первоначальными строками таблиц, которые указаны в предложениях FROM/WHERE.

Когда используется несколько оконных функций, все эти оконные функции синтаксически соответствуют предложениям PARTITION BY и ORDER BY в их определениях окна и гарантируется, что они будут выполнены в один проход. Таким образом, они будут видеть тот же самый порядок сортировки, даже если ORDER BY не определяет уникальный порядок сортировки. Однако, это не гарантируется при выполнении функций, имеющих разные спецификации PARTITION BY или ORDER BY. (В таких случаях, между проходами выполнения оконных функций, обычно требуется шаг сортировки, и сортировка не гарантирует защиту порядка следования строк, которые видит соответствующий ORDER BY.)

В настоящий момент, оконные функции всегда требуют предварительную сортировку данных и таким образом результат запроса будет отсортирован в соответствии с тем или иным предложением PARTITION BY/ORDER BY оконной функции. Тем не менее, не рекомендуется полагаться на это. Используйте явное ORDER BY верхнего уровня, если вы хотите иметь уверенность, что результаты сортируются должным образом.

Back to top

(С) Виктор Вислобоков, 2008-2023