IBM Cloud：Data Engineからwatsonx.dataへの移行について

## はじめに

個人的に推していたIBM Cloudのサービス、Data Engine (旧SQL Query)が、2025年1月18日にEOSを迎えることになりました。[こちらのガイド](https://cloud.ibm.com/docs/sql-query?topic=sql-query-deprecation&locale=ja#migrate-analytics-engine)にあるように、Analytics Engine へのマイグレーションが必要になる、とのことです。

その詳細については、[他の方の記事](https://qiita.com/takason/items/da9d09ae5a66766ace0d)を参照いただくとして、Data Engineに近い使い方が出来そうなwatsonx.dataへの移行についても検証してみることにしました。

## Data Engineで出来ていたこと

例えば、Activity TrackerのアーカイブログをICOSバケットに保管しているとして、そこから2024年4月のICOSに関連するログをCSV形式で抽出したい場合、Data Engineのコンソールを開いて、以下のようなSQLを1本実行すれば結果が得られました。

```
WITH logs (
  SELECT from_unixtime((get_json_object(value, "$._source._ts") / 1000) + 32400, 'yyyy-MM-dd HH:mm:ss') as timestamp,
  get_json_object(value, "$._source._host") as host,
  get_json_object(value, "$._source.message") as message
  FROM cos://jp-tok/[Activity Trackerのアーカイブ先バケット名]/year=2024/month=04/*  STORED AS TEXT
)
SELECT timestamp, host, message FROM logs
WHERE host = 'cloud-object-storage'
ORDER BY timestamp
LIMIT 10
INTO cos://jp-tok/[結果出力先のICOSバケット名]/[適当なプレフィックス] JOBPREFIX NONE STORED AS CSV
```

Activity TrackerとLog Analysisも、同じようなタイミングで[IBM Cloud Logsに置き換わる](https://cloud.ibm.com/docs/log-analysis?topic=log-analysis-deprecation)ようなので、その後の過去ログの検索はそれに任せればいいのですが、既にICOSにアーカイブされているログを2025年1月18日以降Data Engineで検索出来なくなるのは困るので、出来るだけサクッと代替策を用意しておきたいわけです。

このSQLをwatsonx.dataで動かすにはどうすれば良いのかを検証してみました。

## watsonx.dataについて

IBM Technology Zoneというお試し環境が手に入るサイトから、watsonx.dataの環境も手に入りますので、そこで色々確認することにします。IBM Technology Zoneの詳細については、[こちら](https://qiita.com/trtrbohz/items/d33eac79844d5a322b17)を参照してください。

直近に試した環境は「IBM watsonx.data Development Lab - 1.1.2 R1M1」になります。

環境を予約後、準備が出来た旨のメールにあるリンクを開くと、watsonx.dataに含まれる各種コンポーネントへのリンク集のようなページに辿り着きます。そこから、「Watsonx UI」のリンクを開いてログインすると、watsonx.dataの画面が開くので、左側メニューアイコンの「SQL」をクリックすると、見慣れたData Engineに似た画面が確認出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307720/10adf0ba-a751-2732-ad7a-7ad7612a6d46.png)

そうそう、これが欲しかったんだよ、って感じです。ただ、既にICOSに溜まっているログに対してSQLを実行するためには、事前の設定が必要だったり、SQLのお作法が変わっていたりするので、その辺りを整理します。

## SQL実行までの流れ

#### 1. ICOSバケットの追加とアクティベーション

「インフラストラクチャー・マネージャー」の画面で、「コンポーネントの追加」をクリックして、バケットの追加を実施します。
エンドポイント、アクセス・キー、秘密鍵（シークレット・キー）は、読み込みたいログがあるICOSバケットのあるインスタンスの情報を入力します。
そして、「カタログ・タイプ」は、「Apache Hive」を選択します。
また、「今すぐアクティブ化」にチェックを入れておくことで、この後の作業を少しだけ簡略化します。

#### 2. カタログとエンジンの関連付け

上で作成されたカタログの右上のアイコン「関連付けの管理」をクリックし、エンジン名（presto-01）の頭にチェックを入れて、エンジンの再起動を実施します。

#### 3. スキーマの作成

照会ワークスペースに移動し、以下のDDLを実行してスキーマを作成します。

```
CREATE SCHEMA IF NOT EXISTS [事前に作成したカタログ名].[任意のスキーマ名]
WITH (
    location = 's3a://[ログのあるICOSバケット名]/'
);
```

#### 4. テーブルの作成

続けて以下のDDLを実行して、テーブルを作成します。「year=2024」が気になるかもしれませんが、後で触れます。

```
CREATE TABLE [事前に作成したカタログ名].[作成したスキーマ名].[任意のテーブル名] (
  rawdata varchar,
  month int,
  day int
)
WITH (
  external_location = 's3a://[ログのあるICOSバケット名]/year=2024',
  partitioned_by = ARRAY['month', 'day'],
  format = 'TEXTFILE'
);
```

#### 5. おまじない

続けて以下を実行することで、Hive style partitioningな形（今回の場合、`year=*/month=*/day=*`のようなパス）でICOSバケット上に保管されているオブジェクトのメタ情報を取り込みます。（表現が正確ではないかもしれません・・）
このおまじないをしないと、パーティショニング分割されたログをうまく追ってくれません。

```
CALL system.sync_partition_metadata('[作成したスキーマ名]', '[作成したテーブル名]', 'FULL');
```

おまじないの解説は[こちら](https://prestodb.io/docs/current/connector/hive.html#procedures)を参照ください。

#### 6. SELECT実行

さらに続けて以下のようなSELECT文を実行することで、めでたく冒頭のSQLと同じ結果を得ることが出来ました。

```
WITH logs AS (
    SELECT from_unixtime((cast(json_extract(rawdata, '$._source._ts') as double) / 1000) + 32400) as timestamp,
    json_extract_scalar(rawdata, '$._source._host') as host,
    json_extract_scalar(rawdata, '$._source.message') as message
    FROM [事前に作成したカタログ名].[作成したスキーマ名].[作成したテーブル名]
)
SELECT * FROM logs
WHERE host = 'cloud-object-storage'
ORDER BY timestamp
LIMIT 10;
```

・・・と思ったら、最後のCSVへの書き出しが足りていませんね。後日加筆します。（出力用のスキーマとテーブルを作成して、上のSELECTの結果をINSERT INTOに繋げて書き込むイメージになります）

## Data EngineでのSQLとの違い

まず、`SELECT 〜 WITH 〜`のフォーマットが異なるようで、`AS`が必要になります。watsonx.dataでのSQLは、Prestoでサポートされる形式になりますので、詳細は[こちら](https://www.ibm.com/docs/en/watsonxdata/1.1.x?topic=types-presto-sql-statements)で確認します。

また、JSON形式のログから目的のフィールドを抽出するために用いていた`get_json_object`ですが、`json_extract`または`json_extract_scalar`に置き換える必要があります。この2つの違いですが、前者は返り値の型がJSON Arrayのため、`cast`しないとそのままではSQL内で扱えません。後者はスカラー値(ブール値、数値、文字列)になるとのことですが、使用したログの都合で、今のところ「文字列(varchar)」になるケースしか確認できていません。

## 制約

テーブル作成のところで、以下のようにしていましたが、

```
WITH (
  external_location = 's3a://[ログのあるICOSバケット名]/year=2024',
  partitioned_by = ARRAY['month', 'day'],
  format = 'TEXTFILE'
)
```

本当はこうしたいのです。

```
WITH (
  external_location = 's3a://[ログのあるICOSバケット名]/',
  partitioned_by = ARRAY['year', 'month', 'day'],
  format = 'TEXTFILE'
)
```

ところが、上記を指定したCREATE TABLEは、`[作成しようとしたテーブル名] location must not be root path`というエラーで作成に失敗します。これは、既知の制約のようで、[こちら](https://www.ibm.com/docs/en/watsonxdata/1.1.x?topic=watsonxdata-known-issues-limitations#Knwnissulimit2__dl_xfs_xyf_m1c)に以下のように記載されています。

> Hive catalog table creation by using external_location fails due to wrong placement of file
Applies to: 1.1.0 and later
Hive catalog table creation by using external_location fails when the file is placed in the root of the bucket.

LogDNAのアーカイブは、ICOSバケット直下にyear=*で始まる階層を書き出してしまうので、やむなく「year=2024」をexternal_locationに記載する必要があったわけです。（あるいは、既存のすべてのオブジェクトに対して、一律でプレフィックスを付けてもいいかもしれません）

## まとめ

Data Engineは、定常的なコストはかからず、完全な従量課金のサービスでした。ですが、watsonx.dataは、インスタンスを持っているだけで固定のコストがかかります。Analytics Engineは、Data Engineと似たタイプの従量課金のようです。

そのため、既存のコード・手順・運用等の移行方法だけでなく、利用頻度を踏まえたコストを含め、総合的に考慮した上で、最終的な移行先を決める必要がありそうです。