2016年将是Java终于拥有窗口函数的那一年!
你沒聽錯(cuò)。 到目前為止,出色的窗口功能是SQL獨(dú)有的功能。 即使是復(fù)雜的函數(shù)式編程語言似乎也缺少這種漂亮的功能(如果我記錯(cuò)了,請(qǐng)糾正我,Haskell伙計(jì)們)。
我們撰寫了許多有關(guān)窗口函數(shù)的博客文章,并在諸如以下文章中向我們的受眾宣講:
- 可能最酷的SQL功能:窗口函數(shù)
- 使用此整齊的窗口函數(shù)技巧來計(jì)算時(shí)間序列中的時(shí)差
- 如何在SQL中找到最長(zhǎng)的連續(xù)事件系列
- 不要錯(cuò)過具有FIRST_VALUE(),LAST_VALUE(),LEAD()和LAG()的超凡SQL能力
- ROW_NUMBER(),RANK()和DENSE_RANK()之間的區(qū)別
我最喜歡的窗口函數(shù)示例用例之一是運(yùn)行總計(jì) 。 即從以下銀行帳戶交易表中獲取:
| 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 |…到此,并計(jì)算出余額:
| 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 |對(duì)于SQL,這是小菜一碟。 觀察SUM(t.amount) OVER(...)的用法:
SELECTt.*,t.current_balance - NVL(SUM(t.amount) OVER (PARTITION BY t.account_idORDER BY t.value_date DESC,t.id DESCROWS BETWEEN UNBOUNDED PRECEDINGAND 1 PRECEDING),0) AS balance FROM v_transactions t WHERE t.account_id = 1 ORDER BY t.value_date DESC,t.id DESC窗口功能如何工作?
(別忘了預(yù)訂我們的SQL Masterclass來了解窗口函數(shù) ,以及更多!)
盡管有時(shí)語法有些令人恐懼,但窗口函數(shù)確實(shí)非常易于理解。 Windows是您的FROM / WHERE / GROUP BY / HAVING子句中產(chǎn)生的數(shù)據(jù)的“視圖”。 它們使您可以訪問相對(duì)于當(dāng)前行的所有其他行,同時(shí)您可以在SELECT子句中(或者很少在ORDER BY子句中)進(jìn)行計(jì)算。 上面的聲明實(shí)際上是這樣的:
| 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 |也就是說,對(duì)于任何給定的余額,從當(dāng)前余額中減去SUM() “ OVER() ”與當(dāng)前行(同一銀行帳戶)在同一分區(qū)中的所有行的窗口,且這些行嚴(yán)格位于“高于”當(dāng)前行。
或者,詳細(xì)而言:
- PARTITION BY指定“ OVER() ”,該字符將窗口范圍排成一行
- ORDER BY指定窗口的排序方式
- ROWS指定應(yīng)考慮的有序行索引
我們可以使用Java集合嗎?
我們可以! 如果您使用的是jOOλ :我們?cè)O(shè)計(jì)了一個(gè)完全免費(fèi)的開源Apache 2.0許可庫(kù),因?yàn)槲覀冋J(rèn)為JDK 8 Stream和Collector API只是不這樣做。
設(shè)計(jì)Java 8時(shí),很多精力都放在了支持并行流上。 很好,但是當(dāng)然不是可以應(yīng)用函數(shù)式編程的唯一有用領(lǐng)域。 我們創(chuàng)建了jOOλ來填補(bǔ)這一空白-無需實(shí)現(xiàn)所有新的替代集合API,例如Javaslang或功能性Java have。
jOOλ已經(jīng)提供:
通過最近發(fā)布的jOOλ0.9.9,我們添加了兩個(gè)主要新功能:
JDK中許多缺少的收集器
JDK附帶了幾個(gè)收集器,但是它們看起來確實(shí)笨拙且冗長(zhǎng),而且沒有人真正喜歡編寫像此Stack Overflow問題 (以及許多其他問題)中所介紹的收集器那樣的收集器。
但是鏈接問題中公開的用例是非常有效的。 您要匯總?cè)藛T列表中的幾件事:
public class Person {private String firstName;private String lastName;private int age;private double height;private double weight;// getters / setters假設(shè)您有以下列表:
List<Person> personsList = new ArrayList<Person>();personsList.add(new Person("John", "Doe", 25, 1.80, 80)); personsList.add(new Person("Jane", "Doe", 30, 1.69, 60)); personsList.add(new Person("John", "Smith", 35, 174, 70));現(xiàn)在,您希望獲得以下聚合:
- 人數(shù)
- 最高年齡
- 最小高度
- 平均重量
對(duì)于習(xí)慣于編寫SQL的任何人來說,這都是一個(gè)荒謬的問題:
SELECT count(*), max(age), min(height), avg(weight) FROM person做完了 Java有多難? 事實(shí)證明,許多原始代碼需要使用香草JDK 8 API編寫。 考慮給出的復(fù)雜答案
- 由塔吉爾·瓦列夫(Tagir Valeev)
- 通過TriCore
使用jOOλ0.9.9時(shí),再次解決此問題變得非??尚?#xff0c; 并且讀取的內(nèi)容幾乎類似于SQL :
Tuple result = Seq.seq(personsList).collect(count(),max(Person::getAge),min(Person::getHeight),avg(Person::getWeight));System.out.println(result);結(jié)果產(chǎn)生:
(3, Optional[35], Optional[1.69], Optional[70.0])請(qǐng)注意,這并不針對(duì)SQL數(shù)據(jù)庫(kù)運(yùn)行查詢(這就是jOOQ的目的)。 我們正在針對(duì)內(nèi)存中的Java集合運(yùn)行此“查詢”。
現(xiàn)在窗口功能如何?
是的,本文的標(biāo)題并沒有涉及瑣碎的聚合工作。 它承諾了很棒的窗口功能。
但是,窗口函數(shù)不過是數(shù)據(jù)流子集上的聚合(或排名)而已。 您想要維護(hù)原始記錄,而不是將所有流(或表)聚合到單個(gè)記錄中,而是直接在每個(gè)單獨(dú)的記錄上提供聚合。
窗口函數(shù)的一個(gè)很好的入門示例是本文提供的示例,它解釋了ROW_NUMBER(),RANK()和DENSE_RANK()之間的區(qū)別 。 考慮以下PostgreSQL查詢:
SELECTv, ROW_NUMBER() OVER(w),RANK() OVER(w),DENSE_RANK() OVER(w) FROM (VALUES('a'),('a'),('a'),('b'),('c'),('c'),('d'),('e') ) t(v) WINDOW w AS (ORDER BY v);它產(chǎn)生:
| V | ROW_NUMBER | RANK | DENSE_RANK | |---|------------|------|------------| | a | 1 | 1 | 1 | | a | 2 | 1 | 1 | | a | 3 | 1 | 1 | | b | 4 | 4 | 2 | | c | 5 | 5 | 3 | | c | 6 | 5 | 3 | | d | 7 | 7 | 4 | | e | 8 | 8 | 5 |在Java 8中,可以使用jOOλ0.9.9進(jìn)行相同的操作
System.out.println(Seq.of("a", "a", "a", "b", "c", "c", "d", "e").window(naturalOrder()).map(w -> tuple(w.value(),w.rowNumber(),w.rank(),w.denseRank())).format() );屈服…
+----+----+----+----+ | v0 | v1 | v2 | v3 | +----+----+----+----+ | a | 0 | 0 | 0 | | a | 1 | 0 | 0 | | a | 2 | 0 | 0 | | b | 3 | 3 | 1 | | c | 4 | 4 | 2 | | c | 5 | 4 | 2 | | d | 6 | 6 | 3 | | e | 7 | 7 | 4 | +----+----+----+----+同樣,請(qǐng)注意,我們沒有對(duì)數(shù)據(jù)庫(kù)運(yùn)行任何查詢。 一切都在內(nèi)存中完成。
注意兩件事:
- jOOλ的窗口函數(shù)返回0(基于Java API的預(yù)期),而不是SQL(均基于1)。
- 在Java中,無法使用命名列構(gòu)造臨時(shí)記錄。 不幸的是,我確實(shí)希望將來的Java將為此類語言功能提供支持。
讓我們回顧一下代碼中到底發(fā)生了什么:
System.out.println(// This is just enumerating our valuesSeq.of("a", "a", "a", "b", "c", "c", "d", "e")// Here, we specify a single window to be// ordered by the value T in the stream, in// natural order.window(naturalOrder())// The above window clause produces a Window<T>// object (the w here), which exposes....map(w -> tuple(// ... the current value itself, of type String...w.value(),// ... or various rankings or aggregations on// the above window.w.rowNumber(),w.rank(),w.denseRank()))// Just some nice formatting to produce the table.format() );而已! 很簡(jiǎn)單,不是嗎?
我們可以做的更多! 看一下這個(gè):
System.out.println(Seq.of("a", "a", "a", "b", "c", "c", "d", "e").window(naturalOrder()).map(w -> tuple(w.value(), // v0 w.count(), // v1w.median(), // v2w.lead(), // v3w.lag(), // v4w.toString() // v5)).format() );以上產(chǎn)量是多少?
+----+----+----+---------+---------+----------+ | v0 | v1 | v2 | v3 | v4 | v5 | +----+----+----+---------+---------+----------+ | a | 1 | a | a | {empty} | a | | a | 2 | a | a | a | aa | | a | 3 | a | b | a | aaa | | b | 4 | a | c | a | aaab | | c | 5 | a | c | b | aaabc | | c | 6 | a | d | c | aaabcc | | d | 7 | b | e | c | aaabccd | | e | 8 | b | {empty} | d | aaabccde | +----+----+----+---------+---------+----------+現(xiàn)在,您的分析心臟應(yīng)該跳了起來。
等一下。 我們也可以像在SQL中那樣做框架嗎? 我們可以。 就像在SQL中一樣,當(dāng)我們省略窗口定義上的frame子句(但我們確實(shí)指定了ORDER BY子句)時(shí),默認(rèn)情況下將應(yīng)用以下內(nèi)容:
RANGE BETWEEN UNBOUNDED PRECEDINGAND CURRENT ROW我們?cè)谇懊娴氖纠幸呀?jīng)做到了。 可以在第v5列中看到,在該列中我們從第一個(gè)值一直到當(dāng)前值聚合字符串。 因此,讓我們指定框架:
System.out.println(Seq.of("a", "a", "a", "b", "c", "c", "d", "e").window(naturalOrder(), -1, 1) // frame here.map(w -> tuple(w.value(), // v0w.count(), // v1w.median(), // v2w.lead(), // v3w.lag(), // v4w.toString() // v5)).format() );結(jié)果很簡(jiǎn)單:
+----+----+----+---------+---------+-----+ | v0 | v1 | v2 | v3 | v4 | v5 | +----+----+----+---------+---------+-----+ | a | 2 | a | a | {empty} | aa | | a | 3 | a | a | a | aaa | | a | 3 | a | b | a | aab | | b | 3 | b | c | a | abc | | c | 3 | c | c | b | bcc | | c | 3 | c | d | c | ccd | | d | 3 | d | e | c | cde | | e | 2 | d | {empty} | d | de | +----+----+----+---------+---------+-----+如預(yù)期的那樣, lead()和lag()不會(huì)受到影響,與count() , median()和toString()相反
現(xiàn)在,讓我們回顧一下運(yùn)行總計(jì)。
通常,您不會(huì)根據(jù)流本身的標(biāo)量值來計(jì)算窗口函數(shù),因?yàn)樵撝低ǔ2皇菢?biāo)量值,而是元組(或Java語言中的POJO)。 取而代之的是,您從元組(或POJO)中提取值并對(duì)其進(jìn)行匯總。 因此,再次,在計(jì)算BALANCE ,我們需要首先提取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 |這是使用Java 8和jOOλ0.9.9編寫運(yùn)行總計(jì)的方法
BigDecimal currentBalance = new BigDecimal("19985.81");Seq.of(tuple(9997, "2014-03-18", new BigDecimal("99.17")),tuple(9981, "2014-03-16", new BigDecimal("71.44")),tuple(9979, "2014-03-16", new BigDecimal("-94.60")),tuple(9977, "2014-03-16", new BigDecimal("-6.96")),tuple(9971, "2014-03-15", new BigDecimal("-65.95"))) .window(Comparator.comparing((Tuple3<Integer, String, BigDecimal> t) -> t.v1, reverseOrder()).thenComparing(t -> t.v2), Long.MIN_VALUE, -1) .map(w -> w.value().concat(currentBalance.subtract(w.sum(t -> t.v3).orElse(BigDecimal.ZERO)) ));屈服
+------+------------+--------+----------+ | v0 | v1 | v2 | v3 | +------+------------+--------+----------+ | 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 | +------+------------+--------+----------+這里有幾件事發(fā)生了變化:
- 比較器現(xiàn)在考慮兩個(gè)比較。 不幸的是JEP-101并未完全實(shí)現(xiàn) ,這就是為什么我們需要在此處幫助類型編譯器的原因。
- Window.value()現(xiàn)在是一個(gè)元組,而不是單個(gè)值。 因此,我們需要從中提取有趣的列AMOUNT (通過t -> t.v3 )。 另一方面,我們可以簡(jiǎn)單地將附加值concat()給元組
但是已經(jīng)足夠了。 除了比較器的詳細(xì)信息(我們一定會(huì)在將來的jOOλ版本中解決)之外,編寫窗口函數(shù)也是小菜一碟。
我們還能做什么?
本文不是對(duì)新API可以做的所有事情的完整描述。 我們將很快寫一個(gè)后續(xù)博客文章,并附帶其他示例。 例如:
- 未描述partition by子句,但也可用
- 您可以指定比此處公開的單個(gè)窗口更多的窗口,每個(gè)窗口都具有單獨(dú)的PARTITION BY , ORDER BY和框架規(guī)范
另外,當(dāng)前的實(shí)現(xiàn)還很規(guī)范,即,它還沒有(但是)緩存聚合:
- 對(duì)于無序/無框窗口(所有分區(qū)的值相同)
- 嚴(yán)格遞增的窗口(聚合可以基于先前的值,例如SUM()或toString()關(guān)聯(lián)收集器)
就我們而言就是這樣。 下載jOOλ,試用它,并享受一個(gè)事實(shí),那就是所有Java 8開發(fā)人員現(xiàn)在都可以使用最強(qiáng)大的SQL功能!
- https://github.com/jOOQ/jOOL
翻譯自: https://www.javacodegeeks.com/2016/01/2016-will-year-remembered-java-finally-window-functions.html
總結(jié)
以上是生活随笔為你收集整理的2016年将是Java终于拥有窗口函数的那一年!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 苹果8安卓系统版本(苹果8安卓)
- 下一篇: 信用评级备案材料(信用评级备案)