Оптимизация Mysql Query For Group с помощью функций даты - PullRequest
2 голосов
/ 11 февраля 2011

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

report_table
item_id INT(11)
amount Decimal(8,2)
day DATE

Первичный ключ - item_id, день. Эта таблица в настоящее время содержит 37 тысяч записей с 92 различными предметами и 1200 разными днями. Я использую Mysql 5.1.

Вот мой выбор:

SELECT r.day, sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`, 
sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day` 
FROM `client_location_item` AS `cla`
 INNER JOIN `client_location` AS `cl`
 INNER JOIN `report_item_day` AS `r`
 INNER JOIN `report_item_day` AS `r2` 
 WHERE (r.item_id = cla.item_id) 
 AND (cla.location_id = cl.location_id) 
 AND (r.day between from_unixtime(1293840000) and from_unixtime(1296518399)) 
 AND (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999)) 
 AND (cl.location_code = 'LOCATION')
 group by month(r.day);

В настоящее время этот запрос занимает 2,2 секунды в моей среде. План объяснения:

'1', 'SIMPLE', 'cl', 'ALL', 'PRIMARY', NULL, NULL, NULL, '33', 'Using where; Using temporary; Using filesort'
'1', 'SIMPLE', 'cla', 'ref', 'PRIMARY,location_id,location_id_idxfk', 'location_id', '4', 'cl.location_id', '1', 'Using index'
'1', 'SIMPLE', 'r', 'ref', 'PRIMARY', 'PRIMARY', '4', cla.asset_id', '211', 'Using where'
'1', 'SIMPLE', 'r2', 'ALL', NULL, NULL, NULL, NULL, '37602', 'Using where; Using join buffer'

Если я добавлю индекс в столбец «день», вместо того, чтобы мой запрос выполнялся быстрее, он выполняется за 2,4 секунды. План объяснения для запроса на тот момент:

'1', 'SIMPLE', 'r2', 'range', 'report_day_day_idx', 'report_day_day_idx', '3', NULL, '1092', 'Using where; Using temporary; Using filesort'
'1', 'SIMPLE', 'r', 'range', 'PRIMARY,report_day_day_idx', 'report_day_day_idx', '3', NULL, '1180', 'Using where; Using join buffer'
'1', 'SIMPLE', 'cla', 'eq_ref', 'PRIMARY,location_id,location_id_idxfk', 'PRIMARY', '4', 'r.asset_id', '1', 'Using where'
'1', 'SIMPLE', 'cl', 'eq_ref', 'PRIMARY', 'PRIMARY', '4', cla.location_id', '1', 'Using where'

Согласно документации MySQL, наиболее эффективная группа по исполнению - это когда есть индекс для извлечения столбцов группировки. Но в нем также говорится, что единственными функциями, которые действительно могут использовать индексы, являются min () и max (). У кого-нибудь есть идеи, что я могу сделать для дальнейшей оптимизации моего запроса? Или почему моя «проиндексированная» версия работает медленнее, несмотря на то, что в целом она содержит меньше строк, чем неиндексированная версия?

Создать таблицу:

CREATE TABLE `report_item_day` (
  `item_id` int(11) NOT NULL,
  `amount` decimal(8,2) DEFAULT NULL,
  `day` date NOT NULL,
  PRIMARY KEY (`item_id`,`day`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

Конечно, другой вариант, который у меня есть, - это сделать вызовы по 2 дБ, по одному на каждый период времени. Если я это сделаю, сразу запрос для каждого падает до 0,031 с. Тем не менее, я чувствую, что должен быть способ оптимизировать этот запрос для достижения сопоставимых результатов.

Ответы [ 3 ]

2 голосов
/ 15 февраля 2011

Три вещи:

1) Я не вижу в предложении WHERE что-то для r2.item_id.Без этого r2 будет учитываться через декартово произведение и будет суммировать и другие item_ids.

Измените исходный запрос, чтобы он выглядел следующим образом:

SELECT r.day
      ,sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`
      ,sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day`
FROM `client_location_item` AS `cla`
INNER JOIN `client_location` AS `cl`
INNER JOIN `report_item_day` AS `r`
INNER JOIN `report_item_day` AS `r2`
WHERE (r.item_id = cla.item_id) AND (r2.item_id = cla.item_id) AND (cla.location_id = cl.location_id)
AND (r.day between from_unixtime(1293840000) and from_unixtime(1296518399))
AND (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999))
AND (cl.location_code = 'LOCATION')
group by month(r.day); 

Проверьте, изменяется ли EXPLAIN PLANпосле этого.

2) Сделайте следующее: ALTER TABLE report_itme_day ADD INDEX (date,item_id);

Индекс будет отсканирован вместо даты элемента.

Проверьте, не изменится ли ПЛАН ОБЪЯСНЕНИЯ после этого.

3) Последнее средство: реорганизовать запрос

SELECT r.day, sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`, sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day` FROM
(SELECT CLA.item_id FROM client_location CL,client_location_item CLA WHERE CLA.location_code = 'LOCATION' AND CLA.location_id=CL.location_id) A,
report_item_day r,
report_item_day r2,
WHERE (r.item_id  = A.item_id)
AND   (r2.item_id = A.item_id)
AND   (r.day  between from_unixtime(1293840000) and from_unixtime(1296518399))
AND   (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999))
group by month(r.day); 

Это, безусловно, может быть подвергнуто дальнейшему рефакторингу.Я просто переоснащил его.

Дайте ему попробовать !!!

1 голос
/ 15 февраля 2011

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

Я покажу вам два подхода к запросу за 2 периода за один раз.Первый - это объединение всех запросов.Он должен делать то, что уже делает ваш подход с двумя запросами.Он вернет 2 строки, по одной на каждый период.

select sum(r.amount)  / (count(distinct r.item_id)  * count(r.day) ) as curr_avg
  from report_item_day r
  join client_location_item cla using(item_id)
  join client_location      cl  using(location_id)
 where cl.location_code = 'LOCATION'
   and r.day between from_unixtime(1293840000) and from_unixtime(1296518399)
union all
select sum(r.amount)  / (count(distinct r.item_id)  * count(r.day) ) as prev_avg
  from report_item_day r
  join client_location_item cla using(item_id)
  join client_location      cl  using(location_id)
 where cl.location_code = 'LOCATION'
   and r.day between from_unixtime(1291161600) and from_unixtime(1293839999)

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

select period
      ,sum(amount) / (count(distinct item_id) * count(day) ) as avg_day
  from (select case when r.day between from_unixtime(1293840000) and from_unixtime(1296518399) then 'Current'
                    when r.day between from_unixtime(1291161600) and from_unixtime(1293839999) then 'Previous'
                end as period
               ,r.amount
               ,r.item_id
               ,r.day
           from report_item_day r
           join client_location_item cla using(item_id)
           join client_location      cl  using(location_id)
          where cl.location_code = 'LOCATION'
            and (    r.day between from_unixtime(1293840000) and from_unixtime(1296518399)
                  or r.day between from_unixtime(1291161600) and from_unixtime(1293839999)
                )
         ) v
 group 
     by period;

Примечание 1: Вы не дали нам DDL, поэтому я не могу проверить правильность синтаксиса
Примечание 2: рассмотрите возможность создания таблицы календаря с ключом DATE.Добавьте соответствующие столбцы, такие как MONTH, WEEK, FINANCIAL_YEAR и так далее, чтобы иметь возможность поддерживать отчетность, которую вы делаете.Запросы будут намного легче писать и понимать.

1 голос
/ 14 февраля 2011

Прежде всего (и это может быть просто эстетика), почему вы не используете предложения ON / USING в INNER JOIN? Зачем делать JOIN в предложении WHERE, а не в фактической части в FROM?

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

Теперь по запросу. Вот часть документа. в СОЕДИНЕНИЯХ:

The `conditional_expr` used with ON is any conditional expression of the form 
that can be used in a WHERE clause. Generally, you should use the ON clause for
conditions that specify how to join tables, and the WHERE clause to restrict
which rows you want in the result set.

Так что да, переместите условия соединения в предложение FROM. Также вас может заинтересовать синтаксис подсказки индекса: http://dev.mysql.com/doc/refman/5.0/en/index-hints.html

И, наконец, вы можете попробовать использовать представление, но опасайтесь проблем с производительностью: http://www.mysqlperformanceblog.com/2007/08/12/mysql-view-as-performance-troublemaker/

Удачи.

...