メインコンテンツへスキップ
見出し画像
Photo byakisuke0925

【第488回】 Marketing Cloud Next : 個人単位でセグメントを検索する方法

    Nobuyuki Watanabe

    Marketing Cloud Next Growth & Advanced Edition において、「特定の個人がどのセグメントに含まれているか」を確認したい場面は、実務上非常に多くあります。

    これに対して、これまで公式では以下のような機能が提供されています。

    しかしながら、より効率的に検索を行う手段としては、クエリを活用するアプローチが有効です。そこで本記事では、私が実務で実際に行っている、セグメントの検索方法を 2 つご紹介します。


    1. Query Editor を使って検索する方法

    まずは、最もシンプルな方法です。
    Query Editor を利用し、画面上で直接検索します。

    手順

    1. Query Editor タブを開きます。

    画像

    2. 以下のクエリを貼り付けて実行します。

    SELECT
        a.Id__c AS UnifiedId,
        c.ssot__ExternalRecordId__c AS Id,
        c.ssot__FirstName__c AS FirstName,
        c.ssot__LastName__c AS LastName,
    	d.ssot__EmailAddress__c AS CurrentEmail,
        b.ssot__Name__c AS SegmentName,
        a.Delta_Type__c AS DeltaType,
        a.Timestamp__c + interval '9 hour' AS PublishDateTime
    FROM Individual_Unified_SM_1755567709613__dlm a /* ← 自分の Unified Individual - Latest DMO の API 名に変更する */
    JOIN ssot__MarketSegment__dlm b
        ON a.Segment_Id__c = SUBSTRING(b.ssot__Id__c, 1, 15)
    JOIN UnifiedIndividual__dlm c /* ← 自分の Unified Individual DMO の API 名に変更する */
        ON a.Id__c = c.ssot__Id__c
    LEFT JOIN ssot__ContactPointEmail__dlm d
        ON c.ssot__ExternalRecordId__c = d.ssot__PartyId__c
    -- WHERE b.ssot__Name__c = '***' /*Segment Name Filter*/
    -- WHERE c.ssot__ExternalRecordId__c = '***' /*Individual ID Filter*/
    -- WHERE c.ssot__FirstName__c = '***' AND c.ssot__LastName__c = '***' /*Full Name Filter*/
    ORDER BY a.Timestamp__c DESC, a.Delta_Type__c DESC
    LIMIT 10

    変更が必要なポイント

    以下の箇所は、各自の環境に応じて変更が必要です。

    • Unified Individual - Latest DMO
      → 数値部分(例:1755567709613)を自身の環境に合わせて変更

    • Unified Individual DMO
      → 自身のルールセット名に応じて変更
      (※ ルールセット名が空欄なら変更は不要です。)

    検索条件について

    クエリ内の WHERE 部分のコメントアウト(--)を外すことで、
    以下の条件で絞り込みが可能です。

    • セグメント名(API 名ではありません)

    • Individual ID(取引先責任者 ID やリード ID など)

    • 氏名(フルネームで検索しない場合は、片方を削除してください)

    取得できる情報

    このクエリにより、以下の情報を検索できます。

    • Unified Individual ID

    • セグメント名

    • メールアドレス

    • セグメント公開日時

    • 新規/既存判定(Delta Type)

    これを実行することにより、対象の個人が現在どのセグメントに含まれているかを検索することができます。

    画像

    2. スプレッドシートを使って検索する方法

    大量の複数の個人をまとめて検索したい場合や、他人へ分析・共有したい場合には、スプレッドシートを利用する方法が有効です。

    本方法では、Google Apps Script を使用し、Data Cloud のデータを直接取得します。基本設定については、以下の記事を確認してください。

    実装コードについては、以下をご参照ください。

    const SF_LOGIN_URL = "https://login.salesforce.com"; // Sandbox: https://test.salesforce.com
    const CLIENT_ID = "3MVG9GIRg253.lotxyT7qsSZdhj8gAEDPVk6.9Z4NsPAuxUAfTfanzFuk3MV4iTm22RhAYt_rxlIC4hyLKIL";
    const USERNAME = "admin@rbacompany.com";
    const API_VERSION = "66.0";
    
    const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
    MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtTcj7mjX2Kguo
    ahVavRHq+cr+35h3b7jBarCzdlaoJZoayVI6I/NNZzMSMcR+DGhfVG0xXOHubQ2q
    kO6K+FSlMj/wyi2alpq7hS6CCoYwimfuPI4+CnNrVIQiksMe026kXmZ8tEpW6yV5
    lGmC/85p99bE8Pm0CgnlNcsfflx7gYtum9nAVzyu4cqJjYEdSudXh4eSnse0C0Tw
    dWs27aMOz9+7/7QX6QxdgSwkoorkR7KplSE8qrxAoVxPQomzP2ITetaHExbRh+LH
    qC8/wzLo/1uQuFi6v9sp3O6DIcv+K7fNg56o3ATCM27Xt7sFPS+sqIU7LW7mhMja
    CCPs64irAgMBAAECggEAERqQL2S01qqno+N0YBQw5IPqqOTgY0k/brdc4RlYzBeJ
    8gLUfrB1nroErFMFFXucAWyPqkOEeMeChcbwA/8mO3eOH/GUNqGOe9tVD7iCLeA7
    CaQoVa8qXPlmYRMi9rPfQ5Gdg8k3XQSwGiOvliIw+Pxg0ecGfeJPv7NjbKRH9FhX
    FZ0h8v67VPmKIAmRvEfOBV2G8Qp6vIWwS+bCGHspUtqzeIdPDbWbqjSsiddEGqeD
    /pDpvcseMIclaKzY0za0DeR9x2iDTnJPIQ/BxE55RAO/vnkjyo/1Wn0UcGe5E5KE
    3lOFe0gpFObtmHqPaQnO25xJ6EjlmJHFZ9mRmjhYoQKBgQDZccMNagVRJW7WOz/H
    A2D3LFVXQ1247ArD4CA9P3BFNJF2GCLMs3vOOGXKCbnVssW892s9zv6DAoFsbuka
    om4vwocBPXuTrqdnIy0K8IuwQnOzGMyHskO629m46WOG0FfdvG4PMOvwZ8HvkQQJ
    QG7e4eAJNlPbkAvSfD+Hz0TTXQKBgQDMCGU+Ui1BrGcduQWfRbT4Cc8Rz5f7qhAl
    LS8arp0d+HtF5AeqjtRpX4NKGX3sr9xBIZVfwla7jWOe0x6jd20Co5KjlGKvWR2w
    i6tRHGMk0/FOxcdQVR7hWw5+DSaLBmwi3AhE8ro8rqsGqVPqnutzP9BXG7Hr8BDe
    jfPucxDTpwKBgQCX5kbSGhw4waOZ+K3nAs88HDZJzX+tbQdgKjObVbPCRKTREK9O
    vJtiRjelWgH97PMBvP2nofBd6OQssZYZyxqaNpRFI4QueLXs8L/Igp2ytdlJZauL
    p9Z0tJx19mRWizi2Z6mi5xQLTxBFoNJm/CH3hWcSSGdwXEJF+hIPd5Wm6QKBgBMe
    UkZVsvHtcrghRzqWcI+xc5rKpgYp+FtTcY+Bfy14xCxXYrSDr7mz/nxqCRetnujn
    ebTAZBos9IHEbKGKpkdSBoKXe+vMYPDTFZmDHHMt/PWRqMyJPVyGiMQc/ViXoHhf
    v9KeH/9hqpr0MO3SOGPTPfV7nd9q3lnMWWgllhUPAoGAesQ2ItKlppmOG7mnLmz8
    9x5nxc3jJIktMusIQ0WxGnXdZDMTBfIsdZ1eCd7UbX4qfUXq8+JaYMEZ2z7GdJ8n
    y6UZ9hdEw61n7UxekFLUNLST7d/JnShaOdPmmX9oDoQuFEJvgGGRflQqBFoX5bsK
    xY53m5DJwquL0JUzzQZ9M=
    -----END PRIVATE KEY-----`;
    
    const SHEET_NAME = "Data_Cloud_Query";
    const LIMIT_ROWS = 100000;
    
    function run_DataCloudQuery_JWT_toSheets() {
      const token = getAccessTokenByJwt_();
      const accessToken = token.access_token;
      const instanceUrl = token.instance_url;
    
      const sql = `
    
    SELECT
        a.Id__c AS UnifiedId,
        c.ssot__ExternalRecordId__c AS Id,
        c.ssot__FirstName__c AS FirstName,
        c.ssot__LastName__c AS LastName,
    	d.ssot__EmailAddress__c AS CurrentEmail,
        b.ssot__Name__c AS SegmentName,
        a.Delta_Type__c AS DeltaType,
        a.Timestamp__c + interval '9 hour' AS PublishDateTime
    FROM Individual_Unified_SM_1755567709613__dlm a /* ← 自分の Unified Individual - Latest DMO の API 名に変更する */
    JOIN ssot__MarketSegment__dlm b
        ON a.Segment_Id__c = SUBSTRING(b.ssot__Id__c, 1, 15)
    JOIN UnifiedIndividual__dlm c /* ← 自分の Unified Individual DMO の API 名に変更する */
        ON a.Id__c = c.ssot__Id__c
    LEFT JOIN ssot__ContactPointEmail__dlm d
        ON c.ssot__ExternalRecordId__c = d.ssot__PartyId__c
    -- WHERE b.ssot__Name__c = '***' /*Segment Name Filter*/
    -- WHERE c.ssot__ExternalRecordId__c = '***' /*Individual ID Filter*/
    -- WHERE c.ssot__FirstName__c = '***' AND c.ssot__LastName__c = '***' /*Full Name Filter*/
    ORDER BY a.Timestamp__c DESC, a.Delta_Type__c DESC
    
    LIMIT ${LIMIT_ROWS}
    `.trim();
    
      const body = postSqlSync_(instanceUrl, accessToken, sql);
    
      const headers = extractSelectHeaders_(sql);
    
      writeDataCloudResultToSheet_(SHEET_NAME, body, headers);
    }
    
    function getAccessTokenByJwt_() {
      const now = Math.floor(Date.now() / 1000);
      const jwtPayload = {
        iss: CLIENT_ID,
        sub: USERNAME,
        aud: SF_LOGIN_URL,
        exp: now + 180
      };
    
      const assertion = buildJwtRs256_(jwtPayload, PRIVATE_KEY_PEM);
    
      const url = `${SF_LOGIN_URL}/services/oauth2/token`;
      const res = UrlFetchApp.fetch(url, {
        method: "post",
        payload: {
          grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
          assertion
        },
        muteHttpExceptions: true
      });
    
      const code = res.getResponseCode();
      const text = res.getContentText();
    
      Logger.log("=== JWT token ===");
      Logger.log("HTTP: " + code);
      Logger.log("Body(first 2000): " + (text ? text.slice(0, 2000) : ""));
    
      if (code >= 300) throw new Error(`JWT token failed: HTTP ${code} / ${text}`);
    
      const obj = JSON.parse(text);
      if (!obj.access_token || !obj.instance_url) {
        throw new Error("Token response missing access_token/instance_url: " + text);
      }
      return obj;
    }
    
    function buildJwtRs256_(payloadObj, privateKeyPem) {
      const header = { alg: "RS256", typ: "JWT" };
      const encHeader = base64UrlEncode_(JSON.stringify(header));
      const encPayload = base64UrlEncode_(JSON.stringify(payloadObj));
      const signingInput = `${encHeader}.${encPayload}`;
    
      const signatureBytes = Utilities.computeRsaSha256Signature(signingInput, privateKeyPem);
      const encSig = base64UrlEncodeBytes_(signatureBytes);
    
      return `${signingInput}.${encSig}`;
    }
    
    function base64UrlEncode_(str) {
      return base64UrlEncodeBytes_(Utilities.newBlob(str).getBytes());
    }
    
    function base64UrlEncodeBytes_(bytes) {
      const b64 = Utilities.base64Encode(bytes);
      return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
    }
    
    function postSqlSync_(instanceUrl, accessToken, sql) {
      const url = `${instanceUrl}/services/data/v${API_VERSION}/ssot/query-sql`;
    
      const res = UrlFetchApp.fetch(url, {
        method: "post",
        contentType: "application/json",
        headers: { Authorization: `Bearer ${accessToken}` },
        payload: JSON.stringify({ sql }),
        muteHttpExceptions: true
      });
    
      const code = res.getResponseCode();
      const text = res.getContentText();
    
      Logger.log("=== Data Cloud query ===");
      Logger.log("HTTP: " + code);
      Logger.log("Body(first 2000): " + (text ? text.slice(0, 2000) : ""));
    
      if (code >= 300) throw new Error(`Query failed: HTTP ${code} / ${text}`);
    
      const obj = JSON.parse(text);
      if (!obj.data) throw new Error("No data in response (unexpected format): " + text.slice(0, 2000));
      return obj;
    }
    
    function extractSelectHeaders_(sql) {
      const cleaned = stripSqlComments_(sql).trim();
    
      const m = cleaned.match(/\bselect\b([\s\S]*?)\bfrom\b/i);
      if (!m) return null;
    
      const selectPart = m[1].trim();
      if (selectPart === "*" || /\*/.test(selectPart)) return null;
    
      const items = splitByCommaOutsideParens_(selectPart);
    
      const headers = items.map(expr => headerFromSelectItem_(expr)).filter(Boolean);
      return headers.length ? headers : null;
    }
    
    function stripSqlComments_(sql) {
      return sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "");
    }
    
    function splitByCommaOutsideParens_(s) {
      const out = [];
      let buf = "";
      let depth = 0;
    
      for (let i = 0; i < s.length; i++) {
        const ch = s[i];
        if (ch === "(") depth++;
        if (ch === ")") depth = Math.max(0, depth - 1);
    
        if (ch === "," && depth === 0) {
          out.push(buf.trim());
          buf = "";
        } else {
          buf += ch;
        }
      }
    
      if (buf.trim()) out.push(buf.trim());
      return out;
    }
    
    function headerFromSelectItem_(item) {
      const t = item.trim().replace(/\s+/g, " ");
    
      const asMatch = t.match(/\s+as\s+([A-Za-z0-9_]+)$/i);
      if (asMatch) return asMatch[1];
    
      const parts = t.split(" ");
      if (parts.length >= 2) {
        const last = parts[parts.length - 1];
        const beforeLast = parts.slice(0, -1).join(" ");
        if (!/[().]/.test(last) && /[().]/.test(beforeLast) === false) {
          return last;
        }
      }
    
      const col = t.replace(/^.*\./, "");
      return col;
    }
    
    function writeDataCloudResultToSheet_(sheetName, body, explicitHeaders) {
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
      sheet.clearContents();
    
      const data = body.data;
      if (!Array.isArray(data) || data.length === 0) {
        sheet.getRange(1, 1).setValue("No rows.");
        return;
      }
    
      if (Array.isArray(data[0])) {
        const colCount = data[0].length;
    
        const headers =
          Array.isArray(explicitHeaders) && explicitHeaders.length === colCount
            ? explicitHeaders
            : Array.from({ length: colCount }, (_, i) => `col_${i + 1}`);
    
        const CHUNK = 5000;
        sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    
        for (let i = 0; i < data.length; i += CHUNK) {
          const chunk = data.slice(i, i + CHUNK);
          sheet.getRange(2 + i, 1, chunk.length, headers.length).setValues(chunk);
        }
        return;
      }
    
      sheet.getRange(1, 1).setValue("Unexpected data format.");
      sheet.getRange(2, 1).setValue(JSON.stringify(body).slice(0, 5000));
    }

    設定が必要な項目

    以下の情報は、必ず自身の環境の値に置き換えてください。

    • クライアント ID(コンシューマーキー)

    • 実行ユーザー名(権限が付与されているあなたのユーザー名)

    • 秘密鍵(JWT 認証用)

    const SF_LOGIN_URL = "https://login.salesforce.com"; // Sandbox: https://test.salesforce.com
    const CLIENT_ID = "3MVG9GIRg253.lotxyT7qsSZdhj8ADPVk6.9Z4NsPAuxUAfTfanfzFuk3MV4iTm22RhAYt_rxlIC4hyLKIL";
    const USERNAME = "admin@nac-care.com";
    const API_VERSION = "66.0";
    
    const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
    MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtTcj7mjX2Kguo
    ahVavRHq+cr+35h3b7jBarCzdlaoJZoayVI6I/NNZzMSMcR+DGhfVG0xXOHubQ2q
    kO6K+FSlMj/wyi2alpq7hS6CCoYwimfuPI4+CnNrVIQiksMe026kXmZ8tEpW6yV5
    lGmC/85p99bE8Pm0CgnlNcsfflx7gYtum9nAVzyu4cqJjYEdSudXh4eSnse0C0Tw
    dWs27aMOz9+7/7QX6QxdgSwkoorkR7KplSE8qrxAoVxPQomzP2ITetaHExbRh+LH
    qC8/wzLo/1uQuFi6v9sp3O6DIcv+K7fNg56o3ATCM27Xt7sFPS+sqIU7LW7mhMja
    CCPs64irAgMBAAECggEAERqQL2S01qqno+N0YBQw5IPqqOTgY0k/brdc4RlYzBeJ
    8gLUfrB1nroErFMFFXucAWyPqkOEeMeChcbwA/8mO3eOH/GUNqGOe9tVD7iCLeA7
    CaQoVa8qXPlmYRMi9rPfQ5Gdg8k3XQSwGiOvliIw+Pxg0ecGfeJPv7NjbKRH9FhX
    FZ0h8v67VPmKIAmRvEfOBV2G8Qp6vIWwS+bCGHspUtqzeIdPDbWbqjSsiddEGqeD
    /pDpvcseMIclaKzY0za0DeR9x2iDTnJPIQ/BxE55RAO/vnkjyo/1Wn0UcGe5E5KE
    3lOFe0gpFObtmHqPaQnO25xJ6EjlmJHFZ9mRmjhYoQKBgQDZccMNagVRJW7WOz/H
    A2D3LFVXQ1247ArD4CA9P3BFNJF2GCLMs3vOOGXKCbnVssW892s9zv6DAoFsbuka
    om4vwocBPXuTrqdnIy0K8IuwQnOzGMyHskO629m46WOG0FfdvG4PMOvwZ8HvkQQJ
    QG7e4eAJNlPbkAvSfD+Hz0TTXQKBgQDMCGU+Ui1BrGcduQWfRbT4Cc8Rz5f7qhAl
    LS8arp0d+HtF5AeqjtRpX4NKGX3sr9xBIZVfwla7jWOe0x6jd20Co5KjlGKvWR2w
    i6tRHGMk0/FOxcdQVR7hWw5+DSaLBmwi3AhE8ro8rqsGqVPqnutzP9BXG7Hr8BDe
    jfPucxDTpwKBgQCX5kbSGhw4waOZ+K3nAs88HDZJzX+tbQdgKjObVbPCRKTREK9O
    vJtiRjelWgH97PMBvP2nofBd6OQssZYZyxqaNpRFI4QueLXs8L/Igp2ytdlJZauL
    p9Z0tJx19mRWizi2Z6mi5xQLTxBFoNJm/CH3hWcSSGdwXEJF+hIPd5Wm6QKBgBMe
    UkZVsvHtcrghRzqWcI+xc5rKpgYp+FtTcY+Bfy14xCxXYrSDr7mz/nxqCRetnujn
    ebTAZBos9IHEbKGKpkdSBoKXe+vMYPDTFZmDHHMt/PWRqMyJPVyGiMQc/ViXoHhf
    v9KeH/9hqpr0MO3SOGPTPfV7nd9q3lnMWWgllhUPAoGAesQ2ItKlppmOG7mnLmz8
    9x5nxc3jJIktMusIQ0WxGnXdZDMTBfIsdZ1eCd7UbX4qfUXq8+JaYMEZ2z7GdJ8n
    y6UZ9hdEw61n7UxekFLUNLST7d/JnShaOdPmmX9oDoQuFEJvgGGRflQqBFoX5bsK
    xY53m5DJwquL0JzQZ9M=
    -----END PRIVATE KEY-----`;

    注意点

    本方法では個人情報を扱うため、取り扱いには十分注意してください。

    • アクセス権限の管理

    • データの共有範囲

    • 保管場所の管理

    これらを適切に設計したうえで利用することが重要です。

    スクリプトを実行することで、スプレッドシートに以下のように表示されます。

    画像

    いかがでしたでしょうか。

    本記事で紹介した方法により、「個人がどのセグメントに含まれているか」を可視化できるようになります。

    これは以下のような場面で有効です。

    • セグメントの検証

    • 配信対象の確認

    • パーソナライズ設計の前提確認

    • トラブルシューティング

    Marketing Cloud Next Growth & Advanced Edition におけるセグメントは、内部の状態が見えにくい構造になっているため、実際のデータを直接確認する手段を持っておくことは重要です。

    前述した以下の機能もお試しになって、何が相応しいかを確認してみてください。

    今回は以上です。


    次の記事はこちら

    前回の記事はこちら

    私の note のトップページはこちら

     
     
     
    Salesforce Marketing Cloud、Agentforce、Data Cloud、Salesforce 認定資格に関する実践的な情報を発信しています。これらの記事が、皆さまの学習や日々の業務に少しでもお役に立てば幸いです。