Что я могу сделать, чтобы оптимизировать этот запрос MySQL? - PullRequest
2 голосов
/ 13 ноября 2010

Я надеюсь, что некоторые из вас, кто является экспертами в MySQL, могут помочь мне оптимизировать мой поисковый запрос MySQL ...

Сначала немного информации:

Я работаю над небольшим упражнениемПриложение MySQL, имеющее функцию поиска.

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

Вот моя структура данных (упрощена для удобства чтения)

TABLE exercises
  ID
  title

TABLE searchtags
  ID
  title

TABLE exerciseSearchtags
  exerciseID -> exercises.ID
  searchtagID -> searchtags.ID

TABLE categories
  ID
  parentID -> ID
  title

TABLE exerciseCategories
  exerciseID -> exercises.ID
  categoryID -> categories.ID

Все таблицы InnoDB (без полнотекстового поиска).

Столбцы идентификаторов для упражнений, тегов поиска и категорий были проиндексированы.

«exercSearchtags» и «exercCategories» - это множество таблиц соединения, отражающих взаимосвязь между упражнениями и тегами поиска, а также упражнениями и категориями соответственно.Столбцы ExercID и searchtagID были проиндексированы в exercSearchtags, а столбцы exercID и categoryID проиндексированы в exercCategories.

Вот несколько примеров того, как могут выглядеть данные заголовка упражнения, заголовка категории и заголовка поискового тега.Все три типа могут иметь несколько слов в названии.

Exercises
    (ID - title)
    1 - Concentric Shoulder Internal Rotation in Prone
    2 - Straight Leg Raise Dural Mobility (Sural)
    3 - Push-Ups 

 Categories
    (ID - title)
    1 - Flexion
    2 - Muscles of Mastication
    3 - Lumbar Plexus

 Searchtags
    (ID - title)
    1 - Active Range of Motion
    2 - Overhead Press
    3 - Impingement

Теперь перейдем к поисковому запросу:

Поисковая система принимает произвольное количество введенных пользователем ключевых слов.

Я хочу оценить результаты поиска на основеколичество совпадений названий ключевых слов / категорий, совпадений названий ключевых слов / тегов поиска и совпадений названий ключевых слов / упражнений.

Для этого я использую следующий динамически генерируемый SQL:

  SELECT 
   exercises.ID AS ID,
   exercises.title AS title, 
   (

    // for each keyword, the following 
    // 3 subqueries are generated

    (
     SELECT COUNT(1) 
     FROM categories 
     LEFT JOIN exerciseCategories 
     ON exerciseCategories.categoryID = categories.ID 
     WHERE categories.title RLIKE CONCAT('[[:<:]]',?) 
     AND exerciseCategories.exerciseID = exercises.ID
    ) + 

    (
     SELECT COUNT(1) 
     FROM searchtags 
     LEFT JOIN exerciseSearchtags 
     ON exerciseSearchtags.searchtagID = searchtags.ID 
     WHERE searchtags.title RLIKE CONCAT('[[:<:]]',?) 
     AND exerciseSearchtags.exerciseID = exercises.ID
    ) +

    (
     SELECT COUNT(1) 
     FROM exercises AS exercises2 
     WHERE exercises2.title RLIKE CONCAT('[[:<:]]',?) 
     AND exercises2.ID = exercises.ID
    )

    // end subqueries

    ) AS relevance

    FROM 
    exercises

    LEFT JOIN exerciseCategories
      ON exerciseCategories.exerciseID = exercises.ID 

    LEFT JOIN categories
     ON categories.ID = exerciseCategories.categoryID

    LEFT JOIN exerciseSearchtags
     ON exerciseSearchtags.exerciseID = exercises.ID 

    LEFT JOIN searchtags
     ON searchtags.ID = exerciseSearchtags.searchtagID

    WHERE

    // for each keyword, the following 
    // 3 conditions are generated

    categories.title RLIKE CONCAT('[[:<:]]',?) OR 
    exercises.title RLIKE CONCAT('[[:<:]]',?) OR 
    searchtags.title RLIKE CONCAT('[[:<:]]',?) 

    // end conditions

    GROUP BY 
     exercises.ID

    ORDER BY
     relevance DESC

    LIMIT 
       $start, $results 

Все это работает просто отлично.Он возвращает релевантные результаты поиска на основе пользовательского ввода.

Однако меня беспокоит, что моё решение может плохо масштабироваться.Например, если пользователь вводит строку поиска из семи ключевых слов, это приведет к запросу с 21 подзапросом в расчете релевантности, который может начать замедляться, если таблицы станут большими.

Есть ли у кого-нибудькакие-нибудь предложения относительно того, как я могу оптимизировать вышеупомянутое?Есть ли лучший способ выполнить то, что я хочу?Я делаю какие-либо явные ошибки в вышеупомянутом?

Заранее спасибо за вашу помощь.

Ответы [ 2 ]

3 голосов
/ 14 ноября 2010

Я мог бы дать лучший ответ, если бы вы также предоставили некоторые данные, в частности, некоторые примеры ключевых слов и примеры title s из каждой из ваших таблиц, чтобы мы могли получить представление о том, что вы на самом деле пытаетесь найти. на. Но я постараюсь ответить тем, что вы предоставили.

Сначала позвольте мне изложить на английском языке то, что, по моему мнению, будет делать ваш запрос, а затем я объясню причины и способы его устранения.

Perform a full table scan of all instances of `exercises`
  For each row in `exercises`
    Find all categories attached via exerciseCategories
      For each combination of exercise and category
        Perform a full table scan of all instances of exerciseCategories
          Look up corresponding category
            Perform RLIKE match on title
        Perform a full table scan of all instances of exerciseSearchtags      
          Look up corresponding searchtag
            Perform RLIKE match on title
        Join back to exercises table to re-lookup self
            Perform RLIKE match on title

При условии, что у вас есть хотя бы несколько вменяемых индексов, получится E x C x (C + S + 1), где E - количество упражнений, C - среднее количество категорий для данного упражнения, а S среднее количество поисковых тегов для данного. Если у вас нет индексов хотя бы для тех идентификаторов, которые вы перечислили, то это будет работать намного хуже. Таким образом, часть вопроса зависит, в частности, от относительных размеров C и S, о которых я могу только догадываться. Если E равно 1000, а C и S - по 2-3, то вы будете сканировать 8-21000 строк. Если E равно 1 миллиону, C равно 2-3, а S равно 10-15, вы будете сканировать 26-57 миллионов строк. Если E равен 1 миллиону, а C или S - около 1000, то вы будете сканировать более 1 триллиона строк. Так что нет, это не будет хорошо масштабироваться.

1) ЛЕВЫЕ СОЕДИНЕНИЯ внутри ваших подзапросов игнорируются, поскольку предложения WERE в тех же самых запросах заставляют их быть обычными СОЕДИНЕНИЯМИ. Это не сильно влияет на производительность, но затуманивает ваши намерения.

2) RLIKE (и его псевдоним REGEXP) никогда не используют индексы AFAIK, поэтому они никогда не будут масштабироваться. Я могу только догадываться без выборочных данных, но я бы сказал, что если ваши поиски требуют совпадения границ слов, вам нужно нормализовать свои данные. Даже если ваши названия кажутся естественными строками для хранения, поиск по их части означает, что вы действительно рассматриваете их как набор слов. Поэтому вы должны либо использовать возможности полнотекстового поиска mysql , либо разбить заголовки на отдельные таблицы, в которых хранится одно слово в строке. Одна строка на слово, очевидно, увеличит вашу память, но сделает ваши запросы почти тривиальными, поскольку вы, похоже, выполняете только совпадения целых слов (в отличие от похожих слов, корней слов и т. Д.).

3) Последние левые соединения, которые у вас есть, являются причиной того, что в E x C части моей формулы вы будете выполнять одну и ту же работу C раз для каждого упражнения. Теперь, по общему признанию, в большинстве планов запросов подзапросы будут кэшироваться для каждой категории, поэтому на практике это не так плохо, как я предполагаю, но это будет не так в каждом случае, поэтому я даю вам сценарий наихудшего случая. Даже если вы сможете убедиться, что у вас есть правильные индексы, а оптимизатор запросов избежал всех этих дополнительных сканирований таблиц, вы все равно будете возвращать много избыточных данных, потому что ваши результаты будут выглядеть примерно так:

Exercise 1 info
Exercise 1 info
Exercise 1 info
Exercise 2 info
Exercise 2 info
Exercise 2 info
etc

потому что каждая строка упражнения дублируется для каждой записи категории упражнения, даже если вы ничего не возвращаете из категории или категории упражнения (а category.ID в вашем первом подзапросе фактически ссылается на категории, объединенные в этом подзапросе, а НЕ из внешнего запрос).

4) Поскольку большинство поисковых систем возвращают результаты с помощью подкачки страниц, я думаю, вам действительно нужны только первые X-результаты. Добавление LIMIT X к вашему запросу или, что еще лучше, LIMIT Y, X, где Y - текущая страница, а X - количество результатов, возвращаемых на страницу, значительно поможет оптимизировать ваш запрос, если ключевые слова для поиска возвращают много результатов.

Если вы можете предоставить нам немного больше информации о ваших данных, я могу обновить свой ответ, чтобы отразить это.

UPDATE

Основываясь на ваших ответах, вот мой предложенный запрос. К сожалению, без полнотекстового поиска или индексированных слов все еще будут проблемы с масштабированием, если ваша таблица категорий или таблица поисковых тегов очень велики.

 SELECT exercises.ID AS ID,
        exercises.title AS title,

        IF(exercises.title RLIKE CONCAT('[[:<:]]',?), 1, 0)
        +
        (SELECT COUNT(*)
           FROM categories
           JOIN exerciseCategories ON exerciseCategories.categoryID = categories.ID
          WHERE exerciseCategories.exerciseID = exercises.ID
            AND categories.title RLIKE CONCAT('[[:<:]]',?))
        +
        (SELECT COUNT(*)
           FROM searchtags
           JOIN exerciseSearchtags ON exerciseSearchtags.searchtagID = searchtags.ID
          WHERE exerciseSearchtags.exerciseID = exercises.ID
            AND searchtags.title RLIKE CONCAT('[[:<:]]',?))

   FROM exercises

ЗАКАЗАТЬ ПО Уместности DESCИмея релевантность> 0 LIMIT $ start, $ results

Обычно я бы не рекомендовал предложение HAVING, но оно не будет хуже, чем ваш RLIKE ... ИЛИ RLIKE ... и т. Д.

Это решает мои проблемы № 1, № 3, № 4, но № 2 все еще остается.Учитывая данные вашего примера, я бы предположил, что каждая таблица содержит не более нескольких десятков записей.В этом случае неэффективность RLIKE может быть недостаточно болезненной, чтобы стоить оптимизации одного слова в строке, но вы спрашивали о масштабировании.Только запрос с точным равенством (title = ?) или запрос с началом (title LIKE 'foo%') могут использовать индексы, которые являются абсолютной необходимостью, если вы собираетесь масштабировать строки в любой таблице.RLIKE и REGEXP не соответствуют этим критериям, независимо от того, используется ли регулярное выражение (а у вас запрос типа «содержит», который является худшим случаем).(Важно отметить, что title LIKE CONCAT(?, '%') НЕ достаточно хорош, потому что mysql видит, что он должен что-то вычислить, и игнорирует его индекс. Вам нужно добавить «%» в вашем приложении.)

1 голос
/ 13 ноября 2010

Попробуйте запустить план объяснения для запроса и посмотрите на строки, которые в данный момент не используют индекс. Стратегически добавьте индексы для этих строк.

Также, если возможно, уменьшите количество вызовов RLIKE в запросе, так как они будут дорогими.

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

...