Пролог к ​​SQL: Есть ли способ улучшить код SQL для модульных тестов и элегантно исправить крайний случай? - PullRequest
1 голос
/ 09 марта 2020

Вдохновленный этим вопросом StackOverflow:

Найти общий элемент в различных фактах в swi-prolog

У нас есть следующее

Постановка задачи

Приведена база данных "актеров, снимающихся в кино" (например, starsin - это связь актера "bob" с mov ie "a")

starsin(a,bob).
starsin(c,bob).

starsin(a,maria).
starsin(b,maria).
starsin(c,maria).

starsin(a,george).
starsin(b,george).
starsin(c,george).
starsin(d,george).

И дано набор фильмов M , найдите тех актеров, которые снимались во всех фильмах M .

Вопрос изначально был для Пролога.

Решение Prolog

В Prolog элегантное решение включает предикат setof/3, который собирает возможные экземпляры переменных в набор (который действительно является списком без повторяющихся значений):

actors_appearing_in_movies(MovIn,ActOut) :-
    setof(
        Ax,
        MovAx^(setof(Mx,starsin(Mx,Ax),MovAx), subset(MovIn,MovAx)),
        ActOut
    ).    

Я не буду go подробно рассказывать об этом, но давайте посмотрим на тестовый код, который представляет интерес здесь. Вот пять тестовых случаев:

actors_appearing_in_movies([],ActOut),permutation([bob, george, maria],ActOut),!. 
actors_appearing_in_movies([a],ActOut),permutation([bob, george, maria],ActOut),!.
actors_appearing_in_movies([a,b],ActOut),permutation([george, maria],ActOut),!.
actors_appearing_in_movies([a,b,c],ActOut),permutation([george, maria],ActOut),!.
actors_appearing_in_movies([a,b,c,d],ActOut),permutation([george],ActOut),!.

Тест - это вызов предиката actors_appearing_in_movies/2, которому присваивается входной список фильмов (например, [a,b]) и который захватывает результирующий список актеров в ActOut.

Впоследствии нам просто нужно проверить, является ли ActOut перестановкой ожидаемого набора акторов, следовательно, например:

permutation([george, maria],ActOut)`

"Is ActOut a list, который является перестановкой списка [george,maria]?.

Если этот вызов завершается успешно (думаю, не возвращается с false), тест проходит.

Terminal ! является оператором обрезки и используется для того, чтобы подсистема Prolog не пыталась найти другие решения, потому что мы хороши на этом этапе.

Обратите внимание, что для пустого набора фильмов , мы получаем всех актеров . Это возможно правильно: каждый актер снимается во всех фильмах пустого набора ( Vacuous Truth ).

Сейчас в SQL .

Эта проблема прямо в области реляционной алгебры, и есть SQL, поэтому давайте пр. go на этом. Здесь я использую MySQL.

Во-первых, установите факты.

DROP TABLE IF EXISTS starsin;

CREATE TABLE starsin (movie CHAR(20) NOT NULL, actor CHAR(20) NOT NULL);

INSERT INTO starsin VALUES
   ( "a" , "bob" ),
   ( "c" , "bob" ),
   ( "a" , "maria" ),
   ( "b" , "maria" ),
   ( "c" , "maria" ),
   ( "a" , "george" ),
   ( "b" , "george" ),
   ( "c" , "george" ),
   ( "d",  "george" );

Относительно набора фильмов, указанных в качестве входных данных, давая их в форма (временной) таблицы звучит естественно. В MySQL «временные таблицы» являются локальными для сеанса. Хорошо.

DROP TABLE IF EXISTS movies_in;
CREATE TEMPORARY TABLE movies_in (movie CHAR(20) NOT NULL);
INSERT INTO movies_in VALUES ("a"), ("b");

Подход:

Результаты теперь можно получить, получив для каждого актера пересечение набора фильмов, обозначенного movies_in, и набора фильмов в какой актер когда-либо появлялся (создавался для каждого актера с помощью внутреннего соединения), а затем подсчитывал (для каждого актера), имеет ли результирующий набор как минимум столько же записей, сколько набор movies_in.

Обернуть запрос в процедура по практическим соображениям. A разделитель полезен здесь:

DELIMITER $$

DROP PROCEDURE IF EXISTS actors_appearing_in_movies;

CREATE PROCEDURE actors_appearing_in_movies()
BEGIN

SELECT 
     d.actor 
   FROM 
     starsin d, movies_in q
   WHERE 
     d.movie = q.movie 
   GROUP BY 
     actor 
   HAVING 
     COUNT(*) >= (SELECT COUNT(*) FROM movies_in);

END$$

DELIMITER ;

Запустите его!

Появляется проблема A:

Есть ли лучше чем редактировать + копировать-вставить код создания таблицы, выдать CALL и проверить результаты "вручную"?

DROP TABLE IF EXISTS movies_in;
CREATE TEMPORARY TABLE movies_in (movie CHAR(20) NOT NULL);
CALL actors_appearing_in_movies();

Пустой набор!

Появляется проблема B:

Выше не требуется, я хочу "всех актеров", так же, как для решение Пролог. Поскольку я не хочу добавлять в код странное исключение в крайнем случае, мой подход должен быть неверным. Есть ли тот, который естественно охватывает этот случай, но не становится слишком сложным? T- SQL и PostgreSQL однострочники тоже подойдут!

Другие тестовые примеры дают ожидаемые данные:

DROP TABLE IF EXISTS movies_in;
CREATE TEMPORARY TABLE movies_in (movie CHAR(20) NOT NULL);
INSERT INTO movies_in VALUES ("a"), ("b");
CALL actors_appearing_in_movies();
+--------+
| actor  |
+--------+
| george |
| maria  |
+--------+

DROP TABLE IF EXISTS movies_in;
CREATE TEMPORARY TABLE movies_in (movie CHAR(20) NOT NULL);
INSERT INTO movies_in VALUES ("a"), ("b"), ("c");
CALL actors_appearing_in_movies();
+--------+
| actor  |
+--------+
| george |
| maria  |
+--------+

DROP TABLE IF EXISTS movies_in;
CREATE TEMPORARY TABLE movies_in (movie CHAR(20) NOT NULL);
INSERT INTO movies_in VALUES ("a"), ("b"), ("c"), ("d");
CALL actors_appearing_in_movies();
+--------+
| actor  |
+--------+
| george |
+--------+

Ответы [ 2 ]

1 голос
/ 09 марта 2020

И учитывая набор фильмов M, найдите тех актеров, которые снимались во всех фильмах М.

Я бы использовал:

select si.actor
from starsin si
where si.movie in (<M>)
group by si.actor
having count(*) = <n>;

Если у вас есть чтобы иметь дело с пустым набором, тогда вам нужно left join:

select a.actor
from actors a left join
     starsin si
     on a.actor = si.actor and si.movie in (<M>)
group by a.actor
having count(si.movie) = <n>;

<n>, вот количество фильмов в <M>.

Обновление: второй подход в расширенная форма

create or replace temporary table 
   actor (actor char(20) primary key)
   as select distinct actor from starsin;

select 
   a.actor,
   si.actor,si.movie  -- left in for docu
from 
   actor a left join starsin si
     on a.actor = si.actor 
        and si.movie in (select * from movies_in)
group 
   by a.actor
having
   count(si.movie) = (select count(*) from movies_in);

Тогда для пустых movies_in:

+--------+-------+-------+
| actor  | actor | movie |
+--------+-------+-------+
| bob    | NULL  | NULL  |
| george | NULL  | NULL  |
| maria  | NULL  | NULL  |
+--------+-------+-------+

и для этого movies_in например:

+-------+
| movie |
+-------+
| a     |
| b     |
+-------+

movie здесь вершина группы:

+--------+--------+-------+
| actor  | actor  | movie |
+--------+--------+-------+
| george | george | a     |
| maria  | maria  | a     |
+--------+--------+-------+
0 голосов
/ 11 марта 2020

Следующее решение включает подсчет и запись UPDATE

здесь: Простая операция с реляционной базой данных

Мы используем MariaDB / MySQL SQL. T- SQL или PL / SQL более полные.

Обратите внимание, что SQL не имеет вектора типы данных, которые могут быть переданы в процедуры. Нужно работать без этого.

Введите факты в виде таблицы:

CREATE OR REPLACE TABLE starsin 
   (movie CHAR(20) NOT NULL, actor CHAR(20) NOT NULL, 
    PRIMARY KEY (movie, actor));

INSERT INTO starsin VALUES
   ( "a" , "bob" ),
   ( "c" , "bob" ),
   ( "a" , "maria" ),
   ( "b" , "maria" ),
   ( "c" , "maria" ),
   ( "a" , "george" ),
   ( "b" , "george" ),
   ( "c" , "george" ),
   ( "d",  "george" );

Введите процедуру для вычисления решения и фактически ... распечатайте ее.

DELIMITER $$

CREATE OR REPLACE PROCEDURE actors_appearing_in_movies()
BEGIN

   -- collect all the actors
   CREATE OR REPLACE TEMPORARY TABLE tmp_actor (actor CHAR(20) PRIMARY KEY)
     AS SELECT DISTINCT actor from starsin;

   -- table of "all actors x (input movies + '--' placeholder)"
   -- (combinations that are needed for an actor to show up in the result)
   -- and a flag indicating whether that combination shows up for real
   CREATE OR REPLACE TEMPORARY TABLE tmp_needed 
     (actor CHAR(20), 
      movie CHAR(20), 
      actual TINYINT NOT NULL DEFAULT 0,
     PRIMARY KEY (actor, movie))
   AS 
     (SELECT ta.actor, mi.movie FROM tmp_actor ta, movies_in mi)
     UNION
     (SELECT ta.actor, "--" FROM tmp_actor ta);

   -- SELECT * FROM tmp_needed;

   -- Mark those (actor, movie) combinations which actually exist
   -- with a numeric 1
   UPDATE tmp_needed tn SET actual = 1 WHERE EXISTS
      (SELECT * FROM starsin si WHERE
             si.actor = tn.actor AND si.movie = tn.movie);

   -- SELECT * FROM tmp_needed;

   -- The result is the set of actors in "tmp_needed" which have as many
   -- entries flagged "actual" as there are entries in "movies_in"

   SELECT actor FROM tmp_needed GROUP BY actor 
      HAVING SUM(actual) = (SELECT COUNT(*) FROM movies_in);

END$$

DELIMITER ;

Тестирование

Для MariaDB не существует готовой инфраструктуры для модульного тестирования, поэтому мы "тестируем вручную" и пишем процедуру, выход из которой мы проверяем вручную. Variadi c аргументы не существуют, векторные типы данных не существуют. Давайте примем до 4 фильмов в качестве входных данных и проверим результат вручную.

DELIMITER $$

CREATE OR REPLACE PROCEDURE 
   test_movies(IN m1 CHAR(20),IN m2 CHAR(20),IN m3 CHAR(20),IN m4 CHAR(20))
BEGIN
   CREATE OR REPLACE TEMPORARY TABLE movies_in (movie CHAR(20) PRIMARY KEY);   
   CREATE OR REPLACE TEMPORARY TABLE args (movie CHAR(20));
   INSERT INTO args VALUES (m1),(m2),(m3),(m4); -- contains duplicates and NULLs
   INSERT INTO movies_in (SELECT DISTINCT movie FROM args WHERE movie IS NOT NULL); -- clean
   DROP TABLE args;   
   CALL actors_appearing_in_movies();        
END$$

DELIMITER ;

Выше приведены все ручные тесты, в частности:

CALL test_movies(NULL,NULL,NULL,NULL);

+--------+
| actor  |
+--------+
| bob    |
| george |
| maria  |
+--------+
3 rows in set (0.003 sec)

Например, для CALL test_movies("a","b",NULL,NULL);

Сначала настройте стол со всеми актерами против во всех фильмах во входном наборе, включая "не существует" mov ie, представленный заполнителем --.

+--------+--------+-------+
| actual | actor  | movie |
+--------+--------+-------+
|      0 | bob    | --    |
|      0 | bob    | a     |
|      0 | bob    | b     |
|      0 | george | --    |
|      0 | george | a     |
|      0 | george | b     |
|      0 | maria  | --    |
|      0 | maria  | a     |
|      0 | maria  | b     |
+--------+--------+-------+

Затем пометьте те строки 1, где комбинация actor-mov ie действительно существует в starsin.

+--------+--------+-------+
| actual | actor  | movie |
+--------+--------+-------+
|      0 | bob    | --    |
|      1 | bob    | a     |
|      0 | bob    | b     |
|      0 | george | --    |
|      1 | george | a     |
|      1 | george | b     |
|      0 | maria  | --    |
|      1 | maria  | a     |
|      1 | maria  | b     |
+--------+--------+-------+

Наконец выберите актера для включения в решение если SUM(actual) равно количеству записей в таблице входных фильмов (это не может быть больше), так как это означает, что актер действительно появляется во всех фильмах таблицы входных фильмов. В особом случае, когда эта таблица пуста, комбинированная таблица actor-mov ie будет содержать только

+--------+--------+-------+
| actual | actor  | movie |
+--------+--------+-------+
|      0 | bob    | --    |
|      0 | george | --    |
|      0 | maria  | --    |
+--------+--------+-------+

, и, таким образом, будут выбраны все актеры, чего мы и хотим.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...