Postgresql
November 11, 2023

PostgreSQL. Особенности SQL: filter и distinct on.

Сначала создадим в схеме SCOTT табличку и заполним данными:

CREATE TABLE EMP
       (EMPNO integer NOT NULL,
        ENAME VARCHAR(10),
        JOB VARCHAR(9),
        MGR integer,
        HIREDATE DATE,
        SAL integer,
        COMM integer,
        DEPTNO integer)
INSERT INTO EMP VALUES
        (7369, 'SMITH',  'CLERK',     7902,
        '1980-12-17',  800, NULL, 20)
INSERT INTO EMP VALUES
        (7499, 'ALLEN',  'SALESMAN',  7698,
        '1981-2-20', 1600,  300, 30)
INSERT INTO EMP VALUES
        (7521, 'WARD',   'SALESMAN',  7698,
        '1981-2-22', 1250,  500, 30)
INSERT INTO EMP VALUES
        (7566, 'JONES',  'MANAGER',   7839,
        '1981-4-2',  2975, NULL, 20)
INSERT INTO EMP VALUES
        (7654, 'MARTIN', 'SALESMAN',  7698,
        '1981-9-28', 1250, 1400, 30)
INSERT INTO EMP VALUES
        (7698, 'BLAKE',  'MANAGER',   7839,
        '1981-5-1',  2850, NULL, 30)
INSERT INTO EMP VALUES
        (7782, 'CLARK',  'MANAGER',   7839,
        '1981-6-9',  2450, NULL, 10)
INSERT INTO EMP VALUES
        (7788, 'SCOTT',  'ANALYST',   7566,
        '1982-12-9', 3000, NULL, 20)
INSERT INTO EMP VALUES
        (7839, 'KING',   'PRESIDENT', NULL,
        '1981-11-17', 5000, NULL, 10)
INSERT INTO EMP VALUES
        (7844, 'TURNER', 'SALESMAN',  7698,
        '1981-9-8',  1500,    0, 30)
INSERT INTO EMP VALUES
        (7876, 'ADAMS',  'CLERK',     7788,
        '1983-1-12', 1100, NULL, 20)
INSERT INTO EMP VALUES
        (7900, 'JAMES',  'CLERK',     7698,
        '1981-12-3',   950, NULL, 30)
INSERT INTO EMP VALUES
        (7902, 'FORD',   'ANALYST',   7566,
        '1981-12-3',  3000, NULL, 20)
INSERT INTO EMP VALUES
        (7934, 'MILLER', 'CLERK',     7782,
        '1982-1-23', 1300, NULL, 10)
        
Данные в табличке

DISTINCT ON

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

Можно сделать это с помощью оконной функции, например row_number - будет работать не только в PostgreSQL, но и в Oracle и в MS SQL Server.

with x as (
SELECT  empno, ename, job, sal
,row_number () over (partition by job order by sal desc, empno asc) rn
FROM scott.emp)
select x.* from x 
where rn = 1
Результаты выборки. Традиционное решение.

А можно воспользоваться присущей только PostgreSQL кляузой distinct on.

Это решение выглядит так:

select distinct on (job) empno, ename, job, sal, 2 rn
from scott.emp
order by job, sal desc, empno asc
Результаты выборки. Использование distinct on.

Смысл такой же - для каждой уникальной комбинации из списка значений, стоящих в скобках после distinct on - в нашем случае это одно поле - job, - возвращаем единственную запись. Порядок выбора этой записи указывается после order by job: sal desc, empno asc.

Если бы нам понадобилось найти данные уже в разрезе не только должности, но и номера департамента, то запрос выглядел бы так:

select distinct on (job, deptno) empno, ename, job, sal, deptno
from scott.emp
order by job, deptno, sal desc, empno asc

Обратите внимание, слева от order by теперь 2 поля - job, deptno - т.к. они в скобках после distinct on.

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

FILTER

Допустим, на этом же примере мы хотим посчитать в разрезе департаментов число сотрудников с окладами менее 1000, от 1000 до 2000, и более 2000.

Используя стандарт, это можно так сделать:

select deptno 
,count (case when sal < 1000 then 1 end) beggar_qt
,count (case when sal >= 1000 and sal < 2000 then 1 end) middle_qt
,count (case when sal >= 2000 then 1 end) high_qt
from scott.emp
group by deptno
order by 1
Результаты выборки.

Но можно для этого же воспользоваться кляузой PostgreSQL filter - в этот раз запрос действительно смотрится понятней.

select deptno 
,count (*) filter (where sal < 1000) beggar_qt
,count (*) filter (where sal > 1000 and sal < 2000) middle_qt
,count (*) filter (where sal >= 2000 ) high_qt
from scott.emp
group by deptno
order by 1

В книжке пишут, что "Кроме того, FILTER работает быстрее его имитации с помощью конструкции CASE WHEN ... THEN NULL ... ELSE END . Результаты сравнения на реальных данных можно найти по адресуhttps://www.cybertec-postgresql.com/en/postgresql-9-4-aggregation-filters-they-do-pay-off/" (Ссылка от 2015 года).

А ещё кляуза FILTER может использоваться в агрегирующих оконных функциях.