Adobe Campaignクエリー道場(11)〜行間比較で間隔を取得

[前回](https://qiita.com/JJSlider/items/1e85787d7244d0d1f9f5 "Adobe Campaignクエリー道場(10)〜同着を考慮した順位")RowNum()関数を使いながら「自己結合」という考え方をご紹介しました。強力なテクニックなので、引き続き深掘りしていきましょう。
今回の要件は:

::: note
スキーマのプライマリキー分布状況を知りたい。キー数値の間隔が最大なのはどこか？
:::

たとえば本連載の検証環境で「ワークフロー」スキーマを見てみると、こんな感じです:
![2024-01-11_13-12-52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/2f3053f0-f3ce-d5b8-5a31-474a2d6adb9d.png)
Campaignスキーマのレコードは一意識別のためのキーを持ちます(プライマリキー)。キーの数値は連続しているとは限らず、システム状況により分布はまばらです。この「ワークフロー」スキーマの場合、一見して顕著なところでは899,771から1,356,162へと飛んでいて、大きく間隔が空いてますね。このように間隔が空いているのがどこか、その大きさはどれだけか知りたいというのです[^1]。
この要件に対応するワークフローが、RowNum()関数＋自己結合で実装できてしまいます。さっそく見ていきましょう。
今回もまず全体像から:
![2024-01-11_15-12-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/e94ce62e-4c1d-1812-94ad-bd1fa611fb27.png)
アクティビティごとに主要な点に絞って説明していきます。
## クエリー: １から数える
![from1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/fcf9f9b9-f93f-f602-ba70-da1beccc7c10.png)
[前回](https://qiita.com/JJSlider/items/1e85787d7244d0d1f9f5 "Adobe Campaignクエリー道場(10)〜同着を考慮した順位")同様まず分岐です。そして上側の分岐に「クエリ」アクティビティを置き、「ワークフロー」スキーマをディメンジョンに取ります[^2]:
![2024-01-15_12-31-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/b3957b1a-a80a-f65f-8543-b7c4dceef2dc.png)
フィルター条件は、プライマリキー(＠id)が０より大きいとし、全レコードを取ります:
![2024-01-15_12-33-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/9c68cd4a-69ee-99fb-2b40-eccb52ab106c.png)
おなじみの手順[^3]で「クエリ」画面に戻って「データを追加...」から「追加データ」画面へ。「使用可能フィールド」で「プライマリキー」をダブルクリックして「出力列」に追加します。ラベルは「プライマリキー(1)」、エイリアスは「from1」としましょう:
![2024-01-16_10-38-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/4646cd18-ecc7-09aa-3ae5-54a3d6e8e785.png)
「出力列」ペイン右横の「追加」ボタンをクリック、「式」に以下をコピペ:
```
RowNum(, OrderBy(@id))
```
[前々回](https://qiita.com/JJSlider/items/4def108dfba3a522a9d7 "Adobe Campaignクエリー道場(9)〜RowNum()関数でザ・ベストテン！")から解説しているRowNum()[関数](https://experienceleague.adobe.com/docs/campaign-classic/using/getting-started/creating-queries/defining-filter-conditions.html?lang=ja#list-of-functions "関数のリスト")です。「OrderBy(＠id)」でプライマリキーの昇順による番号付けを指定しています[^4]。
エイリアスは「rid」としておきましょう:
![2024-01-16_10-53-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/dea39021-3eba-3fcb-996d-1a208acf14ce.png)
またいつものように「完了」して「クエリ」画面に戻り「追加データを編集...」、「追加列」画面で「詳細設定パラメーター...」クリック。「重複行を削除 (DISTINCT)」をアンチェック、「ターゲティングディメンジョンのプライマリキーの自動追加を無効にする」をチェック:
![2024-01-16_12-19-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/292e56a8-1d40-7b1f-79d3-dbe4c5aabdc5.png)
このアクティビティはこれで出来上がりです。
## クエリー: ０から数える
下側の分岐にも「クエリ」アクティビティを置きます:
![from0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/da999357-b725-addd-6506-f0fdea03d442.png)
内容はほぼほぼ同じ。違う点だけ見ていきますが、その違いが重要なので、刮目してご覧あれ。
ディメンジョンは上と同じく「ワークフロー」。フィルター条件は、プライマリキー(＠id)が０より __大きいか等しい__ とします:
![2024-01-16_12-53-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/9feca112-ed0c-6c4b-99d5-991d573ddf25.png)
「追加データ」画面では上側と同じ設定をしましょう。ただし、プライマリキーのラベルを「プライマリキー(0)」、エイリアスを「from0」とします:
![2024-01-16_13-12-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/d993b3df-9d98-e2ce-4702-66a142a82c8c.png)
出力列２行めのエイリアス「rid」は変えちゃだめですよ。
そして！　「詳細設定パラメーター」。ここが重要です。「ターゲティングディメンジョンのプライマリキーの自動追加を無効にする」に加えて、「0 ID レコードの自動フィルターを無効にする」をチェックしてください:
![0 ID レコードの自動フィルターを無効にする.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/eb8190ca-72ab-3b58-5f56-9e4372812eb4.png)
説明しましょう。「ワークフロー」などの製品標準スキーマにはプライマリキーが０のレコードがデフォルトで存在します。内部ロジックの都合で作られたダミーレコードなのでユーザーが意識することは通常ありません。しかし今回、あえてそのダミーレコードをクエリーに含めたいのです。そのためフィルター条件を０より大きいか等しいとしました。さらに、デフォルトではキー=0のダミーレコードが自動的に排除されているので、「詳細設定パラメーター」で「0 ID レコードの自動フィルターを無効にする」と明示的に指定したのです。
ではなぜ、そんなことを？　謎が謎呼ぶ事件の結末は?!
## 積集合
ひとまず先に進めますね。ふたつの支流を「積集合」アクティビティにつなげ、分岐を合流させます:
![sekishugo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/71cbd146-2711-5814-3463-856806500d9f.png)
「紐付け」の「列の選択」をティック、「プライマリセット」に「１から数える」を選びます。そして「結合に使用する列」で「追加」をクリックし「フィールド」に「rid」と入力してください:
![2024-01-16_16-14-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/9b226975-3e4b-e751-7c46-c9a0523889fa.png)
「積集合」の設定はこれだけ。いよいよ感動のフィナーレへ！
## エンリッチメント
![enrichment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/907cae82-7b16-dc13-eb48-d0a340f8350c.png)
前回は分岐を積集合して終わりでしたが、今回は「エンリッチメント」アクティビティをつなげます。「エンリッチメント」は頻出なのでご存知かもしれませんが、ざっくりいえばデータを追加するアクティビティです[^5]。　
ダブルクリックした設定画面で「プライマリセット」が「積集合」なのを確認。「メインセットからのすべての追加データを維持」をアンチェックし、「データを追加...」をクリックします:
![2024-01-16_16-44-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/1abf7386-d677-31e7-da25-2ff6f91b9beb.png)
「フィルタリングディメンジョンにリンクされたデータ」をティック:
![2024-01-16_16-54-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/bcc64e5b-ba11-cc39-db4d-6b99f522a996.png)
「フィルタリングディメンジョンのデータ」をティック:
![2024-01-16_16-55-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/c355d731-67f9-2189-6273-be70ac789dea.png)
「追加データ」画面に進みました。右ペイン「使用可能なフィールド」で「プライマリキー(0)」「プライマリキー(1)」をそれぞれダブルクリックして「出力列」へ追加:
![2024-01-16_17-10-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/77dc6d02-ce8a-6f8b-00ac-eaaaa9c1375f.png)
ペイン右横の「追加」ボタンをクリックし、「式」に以下を入力します:
```
from1-from0
```
「ラベル」は「ギャップ」、エイリアスは「gap」としましょう:
![2024-01-16_17-18-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/c93d9e6a-7944-760c-d05f-91d9f6c22149.png)
以上です！　アクティビティ・ワークフローを保存してください。ワークフローを実行、「エンリッチメント」からの遷移矢印を右クリックして「ターゲットを表示」[^6]:
![2024-01-16_17-47-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/3e9acae6-7281-2ab4-bd4c-cc37bf68b666.png)
「ギャップ」カラムを適宜クリックし降順ソートさせると──なんということでしょう！　キーとキーとの間隔が得られています。この環境では、「2,265,073」と「3,364,336」とが最も隔たっていることがわかりますね。念のためソーススキーマ画面でも見てみると:
![2024-01-16_17-55-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/f54a545a-0f95-0e3f-77d7-1084f499130d.png)
たしかにギャップが確認できます！　でもどうしてこんな結果が得られるんでしょう？
## 解説
分岐後の両「クエリ」アクティビティで、RowNum()関数により番号を振りましたね。それによりまず上側分岐「１から数える」ではこんな結果を得ます:
![2024-01-16_14-48-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/24b038df-357b-adb7-d7f3-4e81c74c309f.png)
いっぽう下側分岐「０から数える」では、やはりRowNum()関数で番号を振りましたが、キー値０から始めているので、ひとつずれています:
![2024-01-16_14-49-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/0bc75fbf-726d-f813-ead9-45a0eac08ee4.png)
これらは両方、「ワークフロー」スキーマでもともと同じもの。その二者を「積集合」により、RowNum()関数による番号「ライン数」(rid)で組み合わせます。自分と自分とを結合する「自己結合」ですね[^7]。その結果が:
![2024-01-17_9-21-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/a0616661-5e0c-b648-f9a2-d4b759a69119.png)
「ライン数」を介して、ある行とその前の行とが紐づいてます。０から数えてひとつずらした効果です[^8]。
そして、「エンリッチメント」で:
```
from1-from0
```
と、あるキー値と「その前の行」のキー値との差を取り、「ギャップ」としてデータに追加しています:
![2024-01-16_17-18-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/c93d9e6a-7944-760c-d05f-91d9f6c22149.png)
数学で「階差数列」を習った方がいるかもしれませんが、そんな感じですね。この「ギャップ」がまさに、キーとキーとの間隔というわけです:
![2024-01-16_17-47-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/3e9acae6-7281-2ab4-bd4c-cc37bf68b666.png)
***
どうでしょう、なかなか華麗なるスゴ技じゃありませんでしたか？　どや！
[前回](https://qiita.com/JJSlider/items/1e85787d7244d0d1f9f5 "Adobe Campaignクエリー道場(10)〜同着を考慮した順位")もそうでしたが、RowNum()関数と自己結合とはきわめて相性がいい技術です。自分自身を結合させるための紐づけキーを、RowNum()関数で要件に応じて作れるからですね。行間比較のために１ずらした番号を振った今回の解法はまさにその好例でした。
前後のレコード間で比較をしたい、間隔を取りたい、という要件は実戦で出てくることがあるんじゃないでしょうか。そんなときはぜひ、本稿を読み返してみてください。役に立ったらうれしいです！

:::note warn
本稿の内容は筆者のオンプレミス型デモ環境(Adobe Campaign Classic 9359@c636bf3 PostgreSQL 14.9)上で実施した検証に基づきます。別環境における同様の動作を保証するものではありません。またデータは架空のものであり、既存の配信や実在の組織とはいっさい関係がありません。
:::

[^1]: Campaign V7においてスキーマ定義が「autopk="true"」の場合、プライマリキーは32ビット符号付き整数となり、上限値は正領域の最大数で約21億です。万一その上限に達してしまうと、それ以上キーを採番できず新規レコードが作成不可能になってしまいます。作ってしまったレコードの削除やキー変更は反則なので、採番ポインターを移動させるため本稿のように大きな空間を探さねばならないことがあるのです。
[^2]: 「ワークフロー」スキーマを例としたのは、たまたま本稿検証環境でキー分布状況がわかりやすかったからというだけです。 「ワークフロー」スキーマでキー上限値が採番される状況は通常あり得ません。
[^3]: 「おなじみの手順」がわからない場合は「Adobe Campaignクエリー道場」[初回](https://qiita.com/JJSlider/items/dff8bb8a8c48ddad40bf "Adobe Campaignクエリー道場(1)〜ユニークな件数")から復習してください。
[^4]: [前回](https://qiita.com/JJSlider/items/1e85787d7244d0d1f9f5 "Adobe Campaignクエリー道場(10)〜同着を考慮した順位")・[前々回](https://qiita.com/JJSlider/items/4def108dfba3a522a9d7 "Adobe Campaignクエリー道場(9)〜RowNum()関数でザ・ベストテン！")ではDesc()関数による降順ソートを行っていました。しかし今回はDesc()関数なしなので昇順になります。
[^5]: 「エンリッチメント」アクティビティについては[Qiita](https://qiita.com "Qiita")記事「[Adobe Campaign Classic エンリッチメントアクティビティを利用した基本設定手順](https://qiita.com/ASkmt/items/65b1fbd041eebc94858b "Adobe Campaign Classic エンリッチメントアクティビティを利用した基本設定手順")」も併せてお読みください。また[ヘルプ記事](https://experienceleague.adobe.com/docs/campaign-classic/using/automating-with-workflows/use-cases/data-management/enriching-data.html?lang=ja "データのエンリッチメント")では本稿同様「積集合」アクティビティにつなげた使用例が紹介されています。
[^6]: ワークフロープロパティで「２つの実行間の中間母集団の結果を保持」のチェックが必要。毎回いってますが、同設定は本番環境では行わないでください。
[^7]: [前回注３](https://qiita.com/JJSlider/items/1e85787d7244d0d1f9f5#fn-3)で指摘したのと同様、今回も生SQLレベルでは厳密な自己結合とはいえません。しかしワークフロー全体のロジックが自己結合に基づくものなので、そのアイディアを理解してください。
[^8]: キー値０のダミーレコードを考えに入れることで、「その前の行」がないはずの最初の行から処理ができています。キー値０がひっそりながら存在している必然性が見えてきますね。