Статьи

Общие предложения SQL и их эквиваленты в потоках Java 8

Функциональное программирование допускает квази-декларативное программирование на языке общего назначения. Используя мощные свободно распространяемые API, такие как Stream API Java 8, последовательное расширение Stream Seq от jOOλ или более сложные библиотеки, такие как javaslang или functionsjava , мы можем выразить алгоритмы преобразования данных чрезвычайно лаконично. Сравните императивную и функциональную версию того же алгоритма Марио Фуско :

Императив против функциональности — разделение интересов pic.twitter.com/G2cC6iBkDJ

— Марио Фуско (@mariofusco) 1 марта 2015 г.

Используя такие API, функциональное программирование, безусловно, ощущается как настоящее декларативное программирование.

Самый популярный настоящий декларативный язык программирования — это SQL. Когда вы объединяете две таблицы, вы не указываете СУБД, как реализовать это объединение. Он может по своему усмотрению решить, является ли вложенный цикл, объединение слиянием, соединение с хешем или какой-либо другой алгоритм наиболее подходящим в контексте полного запроса и всей доступной метаинформации. Это чрезвычайно полезно, потому что предположения о производительности, которые действительны для простого объединения, могут больше не действовать для сложного, где другой алгоритм будет превосходить исходный. С помощью этой абстракции вы можете легко изменить запрос за 30 секунд, не беспокоясь о деталях низкого уровня, таких как алгоритмы или производительность.

Когда API позволяет вам комбинировать оба (например, jOOQ и Streams ), вы получаете лучшее из обоих миров — и эти миры не слишком отличаются.

В следующих разделах мы сравним обычные конструкции SQL с их эквивалентными выражениями, написанными на Java 8 с использованием Streams и jOOλ , в случае, если Stream API не предлагает достаточную функциональность .

Кортеж

Ради этой статьи мы будем предполагать, что строки / записи SQL имеют эквивалентное представление в Java. Для этого мы будем использовать тип кортежа jOOλ , который по сути:

01
02
03
04
05
06
07
08
09
10
public class Tuple2<T1, T2> {
 
    public final T1 v1;
    public final T2 v2;
 
    public Tuple2(T1 v1, T2 v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
}

… А также множество полезных уловок, таких как Tuple Comparable и т.д.

Обратите внимание, что мы предполагаем следующий импорт в этом и всех последующих примерах.

1
2
3
4
5
6
7
8
import static org.jooq.lambda.Seq.*;
import static org.jooq.lambda.tuple.Tuple.*;
 
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
 
import org.jooq.lambda.*;

Как и строки SQL, кортеж является типом «на основе значений» , что означает, что он на самом деле не имеет идентичности. Два кортежа (1, 'A') и (1, 'A') можно считать абсолютно эквивалентными. Удаление идентификаторов из игры делает SQL и функциональное программирование с неизменными структурами данных чрезвычайно элегантными.

FROM = of (), stream () и т. Д.

В SQL предложение FROM логически (но не синтаксически) предшествует всем остальным предложениям. Он используется для создания набора кортежей как минимум из одной таблицы, возможно, из нескольких соединенных таблиц. Предложение FROM одной таблицы может быть тривиально сопоставлено, например, с Stream.of() или любым другим методом, который просто создает поток:

SQL

1
2
3
4
5
SELECT *
FROM (
  VALUES(1, 1),
        (2, 2)
) t(v1, v2)

получая

1
2
3
4
5
6
+----+----+
| v1 | v2 |
+----+----+
|  1 |  1 |
|  2 |  2 |
+----+----+

Джава

1
2
3
4
Stream.of(
  tuple(1, 1),
  tuple(2, 2)
).forEach(System.out::println);

получая

1
2
(1, 1)
(2, 2)

CROSS JOIN = flatMap ()

Выбор из нескольких таблиц уже более интересен. Самый простой способ объединить две таблицы в SQL — это создать декартово произведение, либо через список таблиц, либо используя CROSS JOIN . Следующие два являются эквивалентными операторами SQL:

SQL

1
2
3
4
5
6
7
8
9
-- Table list syntax
SELECT *
FROM (VALUES( 1 ), ( 2 )) t1(v1),
     (VALUES('A'), ('B')) t2(v2)
 
-- CROSS JOIN syntax
SELECT *
FROM       (VALUES( 1 ), ( 2 )) t1(v1)
CROSS JOIN (VALUES('A'), ('B')) t2(v2)

получая

1
2
3
4
5
6
7
8
+----+----+
| v1 | v2 |
+----+----+
|  1 |  A |
|  1 |  B |
|  2 |  A |
|  2 |  B |
+----+----+

В перекрестном соединении (или декартовом произведении) каждое значение из t1 объединяется с каждым значением из t2 создавая строки size(t1) * size(t2) в целом.

Джава

В функциональном программировании с использованием Java 8 Stream метод Stream.flatMap() соответствует SQL CROSS JOIN как можно видеть в следующем примере:

1
2
3
4
5
6
List<Integer> s1 = Stream.of(1, 2);
Supplier<Stream<String>> s2 = ()->Stream.of("A", "B");
 
s1.flatMap(v1 -> s2.get()
                   .map(v2 -> tuple(v1, v2)))
  .forEach(System.out::println);

получая

1
2
3
4
(1, A)
(1, B)
(2, A)
(2, B)

Обратите внимание, как мы должны обернуть второй поток в Supplier потому что потоки могут потребляться только один раз , но вышеприведенный алгоритм действительно реализует вложенный цикл, объединяя все элементы потока s2 с каждым элементом из потока s1 . Альтернативой было бы не использовать потоки, а списки (что мы сделаем в последующих примерах для простоты):

1
2
3
4
5
6
7
List<Integer> s1 = Arrays.asList(1, 2);
List<String> s2 = Arrays.asList("A", "B");
 
s1.stream()
  .flatMap(v1 -> s2.stream()
                   .map(v2 -> tuple(v1, v2)))
  .forEach(System.out::println);

Фактически, CROSS JOIN может быть легко связан как в SQL, так и в Java:

SQL

01
02
03
04
05
06
07
08
09
10
11
-- Table list syntax
SELECT *
FROM (VALUES( 1 ), ( 2 )) t1(v1),
     (VALUES('A'), ('B')) t2(v2),
     (VALUES('X'), ('Y')) t3(v3)
 
-- CROSS JOIN syntax
SELECT *
FROM       (VALUES( 1 ), ( 2 )) t1(v1)
CROSS JOIN (VALUES('A'), ('B')) t2(v2)
CROSS JOIN (VALUES('X'), ('Y')) t3(v3)

получая

01
02
03
04
05
06
07
08
09
10
11
12
+----+----+----+
| v1 | v2 | v3 |
+----+----+----+
|  1 |  A |  X |
|  1 |  A |  Y |
|  1 |  B |  X |
|  1 |  B |  Y |
|  2 |  A |  X |
|  2 |  A |  Y |
|  2 |  B |  X |
|  2 |  B |  Y |
+----+----+----+

Джава

01
02
03
04
05
06
07
08
09
10
List<Integer> s1 = Arrays.asList(1, 2);
List<String> s2 = Arrays.asList("A", "B");
List<String> s3 = Arrays.asList("X", "Y");
 
s1.stream()
  .flatMap(v1 -> s2.stream()
                   .map(v2 -> tuple(v1, v2)))
  .flatMap(v12-> s3.stream()
                   .map(v3 -> tuple(v12.v1, v12.v2, v3)))
  .forEach(System.out::println);

получая

1
2
3
4
5
6
7
8
(1, A, X)
(1, A, Y)
(1, B, X)
(1, B, Y)
(2, A, X)
(2, A, Y)
(2, B, X)
(2, B, Y)

Обратите внимание, как мы явно удалили кортежи из первой операции CROSS JOIN чтобы сформировать «плоские» кортежи во второй операции. Это необязательно, конечно.

Java с помощью jOOλ’s crossJoin ()

Мы, разработчики jOOQ , мы очень ориентирующиеся на SQL люди, поэтому вполне естественно добавить удобный метод crossJoin() для приведенного выше crossJoin() использования. Итак, наше тройное соединение можно записать так:

1
2
3
4
5
6
7
Seq<Integer> s1 = Seq.of(1, 2);
Seq<String> s2 = Seq.of("A", "B");
Seq<String> s3 = Seq.of("X", "Y");
 
s1.crossJoin(s2)
  .crossJoin(s3)
  .forEach(System.out::println);

получая

1
2
3
4
5
6
7
8
((1, A), X)
((1, A), Y)
((1, B), X)
((1, B), Y)
((2, A), X)
((2, A), Y)
((2, B), X)
((2, B), Y)

В этом случае мы не распаковывали кортеж, созданный в первом перекрестном соединении. С чисто реляционной точки зрения это тоже не имеет значения. Вложенные кортежи — это то же самое, что и плоские кортежи. В SQL мы просто не видим вложенности. Конечно, мы могли бы также откатить, добавив одно дополнительное сопоставление:

1
2
3
4
5
6
7
8
Seq<Integer> s1 = Seq.of(1, 2);
Seq<String> s2 = Seq.of("A", "B");
Seq<String> s3 = Seq.of("X", "Y");
 
s1.crossJoin(s2)
  .crossJoin(s3)
  .map(t -> tuple(t.v1.v1, t.v1.v2, t.v2))
  .forEach(System.out::println);

уступая снова

1
2
3
4
5
6
7
8
(1, A, X)
(1, A, Y)
(1, B, X)
(1, B, Y)
(2, A, X)
(2, A, Y)
(2, B, X)
(2, B, Y)

(Возможно, вы заметили, что map() соответствует SELECT как мы увидим позже)

INNER JOIN = flatMap () с фильтром ()

SQL INNER JOIN по сути является просто синтаксическим сахаром для SQL CROSS JOIN с предикатом, который сокращает набор кортежей после перекрестного соединения. В SQL следующие два способа внутреннего объединения эквивалентны:

SQL

01
02
03
04
05
06
07
08
09
10
11
-- Table list syntax
SELECT *
FROM (VALUES(1), (2)) t1(v1),
     (VALUES(1), (3)) t2(v2)
WHERE t1.v1 = t2.v2
 
-- INNER JOIN syntax
SELECT *
FROM       (VALUES(1), (2)) t1(v1)
INNER JOIN (VALUES(1), (3)) t2(v2)
ON t1.v1 = t2.v2

получая

1
2
3
4
5
+----+----+
| v1 | v2 |
+----+----+
|  1 |  1 |
+----+----+

(обратите внимание, что ключевое слово INNER необязательно).

Таким образом, значения 2 из t1 и значения 3 из t2 «выбрасываются», так как они генерируют любые строки, для которых предикат соединения дает значение true.

То же самое можно выразить легко, но более подробно в Java

Java (неэффективное решение!)

1
2
3
4
5
6
7
8
List<Integer> s1 = Arrays.asList(1, 2);
List<Integer> s2 = Arrays.asList(1, 3);
 
s1.stream()
  .flatMap(v1 -> s2.stream()
                   .map(v2 -> tuple(v1, v2)))
  .filter(t -> Objects.equals(t.v1, t.v2))
  .forEach(System.out::println);

Вышеуказанное правильно дает

1
(1, 1)

Но будьте осторожны, что вы получаете этот результат после создания декартовой системы , кошмара каждого администратора баз данных! Как упоминалось в начале этой статьи, в отличие от декларативного программирования, в функциональном программировании вы инструктируете свою программу точно выполнять порядок операций, который вы укажете. Другими словами:

В функциональном программировании вы определяете точный «план выполнения» вашего запроса .

В декларативном программировании оптимизатор может реорганизовать вашу «программу»

Там нет оптимизатора, чтобы превратить вышесказанное в гораздо более эффективную:

Java (более эффективный)

1
2
3
4
5
6
7
8
List<Integer> s1 = Arrays.asList(1, 2);
List<Integer> s2 = Arrays.asList(1, 3);
 
s1.stream()
  .flatMap(v1 -> s2.stream()
                   .filter(v2 -> Objects.equals(v1, v2))
                   .map(v2 -> tuple(v1, v2)))
  .forEach(System.out::println);

Выше также дает

1
(1, 1)

Обратите внимание, как предикат соединения переместился из «внешнего» потока во «внутренний» поток, который создается в функции, переданной функции flatMap() .

Java (оптимально)

Как упоминалось ранее, функциональное программирование не обязательно позволяет переписывать алгоритмы в зависимости от знания фактических данных. Представленная выше реализация для объединений всегда реализует соединения с вложенными циклами, идущие от первого потока ко второму. Если вы объединяете более двух потоков или если второй поток очень большой, такой подход может быть ужасно неэффективным. Усовершенствованная СУБД никогда бы не применяла вслепую подобные соединения с вложенными циклами, но учитывала бы ограничения, индексы и гистограммы для реальных данных.

Однако углубление в эту тему выходит за рамки данной статьи.

Java с помощью jOOλ’s innerJoin ()

Опять же, вдохновленный нашей работой над jOOQ, мы также добавили удобный метод innerJoin() для приведенного выше innerJoin() использования:

1
2
3
4
5
Seq<Integer> s1 = Seq.of(1, 2);
Seq<Integer> s2 = Seq.of(1, 3);
 
s1.innerJoin(s2, (t, u) -> Objects.equals(t, u))
  .forEach(System.out::println);

получая

1
(1, 1)

… Потому что, в конце концов, при объединении двух потоков единственной действительно интересной операцией является Predicate присоединения. Все остальное (картографирование и т. Д.) — это просто шаблон.

LEFT OUTER JOIN = flatMap () с фильтром () и «по умолчанию»

OUTER JOIN SQL работает как INNER JOIN , за исключением того, что дополнительные строки «по умолчанию» создаются в случае, если предикат JOIN выдает false для пары кортежей. С точки зрения теории множеств / реляционной алгебры это можно выразить так:

dd81ee1373d922122ce1b3e0da74cb28

Или на диалекте SQL-esque:

1
2
3
4
5
6
7
8
R LEFT OUTER JOIN S ::=
 
R INNER JOIN S
UNION (
  (R EXCEPT (SELECT R.* FROM R INNER JOIN S))
  CROSS JOIN
  (null, null, ..., null)
)

Это просто означает, что при левом внешнем соединении S с R в результате будет по крайней мере одна строка для каждой строки в R , возможно, с пустым значением для S

И наоборот, при прямом внешнем соединении S с R в результате будет по крайней мере одна строка для каждой строки в S , возможно, с пустым значением для R

И, наконец, при полном внешнем соединении S с R в результате будет по крайней мере одна строка для каждой строки в R с возможно пустым значением для S AND для каждой строки в S с возможно пустым значением для R

Давайте посмотрим на LEFT OUTER JOIN , который чаще всего используется в SQL.

SQL

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
-- Table list, Oracle syntax (don't use this!)
SELECT *
FROM (SELECT 1 v1 FROM DUAL
      UNION ALL
      SELECT 2 v1 FROM DUAL) t1,
     (SELECT 1 v2 FROM DUAL
      UNION ALL
      SELECT 3 v2 FROM DUAL) t2
WHERE t1.v1 = t2.v2 (+)
 
-- OUTER JOIN syntax
SELECT *
FROM            (VALUES(1), (2)) t1(v1)
LEFT OUTER JOIN (VALUES(1), (3)) t2(v2)
ON t1.v1 = t2.v2

получая

1
2
3
4
5
6
+----+------+
| v1 |   v2 |
+----+------+
|  1 |    1 |
|  2 | null |
+----+------+

(обратите внимание, что ключевое слово OUTER необязательно).

Джава

К сожалению, Stream API JDK не предоставляет нам простой способ получения «хотя бы одного» значения из потока, если поток пустой. Мы могли бы написать служебную функцию, как объяснил Стюарт Маркс о переполнении стека :

01
02
03
04
05
06
07
08
09
10
11
12
13
static <T> Stream<T> defaultIfEmpty(
    Stream<T> stream, Supplier<T> supplier) {
    Iterator<T> iterator = stream.iterator();
 
    if (iterator.hasNext()) {
        return StreamSupport.stream(
            Spliterators.spliteratorUnknownSize(
                iterator, 0
            ), false);
    } else {
        return Stream.of(supplier.get());
    }
}

Или мы просто используем jOOλ’s Seq.onEmpty()

1
2
3
4
5
6
7
8
9
List<Integer> s1 = Arrays.asList(1, 2);
List<Integer> s2 = Arrays.asList(1, 3);
 
seq(s1)
.flatMap(v1 -> seq(s2)
              .filter(v2 -> Objects.equals(v1, v2))
              .onEmpty(null)
              .map(v2 -> tuple(v1, v2)))
.forEach(System.out::println);

(обратите внимание, мы помещаем null в поток. Это не всегда может быть хорошей идеей. Мы рассмотрим это в следующем сообщении в блоге)

Выше также дает

1
2
(1, 1)
(2, null)

Как прочитать неявное левое внешнее соединение?

  • Мы возьмем каждое значение v1 из левого потока s1
  • Для каждого такого значения v1 мы отображаем правый поток s2 для создания кортежа (v1, v2) (декартово произведение, перекрестное соединение)
  • Мы будем применять предикат объединения для каждого такого кортежа (v1, v2)
  • Если предикат соединения не оставляет кортежей для любого значения v2 , мы сгенерируем один кортеж, содержащий значение левого потока v1 и null

Java с JOOλ

Для удобства jOOλ также поддерживает leftOuterJoin() который работает как описано выше:

1
2
3
4
5
Seq<Integer> s1 = Seq.of(1, 2);
Seq<Integer> s2 = Seq.of(1, 3);
 
s1.leftOuterJoin(s2, (t, u) -> Objects.equals(t, u))
  .forEach(System.out::println);

получая

1
2
(1, 1)
(2, null)

RIGHT OUTER JOIN = обратное левое внешнее соединение

Тривиально, RIGHT OUTER JOIN — это просто обратное предыдущее ВЛЕВОЕ НАРУЖНОЕ LEFT OUTER JOIN . Реализация jOOλ в rightOuterJoin() выглядит следующим образом:

1
2
3
4
5
6
default <U> Seq<Tuple2<T, U>> rightOuterJoin(
    Stream<U> other, BiPredicate<T, U> predicate) {
    return seq(other)
          .leftOuterJoin(this, (u, t) -> predicate.test(t, u))
          .map(t -> tuple(t.v2, t.v1));
}

Как вы можете видеть, RIGHT OUTER JOIN инвертирует результаты LEFT OUTER JOIN , вот и все. Например:

1
2
3
4
5
Seq<Integer> s1 = Seq.of(1, 2);
Seq<Integer> s2 = Seq.of(1, 3);
 
s1.rightOuterJoin(s2, (t, u) -> Objects.equals(t, u))
  .forEach(System.out::println);

получая

1
2
(1, 1)
(null, 3)

ГДЕ = фильтр ()

Наиболее простым отображением, вероятно, является WHERE SQL, имеющее точный эквивалент в Stream API: Stream.filter() .

SQL

1
2
3
SELECT *
FROM (VALUES(1), (2), (3)) t(v)
WHERE v % 2 = 0

получая

1
2
3
4
5
+---+
| v |
+---+
| 2 |
+---+

Джава

1
2
3
4
Stream<Integer> s = Stream.of(1, 2, 3);
 
s.filter(v -> v % 2 == 0)
 .forEach(System.out::println);

получая

1
2

Интересная вещь с filter() и Stream API в целом заключается в том, что операция может применяться в любом месте цепочки вызовов, в отличие от WHERE , которое ограничивается размещением сразу после предложения FROM — даже если SQL JOIN .. ON HAVING JOIN .. ON или HAVING семантически похожи.

GROUP BY = collect ()

Наименее прямое сопоставление — GROUP BY против Stream.collect() .

Прежде всего, SQL GROUP BY может быть немного сложным, чтобы полностью понять . Это действительно часть предложения FROM , преобразующего набор кортежей, созданных FROM .. JOIN .. WHERE в группы кортежей, где каждая группа имеет связанный набор агрегируемых кортежей, которые можно объединять в HAVING , SELECT и ORDER BY пункты. Все становится еще интереснее, когда вы используете функции OLAP, такие как GROUPING SETS , которые позволяют дублировать кортежи в соответствии с несколькими комбинациями группировки.

В большинстве реализаций SQL, которые не поддерживают ARRAY или MULTISET , агрегируемые кортежи не доступны как таковые (то есть как вложенные коллекции) в SELECT . Здесь набор функций Stream API превосходен. С другой стороны, Stream API может группировать значения только как терминальную операцию , где в SQL GROUP BY применяется чисто декларативно (и, следовательно, лениво). Планировщик выполнения может решить вообще не выполнять GROUP BY если в этом нет необходимости. Например:

1
2
3
4
5
6
7
SELECT *
FROM some_table
WHERE EXISTS (
    SELECT x, sum(y)
    FROM other_table
    GROUP BY x
)

Вышеприведенный запрос семантически эквивалентен

1
2
3
4
5
6
SELECT *
FROM some_table
WHERE EXISTS (
    SELECT 1
    FROM other_table
)

Группировка в подзапросе была ненужной. Кто-то, возможно, скопировал туда этот подзапрос откуда-то еще или реорганизовал запрос в целом. В Java с использованием Stream API каждая операция всегда выполняется.

Для простоты мы будем придерживаться самых простых примеров здесь

Агрегирование без GROUP BY

Особый случай — когда мы не указываем ни одного предложения GROUP BY . В этом случае мы можем указать агрегации во всех столбцах предложения FROM , производя всегда ровно одну запись. Например:

SQL

1
2
SELECT sum(v)
FROM (VALUES(1), (2), (3)) t(v)

получая

1
2
3
4
5
+-----+
| sum |
+-----+
|   6 |
+-----+

Джава

1
2
3
4
Stream<Integer> s = Stream.of(1, 2, 3);
 
int sum = s.collect(Collectors.summingInt(i -> i));
System.out.println(sum);

получая

1
6

Агрегирование с GROUP BY

Более распространенный случай агрегирования в SQL — это указание явного предложения GROUP BY , как описано выше. Например, мы можем сгруппировать по четным и нечетным числам:

SQL

1
2
3
SELECT v % 2, count(v), sum(v)
FROM (VALUES(1), (2), (3)) t(v)
GROUP BY v % 2

получая

1
2
3
4
5
6
+-------+-------+-----+
| v % 2 | count | sum |
+-------+-------+-----+
|     0 |     1 |   2 |
|     1 |     2 |   4 |
+-------+-------+-----+

Джава

К счастью, для этого простого случая использования группировки / коллекции JDK предлагает служебный метод Collectors.groupingBy() , который создает сборщик, который генерирует тип Map<K, List<V>> например:

1
2
3
4
5
6
7
Stream<Integer> s = Stream.of(1, 2, 3);
 
Map<Integer, List<Integer>> map = s.collect(
    Collectors.groupingBy(v -> v % 2)
);
 
System.out.println(map);

получая

1
{0=[2], 1=[1, 3]}

Это, безусловно, заботится о группировке. Теперь мы хотим произвести агрегации для каждой группы. Немного неуклюжий JDK способ сделать это будет:

01
02
03
04
05
06
07
08
09
10
Stream<Integer> s = Stream.of(1, 2, 3);
 
Map<Integer, IntSummaryStatistics> map = s.collect(
    Collectors.groupingBy(
        v -> v % 2,
        Collectors.summarizingInt(i -> i)
    )
);
 
System.out.println(map);

теперь мы получим:

1
2
{0=IntSummaryStatistics{count=1, sum=2, min=2, average=2.000000, max=2},
 1=IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3}}

Как видите, значения count() и sum() были вычислены где-то в соответствии с приведенным выше описанием.

Более сложный GROUP BY

При выполнении нескольких агрегаций с помощью Stream API Java 8 вы будете быстро вынуждены бороться с низкоуровневым API, реализуя сложные сборщики и накопители самостоятельно. Это утомительно и ненужно. Рассмотрим следующий оператор SQL:

SQL

01
02
03
04
05
06
07
08
09
10
11
12
13
CREATE TABLE t (
  w INT,
  x INT,
  y INT,
  z INT
);
 
SELECT
    z, w,
    MIN(x), MAX(x), AVG(x),
    MIN(y), MAX(y), AVG(y)
FROM t
GROUP BY z, w;

За один раз мы хотим:

  • Группировать по нескольким значениям
  • Агрегировать из нескольких значений

Джава

В предыдущей статье мы подробно объяснили, как этого можно достичь, используя удобный API из jOOλ через Seq.groupBy()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
    }
}
 
Map<
    Tuple2<Integer, Integer>,
    Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map =
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)
    )
);
 
System.out.println(map);

Вышеуказанные урожаи

1
2
3
4
5
6
{(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})}

Для более подробной информации, прочитайте полную статью здесь .

Обратите внимание, что использование Stream.collect() или Seq.groupBy() уже создает неявное предложение SELECT , которое нам больше не нужно получать через map() (см. Ниже).

HAVING = фильтр (), снова

Как уже упоминалось ранее, способов применения предикатов с помощью Stream API не существует, есть только Stream.filter() . В SQL HAVING — это «специальное» предикатное предложение, которое синтаксически ставится после предложения GROUP BY . Например:

SQL

1
2
3
4
SELECT v % 2, count(v)
FROM (VALUES(1), (2), (3)) t(v)
GROUP BY v % 2
HAVING count(v) > 1

получая

1
2
3
4
5
+-------+-------+
| v % 2 | count |
+-------+-------+
|     1 |     2 |
+-------+-------+

Джава

К сожалению, как мы видели ранее, collect() является терминальной операцией в Stream API, что означает, что он охотно создает Map вместо преобразования Stream<T> в Stream<K, Stream<V> , что гораздо лучше сочинять в сложном Stream . Это означает, что любая операция, которую мы хотели бы реализовать сразу после сбора, должна быть реализована в новом потоке, созданном из выходной Map :

01
02
03
04
05
06
07
08
09
10
Stream<Integer> s = Stream.of(1, 2, 3);
 
s.collect(Collectors.groupingBy(
      v -> v % 2,
      Collectors.summarizingInt(i -> i)
  ))
  .entrySet()
  .stream()
  .filter(e -> e.getValue().getCount() > 1)
  .forEach(System.out::println);

получая

1
1=IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3}

Как видите, применяемое преобразование типа:

  • Map<Integer, IntSummaryStatistics>
  • Set<Entry<Integer, IntSummaryStatistics>>
  • Stream<Entry<Integer, IntSummaryStatistics>>

SELECT = map ()

Предложение SELECT в SQL — это не что иное, как функция преобразования кортежей, которая берет декартово произведение кортежей, созданных предложением FROM , и преобразует его в новое выражение кортежа, которое передается либо клиенту, либо некоторому высокоуровневому запросу, если это вложенный SELECT. Иллюстрация:

ОТ выхода

1
2
3
4
5
6
7
8
+------+------+------+------+------+
| T1.A | T1.B | T1.C | T2.A | T2.D |
+------+------+------+------+------+
|    1 |    A |    a |    1 |    X |
|    1 |    B |    b |    1 |    Y |
|    2 |    C |    c |    2 |    X |
|    2 |    D |    d |    2 |    Y |
+------+------+------+------+------+

Применение SELECT

01
02
03
04
05
06
07
08
09
10
SELECT t1.a, t1.c, t1.b || t1.d
 
+------+------+--------------+
| T1.A | T1.C | T1.B || T1.D |
+------+------+--------------+
|    1 |    a |           AX |
|    1 |    b |           BY |
|    2 |    c |           CX |
|    2 |    d |           DY |
+------+------+--------------+

Используя Java 8 Streams, SELECT может быть достигнут очень просто с помощью Stream.map() , как мы уже видели в предыдущих примерах, где мы аннестировали кортежи с помощью map() . Следующие примеры функционально эквивалентны:

SQL

1
2
3
4
5
SELECT t.v1 * 3, t.v2 + 5
FROM (
  VALUES(1, 1),
        (2, 2)
) t(v1, v2)

получая

1
2
3
4
5
6
+----+----+
| c1 | c2 |
+----+----+
|  3 |  6 |
|  6 |  7 |
+----+----+

Джава

1
2
3
4
5
Stream.of(
  tuple(1, 1),
  tuple(2, 2)
).map(t -> tuple(t.v1 * 3, t.v2 + 5))
 .forEach(System.out::println);

получая

1
2
(3, 6)
(6, 7)

DISTINCT = Different ()

Ключевое слово DISTINCT которое может быть предоставлено с предложением SELECT , просто удаляет дубликаты кортежей сразу после того, как они были созданы предложением SELECT . Иллюстрация:

ОТ выхода

1
2
3
4
5
6
7
8
+------+------+------+------+------+
| T1.A | T1.B | T1.C | T2.A | T2.D |
+------+------+------+------+------+
|    1 |    A |    a |    1 |    X |
|    1 |    B |    b |    1 |    Y |
|    2 |    C |    c |    2 |    X |
|    2 |    D |    d |    2 |    Y |
+------+------+------+------+------+

Применение SELECT DISTINCT

1
2
3
4
5
6
7
8
SELECT DISTINCT t1.a
 
+------+
| T1.A |
+------+
|    1 |
|    2 |
+------+

Используя потоки Java 8, SELECT DISTINCT может быть достигнут очень просто с помощью Stream.distinct() сразу после Stream.map() . Следующие примеры функционально эквивалентны:

SQL

1
2
3
4
5
6
SELECT DISTINCT t.v1 * 3, t.v2 + 5
FROM (
  VALUES(1, 1),
        (2, 2),
        (2, 2)
) t(v1, v2)

получая

1
2
3
4
5
6
+----+----+
| c1 | c2 |
+----+----+
|  3 |  6 |
|  6 |  7 |
+----+----+

Джава

1
2
3
4
5
6
7
Stream.of(
  tuple(1, 1),
  tuple(2, 2),
  tuple(2, 2)
).map(t -> tuple(t.v1 * 3, t.v2 + 5))
 .distinct()
 .forEach(System.out::println);

получая

1
2
(3, 6)
(6, 7)

UNION ALL = concat ()

Операции над множествами эффективны как в SQL, так и с использованием Stream API. Операция UNION ALL отображается на Stream.concat() , как показано ниже:

SQL

1
2
3
4
5
SELECT *
FROM (VALUES(1), (2)) t(v)
UNION ALL
SELECT *
FROM (VALUES(1), (3)) t(v)

получая

1
2
3
4
5
6
7
8
+---+
| v |
+---+
| 1 |
| 2 |
| 1 |
| 3 |
+---+

Джава

1
2
3
4
5
Stream<Integer> s1 = Stream.of(1, 2);
Stream<Integer> s2 = Stream.of(1, 3);
 
Stream.concat(s1, s2)
      .forEach(System.out::println);

получая

1
2
3
4
1
2
1
3

Java (используя jOOλ)

К сожалению, concat() существует в Stream только как static метод, в то время как Seq.concat() также существует в экземплярах при работе с jOOλ.

1
2
3
4
5
Seq<Integer> s1 = Seq.of(1, 2);
Seq<Integer> s2 = Seq.of(1, 3);
 
s1.concat(s2)
  .forEach(System.out::println);

UNION = concat () и Different ()

В SQL UNION определяется для удаления дубликатов после объединения двух наборов через UNION ALL . Следующие два утверждения эквивалентны:

01
02
03
04
05
06
07
08
09
10
11
12
SELECT * FROM t
UNION
SELECT * FROM u;
 
-- equivalent
 
SELECT DISTINCT *
FROM (
  SELECT * FROM t
  UNION ALL
  SELECT * FROM u
);

Давайте приведем это в действие:

SQL

1
2
3
4
5
SELECT *
FROM (VALUES(1), (2)) t(v)
UNION
SELECT *
FROM (VALUES(1), (3)) t(v)

получая

1
2
3
4
5
6
7
+---+
| v |
+---+
| 1 |
| 2 |
| 3 |
+---+

Джава

1
2
3
4
5
6
Stream<Integer> s1 = Stream.of(1, 2);
Stream<Integer> s2 = Stream.of(1, 3);
 
Stream.concat(s1, s2)
      .distinct()
      .forEach(System.out::println);

ORDER BY = sorted ()

Отображение ORDER BY тривиально

SQL

1
2
3
SELECT *
FROM (VALUES(1), (4), (3)) t(v)
ORDER BY v

получая

1
2
3
4
5
6
7
+---+
| v |
+---+
| 1 |
| 3 |
| 4 |
+---+

Джава

1
2
3
4
Stream<Integer> s = Stream.of(1, 4, 3);
 
s.sorted()
 .forEach(System.out::println);

получая

1
2
3
1
3
4

LIMIT = limit ()

Отображение LIMIT еще более тривиально

SQL

1
2
3
SELECT *
FROM (VALUES(1), (4), (3)) t(v)
LIMIT 2

получая

1
2
3
4
5
6
+---+
| v |
+---+
| 1 |
| 4 |
+---+

Джава

1
2
3
4
Stream<Integer> s = Stream.of(1, 4, 3);
 
s.limit(2)
 .forEach(System.out::println);

получая

1
2
1
4

OFFSET = пропустить ()

Отображение OFFSET тривиально

SQL

1
2
3
SELECT *
FROM (VALUES(1), (4), (3)) t(v)
OFFSET 1

получая

1
2
3
4
5
6
+---+
| v |
+---+
| 4 |
| 3 |
+---+

Джава

1
2
3
4
Stream<Integer> s = Stream.of(1, 4, 3);
 
s.skip(1)
 .forEach(System.out::println);

получая

1
2
4
3

Вывод

В приведенной выше статье мы рассмотрели практически все полезные предложения SQL SELECT и то, как их можно сопоставить с Java 8 Stream API или с Seq API jOOλ в случае, если Stream не предлагает достаточной функциональности.

В статье показано, что декларативный мир SQL не сильно отличается от функционального мира Java 8. Предложения SQL могут составлять специальные запросы так же, как методы Stream могут использоваться для составления конвейеров функционального преобразования. Но есть принципиальная разница.

Хотя SQL действительно декларативный, функциональное программирование все еще очень поучительно. Stream API не принимает решения по оптимизации на основе ограничений, индексов, гистограмм и другой метаинформации о данных, которые вы преобразуете. Использование Stream API похоже на использование всех возможных советов по оптимизации в SQL, чтобы заставить механизм SQL выбирать один конкретный план выполнения вместо другого. Однако, хотя SQL является абстракцией алгоритма более высокого уровня, Stream API может позволить вам реализовать более настраиваемые алгоритмы.