elasticsearch-sql

Elasticsearch SQL 包括的技術ガイド

目次

  1. はじめに
  2. アーキテクチャ -- SQL から Elasticsearch クエリへの変換
  3. アクセス方法
  4. SQL 構文の詳細
  5. 全文検索との統合
  6. SQL 関数リファレンス
  7. HISTOGRAM による日付集約
  8. _sql/translate API
  9. レスポンスフォーマット
  10. 実践的なユースケース
  11. 制限事項
  12. パフォーマンスとベストプラクティス
  13. まとめ -- 他のクエリ言語との比較

1. はじめに

1.1 Elasticsearch SQL とは

Elasticsearch SQL は、Elasticsearch に対して標準的な SQL 構文でクエリを実行するための機能である。Elasticsearch 6.3 で導入され、X-Pack(現在は Basic ライセンス)の一部として提供されている。リレーショナルデータベースに慣れ親しんだ開発者やアナリストが、Query DSL を習得することなく Elasticsearch のデータにアクセスできるようにすることを目的としている。

1.2 Elasticsearch のクエリ言語体系

Elasticsearch には現在、以下の主要なクエリ言語が存在する。

クエリ言語導入バージョン主な用途対象ユーザー
Query DSL初期汎用検索・集約開発者
Elasticsearch SQL6.3SQL ベースの分析クエリSQL 経験者・アナリスト
EQL (Event Query Language)7.9セキュリティイベント分析セキュリティアナリスト
**ESQL**8.11パイプベースの分析クエリ

Elasticsearch SQL は、SQL の知識を活かしてデータ探索や集約分析を行うための中間層として位置づけられている。内部的にはすべて Query DSL に変換されて実行されるため、パフォーマンス特性は Query DSL と本質的に同一である。

1.3 なぜ Elasticsearch SQL を使うのか

  • 学習コストの低減: SQL は最も広く知られたクエリ言語であり、新規メンバーのオンボーディングが容易になる
  • BI ツールとの統合: JDBC/ODBC ドライバを通じて Tableau や Power BI などのツールと連携可能
  • アドホック分析: 複雑な Query DSL を記述せずに迅速にデータ分析を実行可能
  • 段階的な移行: 既存の SQL スキルを活用しながら Elasticsearch の機能を学ぶことができる

2. アーキテクチャ -- SQL から Elasticsearch クエリへの変換

2.1 変換パイプライン

Elasticsearch SQL の内部処理は以下の段階で実行される。

SQL クエリ
    |
    v
[1. パーサー (SQL Parser)]
    |  SQL 文を抽象構文木 (AST) に変換
    v
[2. アナライザー (Analyzer)]
    |  スキーマ検証、型チェック
    v
[3. オプティマイザー (Optimizer)]
    |  クエリの最適化(定数畳み込み、述語プッシュダウン等)
    v
[4. プランナー (Planner)]
    |  論理プランから物理プランへの変換
    v
[5. 実行エンジン (Execution Engine)]
    |  Query DSL の生成と実行
    v
Elasticsearch クエリ結果

2.2 SQL と Query DSL の対応関係

以下に SQL 句と対応する Query DSL の構造を示す。

SQL 句Query DSL 要素
SELECT_source, fields, docvalue_fields
WHEREquery (bool, term, range など)
GROUP BYaggs (terms, date_histogram など)
HAVINGaggs 内の bucket_selector
ORDER BYsort
LIMITsize

2.3 変換の具体例

以下の SQL クエリが内部でどのように変換されるかを確認する。

SQL クエリ:

SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
WHERE status = 'active' AND hire_date >= '2024-01-01'
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY avg_salary DESC
LIMIT 10

変換後の Query DSL:

{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "status": "active"
          }
        },
        {
          "range": {
            "hire_date": {
              "gte": "2024-01-01",
              "format": "yyyy-MM-dd"
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "groupby": {
      "composite": {
        "size": 10,
        "sources": [
          {
            "department": {
              "terms": {
                "field": "department.keyword"
              }
            }
          }
        ]
      },
      "aggs": {
        "cnt": {
          "value_count": {
            "field": "_index"
          }
        },
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        },
        "having": {
          "bucket_selector": {
            "buckets_path": {
              "cnt_val": "cnt"
            },
            "script": {
              "source": "params.cnt_val > 5"
            }
          }
        }
      }
    }
  }
}

3. アクセス方法

3.1 REST API

最も基本的なアクセス方法であり、_sql エンドポイントを使用する。

基本リクエスト:

POST /_sql?format=txt
{
  "query": "SELECT * FROM employees WHERE department = 'Engineering' LIMIT 5"
}

パラメータ付きリクエスト:

POST /_sql?format=json
{
  "query": "SELECT * FROM employees WHERE department = ? AND salary > ?",
  "params": ["Engineering", 80000],
  "fetch_size": 100,
  "time_zone": "Asia/Tokyo",
  "field_multi_value_leniency": true
}

利用可能なリクエストパラメータ:

パラメータ説明デフォルト値
query実行する SQL 文(必須)
paramsプリペアドステートメントのパラメータ[]
fetch_size1回のレスポンスで返す行数1000
time_zoneタイムゾーンUTC
field_multi_value_leniency複数値フィールドの寛容モードfalse
request_timeoutタイムアウト90s
page_timeoutページネーションのタイムアウト45s
columnarカラムナーレスポンス形式false

3.2 SQL CLI

Elasticsearch に付属するコマンドラインインターフェースを使用する。

# CLI の起動
./bin/elasticsearch-sql-cli https://localhost:9200

# 認証付き
./bin/elasticsearch-sql-cli https://elastic:password@localhost:9200

# SSL/TLS 証明書指定
./bin/elasticsearch-sql-cli --keystore_location /path/to/keystore.p12 \
  https://localhost:9200

CLI 内での操作例:

sql> SELECT department, COUNT(*) FROM employees GROUP BY department;

  department   | COUNT(*)
---------------+----------
Engineering    | 142
Marketing      | 87
Sales          | 65
HR             | 32

sql> DESCRIBE employees;

     column      |    type    | mapping
-----------------+-----------+---------
department       | VARCHAR   | keyword
employee_id      | INTEGER   | integer
hire_date        | TIMESTAMP | date
name             | VARCHAR   | text
salary           | DOUBLE    | double
status           | VARCHAR   | keyword

3.3 JDBC ドライバ

Java アプリケーションや JDBC 対応ツールから接続する場合に使用する。

Maven 依存関係:

<dependency>
  <groupId>org.elasticsearch.plugin</groupId>
  <artifactId>x-pack-sql-jdbc</artifactId>
  <version>8.13.0</version>
</dependency>

Java コード例:

import java.sql.*;
import java.util.Properties;

public class ElasticsearchSQLExample {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:es://https://localhost:9200";
        
        Properties props = new Properties();
        props.put("user", "elastic");
        props.put("password", "changeme");
        props.put("ssl", "true");
        props.put("ssl.truststore.location", "/path/to/truststore.p12");
        props.put("ssl.truststore.pass", "truststore_password");
        
        try (Connection conn = DriverManager.getConnection(url, props)) {
            // プリペアドステートメント
            String sql = "SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_sal " +
                         "FROM employees WHERE status = ? GROUP BY department";
            
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, "active");
                
                try (ResultSet rs = pstmt.executeQuery()) {
                    ResultSetMetaData meta = rs.getMetaData();
                    int colCount = meta.getColumnCount();
                    
                    // カラム名の出力
                    for (int i = 1; i <= colCount; i++) {
                        System.out.printf("%-20s", meta.getColumnName(i));
                    }
                    System.out.println();
                    
                    // データの出力
                    while (rs.next()) {
                        for (int i = 1; i <= colCount; i++) {
                            System.out.printf("%-20s", rs.getObject(i));
                        }
                        System.out.println();
                    }
                }
            }
        }
    }
}

JDBC URL パラメータ:

パラメータ説明
timezoneタイムゾーン設定
connect.timeout接続タイムアウト(ms)
network.timeoutネットワークタイムアウト(ms)
page.timeoutページネーションタイムアウト
page.sizeフェッチサイズ
query.timeoutクエリタイムアウト
sslSSL 有効化
ssl.truststore.locationトラストストアのパス

3.4 ODBC ドライバ

Windows 環境において、ODBC 対応アプリケーション(Excel、Power BI Desktop など)から接続する場合に使用する。

# DSN 設定例 (odbc.ini)
[ElasticsearchDSN]
Driver      = Elasticsearch ODBC
Description = Elasticsearch SQL ODBC
Host        = localhost
Port        = 9200
Secure      = 1
User        = elastic
Password    = changeme

4. SQL 構文の詳細

4.1 SELECT

基本的な SELECT 文とそのバリエーションを示す。

-- 全カラム取得
SELECT * FROM employees LIMIT 10;

-- 特定カラムの選択
SELECT employee_id, name, department, salary FROM employees;

-- エイリアス
SELECT 
  employee_id AS id,
  name AS employee_name,
  salary * 12 AS annual_salary
FROM employees;

-- DISTINCT
SELECT DISTINCT department FROM employees;

-- スカラー関数の使用
SELECT 
  name,
  UPPER(department) AS dept_upper,
  YEAR(hire_date) AS hire_year,
  ROUND(salary, -3) AS salary_rounded
FROM employees;

4.2 WHERE

WHERE 句では様々な条件式を使用できる。

-- 比較演算子
SELECT * FROM employees WHERE salary > 80000;
SELECT * FROM employees WHERE department = 'Engineering';
SELECT * FROM employees WHERE hire_date >= '2024-01-01';

-- 論理演算子
SELECT * FROM employees 
WHERE department = 'Engineering' 
  AND salary > 80000 
  AND status = 'active';

SELECT * FROM employees 
WHERE department = 'Engineering' 
  OR department = 'Marketing';

-- IN
SELECT * FROM employees 
WHERE department IN ('Engineering', 'Marketing', 'Sales');

-- BETWEEN
SELECT * FROM employees 
WHERE salary BETWEEN 60000 AND 100000;

SELECT * FROM employees 
WHERE hire_date BETWEEN '2024-01-01' AND '2024-12-31';

-- LIKE (パターンマッチング)
SELECT * FROM employees WHERE name LIKE 'John%';
SELECT * FROM employees WHERE name LIKE '%smith';
SELECT * FROM employees WHERE name LIKE '_ohn%';

-- IS NULL / IS NOT NULL
SELECT * FROM employees WHERE manager_id IS NULL;
SELECT * FROM employees WHERE department IS NOT NULL;

-- NOT
SELECT * FROM employees 
WHERE NOT department = 'Engineering';

4.3 GROUP BY

集約クエリには GROUP BY を使用する。

-- 基本的な集約
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department;

-- 複数の集約関数
SELECT 
  department,
  COUNT(*) AS cnt,
  MIN(salary) AS min_salary,
  MAX(salary) AS max_salary,
  AVG(salary) AS avg_salary,
  SUM(salary) AS total_salary
FROM employees
GROUP BY department;

-- 複数カラムによるグループ化
SELECT 
  department,
  status,
  COUNT(*) AS cnt,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY department, status;

-- 式によるグループ化
SELECT 
  YEAR(hire_date) AS hire_year,
  COUNT(*) AS cnt
FROM employees
GROUP BY YEAR(hire_date)
ORDER BY hire_year;

4.4 HAVING

GROUP BY の結果をフィルタリングする場合に HAVING を使用する。

-- 基本的な HAVING
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
HAVING COUNT(*) > 10;

-- 複数条件の HAVING
SELECT 
  department,
  COUNT(*) AS cnt,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) > 5 AND AVG(salary) > 70000;

-- WHERE と HAVING の組み合わせ
SELECT 
  department,
  COUNT(*) AS cnt,
  AVG(salary) AS avg_salary
FROM employees
WHERE status = 'active'
GROUP BY department
HAVING COUNT(*) >= 3
ORDER BY avg_salary DESC;

4.5 ORDER BY

-- 昇順(デフォルト)
SELECT * FROM employees ORDER BY salary ASC;

-- 降順
SELECT * FROM employees ORDER BY salary DESC;

-- 複数カラムによるソート
SELECT * FROM employees 
ORDER BY department ASC, salary DESC;

-- NULL の扱い
SELECT * FROM employees 
ORDER BY manager_id ASC NULLS LAST;

SELECT * FROM employees 
ORDER BY manager_id DESC NULLS FIRST;

4.6 LIMIT

-- 上位 N 件の取得
SELECT * FROM employees ORDER BY salary DESC LIMIT 10;

-- 集約結果の制限
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC
LIMIT 5;

4.7 SHOW と DESCRIBE

-- テーブル(インデックス)一覧
SHOW TABLES;

-- パターンマッチによるテーブル一覧
SHOW TABLES LIKE 'log%';
SHOW TABLES 'employee*';

-- テーブル構造の確認
DESCRIBE employees;
DESC employees;

-- SQL 関数一覧
SHOW FUNCTIONS;

-- パターンによる関数検索
SHOW FUNCTIONS LIKE 'AVG';
SHOW FUNCTIONS LIKE '%DATE%';

5. 全文検索との統合

Elasticsearch SQL の大きな特徴の一つは、標準 SQL では利用できない全文検索機能を SQL 構文内から利用できることである。

5.1 MATCH 関数

MATCH 関数は Elasticsearch の match クエリに対応し、全文検索を実行する。

-- 基本的な MATCH
SELECT employee_id, name, department
FROM employees
WHERE MATCH(name, 'John Smith');

-- 複数フィールドに対する MATCH
SELECT title, description, category
FROM articles
WHERE MATCH('title,description', 'elasticsearch performance tuning');

-- フィールドブースト
SELECT title, description
FROM articles
WHERE MATCH('title^3,description', 'elasticsearch sql');

-- analyzer 指定
SELECT title, content
FROM articles
WHERE MATCH(title, 'データベース', 'analyzer=kuromoji');

-- MATCH と他の条件の組み合わせ
SELECT title, author, published_date, SCORE()
FROM articles
WHERE MATCH(title, 'elasticsearch') 
  AND published_date >= '2024-01-01'
  AND category = 'technology'
ORDER BY SCORE() DESC
LIMIT 20;

5.2 QUERY 関数

QUERY 関数は Elasticsearch の query_string クエリに対応し、Lucene クエリ構文を使用できる。

-- 基本的な QUERY
SELECT * FROM articles 
WHERE QUERY('title:elasticsearch AND content:performance');

-- ワイルドカード
SELECT * FROM articles 
WHERE QUERY('title:elast*');

-- ファジー検索
SELECT * FROM articles 
WHERE QUERY('title:elastisearch~2');

-- フレーズ検索
SELECT * FROM articles 
WHERE QUERY('title:"elasticsearch sql"');

-- 範囲検索との組み合わせ
SELECT * FROM articles 
WHERE QUERY('title:elasticsearch AND date:[2024-01-01 TO 2024-12-31]');

-- ブール演算
SELECT * FROM articles 
WHERE QUERY('(title:elasticsearch OR title:opensearch) AND NOT status:draft');

5.3 SCORE 関数

SCORE() 関数は、全文検索の関連性スコア(_score)を取得する。

-- スコア付きの全文検索
SELECT 
  title,
  author,
  SCORE() AS relevance_score
FROM articles
WHERE MATCH(title, 'elasticsearch sql tutorial')
ORDER BY SCORE() DESC;

-- スコア閾値によるフィルタリング
SELECT 
  title,
  SCORE() AS score
FROM articles
WHERE MATCH(title, 'elasticsearch') AND SCORE() > 2.0
ORDER BY SCORE() DESC;

-- スコアを使った複合ソート
SELECT 
  title,
  published_date,
  SCORE() AS score
FROM articles
WHERE MATCH(title, 'elasticsearch')
ORDER BY SCORE() DESC, published_date DESC
LIMIT 50;

6. SQL 関数リファレンス

6.1 集約関数

関数説明使用例
AVG(expr)平均値AVG(salary)
COUNT(expr)件数COUNT(*), COUNT(DISTINCT dept)
SUM(expr)合計SUM(salary)
MIN(expr)最小値MIN(hire_date)
MAX(expr)最大値MAX(salary)
PERCENTILE(expr, pct)パーセンタイルPERCENTILE(salary, 95)
PERCENTILE_RANK(expr, val)パーセンタイルランクPERCENTILE_RANK(salary, 80000)
STDDEV_POP(expr)母標準偏差STDDEV_POP(salary)
STDDEV_SAMP(expr)標本標準偏差STDDEV_SAMP(salary)
VAR_POP(expr)母分散VAR_POP(salary)
VAR_SAMP(expr)標本分散VAR_SAMP(salary)
KURTOSIS(expr)尖度KURTOSIS(salary)
SKEWNESS(expr)歪度SKEWNESS(salary)
SUM_OF_SQUARES(expr)平方和SUM_OF_SQUARES(salary)
MAD(expr)中央絶対偏差MAD(salary)
-- 統計サマリーの例
SELECT 
  department,
  COUNT(*) AS cnt,
  ROUND(AVG(salary), 2) AS avg_salary,
  MIN(salary) AS min_salary,
  MAX(salary) AS max_salary,
  ROUND(STDDEV_POP(salary), 2) AS stddev_salary,
  ROUND(PERCENTILE(salary, 50), 2) AS median_salary,
  ROUND(PERCENTILE(salary, 90), 2) AS p90_salary
FROM employees
WHERE status = 'active'
GROUP BY department
ORDER BY avg_salary DESC;

6.2 文字列関数

関数説明使用例
ASCII(str)ASCII コード値ASCII('A') => 65
BIT_LENGTH(str)ビット長BIT_LENGTH('hello') => 40
CHAR(n)ASCII コードから文字CHAR(65) => 'A'
CHAR_LENGTH(str)文字長CHAR_LENGTH('hello') => 5
CONCAT(str1, str2)文字列結合CONCAT('a', 'b') => 'ab'
INSERT(str, pos, len, newstr)文字列挿入INSERT('abc', 2, 1, 'XY')
LCASE(str) / LOWER(str)小文字変換LOWER('ABC') => 'abc'
UCASE(str) / UPPER(str)大文字変換UPPER('abc') => 'ABC'
LEFT(str, n)左から n 文字LEFT('hello', 3) => 'hel'
RIGHT(str, n)右から n 文字RIGHT('hello', 3) => 'llo'
LENGTH(str)バイト長LENGTH('hello') => 5
LOCATE(substr, str)部分文字列位置LOCATE('ll', 'hello') => 3
LTRIM(str)左トリムLTRIM(' hello')
RTRIM(str)右トリムRTRIM('hello ')
TRIM(str)両端トリムTRIM(' hello ')
REPEAT(str, n)文字列繰り返しREPEAT('ab', 3) => 'ababab'
REPLACE(str, from, to)文字列置換REPLACE('abc', 'b', 'X')
REVERSE(str)文字列反転REVERSE('hello') => 'olleh'
SPACE(n)スペース文字列SPACE(5) => ' '
SUBSTRING(str, pos, len)部分文字列SUBSTRING('hello', 2, 3) => 'ell'
POSITION(substr, str)位置POSITION('ll', 'hello') => 3
STARTS_WITH(str, prefix)プレフィックス判定STARTS_WITH('hello', 'he')
-- 文字列関数の実用例
SELECT 
  employee_id,
  CONCAT(UPPER(LEFT(name, 1)), LOWER(SUBSTRING(name, 2))) AS formatted_name,
  CHAR_LENGTH(name) AS name_length,
  REPLACE(department, 'Engineering', 'Eng') AS dept_short
FROM employees
WHERE STARTS_WITH(name, 'J')
LIMIT 20;

6.3 日付・時刻関数

関数説明使用例
CURRENT_DATE / CURDATE()現在の日付CURRENT_DATE
CURRENT_TIME / CURTIME()現在の時刻CURRENT_TIME
CURRENT_TIMESTAMP / NOW()現在のタイムスタンプNOW()
YEAR(date)YEAR(hire_date)
MONTH(date)MONTH(hire_date)
DAY(date) / DOM(date)DAY(hire_date)
HOUR(date)HOUR(timestamp_field)
MINUTE(date)MINUTE(timestamp_field)
SECOND(date)SECOND(timestamp_field)
DAY_OF_WEEK(date) / DOW(date)曜日(1-7)DOW(hire_date)
DAY_OF_YEAR(date) / DOY(date)年内日数DOY(hire_date)
WEEK(date) / ISO_WEEK(date)週番号WEEK(hire_date)
QUARTER(date)四半期QUARTER(hire_date)
DAY_NAME(date)曜日名DAY_NAME(hire_date)
MONTH_NAME(date)月名MONTH_NAME(hire_date)
DATE_ADD(date, interval)日付加算DATE_ADD('date', INTERVAL 1 DAY)
DATE_DIFF(unit, d1, d2)日付差分DATE_DIFF('day', d1, d2)
DATE_TRUNC(unit, date)日付切り捨てDATE_TRUNC('month', hire_date)
DATE_PARSE(str, fmt)文字列パースDATE_PARSE('2024-01-15', 'yyyy-MM-dd')
DATE_FORMAT(date, fmt)フォーマットDATE_FORMAT(hire_date, 'yyyy/MM')
-- 日付関数の実用例
SELECT 
  employee_id,
  name,
  hire_date,
  YEAR(hire_date) AS hire_year,
  MONTH_NAME(hire_date) AS hire_month,
  DATE_DIFF('day', hire_date, NOW()) AS days_employed,
  DATE_DIFF('year', hire_date, NOW()) AS years_employed,
  DATE_TRUNC('month', hire_date) AS hire_month_start,
  QUARTER(hire_date) AS hire_quarter
FROM employees
WHERE hire_date >= DATE_ADD(NOW(), INTERVAL -5 YEAR)
ORDER BY hire_date DESC
LIMIT 20;

6.4 数学関数

関数説明使用例
ABS(n)絶対値ABS(-5) => 5
CEIL(n) / CEILING(n)切り上げCEIL(4.3) => 5
FLOOR(n)切り捨てFLOOR(4.7) => 4
ROUND(n, d)四捨五入ROUND(3.14159, 2) => 3.14
TRUNCATE(n, d)切り捨て(精度指定)TRUNCATE(3.14159, 2) => 3.14
POWER(base, exp)累乗POWER(2, 10) => 1024
SQRT(n)平方根SQRT(144) => 12
CBRT(n)立方根CBRT(27) => 3
E()ネイピア数E() => 2.718...
PI()円周率PI() => 3.14159...
EXP(n)e の n 乗EXP(1) => 2.718...
LOG(n)自然対数LOG(E()) => 1
LOG10(n)常用対数LOG10(100) => 2
MOD(a, b)剰余MOD(10, 3) => 1
SIGN(n)符号SIGN(-5) => -1
RANDOM(seed)乱数RANDOM(42)
-- 数学関数の実用例
SELECT 
  department,
  ROUND(AVG(salary), 2) AS avg_salary,
  ROUND(STDDEV_POP(salary), 2) AS stddev,
  ROUND(AVG(salary) - 2 * STDDEV_POP(salary), 2) AS lower_bound,
  ROUND(AVG(salary) + 2 * STDDEV_POP(salary), 2) AS upper_bound,
  ROUND(LOG10(SUM(salary)), 4) AS log_total
FROM employees
GROUP BY department;

6.5 型変換関数

-- CAST による型変換
SELECT CAST(salary AS VARCHAR) AS salary_text FROM employees;
SELECT CAST('2024-01-15' AS DATE) AS date_val;
SELECT CAST(employee_id AS VARCHAR) AS id_text FROM employees;
SELECT CAST('12345' AS INTEGER) AS num_val;

-- CONVERT
SELECT CONVERT(salary, VARCHAR) AS salary_text FROM employees;

-- 暗黙の型変換の例
SELECT * FROM employees WHERE salary > '80000';  -- 文字列が自動的に数値に変換

6.6 条件式

-- CASE WHEN
SELECT 
  name,
  salary,
  CASE
    WHEN salary >= 120000 THEN 'Executive'
    WHEN salary >= 90000 THEN 'Senior'
    WHEN salary >= 60000 THEN 'Mid-level'
    ELSE 'Junior'
  END AS salary_band
FROM employees;

-- CASE WHEN を集約と組み合わせ
SELECT 
  department,
  COUNT(*) AS total,
  SUM(CASE WHEN salary >= 90000 THEN 1 ELSE 0 END) AS high_earners,
  SUM(CASE WHEN salary < 60000 THEN 1 ELSE 0 END) AS low_earners
FROM employees
GROUP BY department;

-- IIF (Inline If)
SELECT 
  name,
  salary,
  IIF(salary > 80000, 'High', 'Normal') AS salary_level
FROM employees;

-- COALESCE
SELECT 
  name,
  COALESCE(manager_id, 0) AS manager_id,
  COALESCE(department, 'Unassigned') AS department
FROM employees;

-- NULLIF
SELECT 
  name,
  NULLIF(department, 'Unknown') AS department
FROM employees;

-- IFNULL
SELECT 
  name,
  IFNULL(manager_id, -1) AS manager_id
FROM employees;

-- GREATEST / LEAST
SELECT 
  name,
  GREATEST(base_salary, bonus_salary) AS higher_pay,
  LEAST(base_salary, bonus_salary) AS lower_pay
FROM compensation;

7. HISTOGRAM による日付集約

7.1 HISTOGRAM 関数の概要

Elasticsearch SQL の HISTOGRAM 関数は、Elasticsearch の date_histogram 集約に対応し、GROUP BY 句で使用することで時系列データの集約を実現する。

-- 月次集約
SELECT 
  HISTOGRAM(order_date, INTERVAL 1 MONTH) AS month,
  COUNT(*) AS order_count,
  SUM(amount) AS total_amount,
  AVG(amount) AS avg_amount
FROM orders
GROUP BY HISTOGRAM(order_date, INTERVAL 1 MONTH)
ORDER BY month;

7.2 様々な間隔での集約

-- 日次集約
SELECT 
  HISTOGRAM(timestamp, INTERVAL 1 DAY) AS day,
  COUNT(*) AS event_count
FROM access_logs
WHERE timestamp >= '2024-01-01' AND timestamp < '2024-02-01'
GROUP BY HISTOGRAM(timestamp, INTERVAL 1 DAY)
ORDER BY day;

-- 時間単位の集約
SELECT 
  HISTOGRAM(timestamp, INTERVAL 1 HOUR) AS hour,
  COUNT(*) AS request_count,
  AVG(response_time) AS avg_response_time
FROM access_logs
WHERE timestamp >= '2024-01-15' AND timestamp < '2024-01-16'
GROUP BY HISTOGRAM(timestamp, INTERVAL 1 HOUR)
ORDER BY hour;

-- 週次集約
SELECT 
  HISTOGRAM(created_at, INTERVAL 1 WEEK) AS week,
  COUNT(*) AS ticket_count,
  SUM(CASE WHEN status = 'resolved' THEN 1 ELSE 0 END) AS resolved_count
FROM support_tickets
WHERE created_at >= '2024-01-01'
GROUP BY HISTOGRAM(created_at, INTERVAL 1 WEEK)
ORDER BY week;

-- 四半期集約
SELECT 
  HISTOGRAM(transaction_date, INTERVAL 3 MONTH) AS quarter,
  COUNT(*) AS transaction_count,
  SUM(amount) AS total_revenue,
  COUNT(DISTINCT customer_id) AS unique_customers
FROM transactions
WHERE transaction_date >= '2023-01-01'
GROUP BY HISTOGRAM(transaction_date, INTERVAL 3 MONTH)
ORDER BY quarter;

7.3 数値の HISTOGRAM

日付以外にも、数値フィールドに対して HISTOGRAM を使用できる。

-- 給与帯別の分布
SELECT 
  HISTOGRAM(salary, 10000) AS salary_band,
  COUNT(*) AS employee_count
FROM employees
GROUP BY HISTOGRAM(salary, 10000)
ORDER BY salary_band;

-- レスポンスタイム分布
SELECT 
  HISTOGRAM(response_time_ms, 100) AS response_bucket,
  COUNT(*) AS request_count,
  AVG(response_time_ms) AS avg_time
FROM access_logs
GROUP BY HISTOGRAM(response_time_ms, 100)
ORDER BY response_bucket;

7.4 HISTOGRAM と他の GROUP BY の組み合わせ

-- 日別・ステータス別のアクセスログ集約
SELECT 
  HISTOGRAM(timestamp, INTERVAL 1 DAY) AS day,
  status_code,
  COUNT(*) AS request_count
FROM access_logs
WHERE timestamp >= '2024-01-01' AND timestamp < '2024-01-08'
GROUP BY HISTOGRAM(timestamp, INTERVAL 1 DAY), status_code
ORDER BY day, status_code;

-- 月別・部門別の売上集約
SELECT 
  HISTOGRAM(order_date, INTERVAL 1 MONTH) AS month,
  department,
  SUM(amount) AS total_sales,
  COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY HISTOGRAM(order_date, INTERVAL 1 MONTH), department
ORDER BY month, department;

8. _sql/translate API

8.1 概要

_sql/translate API は、SQL クエリを対応する Query DSL に変換して表示する機能である。実際にクエリを実行することなく、生成される Query DSL を確認できる。デバッグ、最適化、学習目的で非常に有用である。

8.2 基本的な使い方

POST /_sql/translate
{
  "query": "SELECT department, COUNT(*) AS cnt FROM employees GROUP BY department"
}

レスポンス例:

{
  "size": 0,
  "query": {
    "match_all": {}
  },
  "aggs": {
    "groupby": {
      "composite": {
        "size": 1000,
        "sources": [
          {
            "department": {
              "terms": {
                "field": "department.keyword",
                "missing_bucket": true,
                "order": "asc"
              }
            }
          }
        ]
      },
      "aggs": {
        "cnt": {
          "value_count": {
            "field": "_index"
          }
        }
      }
    }
  }
}

8.3 WHERE 句の変換

POST /_sql/translate
{
  "query": "SELECT * FROM employees WHERE department = 'Engineering' AND salary > 80000 AND status IN ('active', 'probation')"
}

レスポンス:

{
  "size": 1000,
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "department.keyword": {
              "value": "Engineering"
            }
          }
        },
        {
          "range": {
            "salary": {
              "gt": 80000
            }
          }
        },
        {
          "terms": {
            "status.keyword": [
              "active",
              "probation"
            ]
          }
        }
      ]
    }
  },
  "_source": {
    "includes": [
      "department",
      "employee_id",
      "hire_date",
      "name",
      "salary",
      "status"
    ]
  },
  "sort": [
    {
      "_doc": {
        "order": "asc"
      }
    }
  ]
}

8.4 全文検索の変換

POST /_sql/translate
{
  "query": "SELECT title, SCORE() FROM articles WHERE MATCH(title, 'elasticsearch performance') ORDER BY SCORE() DESC"
}

レスポンス:

{
  "size": 1000,
  "query": {
    "match": {
      "title": {
        "query": "elasticsearch performance"
      }
    }
  },
  "_source": false,
  "fields": [
    {
      "field": "title"
    }
  ],
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    }
  ]
}

8.5 translate API の活用方法

# 1. SQL クエリを書く
# 2. translate API で Query DSL を確認
# 3. 必要に応じて Query DSL を直接カスタマイズ
# 4. 最適化されたクエリを本番で使用

# 例: translate の結果を jq で整形
curl -s -X POST "localhost:9200/_sql/translate" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT department, COUNT(*) FROM employees GROUP BY department HAVING COUNT(*) > 10"}' \
  | jq .

9. レスポンスフォーマット

9.1 利用可能なフォーマット

フォーマットContent-Type説明
txttext/plain人間が読みやすいテーブル形式
jsonapplication/json構造化された JSON
csvtext/csvカンマ区切り
tsvtext/tab-separated-valuesタブ区切り
yamlapplication/yamlYAML 形式
cborapplication/cborバイナリ CBOR
smileapplication/smileバイナリ Smile

9.2 各フォーマットの出力例

クエリ:

SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY cnt DESC
LIMIT 3

txt フォーマット (format=txt):

  department   |     cnt     |  avg_salary
---------------+-------------+-------------
Engineering    |142          |95234.56
Marketing      |87           |78456.12
Sales          |65           |72345.89

json フォーマット (format=json):

{
  "columns": [
    {"name": "department", "type": "keyword"},
    {"name": "cnt", "type": "long"},
    {"name": "avg_salary", "type": "double"}
  ],
  "rows": [
    ["Engineering", 142, 95234.56],
    ["Marketing", 87, 78456.12],
    ["Sales", 65, 72345.89]
  ]
}

csv フォーマット (format=csv):

department,cnt,avg_salary
Engineering,142,95234.56
Marketing,87,78456.12
Sales,65,72345.89

tsv フォーマット (format=tsv):

department	cnt	avg_salary
Engineering	142	95234.56
Marketing	87	78456.12
Sales	65	72345.89

yaml フォーマット (format=yaml):

columns:
- name: "department"
  type: "keyword"
- name: "cnt"
  type: "long"
- name: "avg_salary"
  type: "double"
rows:
- - "Engineering"
  - 142
  - 95234.56
- - "Marketing"
  - 87
  - 78456.12
- - "Sales"
  - 65
  - 72345.89

9.3 カラムナー(列指向)レスポンス

POST /_sql?format=json
{
  "query": "SELECT department, COUNT(*) AS cnt FROM employees GROUP BY department LIMIT 3",
  "columnar": true
}

レスポンス:

{
  "columns": [
    {"name": "department", "type": "keyword"},
    {"name": "cnt", "type": "long"}
  ],
  "values": [
    ["Engineering", "Marketing", "Sales"],
    [142, 87, 65]
  ]
}

9.4 ページネーション

大量のデータを取得する場合は、カーソルベースのページネーションを使用する。

# 最初のリクエスト
POST /_sql?format=json
{
  "query": "SELECT * FROM employees ORDER BY employee_id",
  "fetch_size": 100
}

# レスポンスにカーソルが含まれる
{
  "columns": [...],
  "rows": [...],
  "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWYUpOYklR..."
}

# 次のページの取得
POST /_sql?format=json
{
  "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWYUpOYklR..."
}

# カーソルのクリア
POST /_sql/close
{
  "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWYUpOYklR..."
}

10. 実践的なユースケース

10.1 BI ツール統合

Tableau との連携: JDBC ドライバを使用して Elasticsearch に接続し、ダッシュボードを作成する。

-- Tableau カスタム SQL の例: 月次売上レポート
SELECT 
  HISTOGRAM(order_date, INTERVAL 1 MONTH) AS month,
  product_category,
  COUNT(*) AS order_count,
  SUM(amount) AS total_revenue,
  AVG(amount) AS avg_order_value,
  COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE order_date >= '2023-01-01'
GROUP BY HISTOGRAM(order_date, INTERVAL 1 MONTH), product_category
ORDER BY month, product_category

Grafana との連携: Elasticsearch データソースで SQL クエリを使用する。

-- Grafana の時系列パネル用クエリ
SELECT 
  HISTOGRAM(timestamp, INTERVAL 5 MINUTE) AS time,
  AVG(cpu_usage) AS avg_cpu,
  MAX(cpu_usage) AS max_cpu,
  AVG(memory_usage) AS avg_memory
FROM server_metrics
WHERE timestamp >= '2024-01-15T00:00:00Z' 
  AND timestamp < '2024-01-16T00:00:00Z'
  AND host = 'web-server-01'
GROUP BY HISTOGRAM(timestamp, INTERVAL 5 MINUTE)
ORDER BY time

10.2 アドホック分析

-- ログ分析: エラー率の推移
SELECT 
  HISTOGRAM(timestamp, INTERVAL 1 HOUR) AS hour,
  COUNT(*) AS total_requests,
  SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END) AS error_count,
  ROUND(
    CAST(SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END) AS DOUBLE) 
    / COUNT(*) * 100, 2
  ) AS error_rate_pct
FROM access_logs
WHERE timestamp >= '2024-01-15' AND timestamp < '2024-01-16'
GROUP BY HISTOGRAM(timestamp, INTERVAL 1 HOUR)
ORDER BY hour;

-- ユーザー行動分析
SELECT 
  user_agent_family,
  COUNT(*) AS session_count,
  AVG(page_views) AS avg_page_views,
  AVG(session_duration_sec) AS avg_duration,
  ROUND(
    CAST(SUM(CASE WHEN converted = true THEN 1 ELSE 0 END) AS DOUBLE) 
    / COUNT(*) * 100, 2
  ) AS conversion_rate
FROM user_sessions
WHERE session_start >= '2024-01-01'
GROUP BY user_agent_family
HAVING COUNT(*) > 100
ORDER BY session_count DESC;

10.3 レポート生成

-- 日次レポート: API パフォーマンスサマリー
SELECT 
  api_endpoint,
  COUNT(*) AS total_calls,
  ROUND(AVG(response_time_ms), 2) AS avg_response_ms,
  ROUND(PERCENTILE(response_time_ms, 50), 2) AS p50_ms,
  ROUND(PERCENTILE(response_time_ms, 95), 2) AS p95_ms,
  ROUND(PERCENTILE(response_time_ms, 99), 2) AS p99_ms,
  MIN(response_time_ms) AS min_ms,
  MAX(response_time_ms) AS max_ms,
  SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) AS error_count
FROM api_logs
WHERE timestamp >= CURRENT_DATE - INTERVAL 1 DAY
  AND timestamp < CURRENT_DATE
GROUP BY api_endpoint
ORDER BY total_calls DESC;

-- 月次レポート: 部門別コスト分析
SELECT 
  department,
  HISTOGRAM(transaction_date, INTERVAL 1 MONTH) AS month,
  COUNT(*) AS transaction_count,
  SUM(amount) AS total_cost,
  AVG(amount) AS avg_transaction,
  MAX(amount) AS largest_transaction
FROM expense_reports
WHERE transaction_date >= '2024-01-01'
  AND status = 'approved'
GROUP BY department, HISTOGRAM(transaction_date, INTERVAL 1 MONTH)
ORDER BY department, month;

10.4 セキュリティ監視

-- ログイン失敗の監視
SELECT 
  source_ip,
  COUNT(*) AS failed_attempts,
  MIN(timestamp) AS first_attempt,
  MAX(timestamp) AS last_attempt,
  COUNT(DISTINCT target_user) AS targeted_users
FROM auth_logs
WHERE event_type = 'login_failed'
  AND timestamp >= NOW() - INTERVAL 1 HOUR
GROUP BY source_ip
HAVING COUNT(*) > 10
ORDER BY failed_attempts DESC;

-- 異常なデータアクセスパターンの検出
SELECT 
  user_id,
  HISTOGRAM(timestamp, INTERVAL 1 HOUR) AS hour,
  COUNT(*) AS access_count,
  COUNT(DISTINCT resource_id) AS unique_resources
FROM data_access_logs
WHERE timestamp >= CURRENT_DATE
GROUP BY user_id, HISTOGRAM(timestamp, INTERVAL 1 HOUR)
HAVING COUNT(*) > 1000
ORDER BY access_count DESC;

10.5 SRE / インフラ監視

-- ディスク使用率の傾向分析
SELECT 
  host,
  HISTOGRAM(timestamp, INTERVAL 1 DAY) AS day,
  ROUND(AVG(disk_usage_pct), 2) AS avg_disk_usage,
  ROUND(MAX(disk_usage_pct), 2) AS max_disk_usage
FROM system_metrics
WHERE timestamp >= NOW() - INTERVAL 30 DAY
  AND metric_type = 'disk'
GROUP BY host, HISTOGRAM(timestamp, INTERVAL 1 DAY)
HAVING MAX(disk_usage_pct) > 80
ORDER BY host, day;

-- サービスレベルの計算
SELECT 
  service_name,
  COUNT(*) AS total_requests,
  SUM(CASE WHEN response_time_ms <= 200 THEN 1 ELSE 0 END) AS fast_requests,
  ROUND(
    CAST(SUM(CASE WHEN response_time_ms <= 200 THEN 1 ELSE 0 END) AS DOUBLE)
    / COUNT(*) * 100, 4
  ) AS sli_pct
FROM request_logs
WHERE timestamp >= NOW() - INTERVAL 7 DAY
GROUP BY service_name
ORDER BY sli_pct ASC;

11. 制限事項

11.1 サポートされていない機能

Elasticsearch SQL には以下の制限がある。これらは Elasticsearch の基盤アーキテクチャに起因するものである。

機能サポート状況代替手段
JOIN非サポートアプリケーション側での結合、非正規化
サブクエリ限定的複数クエリに分割
INSERT / UPDATE / DELETE非サポートREST API (_doc, _bulk, _update_by_query)
CREATE TABLE / DROP TABLE非サポートIndex Management API
CREATE INDEX非サポートIndex API
ALTER TABLE非サポートMapping API
トランザクション非サポートElasticsearch はトランザクション非対応
ストアドプロシージャ非サポートPainless スクリプト
ビュー非サポートAlias, Data View
UNION / INTERSECT / EXCEPT非サポートMulti-search API
ウィンドウ関数非サポートQuery DSL + スクリプト
再帰 CTE非サポートアプリケーション側で実装

11.2 型マッピングの制限

Elasticsearch 型SQL 型注意事項
textVARCHAR集約・ソート不可(keyword サブフィールドが必要)
keywordVARCHAR全文検索不可
longBIGINT-
integerINTEGER-
doubleDOUBLE-
floatREAL-
dateTIMESTAMP-
booleanBOOLEAN-
nestedサポート外flatten 化が必要
objectドット記法address.city のようにアクセス
geo_pointGEO_POINT限定的なサポート

11.3 集約の制限

-- NG: ネストされた集約
-- 以下のようなクエリは直接サポートされない
-- SELECT department, (SELECT MAX(salary) FROM ...) FROM employees GROUP BY department;

-- OK: 代替方法
SELECT 
  department,
  MAX(salary) AS max_salary
FROM employees
GROUP BY department;

-- NG: GROUP BY で ALIAS を使用
-- SELECT YEAR(hire_date) AS yr, COUNT(*) FROM employees GROUP BY yr;

-- OK: 式を繰り返す
SELECT YEAR(hire_date) AS yr, COUNT(*) FROM employees GROUP BY YEAR(hire_date);

11.4 text フィールドの制限

-- text フィールドに対する操作の制限

-- NG: text フィールドでの GROUP BY(集約不可)
-- SELECT description, COUNT(*) FROM articles GROUP BY description;

-- OK: keyword サブフィールドを使用
SELECT description.keyword, COUNT(*) FROM articles GROUP BY description.keyword;

-- OK: text フィールドへの全文検索
SELECT * FROM articles WHERE MATCH(description, 'elasticsearch tutorial');

12. パフォーマンスとベストプラクティス

12.1 クエリ最適化

1. 必要なフィールドのみを SELECT する

-- 非推奨: 全フィールド取得
SELECT * FROM large_index LIMIT 10000;

-- 推奨: 必要なフィールドのみ
SELECT employee_id, name, department FROM employees LIMIT 10000;

2. WHERE 句で早期にフィルタリングする

-- 非推奨: 全データをスキャン後にフィルタ
SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING department = 'Engineering';

-- 推奨: WHERE で先にフィルタ
SELECT department, AVG(salary)
FROM employees
WHERE department = 'Engineering'
GROUP BY department;

3. 適切な fetch_size を設定する

POST /_sql?format=json
{
  "query": "SELECT * FROM large_index",
  "fetch_size": 500
}

4. 時間範囲を明示的に指定する

-- 非推奨: 時間範囲なし
SELECT * FROM access_logs WHERE status_code = 500;

-- 推奨: 時間範囲を指定
SELECT * FROM access_logs 
WHERE status_code = 500 
  AND timestamp >= '2024-01-15' 
  AND timestamp < '2024-01-16';

12.2 インデックス設計のベストプラクティス

{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "department": {
        "type": "keyword"
      },
      "salary": {
        "type": "double"
      },
      "hire_date": {
        "type": "date",
        "format": "yyyy-MM-dd||epoch_millis"
      },
      "description": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 512
          }
        }
      }
    }
  }
}

12.3 translate API によるクエリ検証

本番環境でクエリを実行する前に、translate API で生成される Query DSL を確認する。

# ステップ 1: SQL の作成と translate
POST /_sql/translate
{
  "query": "SELECT department, AVG(salary) FROM employees WHERE status = 'active' GROUP BY department"
}

# ステップ 2: 生成された Query DSL を確認し、最適化の余地がないか検討
# ステップ 3: 問題がなければ実際に実行
POST /_sql?format=json
{
  "query": "SELECT department, AVG(salary) FROM employees WHERE status = 'active' GROUP BY department"
}

12.4 パフォーマンス監視

-- 重いクエリの特定 (Slow Log と組み合わせて使用)
-- elasticsearch.yml での設定:
-- index.search.slowlog.threshold.query.warn: 10s
-- index.search.slowlog.threshold.query.info: 5s
-- index.search.slowlog.threshold.query.debug: 2s

-- インデックスサイズの確認
SHOW TABLES LIKE 'access_logs*';

12.5 セキュリティのベストプラクティス

# ロールベースのアクセス制御
# elasticsearch.yml でのセキュリティ設定例

# SQL 専用ロールの作成
POST /_security/role/sql_reader
{
  "indices": [
    {
      "names": ["employees", "departments"],
      "privileges": ["read"],
      "field_security": {
        "grant": ["employee_id", "name", "department", "hire_date"]
      }
    }
  ]
}
# パラメータ化クエリを使用して SQL インジェクションを防止
POST /_sql?format=json
{
  "query": "SELECT * FROM employees WHERE department = ? AND salary > ?",
  "params": ["Engineering", 80000]
}

13. まとめ -- 他のクエリ言語との比較

13.1 クエリ言語の比較表

機能Query DSLElasticsearch SQLEQLES|QL
構文スタイルJSONSQLイベントクエリパイプベース
学習曲線
全文検索完全MATCH/QUERY 経由限定的限定的
集約完全主要機能サポート非サポートSTATS コマンド
JOINnested/has_child非サポートsequenceLOOKUP
時系列分析date_histogramHISTOGRAMsequenceBUCKET
セキュリティ分析可能だが複雑基本的最適可能
パイプ処理非サポート非サポートpipe 一部ネイティブ
DDL/DMLREST API非サポート非サポート非サポート
BI ツール連携不可JDBC/ODBC不可JDBC/ODBC(開発中)
プログラマティック最適良好良好良好
リアルタイム最適良好最適良好

13.2 ユースケース別の推奨

ユースケース推奨クエリ言語理由
複雑な全文検索Query DSL最も柔軟で強力
BI ダッシュボードElasticsearch SQLJDBC 経由で BI ツールと直結
アドホック分析Elasticsearch SQL / ES|QLSQL の知識で即座に分析可能
セキュリティ脅威検出EQLイベントのシーケンス分析に最適
ログ分析パイプラインES|QLパイプ処理で段階的に絞り込み
アプリケーション検索Query DSLプログラマティック制御が容易
レポート生成Elasticsearch SQLCSV/TSV 出力で自動化が容易
リアルタイム監視Query DSL / EQL低レイテンシ要件に対応

13.3 各クエリ言語の同一タスクの比較

以下は「部門別の平均給与を求め、平均給与が 80000 以上の部門を降順で表示する」というタスクを各言語で記述した例である。

Elasticsearch SQL:

SELECT department, AVG(salary) AS avg_salary
FROM employees
WHERE status = 'active'
GROUP BY department
HAVING AVG(salary) >= 80000
ORDER BY avg_salary DESC

Query DSL:

{
  "size": 0,
  "query": {
    "term": { "status": "active" }
  },
  "aggs": {
    "by_department": {
      "terms": {
        "field": "department.keyword",
        "order": { "avg_salary": "desc" }
      },
      "aggs": {
        "avg_salary": {
          "avg": { "field": "salary" }
        },
        "salary_filter": {
          "bucket_selector": {
            "buckets_path": { "avg": "avg_salary" },
            "script": "params.avg >= 80000"
          }
        }
      }
    }
  }
}

ES|QL:

FROM employees
| WHERE status == "active"
| STATS avg_salary = AVG(salary) BY department
| WHERE avg_salary >= 80000
| SORT avg_salary DESC

13.4 Elasticsearch SQL の今後

Elasticsearch SQL は成熟した機能であり、以下の方向で継続的に改善が行われている。

  • ES|QL との共存: ES|QL は Elasticsearch SQL を置き換えるものではなく、補完する位置づけ
  • 関数の拡充: 新しい SQL 関数やデータ型のサポートが順次追加
  • パフォーマンスの改善: クエリオプティマイザの強化
  • BI ツール互換性の向上: JDBC/ODBC ドライバの継続的な改善

13.5 結論

Elasticsearch SQL は、SQL の知識を Elasticsearch エコシステムで活用するための強力なブリッジである。以下のケースで特に価値を発揮する。

  1. SQL に精通したチームメンバーが Query DSL を学ぶことなく即座にデータ分析を開始できる
  2. BI ツールとの統合により、Elasticsearch のデータを視覚的に探索できる
  3. アドホッククエリの実行速度を向上させ、データ探索のイテレーションを加速する
  4. translate APIを活用することで、SQL から Query DSL への学習パスを提供する

ただし、複雑な検索要件、JOIN が必要なケース、DDL/DML 操作が必要な場合には、Query DSL や他の API を併用する必要がある。Elasticsearch SQL を他のクエリ言語と適切に使い分けることが、Elasticsearch を最大限に活用するための鍵となる。


参考リンク: