MySQL查询太慢了

Posted

技术标签:

【中文标题】MySQL查询太慢了【英文标题】:MySQL query too much slow 【发布时间】:2019-05-14 23:21:59 【问题描述】:

我正在尝试查询以获取一些趋势统计数据,但基准测试真的很慢。查询执行时间约为 134 秒

我有一个名为 table_1mysql 表。

在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

下面的解释

也许我应该将 registration 列中的 timestamp 更改为 datetime

【问题讨论】:

出于好奇,这个表目前在您的系统中有多少行?猜测大约 48m? @ChrisForrence 是的,或多或少 4800 万行。 您应该检查您的 MySQL 性能设置。 - innodb_buffer_pool_size 是用于缓存表、索引和其他一些东西的内存量。 - 您可以在 MySQL 中配置多个 innodb_buffer_pool_instances 以增加读/写线程。尝试从查询中删除 order by 或设置正确的索引。更多详情参考网站:1.percona.com/blog/2014/01/28/… 2.saotn.org/mysql-innodb-performance-improvement 【参考方案1】:

大约 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_idmd 开头,例如

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

如果您有其他类似的报告,您可能需要检查它们是否使用了额外的列,并且可以从覆盖更多列中获益。该索引将需要大约 1.5GB 的存储空间(驱动器读取 1.5GB 所需的时间基本上将单独定义您的执行时间,没有缓存)。

然后将您的查询更改为按此新索引列分组,例如

      ...
            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 &gt;='2019-01-01' and a.Date &lt;= '2019-01-14' 可能不会像您认为的那样。你应该单独run the partSELECT DATE_FORMAT(a.DATE, "%m") as md, ... group by md 看看它做了什么。在目前的状态下,它会给你一个带有元组'01', 0的行,代表“一月”,所以它基本上是一种复杂的select '01', 0的处理方式。除非今天是 15 号或更晚,否则它不会返回任何内容(这可能是无意的)。

特别是,它将将发票日期限制在该特定范围内,而是限制从任何一年(整个)1 月开始的所有发票。如果这是您想要的,您应该(另外)直接添加该过滤器,例如通过使用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   ...

您可能(或可能没有)尝试过这样做。不过,在这种情况下,您将需要一个不同的索引(这将是一个稍微不同的问题)。

1234563子查询只能为您提供 1 到 12 的值,因此可以简化生成 1000 个日期并再次减少它们。但由于它们在 100 多行上运行,它们不会显着影响执行时间,我还没有详细检查这些。其中一些可能是由于获得了正确的输出格式或概括(尽管,如果您按月以外的其他格式动态分组,则需要其他索引/列,但这将是一个不同的问题)。

预先计算值的另一种方法是汇总表,例如每天运行一次您的内部查询(昂贵的group by)并将结果存储在一个表中,然后重用它(通过从该表中选择而不是分组)。这对于像发票这样永不改变的数据尤其可行(尽管否则您可以使用触发器来保持汇总表的最新状态)。如果你有几个场景,它也会变得更加可行,例如如果您的用户可以决定按工作日、年份、月份或十二生肖分组,否则您需要为每个人添加一个索引。如果您需要动态限制发票范围(例如 2019-01-01 ... 2019-01-14),它变得不太可行。如果您需要在报告中包含当天,您仍然可以预先计算然后从表中添加当前日期的值(这应该只涉及非常有限的行数,如果您有一个以您的日期列),或使用触发器即时更新您的汇总表。

【讨论】:

嗨@Solarflare,它是 MySQL 5.6.10。在这种情况下我应该如何添加触发器?考虑到所有建议,您能告诉我查询的最终输出吗? 您可以使用原始查询,只需替换按列分组(我忘了提及,所以我现在明确添加了那部分代码)。备注只是备注。特别是如果您当前的查询返回正确的结果,而且速度很慢,您可以忽略其中的大多数(尽管第一个可以,如果您实际上只打算显示一月份的数据,请提供额外的减速带);但是按 md 而不是 date_format(... 单独分组应该已经显着减少了您的执行时间(如果您有一个 ssd,大约 5-10 秒,对于硬盘可能是 30-40 秒)。 @UgoL 我详细说明了该日期范围部分,因为它可能不是您想要的(您可能从fill in missing dates-question 复制了该代码),因此请检查您当前的查询是否真的为您提供预期结果(这是此优化的先决条件)。 感谢@Solarflare 的所有这些建议。我试图 ALTER 表并设置 md 字段,但它似乎没有尽头。【参考方案2】:

使用PRIMARY KEY(id),拥有INDEX(id, anything) 几乎没有用处。

看看是否可以避免嵌套子查询。

考虑永久构建“日期”表并在其上添加PRIMARY KEY(md)。目前,两个子查询在连接列 (md) 上都没有索引。

您可能患有“爆炸-内爆”综合症。这是JOINs 展开行数的地方,只是为了让GROUP BY 折叠它们。

不要使用COUNT(xx),除非您需要检查xx 是否为NULL。只需COUNT(*)

store_id double -- 真的吗?

TIMESTAMPDATETIME -- 他们的表现大致相同;不要费心去改变它。

既然你只看2019-01,那就去掉

date_format(epl.registration, '%m')

仅此一项,可能会大大加快速度。 (但是,您失去了一般性。)

【讨论】:

您对避免子查询有什么建议吗?您确定在性能方面计算 (*) 是个好主意吗? @ugol - COUNT(*) 等价于 COUNT(1) 并且比 COUNT(x) 快。不要被* 所迷惑,它在这种情况下并不意味着“所有列”。

以上是关于MySQL查询太慢了的主要内容,如果未能解决你的问题,请参考以下文章

MySQL UNION ALL 太慢了

如果mysql里面的数据过多,查询太慢怎么办?

mysql模糊查询的优化方法--亲自实践

具有多个计算的Mysql查询变慢

mySQL 子查询/跨数据库连接?

如何解决mysql 查询和更新速度慢