EC2インスタンスにClickHouseを入れてローカルからscalike-jdbcで接続した

ビッグデータ向け？DBであるClickHouseをscalaから触ってみたので備忘録を
導入方法などを書くだけでベンチマークとかのパフォーマンス検証はありません（でも次に書きたいかも…）
## 全体の流れ
1. EC2インスタンスにClickHouseを入れる
2. テーブルの用意
3. 設定ファイル変更
4. scalaプロジェクトの用意

という感じで説明していきます
## EC2インスタンスにClickHouseを入れる
AmazonLinuxを想定
https://github.com/Altinity/clickhouse-rpm-install
の手順に沿ってyumで入れる

installができたらclientで接続出来ることの確認

```sql
$ clickhouse-client
ClickHouse client version 1.1.54390.
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 1.1.54390.

:)
:) show databases

SHOW DATABASES

┌─name────┐
│ default │
│ system  │
└─────────┘

2 rows in set. Elapsed: 0.026 sec.
```

## テーブルの用意
- DATABASEの用意  
他のRDB同様DDLで作成

```sql
:) CREATE DATABASE my_test

CREATE DATABASE my_test

Ok.

0 rows in set. Elapsed: 0.003 sec.

:) show databases

SHOW DATABASES

┌─name────┐
│ default │
│ my_test │
│ system  │
└─────────┘

3 rows in set. Elapsed: 0.001 sec.

:)

:) use my_test

USE my_test

Ok.

0 rows in set. Elapsed: 0.009 sec.

:)
```
(デフォルトだと使用中のDB名は表示されないっぽい…)

- MergeTree engineでテーブルを用意  
[MergeTree](https://clickhouse.yandex/docs/en/operations/table_engines/mergetree/)はlogなどを集計/検索するのに有効なエンジンらしい？（よくわかってない）

```sql
:) CREATE TABLE log_20180729 (
:-]   write_date Date,
:-]   data1 String,
:-]   data2 String,
:-]   created_at DateTime
:-] ) ENGINE = MergeTree(write_date, (write_date, data1, data2, created_at), 8192)

CREATE TABLE log_20180729
(
    write_date Date,
    data1 String,
    data2 String,
    created_at DateTime
)
ENGINE = MergeTree(write_date, (write_date, data1, data2, created_at), 8192)

Ok.

0 rows in set. Elapsed: 0.004 sec.

:) CREATE TABLE log_20180729 (
:-]   write_date Date,
:-]   data1 String,
:-]   data2 String,
:-]   created_at DateTime
:-] ) ENGINE = MergeTree(write_date, (write_date, data1, data2, created_at), 8192)

CREATE TABLE log_20180729
(
    write_date Date,
    data1 String,
    data2 String,
    created_at DateTime
)
ENGINE = MergeTree(write_date, (write_date, data1, data2, created_at), 8192)

Received exception from server (version 1.1.54390):
Code: 57. DB::Exception: Received from localhost:9000, 127.0.0.1. DB::Exception: Table my_test.log_20180729 already exists..

0 rows in set. Elapsed: 0.039 sec.

:)
```
MergeTreeエンジンのテーブルは日別ごとに作成するといいらしい（よくわかってない）
MergeTreeの引数は `MergeTree(<日付カラム>, <主キー用のタプル>, <索引粒度>)`　となっている（索引粒度ってなんなんだ…）

適当にデータを挿入しておく

```sql
:) INSERT INTO log_20180728 VALUES('2018-07-28', 'foo', 'bar', NOW());

INSERT INTO log_20180728 VALUES

Ok.

1 rows in set. Elapsed: 0.005 sec.

:) INSERT INTO log_20180729 VALUES('2018-07-29', 'foo', 'fuga', NOW());

INSERT INTO log_20180729 VALUES

Ok.

1 rows in set. Elapsed: 0.001 sec.

:) INSERT INTO log_20180729 VALUES('2018-07-29', 'bar', 'foo', NOW());

INSERT INTO log_20180729 VALUES

Ok.

1 rows in set. Elapsed: 0.003 sec.

:)
```

- Mergeテーブルの作成  
[Merge](https://clickhouse.yandex/docs/en/query_language/table_functions/merge/)は直接的な実データを持たず複数のテーブルを一つのテーブルのように扱うことができる

```sql
:) CREATE TABLE log (
:-]     write_date Date,
:-]     data1 String,
:-]     data2 String,
:-]     created_at DateTime
:-] ) ENGINE = Merge('my_test', '^log_')

CREATE TABLE log
(
    write_date Date,
    data1 String,
    data2 String,
    created_at DateTime
)
ENGINE = Merge('my_test', '^log_')

Ok.

0 rows in set. Elapsed: 0.003 sec.

:)
```

- SELECTしてみる

```sql
:) SELECT * FROM log

SELECT *
FROM log

┌─write_date─┬─data1─┬─data2─┬──────────created_at─┐
│ 2018-07-28 │ foo   │ bar   │ 2018-07-28 10:42:03 │
└────────────┴───────┴───────┴─────────────────────┘
┌─write_date─┬─data1─┬─data2─┬──────────created_at─┐
│ 2018-07-29 │ foo   │ fuga  │ 2018-07-28 10:42:03 │
└────────────┴───────┴───────┴─────────────────────┘
┌─write_date─┬─data1─┬─data2─┬──────────created_at─┐
│ 2018-07-29 │ bar   │ foo   │ 2018-07-28 10:42:04 │
└────────────┴───────┴───────┴─────────────────────┘

3 rows in set. Elapsed: 0.004 sec.

:) SELECT data1, COUNT(1) FROM log GROUP BY data1

SELECT
    data1,
    COUNT(1)
FROM log
GROUP BY data1

┌─data1─┬─COUNT(1)─┐
│ bar   │        1 │
│ foo   │        2 │
└───────┴──────────┘

2 rows in set. Elapsed: 0.008 sec.

:)
```

## 設定ファイル変更
- `/etc/clickhouse-server/users.xml` の編集  
ClickHouseのユーザを定義しているファイル  
初期では `default` と `readonly` の二種類のユーザがいるのでそれぞれ  
以下を変更しておく(ipv6表記)

### default
```xml
<networks>
  <ip>::1</ip> <!-- ローカルからのみアクセス可能にしておく -->
</networks>
```

### readonly
```xml
<password>任意のパスワード</password>

<networks>
  <ip>::/0</ip> <!-- すべてのIPからアクセス可能 -->
</networks>
```
(実運用に使う場合は `<hoost_regexp>` などで設定する)
https://clickhouse.yandex/docs/en/operations/access_rights/

- config.xmlを変更
以下を追記

```xml
<!-- 接続可能なhost、ワイルドカード(::)は全てのipv4/ipv6アドレスを受け付ける -->
<listen_host>::</listen_host> 
```

- EC2のセキュリティグループにClickServer用のインバウンドを追加する  
9000番はclickhouse-client用の口  
8123番はhttp-interface用の口  
(EC2エアプなのでお粗末な設定かもしれない…)
<img width="1009" alt="スクリーンショット 2018-07-29 13.38.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/250899/ea278cfc-c1a8-a466-6b74-9113939a6eb4.png">

## scalaプロジェクトの用意
- `sbt new` で新しいプロジェクトを用意
このへんの説明は割愛、好きにやってくれい

- jdbc依存性を追加
clickhouse用jdbcは二種類あって
https://github.com/housepower/ClickHouse-Native-JDBC
https://github.com/yandex/clickhouse-jdbc
今回は２つ目のjdbcを使った(こちらはnaitiveと違ってhttp-interfaceを利用するjdbc)
build.sbt

```scala
libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc" % "2.5.2", // ついでにscalike-jdbcも追加
  "ru.yandex.clickhouse" % "clickhouse-jdbc" % "0.1.40",
  "ch.qos.logback"  %  "logback-classic" % "1.2.3"
)
```

- ClickHouse接続用のコードを書く  
適当に

```scala
import scalikejdbc._
Class.forName("ru.yandex.clickhouse.ClickHouseDriver")
ConnectionPool.singleton("jdbc:clickhouse://<ec2のホスト>/my_test", "readonly", "<設定したパスワード>")

implicit val session = AutoSession

val result =
  sql"""
       |SELECT * FROM log
    """.stripMargin.map(rs => rs.string("data1")).list().apply()

println(result)
```

## さぁ実行だ！

```shell
$ sbt run
...
13:48:16.050 [run-main-0] DEBUG ru.yandex.clickhouse.ClickHouseStatementImpl - Request url: http://***:8123/?extremes=0&user=readonly&database=my_test&password=****&compress=1
13:48:16.105 [run-main-0] DEBUG scalikejdbc.StatementExecutor$$anon$1 - SQL execution completed

  [SQL Execution]
   SELECT * FROM log; (32 ms)

  [Stack Trace]
    ...
    example.Hello$.delayedEndpoint$example$Hello$1(Hello.scala:24)
    example.Hello$delayedInit$body.apply(Hello.scala:5)
    scala.Function0.apply$mcV$sp(Function0.scala:34)
    scala.Function0.apply$mcV$sp$(Function0.scala:34)
    scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    scala.App.$anonfun$main$1$adapted(App.scala:76)
    scala.collection.immutable.List.foreach(List.scala:389)
    scala.App.main(App.scala:76)
    scala.App.main$(App.scala:74)
    example.Hello$.main(Hello.scala:5)
    example.Hello.main(Hello.scala)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:498)
    ...

List(foo, foo, bar)
[success] Total time: 8 s, completed 2018/07/29 13:48:16
```

データが取得できましたね

ログから裏でhttp-interfaceが叩かれているのがわかります
見ての通り(*でぼかしてますが)パスワードが平文（という以前にGETパラメータ）で送られているので十分に気をつけて運用しましょう :relaxed: