* Релационно деление с 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 реално не е нужен - с и без него се получава коректен резултат. А в кои случаи ще е нужен?
Добави коментар