Базы данных | Практическое занятие 10
Создание сложных запросов. Объединение нескольких таблиц и работа с соединениями (Joins).
1. Настройка среды (pgAdmin 4)
- Запустите pgAdmin 4: Откройте приложение pgAdmin 4.
- Подключитесь к серверу: Подключитесь к вашему экземпляру сервера PostgreSQL. Это подключение должно быть уже настроено (согласно P1).
- Выберите базу данных: Перейдите к базе данных, предназначенной для этого курса, по пути
Servers -> [Имя вашего сервера] -> Databases
. Щелкните правой кнопкой мыши по имени базы данных и выберите “Инструмент запросов” (Query Tool). - Пример схемы: Для сегодняшних примеров мы будем использовать гипотетическую схему базы данных университета. Выполните следующий SQL-код в Инструменте запросов (Query Tool) (нажмите F5 или кнопку “Выполнить/Обновить” (значок треугольника/воспроизведения)), чтобы создать и заполнить необходимые таблицы:
-- Удаляем таблицы, если они существуют (чтобы начать с чистого листа)
DROP TABLE IF EXISTS enrollments;
DROP TABLE IF EXISTS courses;
DROP TABLE IF EXISTS instructors;
DROP TABLE IF EXISTS students;
DROP TABLE IF EXISTS departments;
-- Создаем таблицу Departments (Кафедры)
CREATE TABLE departments (
dept_id SERIAL PRIMARY KEY,
dept_name VARCHAR(100) NOT NULL UNIQUE,
building VARCHAR(50)
);
-- Создаем таблицу Instructors (Преподаватели)
CREATE TABLE instructors (
instructor_id SERIAL PRIMARY KEY,
instructor_name VARCHAR(100) NOT NULL,
dept_id INT REFERENCES departments(dept_id)
);
-- Создаем таблицу Courses (Курсы)
CREATE TABLE courses (
course_id SERIAL PRIMARY KEY,
course_name VARCHAR(100) NOT NULL,
credits INT CHECK (credits > 0),
dept_id INT REFERENCES departments(dept_id),
instructor_id INT REFERENCES instructors(instructor_id)
);
-- Создаем таблицу Students (Студенты)
CREATE TABLE students (
student_id SERIAL PRIMARY KEY,
student_name VARCHAR(100) NOT NULL,
major_dept_id INT REFERENCES departments(dept_id),
enrollment_year INT
);
-- Создаем таблицу Enrollments (Зачисления) (Связующая таблица)
CREATE TABLE enrollments (
enrollment_id SERIAL PRIMARY KEY,
student_id INT REFERENCES students(student_id),
course_id INT REFERENCES courses(course_id),
grade CHAR(1) CHECK (grade IN ('A', 'B', 'C', 'D', 'F')),
UNIQUE(student_id, course_id) -- Студент может записаться на курс только один раз
);
-- Вставляем пример данных
INSERT INTO departments (dept_name, building) VALUES
('Computer Science', 'Building A'),
('Mathematics', 'Building B'),
('Physics', 'Building A'),
('History', 'Building C');
INSERT INTO instructors (instructor_name, dept_id) VALUES
('Dr. Alan Turing', 1),
('Dr. Ada Lovelace', 1),
('Dr. Carl Gauss', 2),
('Dr. Marie Curie', 3),
('Dr. Herodotus', 4),
('Dr. Isaac Newton', NULL); -- Преподаватель еще не назначен на кафедру
INSERT INTO courses (course_name, credits, dept_id, instructor_id) VALUES
('Introduction to Programming', 4, 1, 1),
('Data Structures', 4, 1, 2),
('Calculus I', 4, 2, 3),
('Linear Algebra', 3, 2, 3),
('Quantum Mechanics', 4, 3, 4),
('Classical Mechanics', 4, 3, 6), -- Преподает Ньютон
('World History I', 3, 4, 5),
('Database Systems', 3, 1, 2); -- Преподает Лавлейс
INSERT INTO students (student_name, major_dept_id, enrollment_year) VALUES
('Alice Smith', 1, 2022),
('Bob Johnson', 2, 2021),
('Charlie Brown', 1, 2022),
('Diana Prince', 3, 2023),
('Eve Adams', NULL, 2021); -- Специальность не выбрана
INSERT INTO enrollments (student_id, course_id, grade) VALUES
(1, 1, 'A'), -- Алиса, Введение в программирование
(1, 2, 'B'), -- Алиса, Структуры данных
(1, 8, 'A'), -- Алиса, Системы баз данных
(2, 3, 'A'), -- Боб, Математический анализ I
(2, 4, 'B'), -- Боб, Линейная алгебра
(3, 1, 'B'), -- Чарли, Введение в программирование
(3, 8, 'C'), -- Чарли, Системы баз данных
(4, 5, 'A'); -- Диана, Квантовая механика
-- Примечание: Студентка Ева Адамс еще не записана ни на один курс.
-- Примечание: На курс 'World History I' еще нет записей.
2. Объединение нескольких таблиц (трех и более)
Основной принцип остается тем же, что и при соединении двух таблиц: вы последовательно связываете операторы JOIN
. Результат первого соединения (логически, промежуточная таблица) становится левой таблицей для второго соединения, и так далее.
Шаблон синтаксиса:
SELECT
t1.column1,
t2.column2,
t3.column3,
-- ... другие столбцы
FROM
table1 AS t1
JOIN
table2 AS t2 ON t1.joining_column = t2.joining_column
JOIN
table3 AS t3 ON t2.another_joining_column = t3.another_joining_column
-- Дополнительные операторы JOIN могут быть добавлены здесь
WHERE
-- Необязательные условия
ORDER BY
-- Необязательная сортировка;
Ключевые моменты:
- Псевдонимы крайне важны: Используйте псевдонимы таблиц (
t1
,t2
,t3
или более описательные, напримерs
,c
,d
), чтобы ваш запрос был читаемым и однозначным, особенно когда таблицы содержат столбцы с одинаковыми именами (например,dept_id
). - Порядок соединения важен (логически): Убедитесь, что условие
ON
правильно связывает таблицы, соединяемые на данном шаге. Обычно таблицы соединяют на основе связей по внешним ключам. - По умолчанию используется INNER JOIN: Если вы просто пишете
JOIN
, PostgreSQL предполагаетINNER JOIN
.
Пример 1: Вывести список студентов, курсов, на которые они записаны, и преподавателей, ведущих эти курсы.
Это требует соединения таблиц students
, enrollments
, courses
и instructors
.
students
связывается сenrollments
черезstudent_id
.enrollments
связывается сcourses
черезcourse_id
.courses
связывается сinstructors
черезinstructor_id
.
SELECT
s.student_name,
c.course_name,
i.instructor_name
FROM
students AS s
INNER JOIN
enrollments AS e ON s.student_id = e.student_id
INNER JOIN
courses AS c ON e.course_id = c.course_id
INNER JOIN
instructors AS i ON c.instructor_id = i.instructor_id
ORDER BY
s.student_name,
c.course_name;
Выполнение в pgAdmin 4:
- Введите запрос в Инструмент запросов (Query Tool).
- Нажмите F5 или щелкните по кнопке Execute (Выполнить).
- Просмотрите результаты на панели вывода данных (Data Output). Вы должны увидеть имена студентов в паре с курсами, которые они проходят, и именами соответствующих преподавателей.
3. Сочетание различных типов соединений
Иногда для достижения желаемого результата вам нужно сочетать INNER JOIN
с LEFT JOIN
(или RIGHT JOIN
, FULL OUTER JOIN
). Например, вы можете захотеть вывести все записи из одной таблицы, но только совпадающие записи из последующих соединений.
Пример 2: Вывести всех студентов и, если они записаны на какие-либо курсы, вывести эти курсы и их факультеты.
Нам нужны все студенты, поэтому мы начинаем с таблицы students
и используем LEFT JOIN
для таблиц enrollments
и courses
. Мы также хотим получить факультет курса.
- Начать с
students
. LEFT JOIN
сenrollments
(чтобы сохранить всех студентов, даже тех, кто не записан).LEFT JOIN
сcourses
(чтобы сохранить студентов, записанных на курсы, информация о которых может отсутствовать, хотя это менее вероятно при использовании внешних ключей. Концептуально это также поддерживает фокус на “всех студентах”).LEFT JOIN
сdepartments
(чтобы получить название факультета для курса).
SELECT
s.student_name,
c.course_name,
d.dept_name AS course_department
FROM
students AS s
LEFT JOIN
enrollments AS e ON s.student_id = e.student_id
LEFT JOIN
courses AS c ON e.course_id = c.course_id
LEFT JOIN
departments AS d ON c.dept_id = d.dept_id -- Соединение по факультету курса
ORDER BY
s.student_name,
c.course_name;
Пояснение:
students LEFT JOIN enrollments
: Гарантирует, что все студенты (например, ‘Eve Adams’) будут отображены, даже если у них нет совпадающих строк вenrollments
. Для таких студентов столбцы изenrollments
(и последующих соединений) будут иметь значениеNULL
.... LEFT JOIN courses
: Если у студента есть записи о зачислении (enrollments), мы пытаемся найти соответствующий курс.... LEFT JOIN departments
: Если мы нашли курс, мы пытаемся найти его факультет.
Наблюдение: Вы увидите, что ‘Eve Adams’ указана со значениями NULL
для course_name
и course_department
. Студенты, записанные на курсы, будут отображаться с названием курса и факультетом, который предлагает этот курс.
Пример 3: Вывести все факультеты и имена преподавателей на этих факультетах. Включить факультеты, даже если на них в настоящее время нет назначенных преподавателей.
- Начать с
departments
. LEFT JOIN
сinstructors
.
SELECT
d.dept_name,
d.building,
i.instructor_name
FROM
departments AS d
LEFT JOIN
instructors AS i ON d.dept_id = i.dept_id
ORDER BY
d.dept_name,
i.instructor_name;
Наблюдение: Выводятся все факультеты. Если на факультете есть преподаватели, их имена выводятся. Если факультету не назначены преподаватели в таблице instructors
(или если у преподавателя dept_id = NULL
, как у Dr. Isaac Newton), этот факультет все равно появится, возможно, со значением NULL
для имени преподавателя, или такой преподаватель не будет связан ни с одним из отображаемых факультетов.
4. Самосоединения (Self-Joins)
Самосоединение используется для соединения таблицы с самой собой. Это полезно, когда строки в одной таблице имеют связь с другими строками в той же самой таблице. Классический пример — таблица сотрудников (employee
), где один столбец хранит идентификатор (ID) менеджера сотрудника, который также является сотрудником в той же таблице.
Ключевое требование: Вы должны использовать псевдонимы таблиц (aliases), чтобы различать две концептуальные копии соединяемой таблицы.
Пример 4: Вывести список преподавателей и название кафедры, к которой они принадлежат. (Использование самосоединения для таблицы кафедр здесь нетипично, но давайте представим сценарий, где мы моделируем предварительные требования (prerequisites) в таблице courses
).
Изменение для лучшего примера самосоединения: Давайте добавим столбец prerequisite_course_id
в таблицу courses
.
Сначала обновим таблицу и данные (Выполните это в инструменте запросов pgAdmin Query Tool):
-- Добавляем столбец для предварительного курса в таблицу courses
ALTER TABLE courses
ADD COLUMN prerequisite_course_id INT REFERENCES courses(course_id) NULL; -- Разрешаем NULL для курсов без предварительных требований
-- Обновляем некоторые курсы, чтобы у них появились предварительные требования
UPDATE courses SET prerequisite_course_id = 1 WHERE course_name = 'Data Structures'; -- Data Structures требует Intro to Prog
UPDATE courses SET prerequisite_course_id = 3 WHERE course_name = 'Linear Algebra'; -- Linear Algebra требует Calculus I
UPDATE courses SET prerequisite_course_id = 1 WHERE course_name = 'Database Systems'; -- Database Systems требует Intro to Prog
Теперь сам запрос с самосоединением: Вывести список курсов и их предварительных требований.
SELECT
c1.course_name AS course,
c2.course_name AS prerequisite
FROM
courses AS c1 -- Основной курс
LEFT JOIN
courses AS c2 ON c1.prerequisite_course_id = c2.course_id -- Соединяем обратно с таблицей courses для получения предварительного курса
ORDER BY
c1.course_name;
Пояснение:
- Мы обращаемся к таблице
courses
дважды, используя псевдонимыc1
(представляет основной курс) иc2
(представляет предварительный курс). LEFT JOIN
гарантирует, что курсы без предварительных требований (гдеprerequisite_course_id
равенNULL
) также будут включены в список, показываяNULL
в столбцеprerequisite
.- Условие
ON
связываетprerequisite_course_id
основного курса (c1
) сcourse_id
предварительного курса (c2
).
Наблюдение: В результате будет показан каждый курс и, если для него определено предварительное требование, название этого предварительного курса.
5. Сочетание соединений с агрегированием и группировкой
Сложные запросы часто включают сочетание соединений с агрегатными функциями (COUNT
, AVG
и т. д. - P10) и операторами GROUP BY
/ HAVING
(P6).
Пример 5: Вывести список отделов и количество курсов, предлагаемых каждым. Включить только те отделы, которые предлагают более одного курса.
Это требует соединения таблиц departments
и courses
, затем группировки по отделу и фильтрации групп.
SELECT
d.dept_name,
COUNT(c.course_id) AS number_of_courses
FROM
departments AS d
INNER JOIN -- Или LEFT JOIN, если вы хотите видеть отделы с 0 курсов (до фильтрации)
courses AS c ON d.dept_id = c.dept_id
GROUP BY
d.dept_id, d.dept_name -- Группируем по первичному ключу и любым другим выбранным неагрегированным столбцам
HAVING
COUNT(c.course_id) > 1 -- Фильтруем группы на основе агрегированного количества
ORDER BY
number_of_courses DESC,
d.dept_name;
Пояснение:
- Таблица
departments
соединяется с таблицейcourses
поdept_id
.INNER JOIN
включает только те отделы, у которых есть хотя бы один курс.LEFT JOIN
изначально включил бы все отделы. GROUP BY d.dept_id, d.dept_name
: Группирует строки так, чтобы все курсы, принадлежащие одному и тому же отделу, оказались вместе. Мы группируем поdept_id
(уникальный идентификатор) и включаемdept_name
, потому что он указан в спискеSELECT
и не агрегируется.COUNT(c.course_id)
: Подсчитывает количество курсов внутри каждой группы отделов.HAVING COUNT(c.course_id) > 1
: Отфильтровывает группы (отделы), в которых количество курсов не превышает 1 (т.е. оставляет только те, где количество > 1).ORDER BY
: Сортирует итоговый результат.
Пример 6: Найдите средний балл для каждого курса.
Это требует объединения таблиц courses
и enrollments
с последующей группировкой. Примечание: Нам нужно обрабатывать буквенные оценки численно для усреднения.
-- Нам нужен способ преобразования оценок в числовые значения для AVG.
-- Для простоты здесь мы просто подсчитаем количество зачисленных студентов на каждый курс вместо вычисления средней оценки.
-- Настоящий средний балл потребовал бы оператора CASE или отдельной таблицы сопоставления (что может выходить за рамки простоты, принятой в этом контексте).
-- Давайте найдем количество студентов, зачисленных на каждый курс.
SELECT
c.course_name,
COUNT(e.student_id) AS number_of_students_enrolled
FROM
courses AS c
LEFT JOIN -- Используйте LEFT JOIN, чтобы включить курсы с нулевым количеством зачисленных студентов
enrollments AS e ON c.course_id = e.course_id
GROUP BY
c.course_id, c.course_name -- Группируем по курсу
ORDER BY
number_of_students_enrolled DESC,
c.course_name;
Наблюдение: Этот запрос выводит список всех курсов и показывает, сколько студентов зачислено на каждый, включая курсы с 0 зачисленных студентов, такие как ‘World History I’.
6. Лучшие практики для сложных объединений (JOIN)
- Используйте осмысленные псевдонимы (Aliases):
e
дляemployees
,d
дляdepartments
лучше, чемt1
,t2
. - Форматируйте ваш SQL: Используйте отступы и переносы строк, чтобы сделать сложные запросы читаемыми. Выравнивайте операторы
JOIN
,ON
,WHERE
. - Понимайте типы объединений (Join Types): Осознанно выбирайте
INNER JOIN
,LEFT JOIN
,RIGHT JOIN
илиFULL OUTER JOIN
в зависимости от того, нужно ли вам сохранять все строки из левой, правой или обеих таблиц, даже если совпадений нет. - Указывайте столбцы для объединения: Всегда используйте оператор
ON
с правильными столбцами для объединения. Избегайте старого синтаксиса объединения через запятую в оператореFROM
. - Тестируйте поэтапно: Создавайте сложные запросы шаг за шагом. Начните с двух таблиц, проверьте результат, затем добавьте следующее объединение и так далее.