GitLab内のClickHouse

このドキュメントでは、GitLab RailsアプリケーションでClickHouseを使って機能を開発する方法のハイレベルな概要を説明します。

note
ほとんどのツールとAPIは不安定だと考えられています。

GDKのセットアップ

ClickHouseサーバーをローカルにセットアップする方法については、ClickHouseのインストールドキュメントを参照してください。

Railsアプリケーションの設定

  1. サンプルファイルをコピーして、認証情報を設定します:

    cp config/click_house.yml.example
    config/click_house.yml
    
  2. clickhouse-client CLI ツールを使用してデータベースを作成します:

    clickhouse-client --password
    
    create database gitlab_clickhouse_development;
    

セットアップの検証

Railsコンソールを起動し、簡単なクエリを実行します:

ClickHouse::Client.select('SELECT 1', :main)
# => [{"1"=>1}]

データベーススキーマとマイグレーション

ClickHouse データベースについては、確立されたスキーママイグレーション手順はまだありません。私たちは、タイムスタンプ接頭辞付きの SQL ファイルを使用して、テスト環境のデータベーススキーマをスクラッチから構築するための非常に基本的なツールを用意しています。

db/click_house/main フォルダに新しい SQL ファイルを置くことで、テーブルを作成することができます:

// 20230811124511_create_issues.sql
CREATE TABLE issues
(
  id UInt64 DEFAULT 0,
  title String DEFAULT ''
)
ENGINE = MergeTree
PRIMARY KEY (id)

開発環境の内部で作業している場合は、CREATE TABLE ステートメントを実行することで、テーブルスキーマを作成または再作成できます。あるいは、Railsコンソールで次のスニペットを使うこともできます:

require_relative 'spec/support/database/click_house/hooks.rb'

# Drops and re-creates all tables
ClickHouseTestRunner.new.ensure_schema

データベースクエリの作成

ClickHouse データベースでは ORM (Object Relational Mapping) を使いません。主な理由は、GitLab アプリケーションにはActiveRecord PostgresSQL アダプタ用に多くのカスタマイズがあり、アプリケーションは一般的にすべてのデータベースがPostgreSQL を使っていると想定しているからです。ClickHouse関連の機能はまだ開発の初期段階にあるため、複数のActiveRecord アダプタを扱う際にバグを発見するのが難しく、デバッグに時間がかかるのを避けるため、シンプルなHTTPクライアントを実装することにしました。

さらに、ClickHouse はActiveRecord の他のアダプタと同じようには使えないかもしれません。アクセスパターンは従来のトランザクション・データベースとは異なり、ClickHouse:

  • ネストされた集約SELECT GROUP BY 節を持つクエリを使用します。
  • 単一のINSERT ステートメントを使用しません。データはバックグラウンド・ジョブで一括挿入されます。
  • 異なる一貫性特性があり、トランザクションはありません。
  • データベースレベルの検証はほとんどありません。

データベースクエリはClickHouse::Client gem を使って記述・実行します。

events テーブルからのシンプルなクエリ:

rows = ClickHouse::Client.select('SELECT * FROM events', :main)

プレースホルダを含むクエリを使用する場合、ClickHouse::Query オブジェクトを使用することができます。実際の変数の置換、クォート、エスケープはClickHouseサーバーが行います。

raw_query = 'SELECT * FROM events WHERE id > {min_id:UInt64}'
placeholders = { min_id: Integer(100) }
query = ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)

rows = ClickHouse::Client.select(query, :main)

プレースホルダを使用する場合、クライアントはクエリに冗長化されたプレースホルダの値を指定することができます。to_redacted_sql メソッドを呼び出すことで、クエリの冗長化されたバージョンを見ることができます:

puts query.to_redacted_sql

ClickHouseでは、1つのリクエストにつき1つのステートメントしか許可しません。これは、ステートメントが; 文字で閉じられ、その後別のクエリが “注入” されるという、一般的な SQL インジェクション脆弱性を悪用できないことを意味します:

ClickHouse::Client.select('SELECT 1; SELECT 2', :main)

# ClickHouse::Client::DatabaseError: Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed): failed at position 9 (end of query): ; SELECT 2. . (SYNTAX_ERROR) (version 23.4.2.11 (official build))

サブクエリ

クエリ・プレースホルダを特別なSubquery 型で指定することで、ClickHouse::Client::Query クラスで複雑なクエリを構成することができます。ライブラリはクエリとプレースホルダを正しくマージします:

subquery = ClickHouse::Client::Query.new(raw_query: 'SELECT id FROM events WHERE id = {id:UInt64}', placeholders: { id: Integer(10) })

raw_query = 'SELECT * FROM events WHERE id > {id:UInt64} AND id IN ({q:Subquery})'
placeholders = { id: Integer(10), q: subquery }

query = ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
rows = ClickHouse::Client.select(query, :main)

# ClickHouse will replace the placeholders
puts query.to_sql # SELECT * FROM events WHERE id > {id:UInt64} AND id IN (SELECT id FROM events WHERE id = {id:UInt64})

puts query.to_redacted_sql # SELECT * FROM events WHERE id > $1 AND id IN (SELECT id FROM events WHERE id = $2)

puts query.placeholders # { id: 10 }

同じ名前で異なる値を持つプレースホルダが存在する場合、クエリはエラーを発生させます。

クエリ条件の記述

複数のフィルタ条件が存在するような複雑なフォームを扱う場合、 クエリの断片を文字列として連結してクエリを作成すると、 すぐに手が付けられなくなります。複数の条件を含むクエリには、ClickHouse::QueryBuilder クラスを使うことができます。このクラスはArel gem を使ってクエリを生成し、ActiveRecord のようなクエリインターフェイスを提供します。

builder = ClickHouse::QueryBuilder.new('events')

query = builder
  .where(builder.table[:created_at].lteq(Date.today))
  .where(id: [1,2,3])

rows = ClickHouse::Client.select(query, :main)

テスト

ClickHouseはCI/CDで有効ですが、パイプラインのランタイムに大きな影響を与えないよう、:click_house タグが付けられたテストケースのみClickHouseサーバーを実行することにしました。

:click_house タグは、すべてのテストケースの前にデータベーススキーマが適切に設定されていることを保証します。

RSpec.describe MyClickHouseFeature, :click_house do
  it 'returns rows' do
    rows = ClickHouse::Client.select('SELECT 1', :main)
    expect(rows.size).to eq(1)
  end
end

複数のデータベース

設計上、ClickHouse::Client ライブラリは複数のデータベースの設定をサポートしています。私たちはまだ開発の初期段階にあるため、main というデータベースしか持っていません。

複数データベースの設定例:

development:
  main:
    database: gitlab_clickhouse_main_development
    url: 'http://localhost:8123'
    username: clickhouse
    password: clickhouse

  user_analytics: # made up database
    database: gitlab_clickhouse_user_analytics_development
    url: 'http://localhost:8123'
    username: clickhouse
    password: clickhouse

観測可能性

ClickHouse::Client ライブラリを介して実行されるすべてのクエリは、ActiveSupport::Notifications を介してクエリのパフォーマンスメトリクス(タイミング、リードバイト)を公開します。

ActiveSupport::Notifications.subscribe('sql.click_house') do |_, _, _, _, data|
  puts data.inspect
end

さらに、Webインタラクションで実行されたClickHouseクエリを表示するには、パフォーマンスバーのch ラベルの横にあるカウントを選択します。