Эффективный поиск по нескольким тегам в MySQL? - PullRequest
2 голосов
/ 14 апреля 2020

У меня есть простая схема базы данных и пример примерно так:

CREATE TABLE Media (
    id INT AUTO_INCREMENT PRIMARY KEY,
    file VARCHAR(255)
);

CREATE TABLE Tag (
    id INT AUTO_INCREMENT PRIMARY KEY,
    label VARCHAR(255)
);

CREATE TABLE Media_Tag (
    media_id INT,
    tag_id INT,
    PRIMARY KEY(media_id, tag_id)
);

INSERT INTO Media VALUES
    (1, "firetruck.jpg"),
    (2, "duck.jpg"),
    (3, "apple.jpg"),
    (4, "banana.jpg");

INSERT INTO Tag VALUES
    (1, "red"),
    (2, "yellow"),
    (3, "mobile"),
    (4, "immobile");

INSERT INTO Media_Tag VALUES
    (1, 1),
    (1, 3),
    (2, 2),
    (2, 3),
    (3, 1),
    (3, 4),
    (4, 2),
    (4, 4);

Если я хочу выполнить поиск по одному тегу, это довольно просто:

SELECT
    m.*
FROM
    Media m
    LEFT JOIN Media_Tag mt ON mt.media_id = m.id
    LEFT JOIN Tag t ON mt.tag_id = t.id
WHERE
    t.label = ?

Однако я Я заинтересован в поиске по двум тегам. Например, если пользователь искал «red» и «mobile», я хочу, чтобы only возвращал «firetruck.jpg», а не «apple». jpg "(только красный) или" duck.jpg "(только мобильный)


До сих пор я работал над решением, подобным следующему:

SELECT
    m.*
FROM
    Media m
    LEFT JOIN Media_Tag mt1 ON mt1.media_id = m.id
    LEFT JOIN Media_Tag mt2 ON mt1.media_id = mt2.media_id AND mt1.tag_id <> mt2.tag_id
    LEFT JOIN Tag t1 ON t1.id = mt1.tag_id
    LEFT JOIN Tag t2 ON t2.id = mt2.tag_id
WHERE
    t1.label = ? AND
    t2.label = ?

Это работает (и довольно быстро) за исключением того, что мне нужно добавить два дополнительных предложения JOIN для каждого тега, добавленного к параметрам поиска. Если я не знаю, сколько параметров поиска будет передано, мне нужно создать запрос с «максимальным» числом разрешенных параметров поиска, предварительно присоединив количество таблиц X.

Есть ли лучшее решение?

Я возился с такой идеей:

SELECT
    m.*
FROM
    Media m
    LEFT JOIN Media_Tag mt ON mt.media_id = m.id
    LEFT JOIN Tag t ON mt.tag_id = t.id
WHERE
    t.label IN ("red", "mobile")
GROUP BY
    <all fields on m>
HAVING
    COUNT(*) = <count-of-parameters>

Но я столкнулся с двумя проблемами при использовании этого в MySQL Workbench на примере набора данных из 500 000 строк :

  1. Решение с несколькими JOIN s выполнялось за 0,002 секунды, тогда как решение с GROUP BY и HAVING занимало целых 3 секунды
  2. Результаты * Решение 1039 * казалось в случайном порядке, тогда как результаты множественного решения JOIN возвращались в порядке первичного ключа таблицы Media

Я не совсем уверен, почему решение это так невероятно медленно. Может быть, я чего-то не понимаю в том, как пункты HAVING работают внутри. Но независимо от того, что результаты, возвращающиеся в случайном порядке, делают это решение непригодным для использования, потому что, боюсь, оно сломает нумерацию страниц.


Обновление 1:

Я узнал, что решение с несколькими JOIN с, работающими за 0,002 секунды на моем наборе данных 500k, было немного случайно. Сценарий, который я использовал для добавления данных, добавил элемент Media, а затем его теги. Это означало, что все теги для первых 100 элементов мультимедиа можно найти в верхней части таблицы тегов. Когда я выполнил свой поиск, у меня было предложение LIMIT 0,25 для mimi c нумерации страниц. Это заканчивало мой запрос рано, когда он нашел 25 соответствующих элементов мультимедиа, все из которых можно было найти в верхней части таблицы тегов.

С другой стороны, решением HAVING было сканирование * Таблица тегов 1057 * всего . Это объясняет 3 секунды - вот только сколько времени занимает сканирование таблицы из 1 миллиона строк.

Если я изменил свой поиск на что-то, что вернуло менее 25 элементов мультимедиа, то внезапно пришлось сканировать все стол и не может выйти рано, и решение JOIN также заняло 3 секунды.

Обновление 2:

Я не думаю, что я был ясен в своем исходном сообщении, поэтому Я хочу расширить это. Мой приоритет здесь эффективность , а не целостность данных, простота кода или нормализация. Моя текущая схема базы данных нормализована, но я хочу отменить ее, если это позволит более эффективный поиск.

В качестве эксперимента я исправил свою таблицу Media одно новое поле:

UPDATE TABLE Media ADD COLUMN all_tags varchar(255);

UPDATE
    Media m
    INNER JOIN (
        SELECT
            m.id,
            GROUP_CONCAT(t.label ORDER BY t.label ASC) as all_tags
        FROM
            Media b
            LEFT JOIN Media_Tag mt ON mt.media_id = m.id
            LEFT JOIN Tag t ON mt.tag_id = t.id
        GROUP BY
            m.id
        ORDER BY
            m.id
    ) j ON j.id = m.id
    SET m.all_tags = j.all_tags;

Моя новая таблица выглядит так:

+----+---------------+-----------------+
| id |      file     |     all_tags    |
+----+---------------+-----------------+
|  1 | firetruck.jpg |   mobile,red    |
|  2 |    duck.jpg   |  mobile,yellow  |
|  3 |   apple.jpg   |   immobile,red  |
|  4 |   banana.jpg  | immobile,yellow |
+----+---------------+-----------------+

Затем я могу выполнить поиск по тегам следующим образом:

SELECT * FROM Media WHERE all_tags LIKE "%tag1%tag2%...%";

Итак долго как tag1, tag2, et c. в алфавитном порядке (точно так же, как all_tags в алфавитном порядке), тогда это будет работать.

Это позволило выполнить поиск по полной таблице (поиск, который дал меньше предела нумерации страниц) примерно за 350 миллисекунд на мой набор данных из 500 тыс. элементов медиа. Намного лучше, но все же не там, где я хочу. Я стремлюсь к тому, чтобы время отклика было меньше 100 миллисекунд, если это возможно.

Ради интереса я добавил индекс для столбца all_tags и выполнил поиск точного соответствия:

SELECT * FROM Media WHERE all_tags = "mobile,red";

Это закончено в 0,2 миллисекунды . К сожалению, я не могу полагаться на точные совпадения. Кто-то, кто ищет два тега «mobile» и «red», должен также включить пункт «Media» с тегами three «cat», «mobile» и «red» - и так как «cat» появляется до "мобильного" в алфавитном порядке, единственный способ убедиться, что это появляется в наборе результатов, с помощью начального подстановочного знака в моем предложении LIKE, которое предотвращает использование индекса.

Я пытался Подумайте о более умных решениях, таких как 26 столбцов для «all_tags_starting_with_A», «all_tags_starting_with_B» и т. д. c - но я не могу обернуться вокруг наилучшего варианта.

1 Ответ

2 голосов
/ 15 апреля 2020

Решение с GROUP BY, безусловно, проще в обслуживании, поэтому его стоит попробовать, но оно применяется только к объединению Media_Tag и Tag и объединению результатов с Media:

SELECT m.*
FROM Media m
INNER JOIN (
  SELECT mt.media_id
  FROM Media_Tag mt INNER JOIN Tag t 
  ON mt.tag_id = t.id
  WHERE t.label IN ('red', 'mobile')
  GROUP BY mt.media_id
  HAVING COUNT(*) = 2
) t ON t.media_id = m.id;

Я изменил все объединения на INNER, потому что не вижу смысла в LEFT соединениях. Или с оператором IN вместо соединения с Media:

SELECT m.*
FROM Media m
WHERE m.id IN (
  SELECT mt.media_id
  FROM Media_Tag mt INNER JOIN Tag t 
  ON mt.tag_id = t.id
  WHERE t.label IN ('red', 'mobile')
  GROUP BY mt.media_id
  HAVING COUNT(*) = 2
);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...