【问题标题】:Is it a bad idea to keep a subtotal field in database在数据库中保留小计字段是不是一个坏主意
【发布时间】:2011-11-24 02:36:20
【问题描述】:

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

每批货物都有一些费用,例如:

  • 物品成本
  • 运费
  • 处理成本
  • 税收成本

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

  • 项目总成本
  • 总运费
  • TotalHandlingCost
  • TotalTaxCost
  • 总成本
  • 支付总额
  • 总利润

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

在我看来,有一些基本的方法可以解决这个问题:

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

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

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

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

是否有任何最佳实践可供任何人指出以帮助我确定最佳设计是什么?

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

更新:

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

【问题讨论】:

  • 为什么不在应用层缓存小计,每当你对数据库执行更新时,更新应用层中的小计缓存(做基本的算术)然后写入数据库,这样你仅在应用启动时计算小计,偶尔验证缓存。
  • 如果您可以使用比 MySQL 更高级的 RDBMS,您也许可以通过物化视图获得蛋糕并吃掉它。
  • @todda.speot.is +1 吃蛋糕。我猜也适用于物化视图。

标签: mysql database-design denormalization


【解决方案1】:

如果您大部分时间都在进行读取而不是写入,我可能会通过在数据库中缓存小计以获得最快的查询性能来解决此问题。创建更新触发器以在行更改时重新计算小计。

如果行数通常非常少并且访问频率较低,我只会使用视图在 SELECT 上计算它们。如果缓存它们,性能会更好。

【讨论】:

  • 是的,到目前为止,触发器似乎是可行的方法。我以前在 Oracle 数据库中使用过它们,应该意识到 MySQL 支持它们。
【解决方案2】:

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

【讨论】:

  • 我喜欢这个主意。触发器会将逻辑放在一个地方,这样我就不必记住每次(和地点)更新运输成本时都要重新计算。我希望我可以创建一个触发器函数来获取货件的关联订单 ID,并使用计算的相关货件总数更新该订单记录。
  • 您还可以通过明智地选择何时使用非规范化值以及何时采用艰难的方式来帮助避免问题。一旦交易进入历史,它被修改的风险就很小。因此,历史销售报告应使用非规范化列。相反,当您处理销售时,它更有可能是不稳定的,因此(单张)发票打印报告应该很难做到。
  • 除了触发器之外,我认为您还需要一个 CHECK 约束来确保数字相加。如果只有 MySQL 支持 CHECK 约束。 . .
【解决方案3】:

这不是一个坏主意,不幸的是 MySQL 没有一些特性可以使这变得非常容易 - 计算列和索引(物化视图)。您可能可以使用触发器来模拟它。

【讨论】:

    【解决方案4】:

    我会尽可能避免#3。出于不同的原因,我更喜欢这样:

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

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

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

    4. 我倾向于将计算小计的实际逻辑放在应用层,因为小计实际上是与不同上下文相关的重载事物 - 有时您想要“原始小计”,有时您想要应用折扣后的小计。对于不同的场景,您不能一直在订单表中添加列。

    【讨论】:

    • 所有非常好的观点。然而,在我的情况下: 1. 信息显示在管理控制台上,我有一个可能很长的订单列表,需要显示每个订单的 TotalProfit。 2. 似乎触发方法至少会将逻辑放在一个地方,限制错误。 3. 报告复杂化 4. 通过垂直汇总相同字段并让应用程序决定它关心的部分来缓解。
    【解决方案5】:

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

    使用触发器进行更新
    您应该在插入、更新和删除之后使用触发器,以使订单表中的小计与基础数据保持同步。
    在追溯更改价格和内容时要特别小心,因为这需要对所有小计进行全面重新计算。 因此,您将需要很多触发器,而这些触发器通常不会在大多数情况下发挥作用。
    如果税率发生变化,将来会发生变化,对于您还没有的订单 em>

    如果触发器需要很长时间,请确保在非高峰时间进行这些更新。

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

    不对验证查询未处理的订单开具任何发票
    向表 order 添加一个名为 timeoflastsuccesfullvalidation 的额外日期字段,如果验证不成功,则将其设置为 null
    仅在不到 24 小时前为 dateoflastsuccesfullvalidation 开具发票。
    当然,您不需要检查已完全处理的订单,只需检查待处理的订单即可。

    选项 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 相关的内容,但我现在懒得这样做。

    速度提示
    确保您对连接条件中涉及的所有字段都有索引。
    对较小的表使用MEMORY 表,例如taxshippingcost,并为内存表中的id 使用hash 索引。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-03
      • 2018-10-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多