在数据库中保留小计字段是一个坏主意

时间:2011-09-24 03:32:01

标签: mysql database-design denormalization

我有一个MySQL表,代表一个订单列表和一个相关的子表,代表与每个订单关联的货件(某些订单有多个货件,但大多数只有一个)。

每批货都有许多费用,例如:

  • ItemCost
  • ShippingCost
  • HandlingCost
  • TaxCost

应用程序中有许多地方需要获取订单的综合信息,例如:

  • TotalItemCost
  • TotalShippingCost
  • TotalHandlingCost
  • TotalTaxCost
  • TOTALCOST
  • TotalPaid
  • TotalProfit

所有这些字段都取决于相关货件表中的汇总值。此信息用于其他查询,报告,屏幕等,其中一些必须快速为用户返回数万条记录的结果。

正如我所看到的,有几种基本方法可以解决这个问题:

  1. 使用子查询可在需要时从货件表中计算这些项目。对于需要全部或部分此类信息的所有查询,这会使事情变得复杂。它也很慢。

  2. 创建一个将子查询公开为简单字段的视图。这使得需要它们的报告变得简单。

  3. 在订单表中添加这些字段。这些将为我提供我正在寻找的性能,代价是在我对货件记录进行任何更改时必须复制数据并进行计算。

  4. 另一件事,我正在使用业务层来公开函数来获取这些数据(例如GetOrders(filter)),我不需要每次都使用小计(或者某些时候只需要其中的一些),所以每次(甚至从视图)生成子查询可能都是个坏主意。

    是否有任何人可以指出我帮助我确定最佳设计的最佳做法是什么?

    顺便说一下,我最终做了#3主要是出于性能和查询简单的原因。

    更新

    很快得到了很多很棒的反馈,谢谢大家。为了给出更多背景信息,显示信息的地方之一是在管理控制台上,我有一个可能很长的订单列表,需要显示每个订单的TotalCost,TotalPaid和TotalProfit。

5 个答案:

答案 0 :(得分:4)

对统计数据进行汇总并将其存储以提高应用程序性能绝对没有错。请记住,您可能需要创建一组触发器或作业,以使汇总与源数据保持同步。

答案 1 :(得分:3)

如果大多数时候你正在读取而不是写入,我可能会通过缓存数据库中的小计来获得最快的查询性能。创建更新触发器以在行更改时重新计算小计。

如果行数通常非常小并且访问不常见,我只会使用视图在SELECT上计算它们。如果缓存它们,性能会好很多。

答案 2 :(得分:3)

选项3是最快的
如果您遇到性能问题并且无法以其他任何方式解决这些问题,那么选项#3就是您的选择。

使用触发器进行更新
您应该在插入,更新和删除后使用触发器,以使订单表中的小计与基础数据保持同步 在追溯性地改变价格和东西时要特别小心,因为这需要完全重新计算所有小计。 因此,您需要大量的触发器,这些触发器通常在大多数情况下都不会起作用 如果税率发生变化,对于您尚未拥有的订单,它将来会发生变化

如果触发器需要花费大量时间,请确保在非高峰时段进行这些更新。

定期运行自动检查以确保缓存的值正确
您可能还希望保留一个golden子查询来计算所有值,并根据订单表中的存储值检查它们。
每晚运行此查询并让它报告任何异常,以便您可以看到非规范化值何时不同步。

请勿对未经验证查询处理的订单执行任何发票
在名为order的表timeoflastsuccesfullvalidation中添加一个额外的日期字段,如果验证不成功,则将其设置为null
仅发送dateoflastsuccesfullvalidation小于24小时前的商品。
当然,您不需要检查完全处理的订单,只检查待处理的订单。

选项1可能足够快
关于#1

  

它也很慢。

这很大程度上取决于您如何查询数据库 你提到了子选择,在下面的大部分是完整的骨架查询中我没有看到需要很多子选择,所以你让我有点困惑。

SELECT field1,field2,field3
       , oifield1,oifield2,oifield3
       , NettItemCost * (1+taxrate) as TotalItemCost
       , TotalShippingCost
       , TotalHandlingCost
       , NettItemCost * taxRate as TotalTaxCost
       , (NettItemCost * (1+taxrate)) + TotalShippingCost + TotalHandlingCost as TotalCost
       , TotalPaid
       , somethingorother as TotalProfit
FROM (

  SELECT o.field1,o.field2, o.field3
         , oi.field1 as oifield1, i.field2 as oifield2 ,oi.field3 as oifield3
         , SUM(c.productprice * oi.qty) as NettItemCost
         , SUM(IFNULL(sc.shippingperkg,0) * oi.qty * p.WeightInKg) as TotalShippingCost
         , SUM(IFNULL(hc.handlingperwhatever,0) * oi.qty) as TotalHandlingCost
         , t.taxrate as TaxRate
         , IFNULL(pay.amountpaid,0) as TotalPaid
  FROM orders o
  INNER JOIN orderitem oi ON (oi.order_id = o.id)
  INNER JOIN products p ON (p.id = oi.product_id)
  INNER JOIN prices c ON (c.product_id = p.id 
                       AND o.orderdate BETWEEN c.validfrom AND c.validuntil)
  INNER JOIN taxes t ON (p.tax_id = t.tax_id 
                       AND o.orderdate BETWEEN t.validfrom AND t.validuntil) 
  LEFT JOIN shippingcosts sc ON (o.country = sc.country
                       AND o.orderdate BETWEEN sc.validfrom AND sc.validuntil)
  LEFT JOIN handlingcost hc ON (hc.id = oi.handlingcost_id
                       AND o.orderdate BETWEEN hc.validfrom AND hc.validuntil)
  LEFT JOIN (SELECT SUM(pay.payment) as amountpaid FROM payment pay 
             WHERE pay.order_id = o.id) paid ON (1=1)
  WHERE o.id BETWEEN '1245' AND '1299'
  GROUP BY o.id DESC, oi.id DESC ) AS sub  

考虑一下,你需要将这个查询拆分为每个订单和每个order_item相关的东西,但我现在懒得这么做。

速度提示
确保您在连接标准中涉及的所有字段都有索引 对{(1}}和MEMORY等较小的表使用tax表,并在内存表中使用shippingcost索引作为hash

答案 3 :(得分:2)

尽可能避免#3。我更喜欢这个原因:

  1. 没有测量就很难讨论性能。成像用户正在四处购物,将订单商品添加到订单中;每次添加商品时,您都需要更新订单记录,这可能不是必需的(某些网站仅在您点击购物车并准备结帐时显示订单总数)。

  2. 有一个重复的列要求提供错误 - 您不能指望每个未来的开发人员/维护人员都知道这个额外的列。触发器可以提供帮助,但我认为触发器只应作为解决数据库设计错误的最后手段。

  3. 可以使用不同的数据库架构进行报告。报告数据库可以高度去标准化以达到性能目的,而不会使主应用程序复杂化。

  4. 我倾向于在应用层设置计算小计的实际逻辑,因为小计实际上是与不同上下文相关的重载事件 - 有时你想要“原始小计”,有时你想要应用折扣后的小计。您无法继续为订单表添加列以用于不同的方案。

答案 4 :(得分:1)

这不是一个坏主意,不幸的是MySQL没有一些功能可以使这个非常简单 - 计算列和索引(物化视图)。你可以用触发器来模拟它。