window2016 密钥_2016年将是Java终于拥有Window Functions的一年!

window2016 密钥

这篇文章最初发表在jooq.org上 ,这是一个博客,从jOOQ的角度着眼于所有开源,Java和软件开发

我们撰写了许多有关窗口函数的博客文章,并在诸如以下文章中向我们的受众宣讲:

我最喜欢的窗口函数示例用例之一是运行总计 。 即从以下银行帐户交易表中获取:

| 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 |

对于SQL,这是小菜一碟。 观察SUM(t.amount) OVER(...)的用法:

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

窗口功能如何工作?

尽管有时语法有些令人恐惧,但窗口函数确实非常易于理解。 Windows是您的FROM / WHERE / GROUP BY / HAVING子句中产生的数据的“视图”。 它们使您可以访问相对于当前行的所有其他行,同时在SELECT子句中(或很少在ORDER BY子句中)进行计算。 上面的声明实际上是这样做的:

| 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 |

也就是说,对于任何给定的余额,从当前余额中减去SUM()OVER() ”与当前行(同一银行帐户)在同一分区中的所有行的窗口,并且这些行严格位于“当前行。

或者,详细而言:

  • PARTITION BY指定“ OVER() ”,该字符将窗口范围排成一行
  • ORDER BY指定窗口的排序方式
  • ROWS指定应考虑的有序行索引

我们可以使用Java集合吗?

jOOλ-Java 8中的缺失部分jOOλ在专家组关注的其他领域改进了JDK库。 我们可以! 如果您使用的是jOOλ :我们设计了一个完全免费的开源Apache 2.0许可库,因为我们认为JDK 8 Stream和Collector API只是不这样做。

设计Java 8时,很多精力都放在了支持并行流上。 很好,但是当然不是唯一可以应用函数式编程的有用领域。 我们创建了jOOλ来填补这一空白-无需实现所有新的替代集合API,例如Javaslang功能性Java have。

jOOλ已经提供:

  1. 元组类型
  2. 对于有序的,仅顺序的流更有用的东西

通过最近发布的jOOλ0.9.9,我们添加了两个主要新功能:

  1. 大量新收藏家
  2. 视窗功能

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private double height;
    private double weight;
    // getters / setters

假设您有以下列表:

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));

现在,您希望获得以下聚合:

  • 人数
  • 最高年龄
  • 最小高度
  • 平均重量

对于任何习惯编写SQL的人来说,这都是一个荒谬的问题:

SELECT count(*), max(age), min(height), avg(weight)
FROM person

做完了 Java有多难? 事实证明,许多原始代码需要使用香草JDK 8 API编写。 考虑给出的复杂答案

使用jOOλ0.9.9时,再次解决此问题变得非常可笑, 并且读取的内容几乎类似于SQL

Tuple result =
Seq.seq(personsList)
   .collect(
       count(),
       max(Person::getAge),
       min(Person::getHeight),
       avg(Person::getWeight)
   );
 
System.out.println(result);

结果如下:

(3, Optional[35], Optional[1.69], Optional[70.0])

请注意,这不是针对SQL数据库运行查询(这就是jOOQ的目的)。 我们正在针对内存中的Java集合运行此“查询”。

现在窗口功能如何?

是的,本文的标题并没有涉及琐碎的聚合工作。 它承诺了很棒的窗口功能。

但是,窗口函数不过是数据流子集上的聚合(或排名)而已。 您想要维护原始记录,而不是将所有流(或表)聚合到单个记录中,而是直接在每个单独的记录上提供聚合。

窗口函数的一个很好的入门示例是本文提供的示例,它解释了ROW_NUMBER(),RANK()和DENSE_RANK()之间的区别 。 考虑以下PostgreSQL查询:

SELECT
  v, 
  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);

它产生:

| 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进行相同的操作

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 |
+----+----+----+----+

同样,请注意,我们没有对数据库运行任何查询。 一切都在内存中完成。

注意两件事:

  • jOOλ的窗口函数返回0(基于Java API的期望值),而不是SQL(全为1)。
  • 在Java中,无法使用命名列构造临时记录。 不幸的是,我确实希望将来的Java将为此类语言功能提供支持。

让我们回顾一下代码中到底发生了什么:

System.out.println(
 
    // This is just enumerating our values
    Seq.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()
);

而已! 很简单,不是吗?

我们可以做的更多! 看一下这个:

System.out.println(
    Seq.of("a", "a", "a", "b", "c", "c", "d", "e")
       .window(naturalOrder())
       .map(w -> tuple(
            w.value(),   // v0 
            w.count(),   // v1
            w.median(),  // v2
            w.lead(),    // v3
            w.lag(),     // v4
            w.toString() // v5
       ))
       .format()
);

以上产量是多少?

+----+----+----+---------+---------+----------+
| 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 |
+----+----+----+---------+---------+----------+

现在,您的分析心脏应该跳了起来。

4376565 [1]

等一会儿。 我们也可以像在SQL中那样做框架吗? 我们可以。 就像在SQL中一样,当我们省略窗口定义上的frame子句(但我们确实指定了ORDER BY子句)时,默认情况下将应用以下内容:

RANGE BETWEEN UNBOUNDED PRECEDING
  AND CURRENT ROW

我们在前面的示例中已经做到了。 可以在第v5列中看到,在该列中我们从第一个值到当前值聚合字符串。 因此,让我们指定框架:

System.out.println(
    Seq.of("a", "a", "a", "b", "c", "c", "d", "e")
       .window(naturalOrder(), -1, 1) // frame here
       .map(w -> tuple(
            w.value(),   // v0
            w.count(),   // v1
            w.median(),  // v2
            w.lead(),    // v3
            w.lag(),     // v4
            w.toString() // v5
       ))
       .format()
);

结果很简单:

+----+----+----+---------+---------+-----+
| 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  |
+----+----+----+---------+---------+-----+

如预期的那样, lead()lag()不受影响,这与count()median()toString()相反

现在,让我们回顾一下运行总计。

通常,您不会根据流本身的标量值来计算窗口函数,因为该值通常不是标量值,而是元组(或Java语言中的POJO)。 取而代之的是,您从元组(或POJO)中提取值并对其进行汇总。 因此,再次,在计算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编写运行总计的方法

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 |
+------+------------+--------+----------+

这里有几件事发生了变化:

  • 比较器现在考虑两个比较。 不幸的是JEP-101并没有完全实现 ,这就是为什么我们需要在此处帮助类型编译器的原因。
  • Window.value()现在是一个元组,而不是单个值。 因此,我们需要从中提取有趣的列AMOUNT (通过t -> t.v3 )。 另一方面,我们可以简单地将附加值concat()给元组

但是已经足够了。 除了比较器的详细信息(我们一定会在将来的jOOλ版本中解决)之外,编写窗口函数也是小菜一碟。

我们还能做什么?

本文不是对新API可以做的所有事情的完整描述。 我们将很快写一个后续博客文章,并提供其他示例。 例如:

  • 未描述partition by子句,但也可用
  • 您可以指定比此处公开的单个窗口更多的窗口,每个窗口都具有单独的PARTITION BYORDER BY和框架规范

另外,当前的实现还很规范,即它尚未(尚未)缓存聚合:

  • 对于无序/无框窗口(所有分区的值相同)
  • 严格升序的窗口(聚合可以基于先前的值,例如SUM()toString()关联收集器)

就我们而言就是这样。 下载jOOλ,试用它,并享受一个事实,那就是所有Java 8开发人员现在都可以使用最强大SQL功能!
https://github.com/jOOQ/jOOL

翻译自: https://jaxenter.com/2016-will-be-the-year-remembered-as-when-java-finally-had-window-functions-123197.html

window2016 密钥

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值