MySQL запрос слишком медленный - PullRequest
3 голосов
/ 15 мая 2019

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

У меня есть таблица MySQL с именем table_1.

Ниже инструкции create

CREATE TABLE `table_1` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `original_id` bigint(11) DEFAULT NULL,
  `invoice_num` bigint(11) DEFAULT NULL,
  `registration` timestamp NULL DEFAULT NULL,
  `paid_amount` decimal(10,6) DEFAULT NULL,
  `cost_amount` decimal(10,6) DEFAULT NULL,
  `profit_amount` decimal(10,6) DEFAULT NULL,
  `net_amount` decimal(10,6) DEFAULT NULL,
  `customer_id` bigint(11) DEFAULT NULL,
  `recipient_id` text,
  `cashier_name` text,
  `sales_type` text,
  `sales_status` text,
  `sales_location` text,
  `invoice_duration` text,
  `store_id` double DEFAULT NULL,
  `is_cash` int(11) DEFAULT NULL,
  `is_card` int(11) DEFAULT NULL,
  `brandid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_registration_compound` (`id`,`registration`)
) ENGINE=InnoDB AUTO_INCREMENT=47420958 DEFAULT CHARSET=latin1;

Я установил составной индекс из id + registration.

под запросом

SELECT 

store_id,
            CONCAT('[',GROUP_CONCAT(tot SEPARATOR ','),']') timeline_transactions,
            SUM(tot) AS total_transactions,
            CONCAT('[',GROUP_CONCAT(totalRevenues SEPARATOR ','),']') timeline_revenues,
            SUM(totalRevenues) AS revenues,
            CONCAT('[',GROUP_CONCAT(totalProfit SEPARATOR ','),']') timeline_profit,
            SUM(totalProfit) AS profit,
            CONCAT('[',GROUP_CONCAT(totalCost SEPARATOR ','),']') timeline_costs,
            SUM(totalCost) AS costs



 FROM (select t1.md,
COALESCE(SUM(t1.amount+t2.revenues), 0) AS totalRevenues,
COALESCE(SUM(t1.amount+t2.profit), 0) AS totalProfit,
COALESCE(SUM(t1.amount+t2.costs), 0) AS totalCost,
COALESCE(SUM(t1.amount+t2.tot), 0) AS tot,
t1.store_id

from
(
 SELECT a.store_id,b.md,b.amount from ( SELECT DISTINCT store_id FROM  table_1) AS a
  CROSS JOIN 
 (
 SELECT
  DATE_FORMAT(a.DATE, "%m") as md,
  '0' as  amount
  from (
    select curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) month as Date
    from (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as a
    cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as b
    cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as c
  ) a
  where a.Date >='2019-01-01' and a.Date <= '2019-01-14'
  group by md) AS b 
)t1
left join
(
  SELECT
                COUNT(epl.invoice_num) AS tot,
                SUM(paid_amount) AS revenues,
                SUM(profit_amount) AS profit,
                SUM(cost_amount) AS costs,
                store_id,
                date_format(epl.registration, '%m') md
                FROM table_1 epl
                GROUP BY store_id, date_format(epl.registration, '%m')
)t2
ON   t2.md=t1.md AND t2.store_id=t1.store_id
group BY t1.md, t1.store_id) AS t3 GROUP BY store_id  ORDER BY total_transactions desc

под объяснением

enter image description here enter image description here

Может быть, я должен изменить с отметка времени на datetime в registration столбце?

Ответы [ 2 ]

4 голосов
/ 15 мая 2019

Около 90% вашего времени выполнения будет использовано для выполнения GROUP BY store_id, date_format(epl.registration, '%m').

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

alter table table_1 add md varchar(2) as (date_format(registration, '%m')) stored

Я сохранил формат varchar, который вы использовали в этом месяце, вы также можете использовать число (например, tinyint) для месяца.

Для этого требуется MySQL 5.7, в противном случае вы можете использовать триггеры для достижения того же:

alter table table_1 add md varchar(2) null;
create trigger tri_table_1 before insert on table_1
for each row set new.md = date_format(new.registration,'%m');
create trigger tru_table_1 before update on table_1
for each row set new.md = date_format(new.registration,'%m');

Затем добавьте индекс, предпочтительно индекс покрытия, начиная с store_id и md, например,

create index idx_table_1_storeid_md on table_1 
   (store_id, md, invoice_num, paid_amount, profit_amount, cost_amount)

Если у вас есть другие аналогичные отчеты, вы можете проверить, используют ли они дополнительные столбцы и могут ли они извлечь выгоду из покрытия большего количества столбцов. Для индекса потребуется около 1,5 ГБ дискового пространства (и то, сколько времени потребуется вашему диску для чтения 1,5 ГБ, в основном в одиночку определяет время выполнения, за исключением кэширования).

Затем измените запрос на группировку по этому новому проиндексированному столбцу, например,

      ...
            SUM(cost_amount) AS costs,
            store_id,
            md -- instead of date_format(epl.registration, '%m') md
            FROM table_1 epl
            GROUP BY store_id, md -- instead of date_format(epl.registration, '%m')
)t2   ...

Этот индекс также позаботится о других 9% вашего времени выполнения, SELECT DISTINCT store_id FROM table_1, которые получат прибыль от индекса, начинающегося с store_id.

Теперь, когда о 99% вашего запроса позаботились, некоторые дальнейшие замечания:

  • подзапрос b и ваш диапазон дат where a.Date >='2019-01-01' and a.Date <= '2019-01-14' могут не выполнять то, о чем вы думаете. Вы должны запустить деталь SELECT DATE_FORMAT(a.DATE, "%m") as md, ... group by md отдельно, чтобы увидеть, что она делает. В своем текущем состоянии он даст вам одну строку с кортежем '01', 0, представляющим «январь», так что это в основном сложный способ выполнения select '01', 0. Если сегодня не 15-й или более поздний период, он ничего не возвращает (что, вероятно, непреднамеренно).

  • В частности, не ограничивает даты выставления счетов этим конкретным диапазоном, но учитывает все накладные, относящиеся к (всему) январю любого года. Если это именно то, что вы намеревались, вы должны (дополнительно) добавить этот фильтр напрямую, например, используя FROM table_1 epl where epl.md = '01' GROUP BY ..., сократив время выполнения на дополнительный коэффициент около 12. Итак (кроме 15-го и проблем с увеличением), с вашим текущим диапазоном вы должны получить тот же результат, если вы используете

      ...
            SUM(cost_amount) AS costs,
            store_id,
            md 
            FROM table_1 epl
            WHERE md = '01'
            GROUP BY store_id, md 
    )t2   ...
    

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

      ...
            SUM(cost_amount) AS costs,
            store_id,
            md 
            FROM table_1 epl
            WHERE epl.registration >='2019-01-01' 
               and epl.registration <= '2019-01-14'
            GROUP BY store_id, md 
    )t2   ...
    

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

  • могут быть некоторые дополнительные оптимизации, упрощения или улучшения в остальной части вашего запроса, например, group BY t1.md, t1.store_id выглядит избыточно и / или неправильно (указывает на то, что вы на самом деле не в MySQL 5.7), а b - подзапрос может давать только значения от 1 до 12, поэтому создание 1000 дат и их повторное сокращение может быть упрощено. Но так как они работают на 100-рядных строках, они не окажут существенного влияния на время выполнения, и я не проверял их подробно. Возможно, это происходит из-за получения правильного выходного формата или обобщения (хотя, если вы динамически группируете по другим форматам, а не по месяцам, вам нужны другие индексы / столбцы, но это будет другой вопрос).

Альтернативным способом предварительного расчета ваших значений будет сводная таблица, в которой вы, например, запускайте свой внутренний запрос (дорогой group by) один раз в день и сохраняйте результат в таблице, а затем используйте его повторно (выбирая из этой таблицы вместо того, чтобы группировать по). Это особенно актуально для таких данных, как счета-фактуры, которые никогда не меняются (хотя в противном случае вы можете использовать триггеры для обновления сводных таблиц). Это также становится более жизнеспособным, если у вас есть несколько сценариев, например, если ваш пользователь может решить сгруппировать по дням недели, году, месяцу или знаку зодиака, так как в противном случае вам нужно будет добавить индекс для каждого из них. Он становится менее жизнеспособным, если вам необходимо динамически ограничить диапазон счетов (например, 2019-01-01 ... 2019-01-14). Если вам нужно включить в отчет текущий день, вы все равно можете предварительно рассчитать, а затем добавить значения для текущей даты из таблицы (которая должна включать только очень ограниченное количество строк, что быстро, если у вас есть индекс, начинающийся с столбец даты), или используйте триггеры для обновления сводной таблицы на лету.

0 голосов
/ 16 мая 2019
  • С PRIMARY KEY(id) наличие INDEX(id, anything) практически бесполезно.

  • Проверьте, можете ли вы избежать вложенных подзапросов.

  • Подумайте о создании этой таблицы 'date' на постоянной основе и на ней есть PRIMARY KEY(md).В настоящее время ни один из подзапросов не имеет индекса для столбца соединения (md).

  • Возможно, у вас синдром "взрыва-взрыва".Здесь JOINs расширяет количество строк, только чтобы GROUP BY свернул их.

  • Не используйте COUNT(xx), если вам не нужно проверить xx длябыть NULL.Просто сделайте COUNT(*).

  • store_id double - Действительно?

  • TIMESTAMP против DATETIME - они выполняют потак же;не беспокойтесь об этом.

  • Поскольку вы смотрите только на 2019-01, избавьтесь от

    date_format(epl.registration, '%m')
    

    Это само по себе может ускорить егомного.(Однако вы теряете общность.)

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