C, PHP, VB, .NET

Дневникът на Филип Петров


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

 



Добави коментар

Адресът на електронната поща няма да се публикува


*