Adobe Campaignクエリー道場(10)〜同着を考慮した順位

[前回](https://qiita.com/JJSlider/items/4def108dfba3a522a9d7 "Adobe Campaignクエリー道場(9)〜RowNum()関数でザ・ベストテン！")はCampaignのRowNum()関数を利用して「順位」を求めるクエリーを取り上げました。しかしひとつ、困ったことがあるのです。
ベストテンということで上位10件まで絞り込んだのですが、それをせずに下位まで見てみると:
![2023-12-21_17-24-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/4851a072-a7aa-074e-d6ae-3ad2ce928e76.png)
「姓の数」が並んで同着なのに、「ライン数」は異なるところがありますね。RownNum()関数はレコード行にユニークな番号を振るためのもので、それは厳密には「順位」ではないからです。
SQLならばRank()という関数があり、同着を考慮した順位を返します。しかしCampaignには残念ながら相当するものがありません。だから同じことをCampaign上でやろうと思ったら、ちょっと工夫が必要になるのです[^1]。
そこで今回は、同着には同順位を与えるシン順位づけをやってみましょう。引き続き受信者の姓だと数が多くてわかりにくいので、以下の簡略化した「製品」スキーマを作って例に使います:
|キー (＠fid)|品名 (＠name)|価格 (＠price)|
|---:|:---|---:|
|1|かけ|700|
|2|鴨南蛮|1700|
|3|きつね|900|
|4|卓袱|1100|
|5|たぬき|900|
|6|月見|900|
|7|天麩羅|1500|
|8|花巻|1100|

これに価格の高さで順位をつけていきましょう。
なにげにステップが多いので、まず全体像をお見せします:
![全体像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/e37e0d68-996e-6962-0e85-2c270ce3642e.png)
アクティビティごとに主要な点に絞って説明していきましょう。
## 分岐
![fork_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/39878745-1d91-429c-f03d-7af570872e55.png)
まずいきなり、「分岐」アクティビティから始めます。ロジックの流れを二分するアクティビティです。条件設定などはなく、キャンバスにドラッグ＆ドロップするだけ。操作は簡単ですね。でも、どういう意味？　──おいおいわかってきます。ついてきてください。
## 製品スキーマそのまま
![sonomama_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/4865c97b-7f74-63a6-9b9f-17ce45d517f2.png)
上側の分岐に「クエリ」アクティビティを置きます。このアクティビティでは上記の「製品」スキーマを左から右へそのまま受け流します。つまり、ディメンジョンには「製品」を指定し:
![2024-01-09_11-35-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/611c7096-e544-df1d-44bd-7fc245971dd3.png)
フィルター条件はキーが0より大きい、すなわち全レコードが選択されるようにします:
![2024-01-09_11-44-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/59ac4a8e-b9ac-6937-d03d-e0011cc4d1ef.png)
これだけです。わざわざふたつに分けておいて何もせず素通しとは──いよいよ謎が深まりますね。
## 価格で採番
![rownum_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/63ab7cb3-efdc-e1d0-1b93-078800712802.png)
分岐のもう片方ですが、やはり「クエリ」アクティビティです。「製品」をディメンジョンに取り、フィルター条件はキーが0より大きいとして全レコードを得る。さっきと同じです。変化はここから。「クエリ」画面で「データを追加...」:
![2024-01-09_12-05-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/50617e7c-852e-f94a-b300-7d24a8beabe2.png)
例によっておなじみの手順、「フィルタリングディメンジョンにリンクされたデータ」>「フィルタリングディメンジョンのデータ」>「追加するデータ」で「追加列」画面へ遷移。右ペインの「追加」ボタンで「出力列」に新規行を追加し、以下の式を入力します:
```
RowNum(,Desc(OrderBy(@price)))
```
「ラベル」は「価格順」、「エイリアス」は「rowid」としておきましょう:
![2024-01-09_12-19-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/e5674f3c-1d99-51ad-ecf1-0358f99f64a9.png)
「完了」していったん戻った「クエリ」画面で「追加データを編集...」:
![2024-01-09_15-07-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/0be227c3-6a15-ee05-c735-2173c30bfad9.png)
再びの「追加列」画面で「詳細設定パラメーター...」をクリックして「メインセットからのすべての追加データを維持」をチェック、「重複行を削除 (DISTINCT)」をアンチェック:
![2024-01-09_14-38-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/0529d977-43e0-863e-6f99-c5b414131f0a.png)
このアクティビティはこれで出来上がりです。
## 同価格の最小番号
![min_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/3e509797-c280-b315-0f49-3cff84acc1ea.png)
続いてまた「クエリ」アクティビティを置きます。ディメンジョンには前アクティビティの出力結果を指定しましょう:
![2024-01-09_14-47-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/fdec712b-e327-6aa8-21fa-1cecd1076b35.png)
フィルター条件はやはり、順位をつけたい全レコードが選択されるようにします:
![2024-01-09_14-56-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/72e5cb9f-b185-510f-9791-d36f82352d0d.png)
「完了」して「クエリ」画面に戻り「データを追加...」。いつものように「追加列」画面へ進み、左ペイン「出力可能フィールド」で「価格」をダブルクリックしてください。すると「出力列」に追加されるので、その「グループ」をチェックです:
![2024-01-09_18-51-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/cae06241-8419-02da-3adc-9eacadde0a44.png)
続いて「出力列」ペイン横の「追加」ボタンをクリックし、できた新規行に以下の式を入力します:
```
Min(rowid)
```
「ラベル」は「順位」、「エイリアス」は「rank」としましょう:
![2024-01-09_18-53-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/b8136b8b-f2fd-4df6-170b-43b41d55a5b1.png)
「完了」して「クエリ」画面に戻り「追加データを編集...」、「追加列」画面で「詳細設定パラメーター...」。
「インバウンドイベントデータを使用」がチェックされていることを確認してください。そして「重複行を削除 (DISTINCT)」をアンチェック、「ターゲティングディメンジョンのプライマリキーの自動追加を無効にする」をチェック:
![2024-01-09_16-06-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/e5801e3a-ca38-31d6-8aa1-1a9dc5e82309.png)
出来上がりです。ではいよいよ最後の仕上げといきましょう。
## 積集合
![intersection_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/5a114f19-e777-9632-4ba7-a8620dd6a82d.png)
「積集合」アクティビティは、ふたつのスキーマをかけあわせたスキーマを得るものです。かけあわせるとは？　──あとで考えるとして、とりあえず手順を見てましょう。
「積集合」アクティビティをキャンバスにドラッグ＆ドロップし、両分岐のアクティビティから遷移矢印をつなげます。つないだ「積集合」をダブルクリックして開き、「紐付け」の「列の選択」をティックしてください:
![2024-01-09_16-36-39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/d7571086-cf25-80d8-4475-fc6728d32111.png)
「プライマリセット」を選びます。今回どちらでもいいのですが、とりあえず上側分岐による出力のほうにしておきます:
![2024-01-09_16-49-21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/08dbf453-184a-72e1-3b9d-0fdab9246744.png)
「結合に使用する列」ペインで「追加」ボタンをクリック。新規行の「フィールド」に「＠price」と入力してください。「製品」スキーマの「価格」カラムのことです:
![2024-01-09_17-18-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/414b9330-be50-82c7-da8d-3b30f5b253d8.png)
以上です！　ワークフローを保存して、実行してみましょう[^2]:
![2024-01-09_17-44-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/5e51bfe2-14c0-1208-5e6f-3283c4accab0.png)
三番めに高い価格「1,100」を持つ２レコードが同着３位です。４位は飛んで、次に高い価格の３レコードが同着５位。６、７位が飛んで、最安が８位。求めていた結果が得られました！
## 解説
振り返ってみましょう。
実は今回、SQLの世界で「自己結合」[^3]と呼ばれるスーパーテクニックを応用してみました。自己結合とは？ ──自分と自分とを、あたかも別物であるかのように結合させることです。その観点から今回の手順を見直してみると、いろいろ腑に落ちてきませんか。
まず冒頭に「分岐」アクティビティを置きましたね。のちに「結合」させるために２本の支流を作ったのです。そしてどちらの分岐にも「製品」スキーマをディメンジョンに取る「クエリ」アクティビティを置きました。「自分と自分と」を２つに分けたということです。
上側の分岐は「製品スキーマそのまま」つまり自分そのものでした。対して下側の分岐はちょっと加工しています。詳しく見返しましょう。
「価格で採番」では以下の式を使いました:
```
RowNum(,Desc(OrderBy(@price)))
```
[前回](https://qiita.com/JJSlider/items/4def108dfba3a522a9d7 "Adobe Campaignクエリー道場(9)〜RowNum()関数でザ・ベストテン！")紹介したRowNum()関数[^4]です。「価格」(＠price)の降順で番号を振っています。それにより「価格で採番」は以下の出力を得ました:
![2024-01-09_18-31-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/01b105d3-3343-ed1f-381d-281edfd176d8.png)
RowNum()で価格順が出ましたが、同着つまり同価格を同順位にしたいんでしたね。そこでこの結果を受け取る次アクティビティはどうするか。手順を思い出しましょう:
![2024-01-09_18-51-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/cae06241-8419-02da-3adc-9eacadde0a44.png)
「同価格の最小番号」でまず価格によりグループ化しました。同価格に同順位を与えるためです。そして:
![2024-01-09_18-53-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/b8136b8b-f2fd-4df6-170b-43b41d55a5b1.png)
Min()関数[^5]は[過去回](https://qiita.com/JJSlider/items/547d5348b647506ff846 "Adobe Campaignクエリー道場(3)〜最新日付でビバノンノン")で紹介した集計関数のひとつ。グループの中で最小値を取るものです。引数の「rowid」はRowNum()で取った「価格順」のエイリアスでしたね。つまりこの行が得ているのは「同価格の最小番号」というわけです:
![同価格の最小番号.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/c8b30db6-d593-5005-745c-52d9b654d7ce.png)
以上、価格でグループ化し、価格順の最小値を取った結果がこのアクティビティの出力になります:
![2024-01-10_10-45-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/f53f914a-97b6-5ac5-1ada-ac098c7bd924.png)
しかし。これだと、グループ化しちゃったことで、個別の品名がわからなくなっちゃってますね。そこであらためて、価格に対応する品名を元スキーマ「製品」から取得し直さなければなりません。──とすると、わざわざ分岐して「製品」スキーマをそのまま持ってきた意味がわかってきますね。価格で紐づけて品名を取り戻すためです:
![スクリーンショット 2024-01-10 11.35.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/f13a8336-9ff7-a0d5-cc56-26e2157abd9a.png)
このとき紐づけあうスキーマはどちらも「製品」、そもそも同じもの。つまり自分と自分とをかけあわせることになり、これが自己結合というわけです。そして、その価格(＠price)による紐づけで自己結合を実行するのが最後の「積集合」アクティビティでした:
![2024-01-10_15-33-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/ee4036a9-e6f3-58f0-f59b-e25567816d2b.png)
以上により、同着を考慮した順位を得ることができるのです:
![全体像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/e37e0d68-996e-6962-0e85-2c270ce3642e.png)
![2024-01-09_17-44-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516149/5e51bfe2-14c0-1208-5e6f-3283c4accab0.png)

***
おつかれさまでした！　前回の内容から発展させ、複数アクティビティの組み合わせにより「自己結合」という上級テクニックを学んじゃいました。たとえこのとおりの要件はなくても、ここでの考え方を理解しておけば、できることはめちゃ広がりますよ。ぜひ本稿の手順を追って、そのアイディアを自分のものにしてください。

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

[^1]: この場合カスタム関数を作成するという手法もあります([追加の SQL 関数の定義](https://experienceleague.adobe.com/docs/campaign-classic/using/configuring-campaign-classic/api/adding-additional-sql-functions.html?lang=ja "追加の SQL 関数の定義"))。しかし本稿では以下、さらに応用が利くような考え方・テクニックを紹介したいと思います。
[^2]: ワークフロープロパティで「２つの実行間の中間母集団の結果を保持」をチェックしておくことが必要です。つねづね繰り返していますが、この設定は本番環境では行わないでください。
[^3]: 一般的にいう「自己結合」は、SQL１文のなかで自テーブルどうしをJOINすることを指します。Campaignで本要件に対応する場合、複数アクティビティに処理を分けるため異なる一時スキーマ(テーブル)間のJOINにならざるを得ず、その意味で生SQLレベルでの厳密な自己結合とはいえません。しかしワークフロー全体のロジックが自己結合に基づくものなので、そのアイディアを追って理解していただければと思います。
[^4]: ヘルプ記事「[関数のリスト](https://experienceleague.adobe.com/docs/campaign-classic/using/getting-started/creating-queries/defining-filter-conditions.html?lang=ja#list-of-functions "フィルター条件の定義")」の「ウィンドウイング関数」参照。
[^5]: ヘルプ記事「[関数のリスト](https://experienceleague.adobe.com/docs/campaign-classic/using/getting-started/creating-queries/defining-filter-conditions.html?lang=ja#list-of-functions "フィルター条件の定義")」の「集計」参照。