Кафедра ИТКафедра ИТ
Блог
Обучение
  • О кафедре
  • Направления подготовки
  • Друзья и партнеры
  • Структура кафедры
  • Обращение к студентам
  • Официальный сайт «ВШП»
GitHub
Блог
Обучение
  • О кафедре
  • Направления подготовки
  • Друзья и партнеры
  • Структура кафедры
  • Обращение к студентам
  • Официальный сайт «ВШП»
  • ИТ.03 - 41 - Практикум: оконные функции

  1. Главная
  2. Учебные материалы
  3. ИТ.03 - Основы проектиро...
  4. Практикум: оконные функц...

ИТ.03 - 41 - Практикум: оконные функции

Введение

В лекциях 39 и 40 мы разобрали оконные функции MySQL 8.0: синтаксис OVER(), секции PARTITION BY и ORDER BY, ранжирующие функции (ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE()), агрегатные функции в роли оконных (бегущие суммы и средние), функции смещения (LAG(), LEAD(), FIRST_VALUE(), LAST_VALUE()), рамки окна (ROWS/RANGE), функции распределения (CUME_DIST(), PERCENT_RANK(), NTH_VALUE()) и именованные окна WINDOW.

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

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

Примечание

Оконные функции доступны только в MySQL 8.0 и новее. Перед началом работы убедитесь, что версия вашего сервера подходит:

SELECT VERSION();

Цель и формат

  • Работаем локально в MySQL (клиент mysql, MySQL Workbench или любой другой инструмент из лекции 17).
  • Все команды фиксируем в файле practice_log.sql, чтобы преподаватель мог воспроизвести ход работы (подробнее — в разделе «Что сдавать»).
  • На выполнение отводится 90 минут. Задания выполняются последовательно: результат каждого следующего опирается на предыдущее.
  • Если задание не получается — используйте подсказку, а затем сверьте своё решение с образцом. Не переходите к следующему заданию, пока текущий запрос не вернул ожидаемый результат.

Подготовка

  1. Создайте базу данных company и выполните скрипт создания таблиц departments и employees из лекции 39 (для удобства он приведён ниже полностью).

  2. Включите вывод в удобном формате. Для клиента mysql:

mysql -u root -p company
-- В консоли клиента
SELECT * FROM employees\G
Полный скрипт подготовки базы
-- Создание базы
CREATE DATABASE IF NOT EXISTS company;
USE company;

-- Создание таблицы departments
CREATE TABLE departments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL
);

-- Создание таблицы employees
CREATE TABLE employees (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    salary DECIMAL(10,2) DEFAULT 0.00,
    hire_date DATE,
    department_id INT,
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

-- Вставка тестовых данных (как в лекции 39)
INSERT INTO departments (name) VALUES
('Разработка'),
('Маркетинг'),
('Финансы');

INSERT INTO employees (name, salary, hire_date, department_id) VALUES
('Иван Петров',     85000.00, '2019-03-15', 1),
('Мария Сидорова',  92000.00, '2018-11-01', 1),
('Алексей Иванов',  78000.00, '2020-06-20', 2),
('Ольга Кузнецова', 95000.00, '2017-02-10', 3),
('Дмитрий Смирнов', 88000.00, '2021-09-05', 1),
('Елена Волкова',   76000.00, '2022-01-17', 2),
('Сергей Козлов',   92000.00, '2019-08-30', 3),
('Анна Морозова',   85000.00, '2023-04-12', 1),
('Павел Лебедев',   99000.00, '2016-05-23', 3),
('Наталья Орлова',  81000.00, '2021-12-01', 2);

Совет

Подготовьте practice_log.sql заранее: первым комментарием укажите ФИО студента (например, -- Иванов Иван Иванович), далее по ходу занятия вставляйте команды и краткие пояснения — что делает запрос и какой результат ожидается.

После выполнения скрипта в базе company 3 отдела и 10 сотрудников — стартовое состояние перед практикумом.


Задание 1. Расширяем базу: отдел продаж

Чтобы аналитические запросы были интереснее, добавим четвёртый отдел — «Продажи» — и шесть новых сотрудников.

Условие:

  1. Добавьте отдел с названием 'Продажи' (он получит id = 4).
  2. Добавьте шесть сотрудников по данным из таблицы:
namesalaryhire_datedepartment_id
Виктор Ерёмин83000.002020-09-144
Татьяна Белова87000.002018-04-024
Григорий Фомин79000.002022-07-254
Инна Соколова94000.002017-11-304
Кирилл Данилов86000.002023-01-094
Вера Гусева82000.002021-05-184

Решение:

INSERT INTO departments (name) VALUES ('Продажи');

INSERT INTO employees (name, salary, hire_date, department_id) VALUES
('Виктор Ерёмин',   83000.00, '2020-09-14', 4),
('Татьяна Белова',  87000.00, '2018-04-02', 4),
('Григорий Фомин',  79000.00, '2022-07-25', 4),
('Инна Соколова',   94000.00, '2017-11-30', 4),
('Кирилл Данилов',  86000.00, '2023-01-09', 4),
('Вера Гусева',     82000.00, '2021-05-18', 4);

Контроль: запрос SELECT COUNT(*) FROM employees; должен вернуть 16, а SELECT COUNT(*) FROM departments; — 4. Итоговые данные:

idnamesalaryhire_datedepartment_id
1Иван Петров85000.002019-03-151
2Мария Сидорова92000.002018-11-011
3Алексей Иванов78000.002020-06-202
4Ольга Кузнецова95000.002017-02-103
5Дмитрий Смирнов88000.002021-09-051
6Елена Волкова76000.002022-01-172
7Сергей Козлов92000.002019-08-303
8Анна Морозова85000.002023-04-121
9Павел Лебедев99000.002016-05-233
10Наталья Орлова81000.002021-12-012
11Виктор Ерёмин83000.002020-09-144
12Татьяна Белова87000.002018-04-024
13Григорий Фомин79000.002022-07-254
14Инна Соколова94000.002017-11-304
15Кирилл Данилов86000.002023-01-094
16Вера Гусева82000.002021-05-184

Все последующие задания выполняются на этой расширенной базе.


Задание 2. Разминка: OVER () без партиций

Условие:

Выведите список сотрудников, добавив к каждой строке:

  • среднюю зарплату по всей компании (столбец company_avg);
  • долю зарплаты сотрудника в общем фонде оплаты труда в процентах (столбец percent_of_total), округлённую до двух знаков.

Отсортируйте результат по убыванию зарплаты.

Подсказка:

Используйте пустое окно OVER () — оно охватывает всю выборку (лекция 39, пример 3 и 10). Сумму всех зарплат даёт SUM(salary) OVER ().

Решение:

SELECT
    name,
    salary,
    department_id,
    ROUND(AVG(salary) OVER (), 2) AS company_avg,
    ROUND(salary / SUM(salary) OVER () * 100, 2) AS percent_of_total
FROM employees
ORDER BY salary DESC;

Результат:

namesalarydepartment_idcompany_avgpercent_of_total
Павел Лебедев99000.00386375.007.16
Ольга Кузнецова95000.00386375.006.87
Инна Соколова94000.00486375.006.80
Мария Сидорова92000.00186375.006.66
Сергей Козлов92000.00386375.006.66
Дмитрий Смирнов88000.00186375.006.37
Татьяна Белова87000.00486375.006.30
Кирилл Данилов86000.00486375.006.22
Иван Петров85000.00186375.006.15
Анна Морозова85000.00186375.006.15
Виктор Ерёмин83000.00486375.006.01
Вера Гусева82000.00486375.005.93
Наталья Орлова81000.00286375.005.86
Григорий Фомин79000.00486375.005.72
Алексей Иванов78000.00286375.005.64
Елена Волкова76000.00286375.005.50

Контроль: общий фонд оплаты труда — 1 382 000.00 (сумма всех зарплат), средняя — 86 375.00. Сумма столбца percent_of_total должна быть равна 100.00.


Задание 3. PARTITION BY: показатели по отделам

Условие:

Для каждого сотрудника выведите:

  • максимальную зарплату его отдела (dept_max);
  • среднюю зарплату его отдела (dept_avg), округлённую до двух знаков;
  • отклонение его зарплаты от средней по отделу (diff), округлённое до двух знаков.

Отсортируйте результат по номеру отдела и убыванию зарплаты.

Подсказка:

Разбейте окно на партиции секцией PARTITION BY department_id (лекция 39, примеры 4 и 9).

Решение:

SELECT
    name,
    salary,
    department_id,
    MAX(salary) OVER (PARTITION BY department_id) AS dept_max,
    ROUND(AVG(salary) OVER (PARTITION BY department_id), 2) AS dept_avg,
    ROUND(salary - AVG(salary) OVER (PARTITION BY department_id), 2) AS diff
FROM employees
ORDER BY department_id, salary DESC;

Результат:

namesalarydepartment_iddept_maxdept_avgdiff
Мария Сидорова92000.00192000.0087500.004500.00
Дмитрий Смирнов88000.00192000.0087500.00500.00
Иван Петров85000.00192000.0087500.00-2500.00
Анна Морозова85000.00192000.0087500.00-2500.00
Наталья Орлова81000.00281000.0078333.332666.67
Алексей Иванов78000.00281000.0078333.33-333.33
Елена Волкова76000.00281000.0078333.33-2333.33
Павел Лебедев99000.00399000.0095333.333666.67
Ольга Кузнецова95000.00399000.0095333.33-333.33
Сергей Козлов92000.00399000.0095333.33-3333.33
Инна Соколова94000.00494000.0085166.678833.33
Татьяна Белова87000.00494000.0085166.671833.33
Кирилл Данилов86000.00494000.0085166.67833.33
Виктор Ерёмин83000.00494000.0085166.67-2166.67
Вера Гусева82000.00494000.0085166.67-3166.67
Григорий Фомин79000.00494000.0085166.67-6166.67

Контроль: средние по отделам 1–3 совпадают с лекцией 39 (87500.00, 78333.33, 95333.33) — расширение базы их не изменило. Средняя по отделу продаж — 85166.67.

Инфо

Попробуйте выполнить то же самое через GROUP BY + самосоединение и сравните длину запросов: оконная версия короче и не схлопывает строки (лекция 39, раздел «Проблема: агрегаты теряют детализацию»).


Задание 4. Ранжирующие функции

Условие:

  1. Присвойте каждому сотруднику порядковый номер внутри его отдела при сортировке по убыванию зарплаты (ROW_NUMBER()).
  2. Для той же сортировки добавьте столбцы RANK() и DENSE_RANK() по всей компании (без партиций) и сравните поведение трёх функций на строках с одинаковой зарплатой.
  3. Разбейте всех сотрудников на 4 квартиля по зарплате функцией NTILE(4).

Подсказка:

ROW_NUMBER() — уникальные номера; RANK() пропускает номера после дублей (1, 2, 3, 3, 5); DENSE_RANK() не пропускает (1, 2, 3, 3, 4) — лекция 39, примеры 5–7.

Решение (пункты 1–2):

SELECT
    name,
    salary,
    department_id,
    ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num,
    RANK()       OVER (ORDER BY salary DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees
ORDER BY department_id, row_num;

Результат:

namesalarydepartment_idrow_numrnkdense_rnk
Мария Сидорова92000.001144
Дмитрий Смирнов88000.001265
Иван Петров85000.001398
Анна Морозова85000.001498
Наталья Орлова81000.00211311
Алексей Иванов78000.00221513
Елена Волкова76000.00231614
Павел Лебедев99000.003111
Ольга Кузнецова95000.003222
Сергей Козлов92000.003344
Инна Соколова94000.004133
Татьяна Белова87000.004276
Кирилл Данилов86000.004387
Виктор Ерёмин83000.0044119
Вера Гусева82000.00451210
Григорий Фомин79000.00461412

Обратите внимание на пару «Иван Петров / Анна Морозова» (зарплата 85000.00 в отделе 1): row_num различает их (3 и 4), а RANK() и DENSE_RANK() присваивают одинаковый ранг 9 и 8 соответственно.

Решение (пункт 3):

SELECT
    name,
    salary,
    NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees
ORDER BY salary DESC;

Результат:

namesalaryquartile
Павел Лебедев99000.001
Ольга Кузнецова95000.001
Инна Соколова94000.001
Мария Сидорова92000.001
Сергей Козлов92000.002
Дмитрий Смирнов88000.002
Татьяна Белова87000.002
Кирилл Данилов86000.002
Иван Петров85000.003
Анна Морозова85000.003
Виктор Ерёмин83000.003
Вера Гусева82000.003
Наталья Орлова81000.004
Григорий Фомин79000.004
Алексей Иванов78000.004
Елена Волкова76000.004

16 строк разделены на 4 квартиля ровно по 4 строки в каждом.


Задание 5. Накопительные итоги

Условие:

  1. Постройте накопительный (бегущий) итог фонда оплаты труда в порядке даты приёма на работу (running_total).
  2. Для каждой строки покажите также накопительную сумму внутри своего отдела (dept_running_total), отсортировав строки по возрастанию стажа (по hire_date).

Подсказка:

SUM(...) OVER (ORDER BY hire_date) даёт накопительный итог благодаря рамке по умолчанию RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (лекция 39, пример 8; лекция 40, раздел «Рамка по умолчанию»). Для накопительной суммы по отделу добавьте PARTITION BY department_id.

Решение:

SELECT
    name,
    salary,
    department_id,
    hire_date,
    SUM(salary) OVER (ORDER BY hire_date) AS running_total,
    SUM(salary) OVER (PARTITION BY department_id ORDER BY hire_date) AS dept_running_total
FROM employees
ORDER BY hire_date;

Результат:

namesalarydepartment_idhire_daterunning_totaldept_running_total
Павел Лебедев99000.0032016-05-2399000.0099000.00
Ольга Кузнецова95000.0032017-02-10194000.00194000.00
Инна Соколова94000.0042017-11-30288000.0094000.00
Татьяна Белова87000.0042018-04-02375000.00181000.00
Мария Сидорова92000.0012018-11-01467000.0092000.00
Иван Петров85000.0012019-03-15552000.00177000.00
Сергей Козлов92000.0032019-08-30644000.00286000.00
Алексей Иванов78000.0022020-06-20722000.0078000.00
Виктор Ерёмин83000.0042020-09-14805000.00264000.00
Вера Гусева82000.0042021-05-18887000.00346000.00
Дмитрий Смирнов88000.0012021-09-05975000.00265000.00
Наталья Орлова81000.0022021-12-011056000.00159000.00
Елена Волкова76000.0022022-01-171132000.00235000.00
Григорий Фомин79000.0042022-07-251211000.00425000.00
Кирилл Данилов86000.0042023-01-091297000.00511000.00
Анна Морозова85000.0012023-04-121382000.00350000.00

Контроль: последняя строка running_total равна полному фонду оплаты труда — 1 382 000.00. Последние значения dept_running_total для каждого отдела равны его суммарному фонду: отдел 1 — 350000.00, отдел 2 — 235000.00, отдел 3 — 286000.00, отдел 4 — 511000.00.

Примечание

dept_running_total в этом запросе — накопительная сумма по отделам (каждая партиция «обнуляется»), а не итог по отделу. Если нужен именно полный итог по отделу у каждой строки — задайте рамку явно (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), см. задание 7.


Задание 6. LAG() и LEAD(): сравнение с соседними строками

Условие:

  1. Выведите сотрудников в порядке даты приёма. Для каждого покажите зарплату предыдущего принятого сотрудника (prev_salary) и процент изменения зарплаты относительно него (growth_pct), округлённый до двух знаков.
  2. Тем же запросом добавьте зарплату следующего принятого сотрудника (next_salary) с помощью LEAD().

Подсказка:

LAG(столбец) — значение из предыдущей строки окна, LEAD(столбец) — из следующей. Для первой строки LAG() вернёт NULL (лекция 39, пример 11; лекция 40, пример 9).

Решение:

SELECT
    name,
    salary,
    hire_date,
    LAG(salary) OVER (ORDER BY hire_date) AS prev_salary,
    ROUND(
        (salary - LAG(salary) OVER (ORDER BY hire_date))
        / LAG(salary) OVER (ORDER BY hire_date) * 100, 2
    ) AS growth_pct,
    LEAD(salary) OVER (ORDER BY hire_date) AS next_salary
FROM employees
ORDER BY hire_date;

Результат:

namesalaryhire_dateprev_salarygrowth_pctnext_salary
Павел Лебедев99000.002016-05-23NULLNULL95000.00
Ольга Кузнецова95000.002017-02-1099000.00-4.0494000.00
Инна Соколова94000.002017-11-3095000.00-1.0587000.00
Татьяна Белова87000.002018-04-0294000.00-7.4592000.00
Мария Сидорова92000.002018-11-0187000.005.7585000.00
Иван Петров85000.002019-03-1592000.00-7.6192000.00
Сергей Козлов92000.002019-08-3085000.008.2478000.00
Алексей Иванов78000.002020-06-2092000.00-15.2283000.00
Виктор Ерёмин83000.002020-09-1478000.006.4182000.00
Вера Гусева82000.002021-05-1883000.00-1.2088000.00
Дмитрий Смирнов88000.002021-09-0582000.007.3281000.00
Наталья Орлова81000.002021-12-0188000.00-7.9576000.00
Елена Волкова76000.002022-01-1781000.00-6.1779000.00
Григорий Фомин79000.002022-07-2576000.003.9586000.00
Кирилл Данилов86000.002023-01-0979000.008.8685000.00
Анна Морозова85000.002023-04-1286000.00-1.16NULL

Контроль: prev_salary у первого сотрудника и next_salary у последнего равны NULL. Чтобы убрать NULL из отчёта, оберните вычисление в COALESCE() или отфильтруйте строки через CTE.


Задание 7. Рамки окна: ROWS и RANGE

Условие:

  1. Для каждого сотрудника (в порядке даты приёма) вычислите скользящее среднее зарплаты по трём строкам: две предыдущие плюс текущая (rolling_avg_3), округлённое до двух знаков.
  2. Выведите также полную сумму зарплат отдела (dept_total), несмотря на наличие ORDER BY в окне, — используйте рамку ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
  3. Самостоятельно сравните ROWS и RANGE на бегущей сумме при сортировке по убыванию зарплаты и объясните разницу на строках с одинаковой зарплатой (для проверки возьмите пример 2 из лекции 40).

Подсказка:

Скользящее окно — ROWS BETWEEN 2 PRECEDING AND CURRENT ROW (лекция 40, пример 1). Полная партиция при наличии ORDER BY — рамка UNBOUNDED PRECEDING ... UNBOUNDED FOLLOWING (лекция 40, раздел «Сводка границ рамки»).

Решение (пункты 1–2):

SELECT
    name,
    salary,
    department_id,
    hire_date,
    ROUND(AVG(salary) OVER (ORDER BY hire_date
                            ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rolling_avg_3,
    SUM(salary) OVER (PARTITION BY department_id
                      ORDER BY hire_date
                      ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS dept_total
FROM employees
ORDER BY hire_date;

Результат:

namesalarydepartment_idhire_daterolling_avg_3dept_total
Павел Лебедев99000.0032016-05-2399000.00286000.00
Ольга Кузнецова95000.0032017-02-1097000.00286000.00
Инна Соколова94000.0042017-11-3096000.00511000.00
Татьяна Белова87000.0042018-04-0292000.00511000.00
Мария Сидорова92000.0012018-11-0191000.00350000.00
Иван Петров85000.0012019-03-1588000.00350000.00
Сергей Козлов92000.0032019-08-3089666.67286000.00
Алексей Иванов78000.0022020-06-2085000.00235000.00
Виктор Ерёмин83000.0042020-09-1484333.33511000.00
Вера Гусева82000.0042021-05-1881000.00511000.00
Дмитрий Смирнов88000.0012021-09-0584333.33350000.00
Наталья Орлова81000.0022021-12-0183666.67235000.00
Елена Волкова76000.0022022-01-1781666.67235000.00
Григорий Фомин79000.0042022-07-2578666.67511000.00
Кирилл Данилов86000.0042023-01-0980333.33511000.00
Анна Морозова85000.0012023-04-1283333.33350000.00

Контроль: rolling_avg_3 первых двух строк считается по одной и двум строкам (окно «упирается» в начало партиции): 99000.00 и 97000.00. Значения dept_total одинаковы для всех строк отдела и равны суммам из задания 5: 350000.00, 235000.00, 286000.00, 511000.00.

Решение (пункт 3) — сравнение ROWS и RANGE:

SELECT
    name,
    salary,
    SUM(salary) OVER (ORDER BY salary DESC
                      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_running,
    SUM(salary) OVER (ORDER BY salary DESC
                      RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS range_running
FROM employees
ORDER BY salary DESC;

Фрагмент результата на строках с одинаковыми зарплатами (92000.00 и 85000.00):

namesalaryrows_runningrange_running
Мария Сидорова92000.00573000.00665000.00
Сергей Козлов92000.00665000.00665000.00
Иван Петров85000.00874000.00959000.00
Анна Морозова85000.00959000.00959000.00

(Полная таблица — 16 строк; суммы считаются накопительно по убыванию зарплаты.) Разница: ROWS обрабатывает строки с одинаковой зарплатой по очереди, поэтому у первой из пары значение меньше; RANGE включает в рамку все строки с тем же значением ключа сортировки, и обе строки пары получают одинаковое значение.


Задание 8. FIRST_VALUE, LAST_VALUE, NTH_VALUE

Условие:

Для каждого отдела выведите:

  • зарплату самого раннего принятого сотрудника отдела (first_salary, FIRST_VALUE() по hire_date);
  • зарплату самого позднего принятого сотрудника отдела (last_salary, LAST_VALUE() с рамкой UNBOUNDED PRECEDING ... UNBOUNDED FOLLOWING);
  • вторую по величине зарплату отдела (second_highest, NTH_VALUE(salary, 2)).

Отсортируйте результат по номеру отдела и возрастанию даты приёма.

Подсказка:

Без явной рамки LAST_VALUE() вернёт значение текущей строки (лекция 39, предупреждение в примере 12). Для NTH_VALUE(salary, 2) тоже нужна полная рамка, иначе для первой строки результатом будет NULL (лекция 40, пример 5).

Решение:

SELECT
    name,
    salary,
    department_id,
    hire_date,
    FIRST_VALUE(salary) OVER w AS first_salary,
    LAST_VALUE(salary) OVER w AS last_salary,
    NTH_VALUE(salary, 2) OVER w AS second_highest
FROM employees
WINDOW w AS (PARTITION BY department_id
             ORDER BY hire_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
ORDER BY department_id, hire_date;

Результат:

namesalarydepartment_idhire_datefirst_salarylast_salarysecond_highest
Мария Сидорова92000.0012018-11-0192000.0085000.0092000.00
Иван Петров85000.0012019-03-1592000.0085000.0092000.00
Дмитрий Смирнов88000.0012021-09-0592000.0085000.0092000.00
Анна Морозова85000.0012023-04-1292000.0085000.0092000.00
Алексей Иванов78000.0022020-06-2078000.0076000.0078000.00
Наталья Орлова81000.0022021-12-0178000.0076000.0078000.00
Елена Волкова76000.0022022-01-1778000.0076000.0078000.00
Павел Лебедев99000.0032016-05-2399000.0092000.0099000.00
Ольга Кузнецова95000.0032017-02-1099000.0092000.0099000.00
Сергей Козлов92000.0032019-08-3099000.0092000.0099000.00
Инна Соколова94000.0042017-11-3094000.0086000.0094000.00
Татьяна Белова87000.0042018-04-0294000.0086000.0094000.00
Виктор Ерёмин83000.0042020-09-1494000.0086000.0094000.00
Вера Гусева82000.0042021-05-1894000.0086000.0094000.00
Григорий Фомин79000.0042022-07-2594000.0086000.0094000.00
Кирилл Данилов86000.0042023-01-0994000.0086000.0086000.00

Примечание

Обратите внимание на second_highest: рамка отсортирована по hire_date (возрастание), поэтому «вторая строка» окна — это второй по дате приёма сотрудник отдела, а не вторая по величине зарплата. Именно поэтому во всех отделах, кроме 4-го, значение совпадает с first_salary (второй принятый зарабатывает столько же или больше остальных в отсортированной рамке), а в отделе 4 вторым по дате идёт Татьяна Белова с зарплатой 94000.00, но из-за рамки с ORDER BY hire_date NTH_VALUE(salary, 2) вернул 94000.00 — значение второй строки рамки.

Если нужна именно вторая по величине зарплата, окно должно быть отсортировано по убыванию зарплаты — как в примере 5 лекции 40:

SELECT
    name,
    salary,
    department_id,
    NTH_VALUE(salary, 2) OVER (PARTITION BY department_id ORDER BY salary DESC
                               ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS second_highest
FROM employees
ORDER BY department_id, salary DESC;

В этом случае получим: отдел 1 — 88000.00, отдел 2 — 78000.00, отдел 3 — 95000.00, отдел 4 — 87000.00.


Задание 9. Топ-N сотрудников в каждом отделе

Условие:

Выведите двух самых высокооплачиваемых сотрудников каждого отдела, включив название отдела. Используйте CTE с ROW_NUMBER().

Подсказка:

Оконную функцию нельзя использовать в WHERE — сначала пронумеруйте строки в CTE, затем отфильтруйте WHERE rn <= 2 (лекция 39, раздел «Ограничения»; лекция 40, пример 6). Название отдела получите через JOIN с таблицей departments.

Решение:

WITH ranked AS (
    SELECT
        e.name,
        e.salary,
        e.department_id,
        ROW_NUMBER() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS rn
    FROM employees e
)
SELECT
    r.name,
    r.salary,
    d.name AS department
FROM ranked r
JOIN departments d ON d.id = r.department_id
WHERE r.rn <= 2
ORDER BY r.department_id, r.salary DESC;

Результат:

namesalarydepartment
Мария Сидорова92000.00Разработка
Дмитрий Смирнов88000.00Разработка
Наталья Орлова81000.00Маркетинг
Алексей Иванов78000.00Маркетинг
Павел Лебедев99000.00Финансы
Ольга Кузнецова95000.00Финансы
Инна Соколова94000.00Продажи
Татьяна Белова87000.00Продажи

Дополнительно: замените ROW_NUMBER() на RANK() или DENSE_RANK() и объясните, как изменится результат, если в одном отделе появятся сотрудники с одинаковой зарплатой, претендующие на «второе место».


Задание 10. Дедупликация строк

Условие:

Представьте, что при импорте данных в базу попали дубликаты: два сотрудника с именем «Дмитрий Смирнов» в отделе 1 (записи с hire_date 2024-02-01 и 2024-03-01, зарплаты 87000.00 и 86000.00).

  1. Вставьте эти две записи.
  2. Найдите все дубликаты по паре (name, department_id), пронумеровав строки внутри групп через ROW_NUMBER() и оставив только rn > 1.
  3. Удалите лишние (продублированные) строки из таблицы employees.
  4. Проверьте, что в базе снова 16 сотрудников.

Подсказка:

Паттерн дедупликации: ROW_NUMBER() OVER (PARTITION BY name, department_id ORDER BY id) внутри CTE, затем фильтр WHERE rn > 1 (лекция 40, пример 7). Для удаления соедините таблицу с CTE.

Решение:

-- Шаг 1: имитация ошибочного импорта
INSERT INTO employees (name, salary, hire_date, department_id) VALUES
('Дмитрий Смирнов', 87000.00, '2024-02-01', 1),
('Дмитрий Смирнов', 86000.00, '2024-03-01', 1);

-- Шаг 2: поиск дубликатов
WITH numbered AS (
    SELECT
        id,
        name,
        department_id,
        ROW_NUMBER() OVER (PARTITION BY name, department_id ORDER BY id) AS rn
    FROM employees
)
SELECT id, name, department_id, rn
FROM numbered
WHERE rn > 1;

-- Шаг 3: удаление дубликатов
WITH numbered AS (
    SELECT
        id,
        ROW_NUMBER() OVER (PARTITION BY name, department_id ORDER BY id) AS rn
    FROM employees
)
DELETE e
FROM employees e
JOIN numbered n ON n.id = e.id
WHERE n.rn > 1;

-- Шаг 4: контроль
SELECT COUNT(*) AS employees_count FROM employees;

Ожидаемый результат шага 2:

idnamedepartment_idrn
18Дмитрий Смирнов12
19Дмитрий Смирнов13

(id могут отличаться в зависимости от порядка вставки; главное — rn > 1.) После шага 3 employees_count снова равен 16.

Инфо

Такой подход широко применяется после импорта данных из внешних источников: сначала данные загружаются без ограничений, затем дубликаты помечаются и удаляются одним запросом (лекция 40, пример 7).


Задание 11. Функции распределения: CUME_DIST и PERCENT_RANK

Условие:

  1. Для каждого сотрудника вычислите CUME_DIST() и PERCENT_RANK() по зарплате (сортировка по возрастанию), округлив до трёх знаков.
  2. Выделите топ-25% самых высокооплачиваемых сотрудников: оставьте строки, у которых cume_dist >= 0.75.

Подсказка:

CUME_DIST() — доля строк со значением ≤ текущего; PERCENT_RANK() — относительная позиция строки между первой и последней (лекция 40, примеры 3–4). Для фильтрации по значению оконной функции используйте CTE.

Решение (пункт 1):

SELECT
    name,
    salary,
    ROUND(CUME_DIST() OVER (ORDER BY salary), 3) AS cume_dist,
    ROUND(PERCENT_RANK() OVER (ORDER BY salary), 3) AS percent_rank
FROM employees
ORDER BY salary;

Результат:

namesalarycume_distpercent_rank
Елена Волкова76000.000.0630.000
Алексей Иванов78000.000.1250.067
Григорий Фомин79000.000.1880.133
Наталья Орлова81000.000.2500.200
Вера Гусева82000.000.3130.267
Виктор Ерёмин83000.000.3750.333
Иван Петров85000.000.5000.400
Анна Морозова85000.000.5000.400
Кирилл Данилов86000.000.5630.533
Татьяна Белова87000.000.6250.600
Дмитрий Смирнов88000.000.6880.667
Мария Сидорова92000.000.8130.733
Сергей Козлов92000.000.8130.733
Инна Соколова94000.000.8750.867
Ольга Кузнецова95000.000.9380.933
Павел Лебедев99000.001.0001.000

У сотрудников с одинаковой зарплатой (85000.00 и 92000.00) cume_dist одинаков — 0.500 и 0.813 соответственно, потому что доля считается по всем строкам с таким же значением.

Решение (пункт 2):

WITH dist AS (
    SELECT
        name,
        salary,
        CUME_DIST() OVER (ORDER BY salary) AS cume_dist
    FROM employees
)
SELECT name, salary
FROM dist
WHERE cume_dist >= 0.75
ORDER BY salary DESC;

Результат:

namesalary
Павел Лебедев99000.00
Ольга Кузнецова95000.00
Инна Соколова94000.00
Мария Сидорова92000.00
Сергей Козлов92000.00

В топ-25% попали 5 человек: из-за одинаковых зарплат 92000.00 в группу вошли обе строки, что как раз демонстрирует отличие CUME_DIST() от жёсткого деления на корзины через NTILE().


Задание 12. Комбинированные запросы

Условие:

  1. Постройте рейтинг сотрудников внутри отделов по убыванию зарплаты с названием отдела через JOIN и RANK().
  2. Классифицируйте каждого сотрудника относительно средней зарплаты его отдела через CASE: 'выше среднего', 'ниже среднего' или 'равна средней'.
  3. Вычислите медианную зарплату компании через ROW_NUMBER() и COUNT(*) OVER ().

Подсказка:

Оконные функции вычисляются после JOIN — соединение и оконные функции отлично сочетаются (лекция 40, примеры 10–11). Медиана: усреднить значения строк с номерами FLOOR((n+1)/2) и CEIL((n+1)/2) (лекция 40, пример 8).

Решение (пункт 1):

SELECT
    d.name AS department,
    e.name,
    e.salary,
    RANK() OVER (PARTITION BY d.id ORDER BY e.salary DESC) AS dept_rank
FROM employees e
JOIN departments d ON d.id = e.department_id
ORDER BY d.id, dept_rank;

Результат:

departmentnamesalarydept_rank
РазработкаМария Сидорова92000.001
РазработкаДмитрий Смирнов88000.002
РазработкаИван Петров85000.003
РазработкаАнна Морозова85000.003
МаркетингНаталья Орлова81000.001
МаркетингАлексей Иванов78000.002
МаркетингЕлена Волкова76000.003
ФинансыПавел Лебедев99000.001
ФинансыОльга Кузнецова95000.002
ФинансыСергей Козлов92000.003
ПродажиИнна Соколова94000.001
ПродажиТатьяна Белова87000.002
ПродажиКирилл Данилов86000.003
ПродажиВиктор Ерёмин83000.004
ПродажиВера Гусева82000.005
ПродажиГригорий Фомин79000.006

Решение (пункт 2):

SELECT
    name,
    salary,
    department_id,
    CASE
        WHEN salary > AVG(salary) OVER (PARTITION BY department_id) THEN 'выше среднего'
        WHEN salary < AVG(salary) OVER (PARTITION BY department_id) THEN 'ниже среднего'
        ELSE 'равна средней'
    END AS vs_dept_avg
FROM employees
ORDER BY department_id, salary DESC;

Результат:

namesalarydepartment_idvs_dept_avg
Мария Сидорова92000.001выше среднего
Дмитрий Смирнов88000.001выше среднего
Иван Петров85000.001ниже среднего
Анна Морозова85000.001ниже среднего
Наталья Орлова81000.002выше среднего
Алексей Иванов78000.002ниже среднего
Елена Волкова76000.002ниже среднего
Павел Лебедев99000.003выше среднего
Ольга Кузнецова95000.003ниже среднего
Сергей Козлов92000.003ниже среднего
Инна Соколова94000.004выше среднего
Татьяна Белова87000.004выше среднего
Кирилл Данилов86000.004выше среднего
Виктор Ерёмин83000.004ниже среднего
Вера Гусева82000.004ниже среднего
Григорий Фомин79000.004ниже среднего

Решение (пункт 3):

WITH ranked AS (
    SELECT
        salary,
        ROW_NUMBER() OVER (ORDER BY salary) AS rn,
        COUNT(*) OVER () AS total
    FROM employees
)
SELECT ROUND(AVG(salary), 2) AS median_salary
FROM ranked
WHERE rn IN (FLOOR((total + 1) / 2), CEIL((total + 1) / 2));

Результат:

median_salary
85500.00

Проверка вручную: упорядоченный ряд из 16 зарплат — 76000, 78000, 79000, 81000, 82000, 83000, 85000, 85000, 86000, 87000, 88000, 92000, 92000, 94000, 95000, 99000. Восьмая строка — 85000.00, девятая — 86000.00, их среднее равно 85500.00. Средняя зарплата по компании (86375.00) выше медианы — как и в лекции 40, «дорогие» зарплаты тянут среднее вверх, а медиана устойчива к выбросам.


Что сдавать

Сдать нужно папку, названную ФИО студента, например: Иванов Иван Иванович. Внутри папки должны лежать файлы:

  1. practice_log.sql — последовательность команд и комментариев, позволяющая воспроизвести весь практикум от подготовки базы до последнего задания.
  2. (опционально) дамп итоговой базы: mysqldump -u root -p company > company_dump.sql.

Пример структуры папки:

Иванов Иван Иванович/
├── practice_log.sql
└── company_dump.sql

Критерии оценки

Примечание

  • Оцениваются только те задания, которые преподаватель может воспроизвести по practice_log.sql.
  • Итоговое состояние базы после выполнения всех команд лога должно содержать 16 сотрудников и 4 отдела (дубликаты из задания 10 удалены).
  • Каждое задание оформлено отдельным блоком с комментарием -- Задание N: ....
  • Задания 1–4 (базовые конструкции OVER(), PARTITION BY, ранжирование).
  • Задания 5–8 (накопительные итоги, LAG/LEAD, рамки окна, FIRST_VALUE/LAST_VALUE/NTH_VALUE).
  • Задания 9–12 (топ-N через CTE, дедупликация, функции распределения, комбинированные запросы).
Выполнено заданийОценка
0–3неудовлетворительно
4–6удовлетворительно
7–9хорошо
10–12отлично

Заключение

Выполнив практикум, вы закрепили все ключевые приёмы работы с оконными функциями MySQL 8.0: построение окон над всей выборкой и отдельными партициями, ранжирование строк, накопительные и скользящие агрегаты, сравнение с соседними строками, явное управление рамкой окна, фильтрацию результатов оконных вычислений через CTE и дедупликацию данных. Эти навыки напрямую применяются в аналитических отчётах, построении дашбордов и подготовке данных — от «топ-N по группам» до расчёта медиан и процентилей.

Последнее обновление: 01.10.2026, 06:29
Предыдущая
ИТ.03 - 40 - Оконные функции в MySQL: продвинутый уровень
© Кафедра информационных технологий ЧУВО «ВШП», 2026. Версия: 0.35.39
Материалы доступны в соответствии с лицензией: