sql面试题_如何将SQL GROUP BY和聚合转换为Java 8

sql面试题

sql面试题

我无法抗拒。 我已经阅读了Hugo Prudente的Stack Overflow这个问题。 而且我知道必须有比JDK提供的更好的方法。

问题如下:

我正在寻找一个lambda来优化已检索的数据。 我有一个原始的结果集,如果用户不更改我想要的日期,则使用Java的lambda来对结果进行分组。 我对使用Java的lambdas还是陌生的。

我正在寻找的lambda与此查询类似。

SELECT
    z, w, 
    MIN(x), MAX(x), AVG(x), 
    MIN(y), MAX(y), AVG(y) 
FROM table 
GROUP BY z, w;

函数编程不是。

在进行讨论之前,让我们建立一个非常重要的事实。 SQL是一种完全声明性的语言。 Java 8之类的功能性(或“功能性”语言,以使Haskell爱好者保持和平)不是声明性的。 尽管使用函数来表达数据转换算法要比使用对象来表达更为简洁,或更糟糕的是使用命令式指令来表达它们,但您仍在明确地表达该算法。

编写SQL时,您不会编写任何算法。 您只需描述您想要的结果。 SQL引擎的优化程序将为您找出算法-例如,基于您可能在Z上有索引但在W(Z, W)上没有索引的事实。

尽管可以使用Java 8轻松实现此类简单示例,但一旦需要进行更复杂的报告,您将很快遇到Java的局限性。

当然,正如我们之前写过的,将SQL和函数式编程结合起来可以达到最佳效果

如何用Java 8编写?

有多种方法可以做到这一点。 本质是要了解这种转变中的所有参与者。 而且,不管您发现这是简单还是困难(适合Java 8或不足),思考新Stream API的不同,鲜为人知的部分无疑都是值得的。

这里的主要参与者是:

  • Stream :如果您使用的是JDK 8库,那么新的java.util.stream.Stream类型将是您的首选。
  • 收集器:JDK为我们提供了一个相当底层的,因此非常强大的新API,用于数据聚合(也称为“缩减”)。 该API由新的java.util.stream.Collector类型进行了总结,到目前为止,我们在Blogosphere中仅听到了很少的新类型

免责声明

此处显示的某些代码可能无法在您喜欢的IDE中使用。 不幸的是,即使Java 7寿终正寝,所有主要的IDE(Eclipse,IntelliJ,NetBeans),甚至javac编译器仍然存在很多与泛型类型推断和lambda表达式组合有关的错误。 敬请期待,直到修复了这些错误! 并报告您发现的任何错误。 我们都会感谢您!

我们走吧!

让我们回顾一下我们SQL语句:

SELECT
    z, w, 
    MIN(x), MAX(x), AVG(x), 
    MIN(y), MAX(y), AVG(y) 
FROM table 
GROUP BY z, w;

Stream API而言,表本身就是Stream 。 让我们假设我们有一个“表类型” A

class A {
    final int w;
    final int x;
    final int y;
    final int z;

    A(int w, int x, int y, int z) {
        this.w = w;
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public String toString() {
        return "A{" +
                "w=" + w +
                ", x=" + x +
                ", y=" + y +
                ", z=" + z +
                '}';
    }
}

如果需要,还可以添加equals()hashCode()

现在,我们可以使用Stream.of()和一些示例数据轻松组成Stream

Stream<A> stream =
Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5));

现在,下一步是GROUP BY z, w 。 不幸的是, Stream API本身不包含这种便捷方法。 我们必须通过指定更通用的Stream.collect()操作,然后将一个Collector传递给它进行分组,来诉诸于更底层的操作。 幸运的是, Collectors帮助Collectors类中已经提供了各种不同的分组Collectors

因此,我们将其添加到stream

Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(...));

jool-logo-黑色
现在,有趣的部分开始了。 我们如何指定我们要同时按AzAw分组? 我们需要为该groupingBy方法提供一个函数,该函数可以从A类型提取诸如SQL元组之类的东西。 我们可以编写自己的元组,也可以简单地使用jOOλ的元组, jOOλ是我们创建并开源库,可以改善jOOQ集成测试

Tuple2类型大致如下:

public class Tuple2<T1, T2> {

    public final T1 v1;
    public final T2 v2;

    public T1 v1() {
        return v1;
    }

    public T2 v2() {
        return v2;
    }

    public Tuple2(T1 v1, T2 v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
}

public interface Tuple {
    static <T1, T2> Tuple2<T1, T2> tuple(T1 v1, T2 v2) {
        return new Tuple2<>(v1, v2);
    }
}

它具有许多有用的功能,但是这些功能对于本文而言已足够。

在旁注

为什么JDK不附带诸如C#Scala's的内置元组,这使我感到困惑。

没有元组的函数式编程就像没有糖的咖啡:苦涩的表情。

反正…回到正轨

因此,我们按照(Az, Aw)元组进行分组,就像在SQL中一样

Map<Tuple2<Integer, Integer>, List<A>> map =
Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(
    a -> tuple(a.z, a.w)
));

如您所见,这将产生一个冗长但非常具有描述性的类型,一个包含我们的分组元组作为其键的映射,以及一个所收集的表记录的列表作为其值的映射。

运行以下语句:

map.entrySet().forEach(System.out::println);

将产生:

(1, 1)=[A{w=1, x=1, y=1, z=1}, A{w=1, x=2, y=3, z=1}]
(4, 9)=[A{w=9, x=8, y=6, z=4}, A{w=9, x=9, y=7, z=4}]
(5, 2)=[A{w=2, x=3, y=4, z=5}, A{w=2, x=4, y=4, z=5}, A{w=2, x=5, y=5, z=5}]

那已经很棒了! 实际上,它的行为类似于SQL:2011标准的COLLECT()聚合函数,该函数在Oracle 10g +中也可用

现在,我们实际上不是汇总A记录,而是汇总xy的各个值。 JDK为我们提供了两个有趣的新类型,例如java.util.IntSummaryStatistics ,可通过Collectors.summarizingInt()Collectors类型再次方便使用。

附带说明

就我的口味而言,这种大锤数据聚合技术有点古怪。 在JDK库已经离开故意低水平和冗长,也许是为了保持库占地面积小,或防止“可怕”的后果时,在5 - 10年(JDK 9和10后释放), 可以很明显看出一些特点可能已经过早添加

同时,这个IntSummaryStatistics全部或全都不IntSummaryStatistics ,它盲目地为您的集合聚合了这些流行的聚合值:

  • COUNT(*)
  • SUM()
  • MIN()
  • MAX()

显然,一旦有了SUM()COUNT(*) ,就也有AVG() = SUM() / COUNT(*) 。 因此,这将是Java方式。 IntSummaryStatistics

如果您想知道,SQL:2011标准指定了以下聚合函数:

AVG, MAX, MIN, SUM, EVERY, ANY, SOME, COUNT, STDDEV_POP, STDDEV_SAMP, VAR_SAMP, VAR_POP, COLLECT, FUSION, INTERSECTION, COVAR_POP, COVAR_SAMP, CORR, REGR_SLOPE, REGR_INTERCEPT, REGR_COUNT, REGR_R2, REGR_AVGX, REGR_AVGY, REGR_SXX, REGR_SYY, REGR_SXY, PERCENTILE_CONT, PERCENTILE_DISC, ARRAY_AGG

很明显,SQL中还有许多其他特定供应商的聚合和窗口函数。 我们已经在博客上发布了所有内容:

的确如此, MIN, MAX, SUM, COUNT, AVG无疑是最受欢迎的。 但是如果这些默认聚合类型中没有包含它们,而是以一种更加可组合的方式使其可用,那就更好了。

反正…回到正轨

如果您想保持低水平并主要使用JDK API,则可以使用以下技术在两列上实现聚合:

Map<
    Tuple2<Integer, Integer>, 
    Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map = Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(
    a -> tuple(a.z, a.w),
    Collector.of(

        // When collecting, we'll aggregate data
        // into two IntSummaryStatistics for x and y
        () -> tuple(new IntSummaryStatistics(), 
                    new IntSummaryStatistics()),

        // The accumulator will simply take
        // new t = (x, y) values
        (r, t) -> {
            r.v1.accept(t.x);
            r.v2.accept(t.y);
        },

        // The combiner will merge two partial
        // aggregations, in case this is executed
        // in parallel
        (r1, r2) -> {
            r1.v1.combine(r2.v1);
            r1.v2.combine(r2.v2);

            return r1;
        }
    )
));

map.entrySet().forEach(System.out::println);

现在上面将打印

(1, 1)=(IntSummaryStatistics{count=2, sum=3, min=1, average=1.500000, max=2}, 
        IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3})
(4, 9)=(IntSummaryStatistics{count=2, sum=17, min=8, average=8.500000, max=9}, 
        IntSummaryStatistics{count=2, sum=13, min=6, average=6.500000, max=7})
(5, 2)=(IntSummaryStatistics{count=3, sum=12, min=3, average=4.000000, max=5}, 
        IntSummaryStatistics{count=3, sum=13, min=4, average=4.333333, max=5})

但是显然,没有人愿意写那么多代码。 用jOOλ可以用更少的代码来实现相同的目的

Map<
    Tuple2<Integer, Integer>, 
    Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map =

// Seq is like a Stream, but sequential only,
// and with more features
Seq.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))

// Seq.groupBy() is just short for 
// Stream.collect(Collectors.groupingBy(...))
.groupBy(
    a -> tuple(a.z, a.w),

    // ... because once you have tuples, 
    // why not add tuple-collectors?
    Tuple.collectors(
        Collectors.summarizingInt(a -> a.x),
        Collectors.summarizingInt(a -> a.y)
    )
));

您在上面看到的内容可能与原始的非常简单SQL语句非常接近:

SELECT
    z, w, 
    MIN(x), MAX(x), AVG(x), 
    MIN(y), MAX(y), AVG(y) 
FROM table 
GROUP BY z, w;

这里有趣的部分是,我们拥有所谓的“元组收集器”,这是一个Collector ,它可以针对任何程度的元组(最多8个)将数据收集到汇总结果的元组中。 这是Tuple.collectors的代码:

// All of these generics... sheesh!
static <T, A1, A2, D1, D2> 
       Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>> 
collectors(
    Collector<T, A1, D1> collector1
  , Collector<T, A2, D2> collector2
) {
    return Collector.of(
        () -> tuple(
            collector1.supplier().get()
          , collector2.supplier().get()
        ),
        (a, t) -> {
            collector1.accumulator().accept(a.v1, t);
            collector2.accumulator().accept(a.v2, t);
        },
        (a1, a2) -> tuple(
            collector1.combiner().apply(a1.v1, a2.v1)
          , collector2.combiner().apply(a1.v2, a2.v2)
        ),
        a -> tuple(
            collector1.finisher().apply(a.v1)
          , collector2.finisher().apply(a.v2)
        )
    );
}

其中Tuple2<D1, D2>是我们从collector1 (提供D1 )和collector2 (提供D2 )派生的聚合结果类型。

而已。 大功告成!

结论

Java 8是迈向Java函数编程的第一步。 使用Streams和lambda表达式,我们已经可以完成很多工作。 但是,JDK API的级别极低,使用诸如Eclipse,IntelliJ或NetBeans之类的IDE时的体验仍然有些令人沮丧。 在撰写本文(并添加Tuple.collectors()方法)时,我已经向不同的IDE报告了大约10个错误。 在JDK 1.8.0_40之前,某些javac编译器错误尚未修复。 换一种说法:

我只是不断地向泛滥的对象抛出泛型类型参数,直到编译器停止对我不利为止

但是,我们走的很好。 我相信JDK 9(尤其是JDK 10)将附带更多有用的API,届时上述所有内容都有望从新的值类型泛型类型专门化中受益。

jool-logo-黑色
我们创建了jOOλ,将缺少的片段添加到JDK库中。 如果您想全神贯注地进行函数式编程,即当您的词汇表包含诸如monads,monoids,functors之类的时髦术语(无法抗拒)时,我们建议您完全跳过JDK的Streams和jOOλ,然后下载functionaljava马克·佩里( Mark Perry)丹尼尔·迪特里希( Daniel Dietrich)的javaslang

翻译自: https://www.javacodegeeks.com/2015/01/how-to-translate-sql-group-by-and-aggregations-to-java-8.html

sql面试题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值