【问题标题】:Retail inventory Mysql query optimization零售库存Mysql查询优化
【发布时间】:2016-01-01 23:23:58
【问题描述】:

给定零售管理系统的下表:

商店store_id, name

产品product_id, name, cost

PRODUCT_ENTRIESkey, store_id, date

PRODUCT_ENTRIES_CONTENT: product_entries_key, product_id, quantity

PRODUCT_EXITSkey, store_id, product_id, quantity, status, date

销售key, store_id, date

SALES_CONTENT: sales_key, product_id, quantity

返回key, store_id, date

RETURNS_CONTENT: returns_key, product_id, quantity

为了计算库存值,我遍历了 products 表的内容和每个 product_id:

  • product_entries_content 和returns_content 的总数量
  • 减去 product_exits_content(status = 2 或 3)以及 sales_content 的数量

为了计算每个商店的库存成本,我通过 PHP 循环 对每个不同的商店运行以下查询并输出结果:

SELECT

    SUM((((

    (SELECT COALESCE(SUM(product_entries_content.quantity), 0)

    FROM product_entries

    INNER JOIN product_entries_content ON 
product_entries_content.product_entries_key = product_entries.key

    WHERE product_entries_content.product_id = products.id 
    AND product_entries.store_id = '.$row['id'].'   
    AND DATE(product_entries.date) <= DATE(NOW()))


    -

    (SELECT COALESCE(SUM(quantity), 0) 

    FROM sales_content

    INNER JOIN sales ON sales.key  = sales_content.sales_key

    WHERE product_id = products.product_id AND sales.store_id = '.$row['id'].'
    AND DATE(sales_content.date) <= DATE(NOW()))

    +

    (SELECT COALESCE(SUM(quantity), 0) 

    FROM returns_content

    INNER JOIN returns  ON returns.key = returns_content.returns_key

    WHERE product_id = products.product_id AND returns.store_id = '.$row['id'].'
    AND DATE(returns.date) <= DATE(NOW()))

    -

    (SELECT COALESCE(SUM(quantity), 0) 

    FROM product_exits

    WHERE product_id = products.product_id AND (status = 2 OR status = 3) 
AND product_exits.store_id = '.$row['id'].' #store_id
    AND DATE(product_exits.date) <= DATE(NOW()))     

    ) * products.cost) / 100) ) AS "'.$row['key'].'" #store_name

FROM products WHERE 1

所有外键和索引都已正确设置。问题是由于每个商店的大量商店和移动,查询变得越来越繁重,并且因为库存是从每个商店的历史开始计算的,所以它只会随着时间的推移而变慢。

我可以做些什么来优化这个方案?

【问题讨论】:

  • 别说DATE(product_entries.date) &lt;= DATE(NOW()),说product_entries.date &lt;= CURRENT_DATE()。 (我假设您想在今天之后排除行?为什么会有这样的行?)

标签: php mysql query-optimization


【解决方案1】:

理想情况下,每个表的SHOW CREATE TABLE tablename 对任何优化问题都有很大帮助。每列的数据类型对性能非常重要。

也就是说,从您提供的信息来看,假设列数据类型都是合适的,以下应该会有所帮助。

添加以下索引(如果它们不存在)。重要提示:单列索引不是以下复合索引的有效替换。你说

所有外键和索引都已正确设置。

但这并不能告诉我们它们是什么,以及它们是否“适合”优化。

新索引

ALTER TABLE sales
CREATE INDEX `aaaa` (`store_id`,`key`)

ALTER TABLE sales_content
CREATE INDEX `bbbb` (`product_id`,`sales_key`,`date`,`quantity`)

ALTER TABLE returns
CREATE INDEX `cccc` (`store_id`,`date`,`sales_key`)

ALTER TABLE returns_content
CREATE INDEX `dddd` (`product_id`,`returns_key`,`quantity`)

ALTER TABLE product_exits
CREATE INDEX `eeee` (`product_id`,`status`,`store_id`,`date`,`quantity`)

ALTER TABLE product_entries
CREATE INDEX `ffff` (`store_id`,`date`,`key`)

ALTER TABLE product_entries_content
CREATE INDEX `gggg` (`product_id`,`product_entries_key`,`quantity`)

(使用比aaaa 更合适的名称。我只是使用这些名称来节省时间。)

上述每个索引都将允许数据库为每个表读取一行。大多数涉及连接的性能问题都来自所谓的双重查找。

了解索引和双重查找

索引只是表数据的副本。索引中列出的每一列都按照索引中列出的顺序从表中复制,然后将主键附加到索引中的该行。当数据库使用索引查找某个值时,如果索引中没有包含所有信息,则使用主键访问表的聚集索引,获取其余信息。这就是双重查找,它对性能非常不利。

示例

上述所有索引都是为了避免重复查找而设计的。让我们看看第二个子查询,看看与该查询相关的索引是如何工作的。

ALTER TABLE sales
CREATE INDEX `aaaa` (`store_id`,`key`)

ALTER TABLE sales_content
CREATE INDEX `bbbb` (`product_id`,`sales_key`,`date`,`quantity`)

子查询(我添加了别名并调整了日期列的访问方式,但其他方面没有改变):

SELECT COALESCE(SUM(sc.quantity), 0) 
FROM sales_content sc
INNER JOIN sales s 
ON s.key  = sc.sales_key
WHERE sc.product_id = p.product_id 
AND s.store_id = '.$row['id'].'
AND sc.date < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)

使用aaaa 索引,数据库将只能在sales 表中查找与store_id 匹配的行,因为该行列在索引的首位。将此视为电话簿,其中store_id 是姓氏,key 是名字。如果您有姓氏,那么非常容易翻到电话簿的那一点,并快速获取与该姓氏相关的所有名字。同样,数据库能够非常快速地“翻转”到包含给定store_id 值的索引部分,并找到所有key 值。在这种情况下,我们根本不需要主键(在电话簿示例中就是电话号码。)

所以,完成了sales 表,我们从那里获得了我们需要的所有key 值。

接下来,数据库移动到bbbb 索引。我们已经有来自主查询的product_id,并且我们有来自aaaa 索引的sales_key。这就像在电话簿中同时拥有名字和姓氏。唯一需要比较的是日期,这可能就像电话簿中的地址。数据库将按顺序存储所有日期,因此通过给它一个截止值,它可以查看到某一点的所有日期。

bbbb 索引的最后一部分是数量,它的存在是为了让数据库可以快速汇总所有这些数量。要了解为什么这很快,请再次考虑电话簿。想象一下,除了姓氏、名字和地址信息之外,还有一个数量列(关于某事,无所谓)。如果您想要特定姓氏、名字以及以数字 5 或更少开头的所有地址的数量总和,这很容易,不是吗?只需找到第一个,然后将它们按顺序相加,直到到达第一个以大于 5 的数字开头的地址。以这种方式使用日期列时,数据库的好处是相同的(日期就像地址列,在这个例子。)

日期列

最后,我之前提到过,我更改了访问日期列的方式。您永远不想在要与另一个值进行比较的数据库列上运行函数。原因是这样的:如果在进行任何比较之前必须将所有地址转换为罗马数字会发生什么?您将无法像我们之前所做的那样从列表中删除。您必须转换所有值,然后检查每个值以确保它在限制范围内,因为我们不再知道这些值是否正确排序以便能够执行“全部读取然后停止我在上面描述的某个值”快捷方式。

您和我可能知道将日期时间值转换为日期不会更改顺序,但数据库不会知道(它可能会优化此转换,但这不是我想假设的。 ) 所以,保持列纯净。我所做的更改是只取 NOW() 日期,并添加一天,然后将其设为 &lt; 而不是 &lt;=。毕竟,比较两个值并说日期必须等于或小于今天的日期,就等于说日期时间必须小于明天的日期。

查询

以下是我对您的最后询问。如前所述,除了日期更改和别名之外没有太大变化。但是,您在访问products.id 的第一个子查询中有错字。我将id 更正为product_id,因为与您所说的匹配的是products 表的列。

SELECT
SUM(
(
(
(
    (
    SELECT COALESCE(SUM(pec.quantity), 0)
    FROM product_entries pe
    INNER JOIN product_entries_content pec 
    ON pec.product_entries_key = pe.key
    WHERE pec.product_id = p.product_id 
    AND pe.store_id = '.$row['id'].' 
    AND pe.date < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)
    )
    -
    (
    SELECT COALESCE(SUM(sc.quantity), 0) 
    FROM sales_content sc
    INNER JOIN sales s 
    ON s.key  = sc.sales_key
    WHERE sc.product_id = p.product_id 
    AND s.store_id = '.$row['id'].'
    AND sc.date < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)
    )
    +
    (
    SELECT COALESCE(SUM(rc.quantity), 0)
    FROM returns_content rc
    INNER JOIN returns r 
    ON r.key = rc.returns_key
    WHERE rc.product_id = p.product_id 
    AND r.store_id = '.$row['id'].'
    AND r.date < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)
    )
    -
    (
    SELECT COALESCE(SUM(pex.quantity), 0)
    FROM product_exits pex
    WHERE pex.product_id = p.product_id 
    AND (pex.status = 2 OR pex.status = 3)
    AND pex.store_id = '.$row['id'].' #store_id
    AND pex.date < DATE_ADD(DATE(NOW()), INTERVAL 1 DAY)
    )
) 
* p.cost) 
/ 100)
) AS "'.$row['key'].'" #store_name
FROM products p WHERE 1

您可以通过将 product_exits 表上的子查询拆分为 2 个单独的子查询来进一步优化这一点,而不是使用 OR,这在很多时候会表现不佳。最终,您必须对其进行基准测试,以了解数据库对 OR 自身的优化程度。

【讨论】:

  • 如果xx_idPRIMARY KEY,那么拥有INDEX(xx_id, ...) 是低效的。而且,对于 InnoDB,该索引不太可能被使用!
  • 是的。不清楚xx_id 还是key 是主键,所以我没有做任何假设。拥有SHOW CREATE TABLE tablename 语句会有很大帮助。
  • @WillemRenzema 我会调查答案。谢谢。我无法显示 CREATE TABLE 语句,因为它们与原始问题不匹配,因为我从另一种语言翻译了名称。
  • @helloworld 我明白了。如果您能告诉我哪些列或哪些列构成了每个表的 PK,我可以改进我的答案。此外,只是一般的优化:对于每个连接,数据类型应该完全相同。因此,如果 returns.key 是 MEDIUMINT,那么 returns_content.returns_key 也应该是 MEDIUMINT。无论它们是什么,它们都应该匹配每个连接条件,以确保达到最佳性能。
  • @WillemRenzema 完整的方案有点复杂。我将开始一个新线程并从这里链接它。感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-22
  • 2011-07-07
  • 2018-12-21
相关资源
最近更新 更多