7.2. Табличные выражения #
Табличное выражение вычисляет таблицу. Это выражение содержит предложение FROM
, за которым могут следовать предложения WHERE
, GROUP BY
и HAVING
. Тривиальные табличные выражения просто ссылаются на физическую таблицу, её называют также базовой, но в более сложных выражениях такие таблицы можно преобразовывать и комбинировать самыми разными способами.
Необязательные предложения WHERE
, GROUP BY
и HAVING
в табличном выражении определяют последовательность преобразований, осуществляемых с данными таблицы, полученной в предложении FROM
. В результате этих преобразований образуется виртуальная таблица, строки которой передаются списку выборки, вычисляющему выходные строки запроса.
7.2.1. Предложение FROM
#
Предложение FROM
образует таблицу из одной или нескольких ссылок на таблицы, разделённых запятыми.
FROMтабличная_ссылка
[,табличная_ссылка
[, ...]]
Здесь табличной ссылкой может быть имя таблицы (возможно, с именем схемы), производная таблица, например подзапрос, соединение таблиц или сложная комбинация этих вариантов. Если в предложении FROM
перечисляются несколько ссылок, для них применяется перекрёстное соединение (то есть декартово произведение их строк; см. ниже). Список FROM
преобразуется в промежуточную виртуальную таблицу, которая может пройти через преобразования WHERE
, GROUP BY
и HAVING
, и в итоге определит результат табличного выражения.
Когда в табличной ссылке указывается таблица, являющаяся родительской в иерархии наследования, в результате будут получены строки не только этой таблицы, но и всех её дочерних таблиц. Чтобы выбрать строки только одной родительской таблицы, перед её именем нужно добавить ключевое слово ONLY
. Учтите, что при этом будут получены только столбцы указанной таблицы — дополнительные столбцы дочерних таблиц не попадут в результат.
Если же вы не добавляете ONLY
перед именем таблицы, вы можете дописать после него *
, тем самым указав, что должны обрабатываться и все дочерние таблицы. Практических причин использовать этот синтаксис больше нет, так как поиск в дочерних таблицах теперь производится по умолчанию. Однако эта запись поддерживается для совместимости со старыми версиями.
7.2.1.1. Соединённые таблицы #
Соединённая таблица — это таблица, полученная из двух других (реальных или производных от них) таблиц в соответствии с правилами соединения конкретного типа. Общий синтаксис описания соединённой таблицы:
T1
тип_соединения
T2
[условие_соединения
]
Соединения любых типов могут вкладываются друг в друга или объединяться: и T1
, и T2
могут быть результатами соединения. Для однозначного определения порядка соединений предложения JOIN
можно заключать в скобки. Если скобки отсутствуют, предложения JOIN
обрабатываются слева направо.
Типы соединений
- Перекрёстное соединение
T1
CROSS JOINT2
Соединённую таблицу образуют все возможные сочетания строк из
T1
иT2
(т. е. их декартово произведение), а набор её столбцов объединяет в себе столбцыT1
со следующими за ними столбцамиT2
. Если таблицы содержат N и M строк, соединённая таблица будет содержать N * M строк.FROM
равнозначноT1
CROSS JOINT2
FROM
(см. ниже). Эта запись также равнозначнаT1
INNER JOINT2
ON TRUEFROM
.T1
,T2
Примечание
Последняя запись не полностью эквивалентна первым при указании более чем двух таблиц, так как
JOIN
связывает таблицы сильнее, чем запятая. Например,FROM
не равнозначноT1
CROSS JOINT2
INNER JOINT3
ONусловие
FROM
, так какT1
,T2
INNER JOINT3
ONусловие
условие
может ссылаться наT1
в первом случае, но не во втором.- Соединения с сопоставлениями строк
T1
{ [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOINT2
ONлогическое_выражение
T1
{ [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOINT2
USING (список столбцов соединения
)T1
NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOINT2
Слова
INNER
иOUTER
необязательны во всех формах. По умолчанию подразумеваетсяINNER
(внутреннее соединение), а при указанииLEFT
,RIGHT
иFULL
— внешнее соединение.Условие соединения указывается в предложении
ON
илиUSING
, либо неявно задаётся ключевым словомNATURAL
. Это условие определяет, какие строки двух исходных таблиц считаются «соответствующими» друг другу (это подробно рассматривается ниже).Возможные типы соединений с сопоставлениями строк:
INNER JOIN
Для каждой строки R1 из T1 в результирующей таблице содержится строка для каждой строки в T2, удовлетворяющей условию соединения с R1.
LEFT OUTER JOIN
Сначала выполняется внутреннее соединение (INNER JOIN). Затем в результат добавляются все строки из T1, которым не соответствуют никакие строки в T2, а вместо значений столбцов T2 вставляются NULL. Таким образом, в результирующей таблице всегда будет минимум одна строка для каждой строки из T1.
RIGHT OUTER JOIN
Сначала выполняется внутреннее соединение (INNER JOIN). Затем в результат добавляются все строки из T2, которым не соответствуют никакие строки в T1, а вместо значений столбцов T1 вставляются NULL. Это соединение является обратным к левому (LEFT JOIN): в результирующей таблице всегда будет минимум одна строка для каждой строки из T2.
FULL OUTER JOIN
Сначала выполняется внутреннее соединение. Затем в результат добавляются все строки из T1, которым не соответствуют никакие строки в T2, а вместо значений столбцов T2 вставляются NULL. И наконец, в результат включаются все строки из T2, которым не соответствуют никакие строки в T1, а вместо значений столбцов T1 вставляются NULL.
Предложение
ON
определяет наиболее общую форму условия соединения: в нём указываются выражения логического типа, подобные тем, что используются в предложенииWHERE
. Пара строк изT1
иT2
соответствуют друг другу, если выражениеON
возвращает для них true.USING
— это сокращённая запись условия, полезная в ситуации, когда с обеих сторон соединения столбцы имеют одинаковые имена. Она принимает список общих имён столбцов через запятую и формирует условие соединения с равенством этих столбцов. Например, запись соединенияT1
иT2
сUSING (a, b)
формирует условиеON
.T1
.a =T2
.a ANDT1
.b =T2
.bБолее того, при выводе
JOIN USING
исключаются избыточные столбцы: оба сопоставленных столбца выводить не нужно, так как они содержат одинаковые значения. Тогда какJOIN ON
выдаёт все столбцы изT1
, а за ними все столбцы изT2
,JOIN USING
выводит один столбец для каждой пары (в указанном порядке), за ними все оставшиеся столбцы изT1
и, наконец, все оставшиеся столбцыT2
.Наконец,
NATURAL
— сокращённая формаUSING
: она образует списокUSING
из всех имён столбцов, существующих в обеих входных таблицах. Как и сUSING
, эти столбцы оказываются в выходной таблице в единственном экземпляре. Если столбцов с одинаковыми именами не находится,NATURAL JOIN
действует какCROSS JOIN
.Примечание
Предложение
USING
разумно защищено от изменений в соединяемых отношениях, так как оно связывает только явно перечисленные столбцы.NATURAL
считается более рискованным, так как при любом изменении схемы в одном или другом отношении, когда появляются столбцы с совпадающими именами, при соединении будут связываться и эти новые столбцы.
Для наглядности предположим, что у нас есть таблицы 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 "очень_длинное_имя_таблицы" s JOIN "другое_длинное_имя" 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;
В случае неоднозначности определения псевдонимов можно использовать скобки. В следующем примере первый оператор назначает псевдоним 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
. Например, это допустимый SQL-запрос:
SELECT a.* FROM my_table AS a JOIN your_table AS b ON ...
а запрос:
SELECT a.* FROM (my_table AS a JOIN your_table AS b ON ...) AS c
ошибочный, так как псевдоним таблицы a
не виден снаружи определения псевдонима c
.
7.2.1.3. Подзапросы #
Подзапросы, образующие таблицы, должны заключаться в скобки. Им могут назначаться псевдонимы таблиц и, если необходимо, псевдонимы столбцов (как описано в Подразделе 7.2.1.2). Например:
FROM (SELECT * FROM table1) AS псевдоним
Этот пример равносилен записи FROM table1 AS псевдоним
. Более интересные ситуации, которые нельзя свести к простому соединению, возникают, когда в подзапросе используются агрегирующие функции или группировка.
Подзапросом может также быть список VALUES
:
FROM (VALUES ('anne', 'smith'), ('bob', 'jones'), ('joe', 'blow')) AS names(first, last)
Такому подзапросу может быть назначен псевдоним. Назначать псевдонимы столбцам списка VALUES
не требуется, но вообще это хороший приём. Подробнее это описано в Разделе 7.7.
Согласно стандарту SQL подзапросу должен назначаться псевдоним таблицы. PostgreSQL позволяет опускать AS
и псевдоним, тем не менее хорошей практикой считается указывать псевдоним в коде SQL, который может быть портирован в другую систему.
7.2.1.4. Табличные функции #
Табличные функции — это функции, выдающие набор строк, содержащих либо базовые типы данных (скалярных типов), либо составные типы (табличные строки). Они применяются в запросах как таблицы, представления или подзапросы в предложении FROM
. Столбцы, возвращённые табличными функциями, можно включить в выражения SELECT
, JOIN
или WHERE
так же, как столбцы таблиц, представлений или подзапросов.
Табличные функции можно также скомбинировать, используя запись ROWS FROM
. Результаты функций будут возвращены в параллельных столбцах; число строк в этом случае будет наибольшим из результатов всех функций, а результаты функций с меньшим количеством строк будут дополнены значениями NULL.
вызов_функции
[WITH ORDINALITY] [[AS]псевдоним_таблицы
[(псевдоним_столбца
[, ...])]] ROWS FROM(вызов_функции
[, ...] ) [WITH ORDINALITY] [[AS]псевдоним_таблицы
[(псевдоним_столбца
[, ...])]]
Если указано предложение WITH ORDINALITY
, к столбцам результатов функций будет добавлен ещё один, с типом bigint
. В этом столбце нумеруются строки результирующего набора, начиная с 1. (Это обобщение стандартного SQL-синтаксиса UNNEST ... WITH ORDINALITY
.) По умолчанию, этот столбец называется ordinality
, но ему можно присвоить и другое имя с помощью указания AS
.
Специальную табличную функцию UNNEST
можно вызвать с любым числом параметров-массивов, а возвращает она соответствующее число столбцов, как если бы UNNEST
(Раздел 9.19) вызывалась для каждого параметра в отдельности, а результаты объединялись с помощью конструкции ROWS FROM
.
UNNEST(выражение_массива
[, ...] ) [WITH ORDINALITY] [[AS]псевдоним_таблицы
[(псевдоним_столбца
[, ...])]]
Если псевдоним_таблицы
не указан, в качестве имени таблицы используется имя функции; в случае с конструкцией ROWS 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;
В некоторых случаях бывает удобно определить табличную функцию, возвращающую различные наборы столбцов при разных вариантах вызова. Это можно сделать, объявив функцию, не имеющую выходных параметров (OUT
) и возвращающую псевдотип record
. Используя такую функцию, ожидаемую структуру строк нужно описать в самом запросе, чтобы система знала, как разобрать запрос и составить его план. Записывается это так:
вызов_функции
[AS]псевдоним
(определение_столбца
[, ...])вызов_функции
AS [псевдоним
] (определение_столбца
[, ...]) ROWS FROM( ...вызов_функции
AS (определение_столбца
[, ...]) [, ...] )
Без ROWS FROM()
список определения_столбцов
заменяет список псевдонимов, который можно также добавить в предложении FROM
; имена в определениях столбцов служат псевдонимами. С ROWS FROM()
список определения_столбцов
можно добавить к каждой функции отдельно, либо в случае с одной функцией и без предложения WITH ORDINALITY
, список определения_столбцов
можно записать вместо списка с псевдонимами столбцов после ROWS FROM()
.
Взгляните на этот пример:
SELECT * FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text) WHERE proname LIKE 'bytea%';
Здесь функция dblink (из модуля dblink) выполняет удалённый запрос. Она объявлена как функция, возвращающая тип record
, так как он подойдёт для запроса любого типа. В этом случае фактический набор столбцов функции необходимо описать в вызывающем её запросе, чтобы анализатор запроса знал, например, как преобразовать *
.
В этом примере используется конструкция ROWS FROM
:
SELECT * FROM ROWS FROM ( json_to_recordset('[{"a":40,"b":"foo"},{"a":"100","b":"bar"}]') AS (a INTEGER, b TEXT), generate_series(1, 3) ) AS x (p, q, s) ORDER BY p; p | q | s -----+-----+--- 40 | foo | 1 100 | bar | 2 | | 3
Она объединяет результаты двух функций в одном отношении FROM
. В данном случае json_to_recordset()
должна выдавать два столбца, первый integer
и второй text
, а результат generate_series()
используется непосредственно. Предложение ORDER BY
упорядочивает значения первого столбца как целочисленные.
7.2.1.5. Подзапросы LATERAL
#
Перед подзапросами в предложении FROM
можно добавить ключевое слово LATERAL
. Это позволит ссылаться в них на столбцы предшествующих элементов списка FROM
. (Без LATERAL
каждый подзапрос выполняется независимо и поэтому не может обращаться к другим элементам FROM
.)
Перед табличными функциями в предложении FROM
также можно указать LATERAL
, но для них это ключевое слово необязательно; в аргументах функций в любом случае можно обращаться к столбцам в предыдущих элементах FROM
.
Элемент LATERAL
может находиться на верхнем уровне списка FROM
или в дереве JOIN
. В последнем случае он может также ссылаться на любые элементы в левой части JOIN
, справа от которого он находится.
Когда элемент FROM
содержит ссылки LATERAL
, запрос выполняется следующим образом: сначала для строки элемента FROM
с целевыми столбцами, или набора строк из нескольких элементов FROM
, содержащих целевые столбцы, вычисляется элемент LATERAL
со значениями этих столбцов. Затем результирующие строки обычным образом соединяются со строками, из которых они были вычислены. Эта процедура повторяется для всех строк исходных таблиц.
LATERAL
можно использовать так:
SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) ss;
Здесь это не очень полезно, так как тот же результат можно получить более простым и привычным способом:
SELECT * FROM foo, bar WHERE bar.id = foo.bar_id;
Применять LATERAL
имеет смысл в основном, когда для вычисления соединяемых строк необходимо обратиться к столбцам других таблиц. В частности, это полезно, когда нужно передать значение функции, возвращающей набор данных. Например, если предположить, что vertices(polygon)
возвращает набор вершин многоугольника, близкие вершины многоугольников из таблицы polygons можно получить так:
SELECT p1.id, p2.id, v1, v2 FROM polygons p1, polygons p2, LATERAL vertices(p1.poly) v1, LATERAL vertices(p2.poly) v2 WHERE (v1 <-> v2) < 10 AND p1.id != p2.id;
Этот запрос можно записать и так:
SELECT p1.id, p2.id, v1, v2 FROM polygons p1 CROSS JOIN LATERAL vertices(p1.poly) v1, polygons p2 CROSS JOIN LATERAL vertices(p2.poly) v2 WHERE (v1 <-> v2) < 10 AND p1.id != p2.id;
или переформулировать другими способами. (Как уже упоминалось, в данном примере ключевое слово LATERAL
не требуется, но мы добавили его для ясности.)
Особенно полезно бывает использовать LEFT JOIN
с подзапросом LATERAL
, чтобы исходные строки оказывались в результате, даже если подзапрос LATERAL
не возвращает строк. Например, если функция get_product_names()
выдаёт названия продуктов, выпущенных определённым производителем, но о продукции некоторых производителей информации нет, мы можем найти, каких именно, примерно так:
SELECT m.name FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON true WHERE pname IS NULL;
7.2.2. Предложение WHERE
#
Предложение WHERE
записывается так:
WHERE условие_ограничения
где условие_ограничения
— любое выражение значения (см. Раздел 4.2), выдающее результат типа boolean
.
После обработки предложения FROM
каждая строка полученной виртуальной таблицы проходит проверку по условию ограничения. Если результат условия равен true, эта строка остаётся в выходной таблице, а иначе (если результат равен false или NULL) отбрасывается. В условии ограничения, как правило, задействуется минимум один столбец из таблицы, полученной на выходе FROM
. Хотя строго говоря, это не требуется, но в противном случае предложение WHERE
будет бессмысленным.
Примечание
Условие для внутреннего соединения можно записать как в предложении 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. Но для внешних соединений других вариантов нет: их можно записывать только во FROM
. Предложения ON
и USING
во внешних соединениях не равнозначны условию 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
группирует строки таблицы, объединяя их в одну группу при совпадении значений во всех перечисленных столбцах. Порядок, в котором указаны столбцы, не имеет значения. В результате наборы строк с одинаковыми значениями преобразуются в отдельные строки, представляющие все строки группы. Это может быть полезно для устранения избыточности выходных данных и/или для вычисления агрегатных функций, применённых к этим группам. Например:
=>
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
— агрегатная функция, вычисляющая единственное значение для всей группы. Подробную информацию о существующих агрегатных функциях можно найти в Разделе 9.21.
Подсказка
Группировка без агрегатных выражений по сути выдаёт набор различающихся значений столбцов. Этот же результат можно получить с помощью предложения DISTINCT
(см. Подраздел 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
, так как название и цена продукта функционально зависят от кода продукта и можно однозначно определить, какое название и цену возвращать для каждой группы по ID.
В стандарте SQL GROUP BY
может группировать только по столбцам исходной таблицы, но расширение PostgreSQL позволяет использовать в GROUP BY
столбцы из списка выборки. Также возможна группировка по выражениям, а не просто именам столбцов.
Если таблица была сгруппирована с помощью GROUP BY
, но интерес представляют только некоторые группы, отфильтровать их можно с помощью предложения HAVING
, действующего подобно WHERE
. Записывается это так:
SELECTсписок_выборки
FROM ... [WHERE ...] GROUP BY ... HAVINGлогическое_выражение
В предложении HAVING
могут использоваться и группирующие выражения, и выражения, не участвующие в группировке (в этом случае это должны быть агрегирующие функции).
Пример:
=>
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. GROUPING SETS
, CUBE
и ROLLUP
#
Более сложные, чем описанные выше, операции группировки возможны с концепцией наборов группирования. Данные, выбранные предложениями FROM
и WHERE
, группируются отдельно для каждого заданного набора группирования, затем для каждой группы вычисляются агрегатные функции как для простых предложений GROUP BY
, и в конце возвращаются результаты. Например:
=>
SELECT * FROM items_sold;
brand | size | sales -------+------+------- Foo | L | 10 Foo | M | 20 Bar | M | 15 Bar | L | 5 (4 rows)=>
SELECT brand, size, sum(sales) FROM items_sold GROUP BY GROUPING SETS ((brand), (size), ());
brand | size | sum -------+------+----- Foo | | 30 Bar | | 20 | L | 15 | M | 35 | | 50 (5 rows)
В каждом внутреннем списке GROUPING SETS
могут задаваться ноль или более столбцов или выражений, которые воспринимаются так же, как если бы они были непосредственно записаны в предложении GROUP BY
. Пустой набор группировки означает, что все строки сводятся к одной группе (которая выводится, даже если входных строк нет), как описано выше для агрегатных функций без предложения GROUP BY
.
Ссылки на группирующие столбцы или выражения заменяются в результирующих строках значениями NULL для тех группирующих наборов, в которых эти столбцы отсутствуют. Чтобы можно было понять, результатом какого группирования стала конкретная выходная строка, предназначена функция, описанная в Таблице 9.64.
Для указания двух распространённых видов наборов группирования предусмотрена краткая запись. Предложение формы
ROLLUP (e1
,e2
,e3
, ... )
представляет заданный список выражений и всех префиксов списка, включая пустой список; то есть оно равнозначно записи
GROUPING SETS ( (e1
,e2
,e3
, ... ), ... (e1
,e2
), (e1
), ( ) )
Оно часто применяется для анализа иерархических данных, например, для суммирования зарплаты по отделам, подразделениям и компании в целом.
Предложение формы
CUBE (e1
,e2
, ... )
представляет заданный список и все его возможные подмножества (степень множества). Таким образом, запись
CUBE ( a, b, c )
равнозначна
GROUPING SETS ( ( a, b, c ), ( a, b ), ( a, c ), ( a ), ( b, c ), ( b ), ( c ), ( ) )
Элементами предложений CUBE
и ROLLUP
могут быть либо отдельные выражения, либо вложенные списки элементов в скобках. Вложенные списки обрабатываются как атомарные единицы, с которыми формируются отдельные наборы группирования. Например:
CUBE ( (a, b), (c, d) )
равнозначно
GROUPING SETS ( ( a, b, c, d ), ( a, b ), ( c, d ), ( ) )
и
ROLLUP ( a, (b, c), d )
равнозначно
GROUPING SETS ( ( a, b, c, d ), ( a, b, c ), ( a ), ( ) )
Конструкции CUBE
и ROLLUP
могут применяться либо непосредственно в предложении GROUP BY
, либо вкладываться внутрь предложения GROUPING SETS
. Если одно предложение GROUPING SETS
вкладывается внутрь другого, результат будет таким же, как если бы все элементы внутреннего предложения были записаны непосредственно во внешнем.
Если в одном предложении GROUP BY
задаётся несколько элементов группирования, окончательный список наборов группирования образуется как декартово произведение этих элементов. Например:
GROUP BY a, CUBE (b, c), GROUPING SETS ((d), (e))
равнозначно
GROUP BY GROUPING SETS ( (a, b, c, d), (a, b, c, e), (a, b, d), (a, b, e), (a, c, d), (a, c, e), (a, d), (a, e) )
Если задаётся несколько элементов группирования, окончательный список наборов группирования может содержать дублирующиеся результаты. Например:
GROUP BY ROLLUP (a, b), ROLLUP (a, c)
равнозначно
GROUP BY GROUPING SETS ( (a, b, c), (a, b), (a, b), (a, c), (a), (a), (a, c), (a), () )
Если эти дублирующиеся результаты нежелательны, их можно убрать, добавив DISTINCT
непосредственно в предложение GROUP BY
. Таким образом:
GROUP BY DISTINCT ROLLUP (a, b), ROLLUP (a, c)
равнозначно
GROUP BY GROUPING SETS ( (a, b, c), (a, b), (a, c), (a), () )
Это не то же самое, что SELECT DISTINCT
, поскольку результирующие строки всё же могут содержать дублирующиеся записи. Если любой из столбцов, не участвующих в группировании, содержит значение NULL, в результате оно не будет отличаться от NULL, полученного при группировании этого столбца.
Примечание
Конструкция (a, b)
обычно воспринимается в выражениях как конструктор строки. Однако в предложении GROUP BY
на верхнем уровне выражений запись (a, b)
воспринимается как список выражений, как описано выше. Если вам по какой-либо причине нужен именно конструктор строки в выражении группирования, используйте запись ROW(a, b)
.
7.2.5. Обработка оконных функций #
Если запрос содержит оконные функции (см. Раздел 3.5, Раздел 9.22 и Подраздел 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
на верхнем уровне запроса.