【问题标题】:MySQL huge tables JOIN makes database collapseMySQL 巨表 JOIN 使数据库崩溃
【发布时间】:2013-02-21 13:56:04
【问题描述】:

根据我最近的问题Select information from last item and join to the total amount,我在生成表时遇到了一些内存问题

我有两张表 sales1sales2 像这样:

标识 |日期 |客户 |出售

有了这个表定义:

CREATE TABLE sales (
    id int auto_increment primary key, 
    dates date,
    customer int,
    sale int
);

sales1sales2 具有相同的定义,但 sales2 在每个字段中都有 sale=-1。客户可以不在一个表中,也可以在一个表中或两个表中。这两个表都有大约 300.000 条记录和比这里指出的更多的字段(大约 50 个字段)。他们是 InnoDB。

我想为每个客户选择:

  • 购买次数
  • 最后一次购买价值
  • 购买的总金额,当它具有正值时

我使用的查询是:

SELECT a.customer, count(a.sale), max_sale
FROM sales a
INNER JOIN (SELECT customer, sale max_sale 
        from sales x where dates = (select max(dates) 
                                    from sales y 
                                    where x.customer = y.customer
                                    and y.sale > 0
                                   )

       )b
ON a.customer = b.customer
GROUP BY a.customer, max_sale;

问题是:

我必须得到某些计算所需的结果,按日期分开:2012 年的信息、2013 年的信息,还有所有年份的信息。

如果我只做一年,存储所有信息大约需要 2-3 分钟。

但是当我尝试收集这些年来的信息时,数据库崩溃了,我收到如下消息:

InternalError: (InternalError) (1205, u'Lock wait timeout exceeded; try restarting transaction')

似乎连接如此庞大的表对于数据库来说太多了。当我explain查询时,几乎所有百分比的时间都来自creating tmp table

我想把收集的数据分成四份。我们每三个月获得一次结果,然后加入并对其进行排序。但我想这个最终的连接和排序对于数据库来说又是太多了。

那么,只要我不能更改表结构,您的专家会建议如何优化这些查询?

【问题讨论】:

  • 你是如何加入表格的?您不会将 300,000 行交叉连接在一起,是吗?那将是 900 亿行...
  • 但不知何故很棒
  • 300,000 行的表绝对不算大。
  • 我们需要看到分解的select语句;或者你可以使用解释计划来确定你的滞留在哪里。您确实有日期和客户的索引,对吗?

标签: mysql sql performance optimization greatest-n-per-group


【解决方案1】:

300k 行并不是一个巨大的表。我们经常看到 3 亿行表。

您的查询的最大问题是您使用的是相关子查询,因此它必须为外部查询中的每一行重新执行子查询

通常情况下,您不需要在一个 SQL 语句中完成所有您的工作。将其分解为几个更简单的 SQL 语句有好处:

  • 更容易编码。
  • 更易于优化。
  • 更易于调试。
  • 更易于阅读。
  • 如果/当您必须实施新要求时,更易于维护。

购买次数

SELECT customer, COUNT(sale) AS number_of_purchases
FROM sales 
GROUP BY customer;

sales(customer,sale) 索引最适合此查询。

最后一次购买价值

这是经常出现的greatest-n-per-group问题。

SELECT a.customer, a.sale as max_sale
FROM sales a
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND a.dates < b.dates
WHERE b.customer IS NULL;

换句话说,尝试将a 行与具有相同客户和更大日期的假设行b 匹配。如果没有找到这样的行,则a 必须具有该客户的最大日期。

销售索引(客户、日期、销售)最适合此查询。

如果您可能在该最大日期为一位客户进行了多次销售,则此查询将为每位客户返回多行。您需要找到另一列来打破平局。如果您使用自增主键,则它适合作为决胜局,因为它可以保证是唯一的,并且它往往会按时间顺序增加。

SELECT a.customer, a.sale as max_sale
FROM sales a
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND (a.dates < b.dates OR a.dates = b.dates and a.id < b.id)
WHERE b.customer IS NULL;

购买总金额,当它具有正值时

SELECT customer, SUM(sale) AS total_purchases
FROM sales
WHERE sale > 0
GROUP BY customer;

sales(customer,sale) 索引最适合此查询。

您应该考虑使用 NULL 而不是 -1 来表示缺失的销售值。 SUM() 和 COUNT() 等聚合函数会忽略 NULL,因此您不必使用 WHERE 子句来排除 sale


回复:您的评论

我现在拥有的是一个表格,其中包含年、季度、total_sale(关于(年、季度)这对)和销售字段。我想收集的是有关某个时期的信息:本季度,季度,2011 年......信息必须分为顶级客户,销售额较大的客户等。是否有可能从客户那里获得最后的购买价值total_purchases 大于 5?

2012 年第四季度前五名客户

SELECT customer, SUM(sale) AS total_purchases
FROM sales
WHERE (year, quarter) = (2012, 4) AND sale > 0
GROUP BY customer
ORDER BY total_purchases DESC
LIMIT 5;

我想根据真实数据对其进行测试,但我相信销售索引(年、季度、客户、销售)最适合此查询。

总购买次数 > 5 的客户的最后一次购买

SELECT a.customer, a.sale as max_sale
FROM sales a
INNER JOIN sales c ON a.customer=c.customer
LEFT OUTER JOIN sales b
 ON a.customer=b.customer AND (a.dates < b.dates OR a.dates = b.dates and a.id < b.id)
WHERE b.customer IS NULL
GROUP BY a.id
HAVING COUNT(*) > 5;

与上述其他每组最大 n 个查询一样,sales(customer,dates,sale) 的索引最适合此查询。它可能无法同时优化连接和分组依据,因此这将产生一个临时表。但至少它只会做一个临时表而不是很多。


这些查询已经够复杂了。您不应该尝试编写可以给出所有这些结果的单个 SQL 查询。记住 Brian Kernighan 的经典名言:

每个人都知道,调试的难度是编写程序的两倍。因此,如果您在编写它时尽可能聪明,您将如何调试它?

【讨论】:

  • 非常感谢您提供如此完整的答案。现在,使用索引和内部联接而不是子查询,事情变得更快了。我现在拥有的是一个包含字段yearquartertotal_sale(关于(年、季度))和sale 的表格。我想收集的是有关某个时期的信息:本季度,季度,2011 年......信息必须分为顶级客户,销售额较大的客户等。是否有可能从客户那里获得最后的购买价值total_purchases 大于 5?我不能不将所有查询放在一起并使用ORDER BY total_sale LIMIT X, Y
  • 再次:非常感谢@Bill Karwin。您的解决方案为我打开了一个选择的新世界。使用索引,查询变得非常轻松,在不同的查询中拆分结果也有很大帮助。
【解决方案2】:

我认为您应该尝试在sales(customer, date) 上添加索引。子查询可能是性能瓶颈。

【讨论】:

  • 非常有用!谢谢
【解决方案3】:

你可以让这只小狗尖叫。转储整个内部连接查询。真的。这是一个几乎没人知道的技巧。

假设dates 是一个日期时间,转换它为一个可排序的字符串,连接你想要的值,ma​​x(或min) , substring, cast。您可能需要调整日期转换功能(此功能在 MS-SQL 中有效),但此想法适用于任何地方:

SELECT customer, count(sale), max_sale = cast(substring(max(convert(char(19), dates, 120) + str(sale, 12, 2)), 20, 12) as numeric(12, 2))
FROM sales a 
group by customer

瞧。如果您需要更多结果列,请执行以下操作:

SELECT yourkey
            , maxval = left(val, N1)                  --you often won't need this
            , result1 = substring(val, N1+1, N2)
            , result2 = substring(val, N1+N2+1, N3)   --etc. for more values
FROM ( SELECT yourkey, val = max(cast(maxval as char(N1))
                               + cast(resultCol1 as char(N2))
                               + cast(resultCol2 as char(N3)) )
       FROM yourtable GROUP BY yourkey ) t

确保除了最后一个字段之外的所有字段都具有固定长度。这需要一些工作才能让你明白,但它是非常可学习和可重复的。它可以在任何数据库引擎上运行,即使你有排名函数,它通常也会大大优于它们。

更多关于这个非常常见的挑战here

【讨论】:

  • 如果是宽或高的桌子,CREATE INDEX IX_sales1 ON sales (customer, dates) INCLUDE (sale)
  • (nolock) 是 Microsoft SQL Server 的东西。 MySQL 中不存在这样的选项。
  • 哎呀忘了 - 而不是 "str(sale, 12, 2)" 你会想要 "case when sale > 0 then str(sale, 12, 2) else null end"
  • 巧妙的技巧,但这是一个非常粗糙的表达式,如果您需要具有最大日期的行中的多个列,则必须为每一列制作另一个类似的表达式,并将其转换为适当的数据类型。似乎有很多脆弱且难以维护的代码。
  • 不是这样 - 连接每一列感兴趣的列,然后用子字符串将它们全部分解(见上文)。当你习惯它时,它就会变得可读;-p,它一点也不脆弱——它总是有效的。
猜你喜欢
  • 1970-01-01
  • 2012-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-02
  • 2013-03-30
  • 2017-03-26
  • 1970-01-01
相关资源
最近更新 更多