Оптимизация странного MySQL Query - PullRequest
0 голосов
/ 22 декабря 2009

Надеюсь, кто-то может помочь с этим. У меня есть запрос, который извлекает данные из приложения PHP и превращает их в представление для использования в приложении Ruby on Rails. Таблица приложения PHP представляет собой таблицу в стиле E-A-V со следующими бизнес-правилами:

Данные поля: имя, фамилия, адрес электронной почты, номер телефона и оператор мобильной связи:

  • Каждое свойство имеет два пользовательских поля: одно обязательное, другое не обязательное. Клиенты могут использовать один из них, а разные клиенты используют разные в зависимости от своих собственных правил (например, клиент A может не заботиться о имени и фамилии, но может клиент B)
  • Приложение RoR должно обрабатывать каждую "пару" свойств как единственное свойство.

Теперь вот запрос. Проблема в том, что он прекрасно работает примерно с 11 000 записей. Тем не менее, в реальной базе данных содержится более 40 000 запросов, и запрос выполняется крайне медленно, на выполнение которого уходит примерно 125 секунд, что совершенно недопустимо с точки зрения бизнеса. Мы обязательно должны получить эти данные, и нам нужно взаимодействовать с существующей системой.

Часть UserID предназначена для подделки внешнего ключа Rails-esque, который относится к таблице Rails. Я парень SQL Server, а не MySQL, так что, возможно, кто-то может указать, как улучшить этот запрос? Они (бизнес) требуют его ускорения, но я не знаю, как это сделать, поскольку требуются различные вызовы group_concat и ifnull, потому что мне нужно каждое поле для каждого клиента, а затем объединять данные.

select `ls`.`subscriberid` AS `id`,left(`l`.`name`,(locate(_utf8'_',`l`.`name`) - 1)) AS `user_id`,
ifnull(min((case when (`s`.`fieldid` in (2,35)) then `s`.`data` else NULL end)),_utf8'') AS `first_name`,
ifnull(min((case when (`s`.`fieldid` in (3,36)) then `s`.`data` else NULL end)),_utf8'') AS `last_name`,
ifnull(`ls`.`emailaddress`,_utf8'') AS `email_address`,
ifnull(group_concat((case when (`s`.`fieldid` = 81) then `s`.`data` when (`s`.`fieldid` = 154) then `s`.`data` else NULL end) separator ''),_utf8'') AS `mobile_phone`,
ifnull(group_concat((case when (`s`.`fieldid` = 100) then `s`.`data` else NULL end) separator ','),_utf8'') AS `sms_only`,
ifnull(group_concat((case when (`s`.`fieldid` = 34) then `s`.`data` else NULL end) separator ','),_utf8'') AS `mobile_carrier` 
from ((`list_subscribers` `ls` 
    join `lists` `l` on((`ls`.`listid` = `l`.`listid`)))
    left join `subscribers_data` `s` on((`ls`.`subscriberid` = `s`.`subscriberid`)))  
where (left(`l`.`name`,(locate(_utf8'_',`l`.`name`) - 1)) regexp _utf8'[[:digit:]]+') 
group by `ls`.`subscriberid`,`l`.`name`,`ls`.`emailaddress`

EDIT Я удалил регулярное выражение, и это ускорило запрос примерно до 20 секунд вместо почти 120 секунд. Если бы я мог удалить группу к тому времени, это было бы быстрее, но я не могу, поскольку удаление этого приводит к дублированию строк с пустыми данными для каждого поля вместо их агрегирования. Например:

С группой

id     user_id     first_name     last_name     email_address     mobile_phone     sms_only     mobile_carrier
1         1          John           Doe        jdoe@example.com    5551234567       0          Sprint

без группы по

id      user_id      first_name      last_name      email_address      mobile_phone      sms_only      mobile_carrier
1       1            John                           jdoe@xample.com
1       1                             Doe           jdoe@example.com
1       1                                           jdoe@example.com
1       1                                           jdoe@example.com   5551234567

И так далее. Нам нужен первый результат.

РЕДАКТИРОВАТЬ # 2

Похоже, что запрос все еще занимает много времени, но ранее сегодня он выполнялся всего за 20 секунд в рабочей базе данных. Не меняя ничего, тот же запрос теперь снова занимает более 60 секунд. Это по-прежнему неприемлемо ... есть еще идеи, как это улучшить?

Ответы [ 3 ]

1 голос
/ 22 декабря 2009

Это, без сомнения, второй самый отвратительный SQL-запрос, который я когда-либо видел: -)

Мой совет - поменять требования к скорости хранения. Это обычная уловка, используемая, когда вы обнаружите, что в ваших запросах много функций для каждой строки (ifnull, case и т. Д.). Эти функции для каждой строки никогда не масштабируются очень хорошо, так как таблица становится больше.

Создайте в таблице новые поля, в которых будут храниться значения, которые вы хотите извлечь, а затем рассчитать эти значения при вставке / обновлении (с триггером), а не при выборе. Технически это не нарушает 3NF, поскольку триггеры гарантируют согласованность данных между столбцами.

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

Пример того, что я имею в виду. Вы можете заменить:

case when (`s`.`fieldid` in (2,35)) then `s`.`data` else NULL end

с:

`s`.`data_2_35`

в вашем запросе, если ваш триггер вставки / обновления просто устанавливает для столбца data_2_35 значение data или NULL в зависимости от значения fieldid. Затем вы индексируете data_2_35 и, вуаля, мгновенное улучшение скорости за счет небольшого хранилища.

Этот прием можно проделать с пятью предложениями case, битом left/regexp и «голой» ifnull функцией (функции ifnull, содержащие min и group_concat, могут быть сложнее делает).

0 голосов
/ 22 декабря 2009

Первое, что бросается в глаза как источник всех неприятностей:

Таблица приложения PHP представляет собой таблицу в стиле E-A-V ...

Попытка преобразовать данные в формате EAV в обычный реляционный формат на лету с использованием SQL неизбежно будет неудобной и неэффективной. Так что не пытайтесь разбить его в обычный формат столбца на атрибут. Следующий запрос возвращает несколько строк на подписчика, по одной строке на атрибут EAV:

SELECT ls.subscriberid AS id,
  SUBSTRING_INDEX(l.name, _utf8'_', 1) AS user_id,
  COALESCE(ls.emailaddress, _utf8'') AS email_address,
  s.fieldid, s.data
FROM list_subscribers ls JOIN lists l ON (ls.listid = l.listid)
  LEFT JOIN subscribers_data s ON (ls.subscriberid = s.subscriberid
      AND s.fieldid IN (2,3,34,35,36,81,100,154)
WHERE SUBSTRING_INDEX(l.name, _utf8'_', 1) REGEXP _utf8'[[:digit:]]+'

Это исключает GROUP BY, который не очень хорошо оптимизирован в MySQL, - обычно это временная таблица, которая убивает производительность.

id      user_id      email_address     fieldid        data
1       1            jdoe@example.com  2              John
1       1            jdoe@example.com  3              Doe
1       1            jdoe@example.com  81             5551234567

Но вам придется отсортировать атрибуты EAV в коде приложения. То есть в этом случае нельзя использовать ActiveRecord. Извините за это, но это один из недостатков использования нереляционного дизайна, такого как EAV.

Следующее, что я замечаю, это манипуляции со строкой-убийцей (даже после того, как я упростил ее с помощью SUBSTRING_INDEX()). Когда вы выбираете подстроки из столбца, это говорит вам о том, что вы перегружали один столбец двумя различными частями информации. Одним из них является name, а другим - какой-то атрибут типа списка, который вы бы использовали для фильтрации запроса. Храните один фрагмент информации в каждом столбце.

Вы должны добавить столбец для этого атрибута и проиндексировать его. Тогда предложение WHERE может использовать индекс:

SELECT ls.subscriberid AS id,
  SUBSTRING_INDEX(l.name, _utf8'_', 1) AS user_id,
  COALESCE(ls.emailaddress, _utf8'') AS email_address,
  s.fieldid, s.data
FROM list_subscribers ls JOIN lists l ON (ls.listid = l.listid)
  LEFT JOIN subscribers_data s ON (ls.subscriberid = s.subscriberid
      AND s.fieldid IN (2,3,34,35,36,81,100,154)
WHERE l.list_name_contains_digits = 1;

Кроме того, вы должны всегда анализировать SQL-запрос с помощью EXPLAIN, если для них важно иметь хорошую производительность. В MS SQL Server есть аналогичная функция, поэтому вы должны привыкнуть к этой концепции, но терминология MySQL может отличаться.

Вам придется прочитать документацию, чтобы узнать, как интерпретировать отчет EXPLAIN в MySQL, здесь слишком много информации, чтобы описать ее.


Дополнительная информация: Да, я понимаю, что вы не можете покончить со структурой таблицы EAV. Можете ли вы создать дополнительную таблицу? Затем вы можете загрузить в него данные EAV:

CREATE TABLE subscriber_mirror (
  subscriberid INT PRIMARY KEY,
  first_name      VARCHAR(100),
  last_name       VARCHAR(100),
  first_name2     VARCHAR(100),
  last_name2      VARCHAR(100),
  mobile_phone    VARCHAR(100),
  sms_only        VARCHAR(100),
  mobile_carrier  VARCHAR(100)
);

INSERT INTO subscriber_mirror (subscriberid) 
    SELECT DISTINCT subscriberid FROM list_subscribers;

UPDATE subscriber_data s JOIN subscriber_mirror m USING (subscriberid)
SET m.first_name    = IF(s.fieldid = 2,  s.data, m.first_name),
    m.last_name     = IF(s.fieldid = 3,  s.data, m.last_name),
    m.first_name2   = IF(s.fieldid = 35, s.data, m.first_name2),
    m.last_name2    = IF(s.fieldid = 36, s.data, m.last_name2),
    m.mobile_phone  = IF(s.fieldid = 81, s.data, m.mobile_phone),
    m.sms_only      = IF(s.fieldid = 100, s.data, m.sms_only),
    m.mobile_carrer = IF(s.fieldid = 34,  s.data, m.mobile_carrier);

Это займет некоторое время, но вам нужно будет сделать это только тогда, когда вы получите новое обновление данных от поставщика. Впоследствии вы можете запросить subscriber_mirror в гораздо более традиционном SQL-запросе:

SELECT ls.subscriberid AS id, l.name+0 AS user_id,
  COALESCE(s.first_name, s.first_name2) AS first_name,
  COALESCE(s.last_name, s.last_name2) AS last_name,
  COALESCE(ls.email_address, '') AS email_address),
  COALESCE(s.mobile_phone, '') AS mobile_phone,
  COALESCE(s.sms_only, '') AS sms_only,
  COALESCE(s.mobile_carrier, '') AS mobile_carrier
FROM lists l JOIN list_subscribers USING (listid)
JOIN subscriber_mirror s USING (subscriberid)
WHERE l.name+0 > 0

Что касается идентификатора пользователя, который встроен в столбец l.name, то если цифры являются ведущими символами в значении столбца, MySQL позволяет преобразовать целочисленное значение намного проще:

Выражение типа '123_bill'+0 дает целочисленное значение 123. Выражение типа 'bill_123'+0 не имеет цифр в начале, поэтому оно возвращает целочисленное значение 0.

0 голосов
/ 22 декабря 2009

Проблема, скорее всего, ГДЕ условие:

where (left(`l`.`name`,(locate(_utf8'_',`l`.`name`) - 1)) regexp _utf8'[[:digit:]]+') 

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

...