如果有的话,我们在各种JUG和会议上的jOOQ演讲几乎揭示了一件事:
Java开发人员不了解SQL。
甚至不一定是我们的错。 现在我们只是不接触SQL。
但是考虑一下:我们的开发人员(或我们的客户)每年因其出色的RDBMS而向Oracle,Microsoft,IBM,SAP支付数百万美元,而只是忽略了其90%的数据库功能并执行了一点CRUD和一个与Hibernate等ORM一起使用ACID。 我们已经忘记了为什么这些RDBMS首先如此昂贵。 我们没有关注对SQL标准的各种改进,包括SQL:1999 , SQL:2003 , SQL:2008以及最近的SQL:2011 , 这些几乎都不受JPA支持 。
只要让我们的Java开发人员知道用五行SQL替换成千上万行错误的Java代码有多么容易和有趣。 不相信吗? 看一下这个:
推广SQL语言
我们不再只是推广jOOQ,而是开始帮助Java开发人员欣赏实际的SQL,无论他们使用的访问方式是什么。 由于可以通过以下任何API来欣赏真正的SQL:
- JDBC
- jOOQ (是的,令人震惊,我知道)
- Spring的JDBC扩展
- MyBatis
- Apache DbUtils
- Groovy 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 XE和running-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的执行计划
不。 如您所见,对于相对简单的样本数据集(仅在第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
公用表表达式,因此它将继续进行直到我们到达“最后一个”事务为止。
现在让我们进行有根据的猜测,看看效果是否良好……
好。 没有。 我们在内存中获得了更多的行,即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子句的另一个很好的用例 。 有关所有详细信息,请考虑阅读Oracle的官方MODEL子句白皮书
自己做
你喜欢上面的例子吗? 不用担心 当您第一次看到这些东西时,它们可能会令人困惑。 但从本质上讲,它们实际上并不那么复杂。 而且,一旦您在工具链中拥有了这些功能,您将比必须用命令式编程语言写出所有这些算法的方式无限地工作。
如果您想使用这些示例,请下载Oracle XE和running-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