* Релационно деление с MySQL
Публикувано на 29 април 2015 в раздел Бази от Данни.
Вече знаете за Декартовото произведение на таблици и как то се осъществява с JOIN операция без ON условие. Това е случаят, когато всеки елемент от едната релация се свърже с всеки елемент от другата. Бележи се с C = A x B. Операцията "релационно деление" e обратната операция на Декартовото произведение - можем да кажем, че A = C ÷ B, както и B = C ÷ A.
Нека вземем един пример - имаме списък със студенти, имаме списък с учебни предмети и имаме списък кой студент каква оценка има по даден учебен предмет:
CREATE TABLE students(
fn BIGINT UNSIGNED PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
INSERT INTO students (fn, name) VALUES
(123, "Ivan"), (456, "Maria"), (789, "Petar");
CREATE TABLE courses(
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE
);
INSERT INTO courses(name) VALUES
("Databases"), ("PIK3"), ("PTSK");
CREATE TABLE assessments(
student_fn BIGINT UNSIGNED NOT NULL,
FOREIGN KEY(student_fn) REFERENCES students(fn),
course_id SMALLINT UNSIGNED NOT NULL,
FOREIGN KEY(course_id) REFERENCES courses(id),
assessment DECIMAL(3,2) NOT NULL,
PRIMARY KEY(student_fn, course_id)
);
INSERT INTO assessments(student_fn, course_id, assessment)
VALUES (123, 1, 5.50), (123, 2, 3.80),
(456, 1, 4.50), (456, 2, 3.00), (456, 3, 4.00),
(789, 1, 5.50), (789, 2, 4.00), (789, 3, 6.00);
Искаме да изкараме списък с имената на тези студенти, които са завършили семестриално, т.е. с тези, които имат оценка по ВСИЧКИ. Вече може би се досещате, че тази информация може да бъде извадена чрез употребата на деление на таблицата assessments с таблицата courses - резултатът от едно релационно деление на тези две таблици би дало релация, в която ще участват само тези студенти, които биха участвали в Декартовото произведение на студенти и предмети, т.е. тези студенти, които са си взели всички предмети.
Извършването на релационно деление в SQL никак не е тривиална операция. Единият начин е да преброим веднъж кой студент колко предмета е взел, след това да преброим общия брой на предметите и накрая да премахнем студентите, които са взели по-малко от общия брой предмети:
SELECT students.name, COUNT(*) AS broi_pr
FROM students
JOIN assessments ON students.fn = assessments.student_fn
GROUP BY students.fn
HAVING broi_pr = ( SELECT COUNT(*)
FROM courses );
Виждаме, че тук не се вижда особена аналогия с операцията Декартово произведение. По-скоро може да се каже, че прилагаме някакъв изкуствен трик.
Класическото решение е с двойно отрицание.
SELECT DISTINCT name FROM students
WHERE NOT EXISTS (
SELECT 1 FROM courses
WHERE NOT EXISTS (
SELECT 1 FROM assessments
WHERE assessments.student_fn = students.fn
AND
assessments.course_id = courses.id
)
);
Двойното отрицание не е добре прието в ежедневния ни изказ. Тази заявка реално казва "вземи имената на хората, за които НЯМА курсове, които НЕ са взели". Този метод е коренно различен от горния, въпреки че дават по същество еднакви резултати.
Друг много подобен начин да се реши тази задача е следния:
SELECT DISTINCT name
FROM students
WHERE( SELECT id FROM courses
WHERE id NOT IN (
SELECT course_id
FROM assessments
WHERE student_fn = students.fn
)
) IS NULL;
С тази заявка реално казваме "вземи имената на студентите, за които курсовете, които НЕ са взели са празно множество".
За повече примери и по-подробна информация относно релационното деление, четете в статията Divided We Stand: The SQL of Relational Division.
Задача. В горните примери DISTINCT реално не е нужен - с и без него се получава коректен резултат. А в кои случаи ще е нужен?
Добави коментар