NoSQL? 不,SQL! –如何计算运行总计

如果有的话,我们在各种JUG和会议上的jOOQ演讲几乎揭示了一件事:

Java开发人员不了解SQL。

甚至不一定是我们的错。 现在我们只是不接触SQL。

但是考虑一下:我们的开发人员(或我们的客户)每年因其出色的RDBMS而向Oracle,Microsoft,IBM,SAP支付数百万美元,而只是忽略了其90%的数据库功能并执行了一点CRUD和一个与Hibernate等ORM一起使用ACID。 我们已经忘记了为什么这些RDBMS首先如此昂贵。 我们没有关注对SQL标准的各种改进,包括SQL:1999SQL:2003SQL:2008以及最近的SQL:2011这些几乎都不受JPA支持

只要让我们的Java开发人员知道用五行SQL替换成千上万行错误的Java代码有多么容易和有趣。 不相信吗? 看一下这个:

推广SQL语言

我们不再只是推广jOOQ,而是开始帮助Java开发人员欣赏实际的SQL,无论他们使用的访问方式是什么。 由于可以通过以下任何API来欣赏真正的SQL:

在此处可以看到如何在Java 8中利用上述API。

并且相信我们,当大多数开发人员看到我们的NoSQL时,他们对SQL的强大功能感到惊讶 不,SQL! 谈论:

计算运行总计

因此,让我们深入研究谈话的本质,并使用SQL计算运行总计。 总计是多少? 这简单。 假设您的数据库中有以下银行帐户交易数据:

| ID   | VALUE_DATE | AMOUNT |
|------|------------|--------|
| 9997 | 2014-03-18 |  99.17 |
| 9981 | 2014-03-16 |  71.44 |
| 9979 | 2014-03-16 | -94.60 |
| 9977 | 2014-03-16 |  -6.96 |
| 9971 | 2014-03-15 | -65.95 |

您会立即注意到,缺少每个帐户交易的余额。 是的,我们要这样计算余额:

| ID   | VALUE_DATE | AMOUNT |  BALANCE |
|------|------------|--------|----------|
| 9997 | 2014-03-18 |  99.17 | 19985.81 |
| 9981 | 2014-03-16 |  71.44 | 19886.64 |
| 9979 | 2014-03-16 | -94.60 | 19815.20 |
| 9977 | 2014-03-16 |  -6.96 | 19909.80 |
| 9971 | 2014-03-15 | -65.95 | 19916.76 |

如果假设我们知道银行帐户上的当前余额,则可以使用每个帐户交易的AMOUNT值,并将其从当前余额中减去。 另外,我们可以假设初始余额为零,然后将所有AMOUNT值累加到今天。 这在这里说明:

| ID   | VALUE_DATE | AMOUNT |  BALANCE |
|------|------------|--------|----------|
| 9997 | 2014-03-18 |  99.17 | 19985.81 |
| 9981 | 2014-03-16 | +71.44 |=19886.64 | n
| 9979 | 2014-03-16 | -94.60 |+19815.20 | n + 1
| 9977 | 2014-03-16 |  -6.96 | 19909.80 |
| 9971 | 2014-03-15 | -65.95 | 19916.76 |

可以通过以下任一公式计算每笔交易的余额:

BALANCE(ROWn) = BALANCE(ROWn+1) + AMOUNT(ROWn)
BALANCE(ROWn+1) = BALANCE(ROWn) – AMOUNT(ROWn)

因此,这是一个总计。 容易吧?

但是,如何在SQL中做到这一点呢?

我们大多数人可能会掏出一个小的Java程序,将所有数量保留在内存中,编写单元测试,修复各种错误(毕竟,我们不是数学家),与BigDecimals角力,等等。我们中的大多数人可能会遇到在PL / SQL或T-SQL或您配置的任何其他程序语言中进行相同操作的麻烦,并且可能在插入/更新新事务时将每个余额直接更新到表中。

但是到目前为止,您可能已经猜到了,我们在这里寻找的解决方案是SQL中的解决方案。 在进行示例操作时,请与我们联系。 他们越来越好。 而且,如果您想使用这些示例,请下载Oracle XErunning-totals.sql脚本并开始运行!

我们从大学/ SQL-92学到的知识可能涉及到……

使用嵌套的SELECT

假设我们有一个类似v_transactions的视图,该视图已经将accounts表连接到account Transactions表,以便访问current_balance 。 然后,我们将按照以下方式编写此查询:

SELECT
  t1.*,
  t1.current_balance - (
    SELECT NVL(SUM(amount), 0)
    FROM v_transactions t2
    WHERE t2.account_id = t1.account_id
    AND  (t2.value_date, t2.id) >
         (t1.value_date, t1.id)
  ) AS balance
FROM     v_transactions t1
WHERE    t1.account_id = 1
ORDER BY t1.value_date DESC, t1.id DESC

请注意,嵌套的SELECT如何使用行值表达式谓词来表示过滤条件。 如果您的数据库不支持SQL标准行值表达式谓词( 并且您没有使用jOOQ对其进行仿真 ),则可以自己将它们分解出来以形成此等效查询,而可以:

SELECT
  t1.*,
  t1.current_balance - (
    SELECT NVL(SUM(amount), 0)
    FROM v_transactions t2
    WHERE t2.account_id = t1.account_id
    AND ((t2.value_date > t1.value_date) OR
         (t2.value_date = t1.value_date AND
          t2.id         > t1.id))
  ) AS balance
FROM     v_transactions t1
WHERE    t1.account_id = 1
ORDER BY t1.value_date DESC, t1.id DESC

因此,从本质上讲,对于任何给定的帐户交易,嵌套的SELECT都只获取比当前计划的帐户交易更新的帐户交易的所有AMOUNT值之和。

| ID   | VALUE_DATE |  AMOUNT |  BALANCE |
|------|------------|---------|----------|
| 9997 | 2014-03-18 | -(99.17)|+19985.81 |
| 9981 | 2014-03-16 | -(71.44)| 19886.64 |
| 9979 | 2014-03-16 |-(-94.60)| 19815.20 |
| 9977 | 2014-03-16 |   -6.96 |=19909.80 |
| 9971 | 2014-03-15 |  -65.95 | 19916.76 |

它执行吗?

嵌套SELECT的执行计划

嵌套SELECT的执行计划

不。 如您所见,对于相对简单的样本数据集(仅在第9行中从account_id = 1过滤掉了1101条记录),有一个INDEX RANGE SCAN实现了总共1212K行的内存。 看起来我们有O(n 2 )复杂度。 即正在应用非常幼稚的算法。

(不要以为770ms对于这个琐碎的查询来说很快!)

尽管您可能会稍微调整该查询,但我们仍然应该认为Oracle应该能够为这个简单的任务设计O(n)算法。

使用递归SQL

没有人喜欢编写递归SQL。 没有人。 让我说服你。

为简单起见,我们假设我们还有一个TRANSACTION_NR列,该列按其排序顺序枚举了事务,可用于简化递归:

| ID   | VALUE_DATE | AMOUNT | TRANSACTION_NR |
|------|------------|--------|----------------|
| 9997 | 2014-03-18 |  99.17 |              1 |
| 9981 | 2014-03-16 |  71.44 |              2 |
| 9979 | 2014-03-16 | -94.60 |              3 |
| 9977 | 2014-03-16 |  -6.96 |              4 |
| 9971 | 2014-03-15 | -65.95 |              5 |

准备? 看看这段华丽的SQL!

WITH ordered_with_balance (
  account_id, value_date, amount, 
  balance, transaction_number
)
AS (
  SELECT t1.account_id, t1.value_date, t1.amount, 
         t1.current_balance, t1.transaction_number
  FROM   v_transactions_by_time t1
  WHERE  t1.transaction_number = 1

  UNION ALL

  SELECT t1.account_id, t1.value_date, t1.amount, 
         t2.balance - t2.amount, t1.transaction_number
  FROM   ordered_with_balance t2
  JOIN   v_transactions_by_time t1
  ON     t1.transaction_number = 
         t2.transaction_number + 1
  AND    t1.account_id = t2.account_id
)
SELECT   *
FROM     ordered_with_balance
WHERE    account_id= 1
ORDER BY transaction_number ASC

阿奇…如何读懂这种美丽?

本质上,我们是在自我声明将要声明的视图(公用表表达式):

WITH ordered_with_balance (
  account_id, value_date, amount, 
  balance, transaction_number
)
AS (
  SELECT t1.account_id, t1.value_date, t1.amount, 
         t1.current_balance, t1.transaction_number
  FROM   v_transactions_by_time t1
  WHERE  t1.transaction_number = 1

  UNION ALL

  SELECT t1.account_id, t1.value_date, t1.amount, 
         t2.balance - t2.amount, t1.transaction_number
  FROM   ordered_with_balance t2
  JOIN   v_transactions_by_time t1
  ON     t1.transaction_number = 
         t2.transaction_number + 1
  AND    t1.account_id = t2.account_id
)
SELECT   *
FROM     ordered_with_balance
WHERE    account_id= 1
ORDER BY transaction_number ASC

UNION ALL表达式的第一个子选择中,我们仅针对第一个transaction_number投影帐户的current_balance

UNION ALL表达式的第二个子选择中,我们计划previous帐户交易的余额与当前帐户交易的AMOUNT之差。

WITH ordered_with_balance (
  account_id, value_date, amount, 
  balance, transaction_number
)
AS (
  SELECT t1.account_id, t1.value_date, t1.amount, 
         t1.current_balance, t1.transaction_number
  FROM   v_transactions_by_time t1
  WHERE  t1.transaction_number = 1

  UNION ALL

  SELECT t1.account_id, t1.value_date, t1.amount, 
         t2.balance - t2.amount, t1.transaction_number
  FROM   ordered_with_balance t2
  JOIN   v_transactions_by_time t1
  ON     t1.transaction_number = 
         t2.transaction_number + 1
  AND    t1.account_id = t2.account_id
)
SELECT   *
FROM     ordered_with_balance
WHERE    account_id= 1
ORDER BY transaction_number ASC

并且由于我们要递归到ordered_with_balance公用表表达式,因此它将继续进行直到我们到达“最后一个”事务为止。

现在让我们进行有根据的猜测,看看效果是否良好……

递归SQL的执行计划

递归SQL的执行计划

好。 没有。 我们在内存中获得了更多的行,即11M行最多为1101行 。 该计划的部分原因是由于TRANSACTION_NUMBER实用程序列是Oracle无法优化的另一个计算列。 但是,这里的本质是事实,要正确地解决它已经非常困难,而要快速解决它甚至更加困难。

使用视窗功能

所以,我们受够了。 让我们听听一些好消息。

窗口函数前有SQL,窗口函数后有SQL
Dimitri Fontaine这篇出色的文章中

解决此问题的最佳方法是:

SELECT
  t.*,
  t.current_balance - NVL(
    SUM(t.amount) OVER (
      PARTITION BY t.account_id
      ORDER BY     t.value_date DESC,
                   t.id         DESC
      ROWS BETWEEN UNBOUNDED PRECEDING
           AND     1         PRECEDING
    ),
  0) AS balance
FROM     v_transactions t
WHERE    t.account_id = 1
ORDER BY t.value_date DESC,
         t.id         DESC

本质上,我们在做与嵌套SELECT完全相同的事情。 我们在行的子集上方减去所有AMOUNT值的SUM()

  • 与当前行位于相同的PARTITION中(即具有相同的account_id
  • 通过与帐户交易相同的排序标准进行排序(来自外部查询)
  • 按照上述顺序严格定位当前行之前

或者,再次,在视觉上:

| ID   | VALUE_DATE |  AMOUNT |  BALANCE |
|------|------------|---------|----------|
| 9997 | 2014-03-18 | -(99.17)|+19985.81 |
| 9981 | 2014-03-16 | -(71.44)| 19886.64 |
| 9979 | 2014-03-16 |-(-94.60)| 19815.20 |
| 9977 | 2014-03-16 |   -6.96 |=19909.80 |
| 9971 | 2014-03-15 |  -65.95 | 19916.76 |

现在, 执行吗?

窗口功能的执行计划

窗口功能的执行计划

哈利路亚!

它再快不过了! 窗口函数可能是最被低估的SQL功能

使用Oracle MODEL子句

现在,对于那些想要以怪异,古怪的SQL激怒其他开发人员的SQL书呆子来说,这更是一种特殊待遇。 MODEL子句(仅在Oracle中可用)。

SELECT account_id, value_date, amount, balance
FROM (
  SELECT id, account_id, value_date, amount,
         current_balance AS balance
  FROM   v_transactions
) t
WHERE account_id = 1
MODEL
  PARTITION BY (account_id)
  DIMENSION BY (
    ROW_NUMBER() OVER (
      ORDER BY value_date DESC, id DESC
    ) AS rn
  )
  MEASURES (value_date, amount, balance)
  RULES (
    balance[rn > 1] = balance[cv(rn) - 1] 
                    - amount [cv(rn) - 1]
  )
ORDER BY rn ASC

现在,如何阅读这只野兽? 我们正在获取样本数据并将其转换为:

  • PARTITION “由通常的标准编
  • 按照排序顺序(即交易行号)进行DIMENSION
  • MEASURE ,即提供日期,金额,余额(其中日期和金额保持不变的原始数据)的计算值
  • 根据RULES计算,该RULES将每笔交易(第一笔交易除外)的余额定义为前一笔交易的余额减去前笔交易的金额

还是太抽象了? 我知道。 但是这样想:

它使您想起某些事吗?

它使您想起某些事吗?

MS Excel! 每当您的项目经理认为他的MS Excel电子表格无法解决问题时,MODEL子句就是您的朋友!

它执行吗?

MODEL子句的执行计划

MODEL子句的执行计划

…几乎是这样(尽管不应将以上内容与实际基准相混淆)。

如果您还没有看到足够的信息, 请在此处查看有关MODEL子句的另一个很好的用例 。 有关所有详细信息,请考虑阅读Oracle的官方MODEL子句白皮书

自己做

你喜欢上面的例子吗? 不用担心 当您第一次看到这些东西时,它们可能会令人困惑。 但从本质上讲,它们实际上并不那么复杂。 而且,一旦您在工具链中拥有了这些功能,您将比必须用命令式编程语言写出所有这些算法的方式无限地工作。

如果您想使用这些示例,请下载Oracle XErunning-totals.sql脚本并开始运行!

结论

Data Geekery ,我们总是说:

SQL是仅凭其强大功能才能揭开神秘面纱的设备

有时,我们还会引用温斯顿·丘吉尔(Winston Churchill)所说的话:

SQL是最糟糕的数据库查询形式,除了所有其他形式

确实,正如MODEL子句向我们展示的那样,SQL可能变得非常极端。 但是,一旦掌握了窍门和表达式(最重要的是, 窗口函数 ),如果手头的任务是对简单或复杂数据集进行批量计算,那么使用SQL的效率将比使用其他许多技术高得多。 而且,查询通常比手写查询要快,至少在使用体面的数据库时。

因此,让我们在软件中实施SQL!

您有兴趣托管我们的NoSQL吗? 不,SQL! 在您当地的JUG上讨论还是作为内部演示? 与我们联系 ,我们非常乐于帮助您提高SQL技能!

翻译自: https://www.javacodegeeks.com/2014/05/nosql-no-sql-how-to-calculate-running-totals.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值