DISTANCEとGEOLOCATIONで距離条件を集計する書き方
商談で選択したレコードごとに、位置情報が一定距離内にあるレコードの件数を数えてページへ表示します。DISTANCE関数とGEOLOCATION関数の書式、文字列連結のSOQLをバインド変数に直した理由、1レコードごとにクエリが必要になる制約まで説明します。
なぜ距離で絞り込むのか
商談の位置から一定距離にある拠点だけを数えたい、という要件はよくあります。緯度・経度をApexで取り出し、三角関数で距離を計算してからループで絞り込む実装も書けますが、計算式を自分で持つ分だけコードが増え、間違いも起きやすくなります。
SOQLにはDISTANCE関数とGEOLOCATION関数があり、距離の条件をクエリのWHERE句にそのまま書けます。Apex側に距離計算のロジックを持たずに済みます。
DISTANCE関数とGEOLOCATION関数の書式
| 関数 | 書式 | 使える場所 |
|---|---|---|
| GEOLOCATION | GEOLOCATION(緯度, 経度) | WHERE句・ORDER BY句(SELECT句では使えません) |
| DISTANCE | DISTANCE(位置情報項目, GEOLOCATION(...), '単位') | SELECT句・WHERE句・ORDER BY句(GROUP BY句では使えません) |
単位は'km'(キロメートル)か'mi'(マイル)のどちらかです。DISTANCE関数が対応する演算子は>と<だけです。指定した半径の内側・外側を絞り込む形にしか使えません。
対応する項目の型は、緯度・経度をひとまとめに持つ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というエラーで画面が表示できなくなります。集計対象の商談が増えるページでは、表示に使わない項目まで保持しないよう注意してください。動作確認の手順
商談レコードを作成する
位置情報項目(MapData)に緯度・経度を入れて作成します。
カスタムオブジェクトのレコードを複数作成する
testObject1__cのレコードを、緯度・経度が入る状態で複数作成します。
商談の一覧からボタンを実行する
集計したい商談を選択し、一覧ビューのボタンからこの拡張機能を開きます。
結果を確認する
選択した商談ごとに、一定距離内のカスタムオブジェクトの件数が表に表示されることを確認します。
ここで間違えやすい
| 間違い | 何が起きるか |
|---|---|
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との連携まで承ります。状況を伺ったうえで、進め方をご提案します。