MySQL:根据另一个表更新正在运行的“帐户余额”

时间:2019-03-06 16:53:00

标签: mysql

小提琴: 请参阅此SQLFiddle:http://sqlfiddle.com/#!9/af05d8/1/0

场景:我需要根据列出收到的工作/服务的表格来更新客户帐户余额。通过向总账表中添加新行并添加新的客户帐户余额(previous_balance-价格),向每个客户帐户提供每个工作/服务的给定价格。

问题:当客户表中只有一项需要更新余额的工作时,提琴/查询工作正常。但是,当客户在作业表中有多于一行的内容时,不会为每个作业使用实际帐户余额-而是为所有作业使用原始帐户余额。

进行澄清:该问题已由问题1中的spencer5973确定。 SELECT正在表中运行,因为它位于语句的开头。通过查询添加到分类帐中的新行不会用于后续的SELECT,这意味着对于作业表中具有多行的客户,不会选择正确的运行总计。

详细信息:

tmptblLedger :所有客户的帐户余额(分类帐条目)的运行列表。每次更改客户帐户余额(付款,购买等)时,都会添加一个新行。每行记录期初余额(与上一个期末余额相同)和新的期末余额。

编辑以澄清:可以通过支付工作(如小提琴和这个问题所示)来减少客户帐户余额,或者通过向帐户付款来增加(未显示),或者否则由与Job表无关的事务更改。这会阻止spencer5973的以下答案起作用。分类帐是客户帐户余额中所有交易/调整的唯一权威来源。

tmptblJobs :一个“负责完成的工作”跟踪表。每次客户完成某项工作(一项工作)时,都会添加一行。该行指定完成了多少工作,以及每项工作的价格。

要求非常简单-必须更新每个客户的分类帐以反映完成的工作。用伪代码:

foreach(row in tmptblJobs)
{
    get row.Customer ID;
    get Customer's current account balance (MAX LedgerID in Ledger table)
    new_balance = subtract (row.NumberOfJobs * row.PricePerJob) from current balance
    write new row with new_balance to ledger table
}

当“客户”工作表中只有一行时,查询将正常工作。

问题似乎是,当客户在Jobs表中有多于1行时,Jobs表中的后续行将忽略写入Ledger表的新行(新余额)。而是使用查询之前的原始余额-就像缓存了结果一样。

我可以相信我缺少一些SQL或功能来完成这项工作,但是我不知道是什么...

表结构和示例数据:

DROP TABLE IF EXISTS tmptblLedger;
DROP TABLE IF EXISTS tmptblJobs;

CREATE TABLE `tmptblLedger` (
  `LedgerID` int(10) UNSIGNED NOT NULL COMMENT 'Unique transaction ID',
  `Timestamp` DATETIME,
  `BalanceOpen` float NOT NULL COMMENT 'Last balance for customer before this record was created',
  `BalanceClose` float NOT NULL  COMMENT 'Balance now that record has been created = BalanceOpen + Adjustment Amount',
  `Customer_CustID` int(10) UNSIGNED NOT NULL
);
ALTER TABLE `tmptblLedger` 
    MODIFY `LedgerID` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1,
    ADD UNIQUE KEY (`LedgerID`);

/* some representative data, this example only uses Customer_CustID == 1). All ledgers start at zero, this example has a second ledger entry for CustID == 1 to put the balance to 100 */
INSERT INTO `tmptblLedger` (`Timestamp`, `BalanceOpen`, `BalanceClose`, `Customer_CustID`)  VALUES (NOW(), 0, 0, 1), (NOW(), 0, 100, 1), (NOW(), 0, 5, 2), (NOW(), 0, 7, 3);

CREATE TABLE `tmptblJobs` (
  `ScheduledTargetID` bigint(20) NOT NULL,
  `Customer_CustID` int(10) UNSIGNED NOT NULL,
  `NumberOfJobsCompleted` int UNSIGNED,
  `PricePerJob` float UNSIGNED
);
ALTER TABLE `tmptblJobs`
    MODIFY `ScheduledTargetID` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1,
    ADD UNIQUE KEY(`ScheduledTargetID`);

/* insert 3 jobs for CustID 1, and an example 1 job for CustID 2 to represent real world data */
INSERT INTO `tmptblJobs` (`Customer_CustID`, `NumberOfJobsCompleted`, `PricePerJob`)    VALUES (1, 2, 5), (1, 1, 3), (1, 1, 1), (2, 1, 1);

查询:

INSERT INTO `tmptblLedger` (`Timestamp`, `BalanceOpen`, `BalanceClose`, `Customer_CustID`)
    SELECT
        NOW(),
        derivedLedger.LedgerCurrentBalance,
        derivedLedger.LedgerCurrentBalance - ( @TotalPrice:=(tmptblJobs.NumberOfJobsCompleted * tmptblJobs.PricePerJob) ) AS NEWBALANCE,
        tmptblJobs.Customer_CustID
    FROM
        (
            /* Obtain current balance for each CustID, using newest LedgerID */
            SELECT
                derivedNewestLedgerRow.LedgerCustID,
                tmptblLedger.BalanceClose AS LedgerCurrentBalance /* current balance == closing balance of newest ledger entry */
            FROM
                tmptblLedger
            RIGHT JOIN
            (
                /* Obtain newest LedgerID for each CustID */
                SELECT
                    tmptblLedger.Customer_CustID AS LedgerCustID,
                    MAX(LedgerID) AS NewestCustLedgerID
                FROM
                    tmptblLedger
                GROUP BY tmptblLedger.Customer_CustID
            ) as derivedNewestLedgerRow ON LedgerID = NewestCustLedgerID
        ) as derivedLedger
    INNER JOIN tmptblJobs ON tmptblJobs.Customer_CustID = derivedLedger.LedgerCustID
    WHERE 1;

/* LegerID rows 5, 6 and 7 will now all start from a BalanceOpen of 100, instead of BalanceOpen 100, then 90 (for ScheduledTargetID 1), then 87 (ScheduledTargetID 2), then 86 (ID 3)... */ 
SELECT * FROM tmptblLedger;

结果:

LedgerID    Timestamp   BalanceOpen BalanceClose    Customer_CustID
1   2019-03-06T16:03:11Z    0   0   1
2   2019-03-06T16:03:11Z    0   100 1
3   2019-03-06T16:03:11Z    0   5   2
4   2019-03-06T16:03:11Z    0   7   3
5   2019-03-06T16:03:11Z    100 90  1
6   2019-03-06T16:03:11Z    100 97  1
7   2019-03-06T16:03:11Z    100 99  1
8   2019-03-06T16:03:11Z    5   4   2

第5、6和7行是错误的。 BalanceClose应该为:

Row 5 = 90
Row 6 = 87
Row 7 = 86

1 个答案:

答案 0 :(得分:1)

注释1:“使用INSERT INTO tmptblLedger SELECT ... FROM tmptblLedger ...语句,SELECT将不会读取正在插入的行。{{1} }在该表的开头对表运行。我们可以只运行SELECT(不插入INSERT)。这就是要插入的行集。”

评论2:“解决此问题的一种方法是将工作SELECT汇总到每个客户的行中。因此,与其添加< em> custid = 1的三​​行(5,6,7),(每项工作一个),我们将添加一个行,其balance_open = 100和balance_close = 86。( 100-((2 * 5)+(1 * 3)+(1 * 1)))

评论3:“如果我们需要为custid = 1添加三行(5,6,7),那么open_balance不仅是最新分类帐ID的余额。我们需要包括调整以前的工作。必须有一些方法可以唯一地对工作进行排序,...我在考虑一个相关子查询,该子查询获取当前行之前的工作的SUM(qty * unit_price)。从最新分类账行的余额中获取该余额,以获取当前行的balance_open。”


不可能获得SUM(qty*unit_price)来读取语句开始执行时不存在的行,而不是在单个SQL语句的上下文中。 (我们可以在讨论事务隔离级别时深入了解杂草,并允许在事务的上下文中进行“脏读”操作,这有可能导致读取一些未提交的更改,但这是不可靠的,无法保证。甚至隔离级别允许脏读取,在执行SELECT语句时仍然不会发生这种情况。

无法“修复” SQL语句在执行时无法读取正在插入的行。这不是需要解决的问题。这种限制实际上是关系数据库的设计特征。它实际上解决了可能发生的问题。

最重要的是,没有一种方法可以使SELECT读取正在插入的行。

我们需要做的是从要插入的其他行中获取值,然后,我们必须重新计算前几行中的值,并将这些值包括在当前行的结果中。

我们可以使用相关子查询来做到这一点。

当前,SELECT语句的格式为:

INSERT ... SELECT

我们可以引入一个相关子查询来为客户的先前工作获取SELECT NOW() , d.ledgercurrentbalance , d.ledgercurrentbalance - (j.numberofjobscompleted * j.priceperjob) AS newbalance , j.customer_custid FROM ( SELECT n.ledgercustid , t.balanceclose AS ledgercurrentbalance /* current balance == closing balance of newest ledger entry */ FROM ( /* Obtain newest LedgerID for each CustID */ SELECT tmptblLedger.Customer_CustID AS LedgerCustID , MAX(LedgerID) AS NewestCustLedgerID FROM tmptblLedger l GROUP BY l.customer_custid ) n LEFT JOIN tmptblLedger t ON t.ledgerid = n.newestcustledgerid ) d JOIN tmptblJobs j ON j.customer_custid = d.ledgercustid 。鉴于SUM(qty*price)在作业表中是唯一的,我们可以将其用于排序。

类似这样的东西:

scheduledtargetid

一些未解决的问题:

每个客户在目标SELECT NOW() AS `Timestamp` , b.balanceclose - b.prevjobs_cost AS `BalanceOpen` , b.balanceclose - b.prevjobs_cost - b.currjob_cost AS `BalanceClose` , b.customer_custid AS `Customer_CustID` FROM ( SELECT j.scheduledtargetid , j.customer_custid , d.balanceclose , ( j.numberofjobscompleted * j.priceperjob ) AS currjob_cost , IFNULL( ( /* correlated subquery to get total cost of previous jobs */ SELECT SUM(p.numberofjobscompleted * p.priceperjob) FROM `tmptblJobs` p WHERE p.customer_custid = j.customer_custid AND p.scheduledtargetid < j.scheduledtargetid ) ,0) AS prevjobs_cost FROM ( SELECT n.ledgercustid , t.balanceclose FROM ( /* latest ledgerid for each custid */ SELECT l.customer_custid AS ledgercustid , MAX(l.ledgerid) AS newestcustledgerid FROM `tmptblLedger` l GROUP BY l.customer_custid ) n LEFT JOIN `tmptblLedger` t ON t.ledgerid = n.newestcustledgerid ) d JOIN `tmptblJobs` j ON j.customer_custid = d.ledgercustid ) b ORDER BY b.customer_custid , b.scheduledtargetid 表中至少需要有一行。如果不存在该查询,则该查询不会为该客户生成新行。