Apex

DISTANCEとGEOLOCATIONで距離条件を集計する書き方

商談で選択したレコードごとに、位置情報が一定距離内にあるレコードの件数を数えてページへ表示します。DISTANCE関数とGEOLOCATION関数の書式、文字列連結のSOQLをバインド変数に直した理由、1レコードごとにクエリが必要になる制約まで説明します。

2024.11.14

なぜ距離で絞り込むのか

商談の位置から一定距離にある拠点だけを数えたい、という要件はよくあります。緯度・経度をApexで取り出し、三角関数で距離を計算してからループで絞り込む実装も書けますが、計算式を自分で持つ分だけコードが増え、間違いも起きやすくなります。

SOQLにはDISTANCE関数とGEOLOCATION関数があり、距離の条件をクエリのWHERE句にそのまま書けます。Apex側に距離計算のロジックを持たずに済みます。

DISTANCE関数とGEOLOCATION関数の書式

関数書式使える場所
GEOLOCATIONGEOLOCATION(緯度, 経度)WHERE句・ORDER BY句(SELECT句では使えません)
DISTANCEDISTANCE(位置情報項目, GEOLOCATION(...), '単位')SELECT句・WHERE句・ORDER BY句(GROUP BY句では使えません)

単位は'km'(キロメートル)か'mi'(マイル)のどちらかです。DISTANCE関数が対応する演算子は>と<だけです。指定した半径の内側・外側を絞り込む形にしか使えません。

DISTANCE関数の引数は「位置情報項目が先、GEOLOCATIONが後」の順で固定です。逆にするとクエリが構文エラーになります。

対応する項目の型は、緯度・経度をひとまとめに持つGeolocationカスタム項目と、ジオコーディング済みの標準の住所項目です。緯度・経度のどちらかがnullのレコードは、距離の計算から除外されます。

Visualforceページ

商談の一覧から複数選択して実行するボタンです。選択した商談ごとに、一定距離内のカスタムオブジェクトのレコード数を表として表示します。

<apex:page standardController="Opportunity" recordSetVar="records" apiVersion="67.0" lightningStyleSheets="true" extensions="SummaryOfFixedDistanceNumbers">
    <apex:pageMessages id="pageMessages" />
    <apex:form>
        <apex:pageBlock title="商談からの一定距離のレコード数">
            <apex:pageBlockTable value="{!targetList}" var="target">
                <apex:column headerValue="商談Id">
                    <apex:outputLink value="/{!target.id}" target="_blank">{!target.id}</apex:outputLink>
                </apex:column>
                <apex:column headerValue="一定距離のレコード数" value="{!target.summaryOfFixedDistanceNumbers}" />
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>

商談の一覧ビューに置くカスタムボタンから開くページです。選択レコードだけを対象にします。

Apexクラス

public with sharing class SummaryOfFixedDistanceNumbers {
    public final String INFO_MSG = 'エラー';
    public final Integer MAXNUM = 99;
    public List<Opportunity> selectOppList {get; set;}
    public List<targetOpportunity> targetList {get; set;}

    public SummaryOfFixedDistanceNumbers(ApexPages.StandardSetController controller) {
        this.selectOppList = controller.getSelected();
        this.targetList = new List<targetOpportunity>();
        searchTargetOpportunity();
    }

    void searchTargetOpportunity() {
        List<Opportunity> oppList = [
            SELECT Id, MapData__c
            FROM Opportunity
            WHERE Id IN :(new Map<Id, Opportunity>(selectOppList)).keySet()
            AND MapData__Latitude__s != null
            AND MapData__Longitude__s != null
            ORDER BY Id ASC
            LIMIT 100
        ];
        if (oppList.size() == 0) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, INFO_MSG));
        } else if (oppList.size() > MAXNUM) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, INFO_MSG));
        } else {
            Double minDistanceKm = 0.1;
            for (Opportunity opp : oppList) {
                Location oppLocation = opp.MapData__c;
                Double oppLatitude = oppLocation.latitude;
                Double oppLongitude = oppLocation.longitude;
                List<testObject1__c> propertyList = [
                    SELECT Id, MapData__c
                    FROM testObject1__c
                    WHERE MapData__Latitude__s != null
                    AND MapData__Longitude__s != null
                    AND DISTANCE(MapData__c, GEOLOCATION(:oppLatitude, :oppLongitude), 'km') > :minDistanceKm
                    ORDER BY Id ASC
                ];
                if (propertyList.size() > 0) {
                    targetOpportunity target = new targetOpportunity();
                    target.id = opp.Id;
                    target.summaryOfFixedDistanceNumbers = propertyList.size();
                    this.targetList.add(target);
                }
            }
        }
    }

    public class targetOpportunity {
        public String id {get; set;}
        public Integer summaryOfFixedDistanceNumbers {get; set;}
    }
}

選択した商談ごとに、距離条件を満たすカスタムオブジェクトの件数を数えます。

文字列連結のSOQLをバインド変数に直した理由

緯度・経度を文字列として連結し、Database.query(soql)で動的SOQLを組み立てる書き方もありますが、文字列連結でSOQLを組み立てる書き方は原則として使いません。公式リファレンスが明記しているバインド変数の制約は「DISTANCE関数の単位パラメータには、Apexのバインド変数を使えない」という1点だけです。緯度・経度の値は、GEOLOCATION関数の引数として通常のバインド変数(:oppLatitude)がそのまま使えます。単位の'km'だけをリテラルの文字列として残せば、文字列連結もDatabase.queryも不要になります。上のApexクラスは、この形で書いています。

それでも1レコードごとにクエリが必要になる理由

DISTANCE関数は、1回のクエリにつき1つの基準点しか受け取れません。商談ごとに基準点が変わるこの処理では、商談の件数だけカスタムオブジェクトへのクエリが必要になる構造は、書き方を直しても変わりません。

同期処理でSOQLを発行できる回数は、1トランザクションあたり100回までです。商談の一覧を取得するクエリが1回、商談1件につきカスタムオブジェクトへのクエリが1回発生するので、選択した商談が99件を超えると100回を超えます。コードのMAXNUM = 99は、この壁に収まるように置かれた安全装置です。件数がさらに多い場合は、Batch Apexへ移してください。executeが呼ばれるたびにガバナ制限がリセットされ、非同期処理のSOQL発行回数の上限は200回に広がります。

targetListはapex:formの中でapex:pageBlockTableにバインドされているため、ビューステートに乗ります。ビューステートのサイズには170KBという上限があり、超えるとMaximum view state size limit (170KB) exceededというエラーで画面が表示できなくなります。集計対象の商談が増えるページでは、表示に使わない項目まで保持しないよう注意してください。

動作確認の手順

  1. 商談レコードを作成する

    位置情報項目(MapData)に緯度・経度を入れて作成します。

  2. カスタムオブジェクトのレコードを複数作成する

    testObject1__cのレコードを、緯度・経度が入る状態で複数作成します。

  3. 商談の一覧からボタンを実行する

    集計したい商談を選択し、一覧ビューのボタンからこの拡張機能を開きます。

  4. 結果を確認する

    選択した商談ごとに、一定距離内のカスタムオブジェクトの件数が表に表示されることを確認します。

商談テスト1の詳細画面。MapData項目に緯度と経度の値が入っている
動作確認に使う商談です。MapData項目に緯度・経度が入っているかを確認してください。
カスタムオブジェクトtestObject1のレコードTO1-0012の詳細画面
集計される側のレコードです。testObject1側にも緯度・経度が入っている必要があります。
testObject1のレコードTO1-0013の詳細画面。MapDataの緯度経度が異なる
2件目のレコードです。MapDataの値が1件ずつ違う位置になっている点を見てください。
testObject1のレコードTO1-0014の詳細画面。MapDataに緯度経度が入っている
3件目のレコードです。緯度・経度がnullのレコードは距離の計算から外れるので入力します。
testObject1のレコードTO1-0015の詳細画面。MapDataに緯度経度が入っている
4件目のレコードです。この4件が、あとで見る集計結果の「4」に対応します。
商談の一覧ビュー。1件を選択した状態で、右上のカスタムボタンが赤枠で囲まれている
商談を選んでから、右上の赤枠のボタンを押します。選択したレコードだけが対象になります。
集計結果の表。商談IDと、一定距離のレコード数4が赤枠で示されている
実行した結果です。選択した商談ごとに、距離条件を満たしたレコード件数が並びます。

ここで間違えやすい

間違い何が起きるか
DISTANCEとGEOLOCATIONの引数の順序を逆にするクエリが構文エラーになります
単位パラメータにバインド変数を使おうとする単位だけはバインド変数に対応していません
文字列連結で動的SOQLを組み立てる意図しないクエリになる危険があり、保守もしにくくなります
緯度・経度を入力し忘れるnullのレコードは距離の計算から外れ、件数に含まれません
選択レコード数の上限チェックを外すSOQL発行回数の上限(100回)に届いて失敗します

確認した環境

  • 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点の公式リファレンスで、DISTANCE関数・GEOLOCATION関数の書式とガバナ制限、ビューステートの上限を確認しています
  • コードはAPIバージョン67.0で書いています

まとめ

  • DISTANCE関数とGEOLOCATION関数を使うと、距離の条件をSOQLのWHERE句にそのまま書けます
  • 単位パラメータだけはバインド変数に対応していません。緯度・経度や比較する数値は通常のバインド変数で渡せます
  • 文字列連結で動的SOQLを組み立てる書き方はやめて、静的SOQL+バインド変数に直します
  • DISTANCE関数は基準点を1つしか取れないため、基準点が変わる件数だけクエリが必要になります。同期処理のSOQL発行回数は100回までです
  • 選択件数が増える場合は、Batch Apexへ移してガバナ制限をリセットしながら処理します

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。