HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

C++の標準入出力についてのメモ

プログラミングコンテストなどでよく見かける以下のコード断片を「おまじない」で片づけるのが嫌だったので調べてみたメモ。

ios_base::sync_with_stdio(false);
cin.tie(NULL);

答えは以下のサイトで解説されているのだが‥

試してみないと気が済まなかったので試してみた。

ios_base::sync_with_stdio(false)の検証

以下のようなコードを実行してみる。

#include <cstdio>
#include <iostream>

using namespace std;

void test1(int n, bool sync = false) {
    for (int i = 0; i < n; ++i) {
        cout << 'a';
        if (sync) {
            flush(cout);
        }
        printf("A");
        if (sync) {
            fflush(stdout);
        }
    }

    fflush(stdout);
    cout << endl;
}

int main(int argc, char* argv[]) {
    int n = 10;

    test1(n);

    ios_base::sync_with_stdio(false);
    test1(n);

    test1(n, true);
}

これを実行すると、以下のような出力が得られる。

aAaAaAaAaAaAaAaAaAaA
AAAAAAAAAAaaaaaaaaaa
aAaAaAaAaAaAaAaAaAaA
  • 1行目の出力は、coutstdoutが同期されている状態なので、flushとかfflushをしなくても、それぞれに書き込んだ文字が交互に出力されることの確認。
  • 2行目の出力は、coutstdoutの同期を切っているので、末尾でfflushを先にしているstdoutへの出力が先にまとめて出てきて、その後にcoutへの出力がまとめて出てくることの確認。
  • 3行目の出力は、手動で同期をしているので、1行目と同じ出力になることの確認。

上記サイトの回答にあるように、これの副作用として標準出力への出力のパフォーマンスが上がるというわけか。

cin.tie(NULL)の検証

以下のようなコードを実行してみる。

#include <iostream>

using namespace std;

void test2(bool tie = false) {
    string name;

    cout << "Enter name:";
    if (tie) {
        flush(cout);
    }
    cin >> name;
    cout << name << endl;
}

int main(int argc, char* argv[]) {
    test2();

    cin.tie(NULL);
    test2();

    ios_base::sync_with_stdio(false);
    test2();

    test2(true);
}

これを実行し、4回の入力が求められるので、"A"、"B"、"C"、"D"を順にコンソールから入力した結果が以下。

Enter name:A
A
Enter name:B
B
C
Enter name:C
Enter name:D
D
  • 1回目は、cincoutが結び付けられた状態なので、cinで読み込む前にcoutに書き込んだプロンプト("Enter name:")が画面に出てくることの確認。
  • 2回目は、cincoutの結びつきを切ってみたのだが、1回目と出力が変わらなかった。
  • 3回目は、ならば、と先に試したios_base::sync_with_stdio(false)をやってみたらどうだろうと試した結果、今度は期待通りにcoutに書き込んだプロンプトが即座には出てこなかった。
  • 4回目は、手動でflushしているので、1回目と同じ結果になることの確認。

cin.tie(NULL)だけでは動作上の変化が無くて、ios_base::sync_with_stdio(false)も合わせてしてやらないといけないらしい。cinから読み込むときに、stdioとの同期が有効になっているとstdinとも結びついていることになるから、同期しようとして2回目のケースでプロンプトが先に出てくるということか。

標準入力から1行ずつ読み込んで数値解析して標準出力に吐き出す

唐突に、手元にある各言語で標準入力から1行ずつ読み込んで、行の先頭の数値として解析できる部分を数値に変換して標準出力に吐き出すプログラムを書いてみようと思ったメモ。

例えば、「+123i456」という行があったら、「+123」までが数値として解析できる(その後ろの「i」が数値を構成する要素でない)ので、「123」と出力する(数値型に変換したら、普通の言語では「+」記号は出力されないため)。

環境

手元にあるものということで、環境は以下のものに限定する。

入力ファイル

  • 001.txt
123
123#
123i456
123 789
0x123
+123
+123#
+123i456
+123 789
+0x123
-123
-123#
-123i456
-123 789
-0x123

期待される出力

  • 001.txt
123
123
123
123
0
123
123
123
123
0
-123
-123
-123
-123
0

Java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            String buf;
            while ((buf = in.readLine()) != null) {
                buf = buf.replaceAll("^([+-]?[0-9]+).*$", "$1");
                int num = Integer.parseInt(buf);
                System.out.println(num);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

C

#include <stdio.h>

int main(int argc, char** argv) {
    char buf[1024];

    while (fgets(buf, sizeof(buf), stdin)) {
        int num;
        sscanf(buf, "%d", &num);
        printf("%d\n", num);
    }

    return 0;
}

C++

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    char buf[1024];

    while (cin.getline(buf, sizeof(buf))) {
        int num;
        sscanf(buf, "%d", &num);
        cout << num << endl;
    }

    return EXIT_SUCCESS;
}

PHP

<?php

$lines = file('php://stdin');
foreach ($lines as $line) {
    $num = (int)$line;
    printf("%d\n", $num);
}

Python 2

import sys
import re

patStr = r'^[+-]?[0-9]+'
pattern = re.compile(patStr)

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    matcher = pattern.match(line)
    line = line[matcher.start():matcher.end()]
    num = int(line)
    print num

Python 3

import sys
import re

patStr = r'^[+-]?[0-9]+'
pattern = re.compile(patStr)

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    matcher = pattern.match(line)
    line = line[matcher.start():matcher.end()]
    num = int(line)
    print(num)

Ruby

while line = STDIN.gets
    num = line.to_i
    print num,"\n"
end

Perl

my $line;
while ($line = readline(STDIN)) {
    my $num = $line + 0;
    print $num,"\n";
}

Go

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "strconv"
    "regexp"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    buf := make([]byte, 0, 1024)
    for {
        line, prefix, err := stdin.ReadLine()
        if err == io.EOF {
            break
        }
        buf = append(buf, line...)
        if prefix {
            continue
        }
        s := string(buf)
        s = regexp.MustCompile(`^([+-]?[0-9]+).*`).ReplaceAllString(s, "$1")
        num, err2 := strconv.Atoi(s)
        if err2 != nil {
            panic(err2)
        }
        fmt.Println(num)
        buf = make([]byte, 0, 1024)
    }
}

bash

#! /bin/bash

while IFS=$'\n' read line ; do
    num=$(echo ${line} | sed -e 's/^\([+-]*[0-9][0-9]*\).*/\1/')
    num=$((num+0))
    echo ${num}
done

Awk

{
    s = gensub(/^([-+]?[0-9]+).*$/, "\\1", "g", $0);
    print s + 0;
}

標準入力から1バイトずつ読み込んで、大文字小文字変換をして標準出力に吐き出すプログラムを書いてみる

唐突に、手元にある各言語で標準入力から1バイトずつ読み込んで、大文字小文字変換をしたうえで標準出力に吐き出すプログラムを書いてみようと思ったメモ。

大文字小文字判定等を行う関数をまじめに使った言語もあれば、正規表現に頼った言語もあったり、果てはASCII文字のコード値に頼ったプログラムになる言語があったりといろいろだが、とりあえず現状の知識ということで気にしない。

(2017/10/18追記)Go言語でbyteからruneに変換すればunicodeパッケージの関数で対応できることが分かったので書き換え。

環境

手元にあるものということで、環境は以下のものに限定する。

入力ファイル

od -cした結果を載せておく。

  • 001.txt
0000000   H   e   l   l   o       W   o   r   l   d   !  \n
0000015
  • 002.txt
0000000       !   "   #   $   %   &   '   (   )   *   +   ,   -   .   /
0000020   0   1   2   3   4   5   6   7   8   9   :   ;   <   =   >   ?
0000040   @   A   B   C   D   E   F   G   H   I   J   K   L   M   N   O
0000060   P   Q   R   S   T   U   V   W   X   Y   Z   [   \   ]   ^   _
0000100   `   a   b   c   d   e   f   g   h   i   j   k   l   m   n   o
0000120   p   q   r   s   t   u   v   w   x   y   z   {   |   }   ~  \n
0000140

Java

import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try {
            int ch;
            while ((ch = System.in.read()) != -1) {
                if (Character.isLowerCase(ch)) {
                    ch = Character.toUpperCase(ch);
                } else if (Character.isUpperCase(ch)) {
                    ch = Character.toLowerCase(ch);
                }
                System.out.write(ch);
            }
            System.out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

C

#include <stdio.h>
#include <ctype.h>

int main(int argc, char** argv) {
    int ch;

    while ((ch = getchar()) != -1) {
        if (islower(ch)) {
            ch = toupper(ch);
        } else if (isupper(ch)) {
            ch = tolower(ch);
        }
        putchar(ch);
    }

    return 0;
}

C++

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    char ch;

    while (cin.get(ch)) {
        if (islower(ch)) {
            ch = toupper(ch);
        } else if (isupper(ch)) {
            ch = tolower(ch);
        }
        cout << ch;
    }

    return EXIT_SUCCESS;
}

cctypeはincludeしなくていいのか?と思ったら、iostreamの中身を追っていくと、cctypeをincludeしているので、自分で明示する必要が無いのだった。

PHP

<?php

$in = fopen('php://stdin', 'r');
$out = fopen('php://stdout', 'w');
while (($ch = fgetc($in)) !== FALSE) {
    if (preg_match('/[a-z]/', $ch)) {
        $ch = strtoupper($ch);
    } else if (preg_match('/[A-Z]/', $ch)) {
        $ch = strtolower($ch);
    }
    fputs($out, $ch);
}

最初、ctype系の関数を使おうと思ったのだが、以下の記事を読んだら嫌な予感がしてきたので、正規表現で判定するようにした。

Python 2

import sys;

while True:
    ch = sys.stdin.read(1)
    if ch == '':
        break
    if ch.islower():
        ch = ch.upper()
    elif ch.isupper():
        ch = ch.lower()
    sys.stdout.write(ch)

Pythonのislower/isupperには以下のような罠が潜んでいるらしいので注意。今回は1文字ずつに切り分けているのでハマることはなかったが。

Python 3

import sys;

while True:
    ch = sys.stdin.buffer.read(1)
    if ch == b'':
        break
    if ch.islower():
        ch = ch.upper()
    elif ch.isupper():
        ch = ch.lower()
    sys.stdout.buffer.write(ch)

Ruby

while ch = STDIN.getc
    ch = ch.swapcase
    STDOUT.putc(ch.chr)
end

大文字小文字判定をするメソッドが見つけられなかったので逃げた例‥

Perl

binmode(STDIN);
while (undef != read(STDIN, $ch, 1)) {
    if ($ch =~ /[a-z]/) {
        $ch = uc($ch);
    } elsif ($ch =~ /[A-Z]/) {
        $ch = lc($ch);
    }
    print $ch;
}

Go

  • 初期版
package main

import (
    "bufio"
    "io"
    "os"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    stdout := bufio.NewWriter(os.Stdout)
    for {
        ch, err := stdin.ReadByte()
        if err == io.EOF {
            break
        }
        if ('a' <= ch && ch <= 'z') {
            ch = ch - ('a' - 'A')
        } else if ('A' <= ch && ch <= 'Z') {
            ch = ch + ('a' - 'A')
        }
        stdout.WriteByte(ch)
    }
    stdout.Flush()
}

byteをbyteのままで大文字小文字判定および変換する手段を見つけられずに、ASCIIコードのコード値に頼ってしまいました‥

  • 2017/10/18書き換え版
package main

import (
    "bufio"
    "io"
    "os"
    "unicode"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    stdout := bufio.NewWriter(os.Stdout)
    for {
        ch, err := stdin.ReadByte()
        if err == io.EOF {
            break
        }
        r := rune(ch)
        if (unicode.IsLower(r)) {
            r = unicode.ToUpper(r)
        } else if (unicode.IsUpper(r)) {
            r = unicode.ToLower(r)
        }
        stdout.WriteRune(r)
    }
    stdout.Flush()
}

bash

#! /bin/bash

while IFS= read -r -N 1 ch ; do
    printf "%c" "${ch}" | tr 'a-zA-Z' 'A-Za-z'
done

各言語で標準入力から1バイトずつ読み込んで標準出力に吐き出すプログラムを書いてみる

唐突に、手元にある各言語で標準入力から1バイトずつ読み込んで標準出力にそのまま吐き出すプログラムを書いてみようと思ったメモ。

普段は使わない言語も混じっているが、まぁ気にしない。

環境

手元にあるものということで、環境は以下のものに限定する。

入力ファイル

所謂バイナリデータも含むので、od -cした結果を載せておく。

  • 001.txt
0000000   H   e   l   l   o       W   o   r   l   d   !  \n
0000015
  • 002.txt
0000000  \0 001 002 003 004 005 006  \a  \b  \t  \n  \v  \f  \r 016 017
0000020 020 021 022 023 024 025 026 027 030 031 032 033 034 035 036 037
0000040       !   "   #   $   %   &   '   (   )   *   +   ,   -   .   /
0000060   0   1   2   3   4   5   6   7   8   9   :   ;   <   =   >   ?
0000100   @   A   B   C   D   E   F   G   H   I   J   K   L   M   N   O
0000120   P   Q   R   S   T   U   V   W   X   Y   Z   [   \   ]   ^   _
0000140   `   a   b   c   d   e   f   g   h   i   j   k   l   m   n   o
0000160   p   q   r   s   t   u   v   w   x   y   z   {   |   }   ~ 177
0000200 200 201 202 203 204 205 206 207 210 211 212 213 214 215 216 217
0000220 220 221 222 223 224 225 226 227 230 231 232 233 234 235 236 237
0000240 240 241 242 243 244 245 246 247 250 251 252 253 254 255 256 257
0000260 260 261 262 263 264 265 266 267 270 271 272 273 274 275 276 277
0000300 300 301 302 303 304 305 306 307 310 311 312 313 314 315 316 317
0000320 320 321 322 323 324 325 326 327 330 331 332 333 334 335 336 337
0000340 340 341 342 343 344 345 346 347 350 351 352 353 354 355 356 357
0000360 360 361 362 363 364 365 366 367 370 371 372 373 374 375 376 377
0000400

2つ目のファイルは、制御文字も含めて、8ビットcharとしてありうるものを256個すべて並べたもの。

Java

import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try {
            int ch;
            while ((ch = System.in.read()) != -1) {
                System.out.write(ch);
            }
            System.out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

C

#include <stdio.h>

int main(int argc, char** argv) {
    int ch;

    while ((ch = getchar()) != -1) {
        putchar(ch);
    }

    return 0;
}

C++

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    char ch;

    while (cin.get(ch)) {
        cout << ch;
    }

    return EXIT_SUCCESS;
}

PHP

<?php

$in = fopen('php://stdin', 'r');
$out = fopen('php://stdout', 'w');
while (($ch = fgetc($in)) !== FALSE) {
    fputs($out, $ch);
}

Python 2

import sys;

while True:
    ch = sys.stdin.read(1)
    if ch == '':
        break
    sys.stdout.write(ch)

Python 3

import sys;

while True:
    ch = sys.stdin.buffer.read(1)
    if ch == b'':
        break
    sys.stdout.buffer.write(ch)

Ruby

while ch = STDIN.getc
    STDOUT.putc(ch.chr)
end

Perl

binmode(STDIN);
while (undef != read(STDIN, $ch, 1)) {
    print $ch;
}

Go

package main

import (
    "bufio"
    "io"
    "os"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    stdout := bufio.NewWriter(os.Stdout)
    for {
        ch, err := stdin.ReadByte()
        if err == io.EOF {
            break
        }
        stdout.WriteByte(ch)
    }
    stdout.Flush()
}

bash

実を言うと、bashの場合は2つ目のファイルで正しい結果が得られないという問題がある。readコマンドで読み込むときに1バイトを超えて読み込む場合があるようで、後半部分の出力が期待値と合わない。ただ、7ビットASCIIの範囲では問題なく読み書きできているので一応載せておく。

#! /bin/bash

while IFS= read -r -N 1 ch ; do
    printf "%c" "${ch}"
done

PHPでのファイルアップロードの処理

ファイルアップロードのファイルサイズチェック - Qiitaを読んでて非常にもやっとしたので、PHPプログラム側のエラー処理を自分なりに整理してみたメモ。

ちなみに検証環境は訳あって以下だが、CentOS 7でも同様だと思う。

php.iniの設定は以下のようになっている。

;;;;;;;;;;;;;;;;;
; Data Handling ;
;;;;;;;;;;;;;;;;;

; Maximum size of POST data that PHP will accept.
; http://www.php.net/manual/en/ini.core.php#ini.post-max-size
post_max_size = 8M

;;;;;;;;;;;;;;;;
; File Uploads ;
;;;;;;;;;;;;;;;;

; Maximum allowed size for uploaded files.
; http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize
upload_max_filesize = 2M

送信側のHTMLおよび受信側のPHPプログラムをベタで書き起こすと以下のような感じになるだろう。

  • ファイル送信側のHTML
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>$_FILES test</title>
    </head>
    <body>
        <h1>[php.iniのupload_max_filesize] &gt; [hiddenのMAX_FILE_SIZE]</h1>
        <form action="upload.php" method="post" enctype="multipart/form-data">
            <input type="hidden" name="MAX_FILE_SIZE" value="1048576" />
            <input type="file" name="file1" />
            <input type="submit" value="アップロード1" />
        </form>
        <h1>[php.iniのupload_max_filesize] &lt; [hiddenのMAX_FILE_SIZE]</h1>
        <form action="upload.php" method="post" enctype="multipart/form-data">
            <input type="hidden" name="MAX_FILE_SIZE" value="3145728" />
            <input type="file" name="file1" />
            <input type="submit" value="アップロード2" />
        </form>
    </body>
</html>
  • 受信側のPHPプログラム
<a href="./">Back to Home</a>
<hr />
<pre>
<?php
var_dump($_POST);
var_dump($_FILES);
?>
</pre>
<hr />
<?php
$param = 'file1';
if (isset($_FILES[$param])) {
    $err = $_FILES[$param]['error'];
    if ($err === UPLOAD_ERR_OK) {
        if (is_uploaded_file($_FILES[$param]['tmp_name'])) {
            print('OK。何の問題もありません。');
        } else {
            print('えっ!?何が起きた?');
        }
    } else if ($err === UPLOAD_ERR_INI_SIZE) {
        print('ファイルサイズが大きすぎます(php.iniのupload_max_filesize)。' . ini_get('upload_max_filesize') . 'バイト以下にしてください。');
    } else if ($err === UPLOAD_ERR_FORM_SIZE) {
        print('ファイルサイズが大きすぎます(formのMAX_FILE_SIZE)。' . $_POST['MAX_FILE_SIZE'] . 'バイト以下にしてください。');
    } else if ($err === UPLOAD_ERR_PARTIAL) {
        // XXX アップロード中にキャンセルした場合とか?
        print('「アップロードされたファイルは一部のみしかアップロードされていません。」');
    } else if ($err === UPLOAD_ERR_NO_FILE) {
        print('「ファイルはアップロードされませんでした。」');
    } else if ($err === UPLOAD_ERR_NO_TMP_DIR) {
        // XXX upload_tmp_dirで設定したディレクトリが存在しない場合かと思ったが、
        // 「ここで指定したディレクトリに書き込むことができない場合、 PHP はかわりにシステムのデフォルトテンポラリディレクトリを使用します。」らしい。
        print('「テンポラリフォルダがありません。」');
    } else if ($err === UPLOAD_ERR_CANT_WRITE) {
        // XXX ディスク容量不足の場合とか?
        print('「ディスクへの書き込みに失敗しました。」');
    } else if ($err === UPLOAD_ERR_EXTENSION) {
        print('「PHP の拡張モジュールがファイルのアップロードを中止しました。」');
    }
} else {
    // post_max_sizeを超えている
    print('ファイルサイズがあまりに大きすぎます。' . ini_get('upload_max_filesize') . 'バイト以下にしてください。');
}

とまぁ、結構めんどくさいことになるので、必要なエラーコードだけ拾ってきちんと処理し、残りは適当にあしらうのが良いのかなと。「UPLOAD_ERR_NO_TMP_DIR」とかなんか完全にサーバー側の設定の問題だしな。

参考

ARRAY_TO_STRINGの結果に対してLIKE検索している部分が遅い問題の一つの解決案

割と複雑なクエリで4秒とか掛かるものにぶち当たり、explainを取ってみたところ、表題の通りARRAY_TO_STRINGした結果得られる文字列に対するLIKE検索をしている部分でコストが増大していることが分かり、軽く検証してみたメモ。

細かいところは追い追い書いていくとして、まずは準備。

環境

準備

以下のようなクエリを流して、テーブルおよびデータを作る。結構時間が掛かるので、実行する際には心してお待ちください。

#! /bin/bash

psql -U postgres -q -c 'DROP DATABASE IF EXISTS test_db'
psql -U postgres -q -c 'CREATE DATABASE test_db'
psql -U postgres -q -c 'CREATE TABLE IF NOT EXISTS user_master (
                              id BIGSERIAL PRIMARY KEY
                            , name VARCHAR(128) NOT NULL
                        )' test_db
psql -U postgres -q -c 'CREATE TABLE IF NOT EXISTS label_list (
                              id BIGSERIAL PRIMARY KEY
                            , user_id BIGINT NOT NULL
                            , label VARCHAR(128) NOT NULL
                        )' test_db
psql -U postgres -q -c 'CREATE INDEX ON label_list(user_id)' test_db
psql -U postgres -q -c 'CREATE INDEX ON label_list(label)' test_db

# 全ユーザ
for ((i = 1; i <= 10000; ++i)); do
        psql -U postgres -q -c "INSERT INTO user_master(name) VALUES('name$(printf %06d ${i})')" test_db
        psql -U postgres -q -c "INSERT INTO label_list(user_id, label) VALUES(${i}, 'A')" test_db
done
# 奇数IDのユーザ
for ((i = 1; i <= 10000; i += 2)); do
        psql -U postgres -q -c "INSERT INTO label_list(user_id, label) VALUES(${i}, 'B')" test_db
done
# 3の倍数+1のIDのユーザ
for ((i = 1; i <= 10000; i += 3)); do
        psql -U postgres -q -c "INSERT INTO label_list(user_id, label) VALUES(${i}, 'C')" test_db
done
# 5の倍数+1のIDのユーザ
for ((i = 1; i <= 10000; i += 5)); do
        psql -U postgres -q -c "INSERT INTO label_list(user_id, label) VALUES(${i}, 'D')" test_db
done
# 素数IDのユーザ
for ((i = 1; i <= 10000; ++i)); do
        if [ $(factor ${i} | wc -w) -eq 2 ]; then
                psql -U postgres -q -c "INSERT INTO label_list(user_id, label) VALUES(${i}, 'E')" test_db
        fi
done

説明は・・要るかな・・一応軽く書くと、user_masterテーブルに各ユーザの情報が格納されており、各ユーザに対して付けられているいくつかのラベルがlabel_listテーブルに1ラベル1行として格納されている、という構造。

クエリ1(書き換え前)

オリジナルのクエリからARRAY_TO_STRINGを使って検索している部分を抜き出したもの。

  • クエリ1-1
SELECT DISTINCT u.id
    FROM user_master u
    WHERE u.id IN (
        SELECT user_id
            FROM (
                SELECT DISTINCT
                      user_id
                    , ARRAY_TO_STRING(
                          ARRAY(
                            SELECT label
                                FROM label_list l2
                                WHERE l1.user_id = l2.user_id
                          )
                        , ',') label_str
                    FROM label_list l1
            ) l0
            WHERE label_str LIKE '%A%'
              AND label_str LIKE '%B%'
              AND label_str LIKE '%E%'
)
  • クエリ1-2
SELECT DISTINCT u.id
    FROM user_master u
    WHERE u.id IN (
        SELECT user_id
            FROM (
                SELECT DISTINCT
                      user_id
                    , ARRAY_TO_STRING(
                          ARRAY(
                            SELECT label
                                FROM label_list l2
                                WHERE l1.user_id = l2.user_id
                          )
                        , ',') label_str
                    FROM label_list l1
            ) l0
            WHERE label_str LIKE '%A%'
              AND label_str LIKE '%B%'
              AND label_str NOT LIKE '%C%'
              AND label_str NOT LIKE '%D%'
              AND label_str LIKE '%E%'
)

いずれも、ARRAY_TO_STRINGであるユーザにつけられているラベルをカンマ区切りの1つの文字列にしてLIKE検索をしている。

2つのクエリの違いはどのラベルを含むか含まないかの条件の数だけ。

クエリ2(書き換え後)

ARRAY_TO_STRINGを使わないようにするには、検索したいキーワードの数だけlabel_listをキーワードで絞り込んでJOINしていくしかない。

  • クエリ2-1
SELECT DISTINCT u.id, u.name
    FROM user_master u
    WHERE u.id IN (
        SELECT l0.user_id
            FROM label_list l0
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'A'
                    )
            ) l1
            ON l0.user_id = l1.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'B'
                    )
            ) l2
            ON l0.user_id = l2.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'E'
                    )
            ) l3
            ON l0.user_id = l3.user_id
)
  • クエリ2-2
SELECT DISTINCT u.id, u.name
    FROM user_master u
    WHERE u.id IN (
        SELECT l0.user_id
            FROM label_list l0
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'A'
                    )
            ) l1
            ON l0.user_id = l1.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'B'
                    )
            ) l2
            ON l0.user_id = l2.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id NOT IN (
                        SELECT user_id FROM label_list WHERE label = 'C'
                    )
            ) l3
            ON l0.user_id = l3.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id NOT IN (
                        SELECT user_id FROM label_list WHERE label = 'D'
                    )
            ) l4
            ON l0.user_id = l4.user_id
            JOIN (
                SELECT user_id
                    FROM label_list
                    WHERE user_id IN (
                        SELECT user_id FROM label_list WHERE label = 'E'
                    )
            ) l5
            ON l0.user_id = l5.user_id
)

実行結果

全部を載せるのは無理があるので、件数だけ載せておく。

$ cat query1.sql query2.sql | psql -U postgres test_db | grep '^[(]'
(1228)
(463)
(1228)
(463)

実際には、両方のクエリで結果が同じになるということは検証してある。

explainの結果

最後にexplainを取った結果を載せておく。

  • クエリ1-1
                                                                                                          QUERY PLAN                                                                                                          
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=1118038.76..1118416.52 rows=5000 width=8)
   ->  Merge Join  (cost=1118038.76..1118404.02 rows=5000 width=8)
         Merge Cond: (u.id = l1.user_id)
         ->  Index Only Scan using user_master_pkey on user_master u  (cost=0.00..337.25 rows=10000 width=8)
         ->  Sort  (cost=1118038.76..1118039.26 rows=200 width=8)
               Sort Key: l1.user_id
               ->  HashAggregate  (cost=1118029.12..1118031.12 rows=200 width=8)
                     ->  HashAggregate  (cost=997726.56..1117899.45 rows=10374 width=8)
                           ->  Seq Scan on label_list l1  (cost=0.00..997618.90 rows=21533 width=8)
                                 Filter: ((array_to_string((SubPlan 2), ','::text) ~~ '%A%'::text) AND (array_to_string((SubPlan 3), ','::text) ~~ '%B%'::text) AND (array_to_string((SubPlan 4), ','::text) ~~ '%E%'::text))
                                 SubPlan 1
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 2
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 3
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 4
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
(30 行)
  • クエリ1-2
                                                                                                                                                                        QUERY PLAN                                                                                                                                                                        
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=1247100.97..1247599.82 rows=1 width=8)
   ->  Nested Loop Semi Join  (cost=1247100.97..1247599.82 rows=1 width=8)
         Join Filter: (u.id = l0.user_id)
         ->  Index Only Scan using user_master_pkey on user_master u  (cost=0.00..337.25 rows=10000 width=8)
         ->  Materialize  (cost=1247100.97..1247112.57 rows=1 width=8)
               ->  Subquery Scan on l0  (cost=1247100.97..1247112.56 rows=1 width=8)
                     ->  HashAggregate  (cost=1247100.97..1247112.55 rows=1 width=8)
                           ->  Seq Scan on label_list l1  (cost=0.00..1247100.97 rows=1 width=8)
                                 Filter: ((array_to_string((SubPlan 2), ','::text) ~~ '%A%'::text) AND (array_to_string((SubPlan 3), ','::text) ~~ '%B%'::text) AND (array_to_string((SubPlan 4), ','::text) !~~ '%C%'::text) AND (array_to_string((SubPlan 5), ','::text) !~~ '%D%'::text) AND (array_to_string((SubPlan 6), ','::text) ~~ '%E%'::text))
                                 SubPlan 1
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 2
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 3
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 4
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 5
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
                                 SubPlan 6
                                   ->  Bitmap Heap Scan on label_list l2  (cost=4.27..11.57 rows=2 width=2)
                                         Recheck Cond: (l1.user_id = user_id)
                                         ->  Bitmap Index Scan on label_list_user_id_idx  (cost=0.00..4.27 rows=2 width=0)
                                               Index Cond: (l1.user_id = user_id)
(39 行)
  • クエリ2-1
                                                                            QUERY PLAN                                                                            
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=2545.00..2600.26 rows=5526 width=8)
   ->  Hash Semi Join  (cost=2194.33..2531.18 rows=5526 width=8)
         Hash Cond: (u.id = public.label_list.user_id)
         ->  Seq Scan on user_master u  (cost=0.00..164.00 rows=10000 width=8)
         ->  Hash  (cost=2125.25..2125.25 rows=5526 width=56)
               ->  Hash Join  (cost=1635.83..2125.25 rows=5526 width=56)
                     Hash Cond: (l0.user_id = public.label_list.user_id)
                     ->  Seq Scan on label_list l0  (cost=0.00..353.39 rows=21539 width=8)
                     ->  Hash  (cost=1603.07..1603.07 rows=2621 width=48)
                           ->  Nested Loop  (cost=301.13..1603.07 rows=2621 width=48)
                                 ->  Nested Loop  (cost=301.13..1162.32 rows=1243 width=40)
                                       ->  Nested Loop Semi Join  (cost=301.13..953.11 rows=590 width=32)
                                             ->  Hash Join  (cost=301.13..748.45 rows=590 width=24)
                                                   Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                                   ->  Hash Join  (cost=27.87..466.52 rows=1165 width=16)
                                                         Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                                         ->  Seq Scan on label_list  (cost=0.00..353.39 rows=21539 width=8)
                                                         ->  Hash  (cost=25.20..25.20 rows=213 width=8)
                                                               ->  HashAggregate  (cost=23.07..25.20 rows=213 width=8)
                                                                     ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..21.97 rows=441 width=8)
                                                                           Index Cond: ((label)::text = 'E'::text)
                                                   ->  Hash  (cost=242.01..242.01 rows=2500 width=8)
                                                         ->  HashAggregate  (cost=217.01..242.01 rows=2500 width=8)
                                                               ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..204.04 rows=5188 width=8)
                                                                     Index Cond: ((label)::text = 'B'::text)
                                             ->  Index Scan using label_list_user_id_idx on label_list  (cost=0.00..0.34 rows=1 width=8)
                                                   Index Cond: (user_id = public.label_list.user_id)
                                                   Filter: ((label)::text = 'A'::text)
                                       ->  Index Only Scan using label_list_user_id_idx on label_list  (cost=0.00..0.33 rows=2 width=8)
                                             Index Cond: (user_id = public.label_list.user_id)
                                 ->  Index Only Scan using label_list_user_id_idx on label_list  (cost=0.00..0.33 rows=2 width=8)
                                       Index Cond: (user_id = public.label_list.user_id)
(32 行)
  • クエリ2-2
                                                                                      QUERY PLAN                                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=6787.71..6849.12 rows=6141 width=8)
   ->  Hash Semi Join  (cost=6403.91..6772.36 rows=6141 width=8)
         Hash Cond: (u.id = public.label_list.user_id)
         ->  Seq Scan on user_master u  (cost=0.00..164.00 rows=10000 width=8)
         ->  Hash  (cost=6327.15..6327.15 rows=6141 width=72)
               ->  Hash Join  (cost=4141.44..6327.15 rows=6141 width=72)
                     Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                     ->  Hash Join  (cost=3670.68..5458.41 rows=53850 width=56)
                           Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                           ->  Hash Join  (cost=3048.05..3818.39 rows=25541 width=48)
                                 Hash Cond: (l0.user_id = public.label_list.user_id)
                                 ->  Seq Scan on label_list l0  (cost=0.00..353.39 rows=21539 width=8)
                                 ->  Hash  (cost=2896.62..2896.62 rows=12114 width=40)
                                       ->  Hash Join  (cost=2287.46..2896.62 rows=12114 width=40)
                                             Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                             ->  Seq Scan on label_list  (cost=0.00..353.39 rows=21539 width=8)
                                             ->  Hash  (cost=2215.64..2215.64 rows=5746 width=32)
                                                   ->  Hash Join  (cost=1683.63..2215.64 rows=5746 width=32)
                                                         Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                                         ->  Seq Scan on label_list  (cost=90.75..497.99 rows=10770 width=8)
                                                               Filter: (NOT (hashed SubPlan 2))
                                                               SubPlan 2
                                                                 ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..85.57 rows=2075 width=8)
                                                                       Index Cond: ((label)::text = 'D'::text)
                                                         ->  Hash  (cost=1524.75..1524.75 rows=5450 width=24)
                                                               ->  Hash Join  (cost=962.95..1524.75 rows=5450 width=24)
                                                                     Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                                                     ->  Hash Join  (cost=420.70..908.14 rows=5450 width=16)
                                                                           Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                                                           ->  Seq Scan on label_list  (cost=147.43..554.67 rows=10770 width=8)
                                                                                 Filter: (NOT (hashed SubPlan 1))
                                                                                 SubPlan 1
                                                                                   ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..138.79 rows=3459 width=8)
                                                                                         Index Cond: ((label)::text = 'C'::text)
                                                                           ->  Hash  (cost=242.01..242.01 rows=2500 width=8)
                                                                                 ->  HashAggregate  (cost=217.01..242.01 rows=2500 width=8)
                                                                                       ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..204.04 rows=5188 width=8)
                                                                                             Index Cond: ((label)::text = 'B'::text)
                                                                     ->  Hash  (cost=479.76..479.76 rows=4999 width=8)
                                                                           ->  HashAggregate  (cost=429.77..479.76 rows=4999 width=8)
                                                                                 ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..403.83 rows=10376 width=8)
                                                                                       Index Cond: ((label)::text = 'A'::text)
                           ->  Hash  (cost=353.39..353.39 rows=21539 width=8)
                                 ->  Seq Scan on label_list  (cost=0.00..353.39 rows=21539 width=8)
                     ->  Hash  (cost=456.20..456.20 rows=1165 width=16)
                           ->  Hash Semi Join  (cost=27.48..456.20 rows=1165 width=16)
                                 Hash Cond: (public.label_list.user_id = public.label_list.user_id)
                                 ->  Seq Scan on label_list  (cost=0.00..353.39 rows=21539 width=8)
                                 ->  Hash  (cost=21.97..21.97 rows=441 width=8)
                                       ->  Index Scan using label_list_label_idx on label_list  (cost=0.00..21.97 rows=441 width=8)
                                             Index Cond: ((label)::text = 'E'::text)
(51 行)

explainの結果からも分かるように、最終的なコストが、クエリ1-1が「1118416.52」、クエリ1-2が「1247599.82』であるのに対して、クエリ2-1が「2600.26」、クエリ2-2が「6849.12」と激減している。

実のところ、検証環境でクエリを流してみるといずれも1秒以内で返ってくるので差があまり実感できなかったが、この書き換えを組み込んだ実際のクエリで試してみたところ、書き換え前が4秒弱に対して書き換え後が200ミリ秒と約20分の1になっていたので、効果はあるということだろう。問題は、書き換え後のクエリではJOINの嵐になるということである。実際に使う際には、キーワードの数を5個くらいまでに絞るとかそういう制限を設けないときついだろう。

IntStreamで遊んでみた

暇だったので、Java 8から追加されたjava.util.stream.IntStreamでちょっと遊んでみた。

ちなみに、IntStreaminterfaceであるが、ソースを見るとstaticメソッドの実装が書いてある。IntStream#ofとか。まぁそれは余談。

以下のようなソースで出力を見ていく。

import java.util.Arrays;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int[] ary = { 5, 2, 8, 2, 9, 1, 3, 0, 5, 6, 4, 7, };

        System.out.println("--------countTest");
        countTest(ary);
        System.out.println("--------sumTest");
        sumTest(ary);
        System.out.println("--------maxTest");
        maxTest(ary);
        System.out.println("--------minTest");
        minTest(ary);
        System.out.println("--------sortTest");
        sortTest(ary);
        System.out.println("--------1つのstreamで連続処理できるのかテスト");
        test(ary);
        System.out.println("--------パフォーマンスを計ってみる(sumを例に)");
        for (int ct = 1; ct <= 100000000; ct *= 10) {
            performance(ary, ct);
        }
        System.out.println("--------その他");
        othersTest(ary);
        System.out.println("--------classTest");
        classTest(ary);
    }
    private static void countTest(int[] ary) {
        // 配列が手元にある場合に最もポピュラーな手段
        System.out.println("count1=" + ary.length);

        // Streamしか手元にない場合は仕方ない
        IntStream stream = IntStream.of(ary);
        System.out.println("count2=" + stream.count());
    }
    private static void sumTest(int[] ary) {
        // ループ変数を使って要素にアクセス
        int sum1 = 0;
        for (int i = 0; i < ary.length; ++i) {
            sum1 += ary[i];
        }
        System.out.println("sum1=" + sum1);

        // 拡張for文
        int sum2 = 0;
        for (int a : ary) {
            sum2 += a;
        }
        System.out.println("sum2=" + sum2);

        // java.util.Arrays.stream()
        System.out.println("sum3=" + Arrays.stream(ary).sum());

        // java.util.stream.IntStream
        System.out.println("sum4=" + IntStream.of(ary).sum());
    }
    private static void maxTest(int[] ary) {
        // ループ変数を使って要素にアクセス
        int max1 = Integer.MIN_VALUE;
        for (int i = 0; i < ary.length; ++i) {
            max1 = Math.max(max1, ary[i]);
        }
        System.out.println("max1=" + max1);

        // 拡張for文
        int max2 = Integer.MIN_VALUE;
        for (int a : ary) {
            max2 = Math.max(max2, a);
        }
        System.out.println("max2=" + max2);

        // java.util.Arrays.stream()
        System.out.println("max3=" + Arrays.stream(ary).max());

        // java.util.stream.IntStream
        System.out.println("max4=" + IntStream.of(ary).max());
    }
    private static void minTest(int[] ary) {
        // ループ変数を使って要素にアクセス
        int min1 = Integer.MAX_VALUE;
        for (int i = 0; i < ary.length; ++i) {
            min1 = Math.min(min1, ary[i]);
        }
        System.out.println("min1=" + min1);

        // 拡張for文
        int min2 = Integer.MAX_VALUE;
        for (int a : ary) {
            min2 = Math.min(min2, a);
        }
        System.out.println("min2=" + min2);

        // java.util.Arrays.stream()
        System.out.println("min3=" + Arrays.stream(ary).min());

        // java.util.stream.IntStream
        System.out.println("min4=" + IntStream.of(ary).min());
    }
    private static void sortTest(int[] ary) {
        // java.util.Arraysのsortメソッド(副作用あり)
        int[] ary1 = ary.clone();
        Arrays.sort(ary1);
        System.out.println("sort1=" + Arrays.toString(ary1));

        // java.util.stream.IntStreamのソート済みstreamを取得するsortedメソッド(副作用無し)
        IntStream stream2 = IntStream.of(ary).sorted();
        System.out.println("sort2=" + Arrays.toString(stream2.toArray()));
        System.out.println("orig =" + Arrays.toString(ary));
    }
    private static void test(int[] ary) {
        try {
            IntStream stream = IntStream.of(ary);
            System.out.println("sumT=" + stream.sum());
            System.out.println("maxT=" + stream.max());
            System.out.println("minT=" + stream.min());
            System.out.println("sortT=" + Arrays.toString(stream.sorted().toArray()));
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }
    private static void performance(int[] ary, int loopCount) {
        // ループ変数を使って要素にアクセス
        long S1 = System.currentTimeMillis();
        for (int ct = 0; ct < loopCount; ++ct) {
            int sum1 = 0;
            for (int i = 0; i < ary.length; ++i) {
                sum1 += ary[i];
            }
        }
        long G1 = System.currentTimeMillis();

        // 拡張for文
        long S2 = System.currentTimeMillis();
        for (int ct = 0; ct < loopCount; ++ct) {
            int sum2 = 0;
            for (int a : ary) {
                sum2 += a;
            }
        }
        long G2 = System.currentTimeMillis();

        // java.util.Arrays.stream()
        long S3 = System.currentTimeMillis();
        for (int ct = 0; ct < loopCount; ++ct) {
            int sum3 = Arrays.stream(ary).sum();
        }
        long G3 = System.currentTimeMillis();

        // java.util.stream.IntStream
        long S4 = System.currentTimeMillis();
        for (int ct = 0; ct < loopCount; ++ct) {
            int sum4 = IntStream.of(ary).sum();
        }
        long G4 = System.currentTimeMillis();

        System.out.printf("sum(ms) %16dct | %5d | %5d | %5d | %5d |%n",
            loopCount, G1 - S1, G2 - S2, G3 - S3, G4 - S4);
    }
    private static void othersTest(int[] ary) {
        System.out.println("orig    =" + Arrays.toString(ary));
        // distinct
        IntStream stream1 = IntStream.of(ary).distinct();
        System.out.println("distinct=" + Arrays.toString(stream1.toArray()));
        // skip
        IntStream stream2 = IntStream.of(ary).skip(1);
        System.out.println("skip    =" + Arrays.toString(stream2.toArray()));
        // limit
        IntStream stream3 = IntStream.of(ary).limit(5);
        System.out.println("limit   =" + Arrays.toString(stream3.toArray()));
    }
    private static void classTest(int[] ary) {
        IntStream stream;

        stream = IntStream.empty();
        System.out.println("empty     =" + stream.getClass().getName());
        stream = IntStream.range(1, 2);
        System.out.println("range     =" + stream.getClass().getName());
        stream = IntStream.concat(IntStream.empty(), IntStream.empty());
        System.out.println("concat    =" + stream.getClass().getName());
        stream = IntStream.of(ary);
        System.out.println("of        =" + stream.getClass().getName());
        stream = IntStream.of(ary).sorted();
        System.out.println("sorted    =" + stream.getClass().getName());
        stream = IntStream.of(ary).filter(i -> true);
        System.out.println("filter    =" + stream.getClass().getName());
        stream = IntStream.of(ary).map(i -> i);
        System.out.println("map       =" + stream.getClass().getName());
        stream = IntStream.of(ary).distinct();
        System.out.println("distinct  =" + stream.getClass().getName());
        stream = IntStream.of(ary).skip(1);
        System.out.println("skip      =" + stream.getClass().getName());
        stream = IntStream.of(ary).limit(1);
        System.out.println("limit     =" + stream.getClass().getName());
    }
}

各々の出力結果

countTest

--------countTest
count1=12
count2=12

int配列が手元にあるなら要らないだろうというメソッドcount()

Streamしか手元にないなら仕方ないだろうが、Streamの要素を数え上げるという処理によって要素が消費されてその後は何もできなくなる(後述)ので、要素数を取得してStreamを捨ててもよいという場合にしか使えない。

sumTest

--------sumTest
sum1=52
sum2=52
sum3=52
sum4=52

これは、配列の要素のsumを計算するのに1行で書けるのでちょっとうれしいかも。

ちなみに、IntStream.of(int...)は内部でArrays.stream(int[])を呼んでいる。

maxTest

--------maxTest
max1=9
max2=9
max3=OptionalInt[9]
max4=OptionalInt[9]

これは、OptionalIntが返ってくるのがちょっと微妙なところ。max値がInteger.MIN_VALUEかどうかと比較するのとどっちがいいかという感じ。

minTest

--------minTest
min1=0
min2=0
min3=OptionalInt[0]
min4=OptionalInt[0]

maxと同じく。

sortTest

--------sortTest
sort1=[0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9]
sort2=[0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9]
orig =[5, 2, 8, 2, 9, 1, 3, 0, 5, 6, 4, 7]

元の配列への副作用を発生させないために、ary.clone()するかIntStream#sortedを使うかという感じ。ソートされたStreamを取得して更にあれこれやりたい場合には有用。

test

--------1つのstreamで連続処理できるのかテスト
sumT=52
java.lang.IllegalStateException: stream has already been operated upon or closed
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
        at java.util.stream.IntPipeline.reduce(IntPipeline.java:461)
        at java.util.stream.IntPipeline.max(IntPipeline.java:424)
        at Main.test(Main.java:111)
        at Main.main(Main.java:19)

これはまぁ予想通り。java.io.InputStreamやjava.io.Readerからの読み込みを(一部を除いて)巻き戻し再実行できないのと同じ。

(補足)sum、max、min、average、countが欲しいだけならsummaryStatistics()メソッドを使えばいいらしい。

performance

--------パフォーマンスを計ってみる(sumを例に)
sum(ms)                1ct |     0 |     0 |     0 |     0 |
sum(ms)               10ct |     0 |     0 |     0 |     0 |
sum(ms)              100ct |     0 |     0 |     1 |     0 |
sum(ms)             1000ct |     0 |     0 |     9 |     6 |
sum(ms)            10000ct |     2 |     2 |     4 |    16 |
sum(ms)           100000ct |     5 |    28 |     8 |    18 |
sum(ms)          1000000ct |    75 |    66 |   102 |    53 |
sum(ms)         10000000ct |     0 |     0 |   649 |   607 |
sum(ms)        100000000ct |     0 |     0 |  6118 |  6154 |

まぁ、IntStreamオブジェクトを生成するから仕方ない。

othersTest

--------その他
orig    =[5, 2, 8, 2, 9, 1, 3, 0, 5, 6, 4, 7]
distinct=[5, 2, 8, 9, 1, 3, 0, 6, 4, 7]
skip    =[2, 8, 2, 9, 1, 3, 0, 5, 6, 4, 7]
limit   =[5, 2, 8, 2, 9]

distinctは重複要素を除去した新たなStreamを生成する。

skipは先頭の指定された要素数をスキップした新たなStreamを生成する。

limitは先頭の指定された要素数だけを返す新たなStreamを生成する。

その他にfilterとかmapとか色々あるが割愛(謎)。lambda式から逃げてるだけとも言わなくもない(ぉ‥

classTest

--------classTest
empty     =java.util.stream.IntPipeline$Head
range     =java.util.stream.IntPipeline$Head
concat    =java.util.stream.IntPipeline$Head
of        =java.util.stream.IntPipeline$Head
sorted    =java.util.stream.SortedOps$OfInt
filter    =java.util.stream.IntPipeline$9
map       =java.util.stream.IntPipeline$3
distinct  =java.util.stream.ReferencePipeline$4
skip      =java.util.stream.SliceOps$2
limit     =java.util.stream.SliceOps$2

java.util.stream.IntStreamがインタフェースだということで、実装クラスがどうなっているのか気になったので列挙してみた。