2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【備忘録】SOQLとSQLは何が違う? ― JOINがない理由をリレーションで読む

2
Posted at

はじめに

SQLに慣れた状態でSalesforceの SOQL(Salesforce Object Query Language) を書き始めると、多くの人が最初にこう感じると思います。

「あれ、JOIN が書けない……?」

SELECT も WHERE も ORDER BY もあるのに、JOIN ... ON だけが見当たりません。最初は「機能が足りないSQL」に見えるのですが、調べていくと 「JOINがない」のではなく「JOINの代わりにリレーションをたどる言語」 と捉えたほうが理解しやすいと感じました。

本記事では、次の観点で整理します。

  • SQLとSOQLの根本的な考え方の違い
  • 子→親(ドット表記)と親→子(サブクエリ)の2つのたどり方
  • SQLの JOIN / EXISTS / SELECT * を SOQL でどう読み替えるか
  • SQL経験者がつまずきやすいポイント

※本記事は個人の整理メモです。内容は2026-10-13時点で確認した公式ドキュメントをもとにしていますが、上限値などはAPIバージョンで変わることがあるため、実装時は必ず公式ドキュメントを確認してください。


1. まず全体像:表をつなぐか、関係をたどるか

fig_01_sql_vs_soql.png

SQLのJOINでは、2つのテーブルをどうつなぐかを クエリを書く人が ON 句で決めます。この例では、親の値が子の行ごとに繰り返される フラットな表 になります。

-- SQL:結合条件はクエリ側で指定
SELECT a.name, c.last_name
FROM account a
JOIN contact c ON c.account_id = a.id;

一方SOQLでは、オブジェクト同士のつながりは 項目定義(参照関係・主従関係)として、あらかじめスキーマ側に定義 されています。クエリはその定義済みのリレーションを「名前でたどる」だけです。

-- SOQL:定義済みのリレーション名(Contacts)でたどる
SELECT Name, (SELECT LastName FROM Contacts)
FROM Account

この親→子のクエリでは、親レコードの中に子レコードのリストが入れ子になったオブジェクトの形 で返ります(ApexならAccountのsObjectに Contacts のリストがぶら下がる形)。

※図1と上のコードは、結果の構造を比較する例です。SQL例の JOIN は内部結合なのでContactのないAccountを除外しますが、SOQL例は子のないAccountも返します。取得対象まで完全に同じになるわけではありません。

👉 SQLのJOINは「表をつなぐ」、SOQLのリレーションクエリは「オブジェクトの関係をたどる」と整理すると、書き方の違いを理解しやすそうです。

なぜこういう設計なのか(個人的な理解)

ここでは公式の仕様説明と分けて、私なりの理解を整理します。以下は、SOQLが JOIN ... ON を持たない設計理由として公式に確認したものではありません。

  • Salesforceはマルチテナント環境で、利用者が直接テーブル構造に触れない(オブジェクトという抽象を通して扱う)
  • オブジェクト間の関係は メタデータ(参照関係/主従関係の項目) として管理され、画面やAPI、レポートなどでも使われる
  • クエリも「定義済みの関係をたどる」と考えると、Salesforceのデータモデルに沿って理解しやすい

と捉えると納得しやすそうです。


2. 子 → 親:ドット表記でたどる

fig_02_child_to_parent.png

子オブジェクトから親オブジェクトの項目を取りたい場合は、リレーション名を . でつなぎます。

SELECT LastName, Account.Name, Account.Owner.Name
FROM Contact

SQLでいえば contact → account → user と2段階の結合を書くような参照を、ドット表記で表現できます。ただし、親がない場合の扱いは内部結合とは異なります。

つまずきポイント

  • 項目名ではなくリレーション名を書く
    • 標準項目:参照項目 AccountId に対して、リレーション名は Account
    • カスタム項目:Invoice__c に対して、リレーション名は Invoice__r(__c → __r)
  • 子→親は最大5階層まで たどれます
  • 上のように親の存在を条件にしないクエリでは、参照項目が空のレコードも結果に残り、親側の項目は null になります(SQLの左外部結合に近い挙動)。WHERE Account.Name = 'Acme' のような条件を加えた場合は、その条件を満たすレコードだけが返ります
  • 1クエリで指定できる子→親リレーションの数にも上限があります(公式ドキュメントでは55)

ポリモーフィックな参照項目(ToDoの WhatId / WhoId など)は、参照先の型によって取りたい項目が変わるため、TYPEOF 句を使う方法も用意されています。


3. 親 → 子:サブクエリでたどる

fig_03_parent_to_child.png

逆に、親から子の一覧を取りたい場合は、SELECT句の中にサブクエリ を書きます。

SELECT Name,
  (SELECT LastName FROM Contacts WHERE Email != null)
FROM Account

ここで一番間違えやすいのが、サブクエリの FROM に書くのが オブジェクト名(Contact)ではなく子リレーション名(Contacts) という点です。

  • 標準オブジェクト:Contacts, Opportunities, Cases など(多くは複数形)
  • カスタムリレーション:Invoices__r のように __r 付き

子リレーション名はオブジェクトマネージャーの参照項目定義や、describe の結果で確認できます。

※図3の「複数形」は標準リレーションの名前の傾向を表しています。オブジェクト名を複数形にすれば必ず使えるわけではありません。カスタムリレーションでは、設定された子リレーション名に __r を付けて使います。

返り方は「左外部結合」に近い

このサブクエリは、子が1件もない親も結果に残ります。また、子側の WHERE Email != null に合う子がいない場合も、親は結果に残ります。SQLの LEFT JOIN に近いイメージです。

「子が存在する親だけ」に絞りたい場合は、次の semi-join(IN サブクエリ)を使います。

-- 受注済み商談を持つ取引先だけを取得(semi-join)
SELECT Id, Name
FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Won')

-- 商談を1件も持たない取引先(anti-join)
SELECT Id, Name
FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Opportunity)

👉 「親→子のサブクエリ(SELECT句)」と「semi-join / anti-join(WHERE句)」は役割が違う、と分けて覚えると混乱しにくいです。

なお、親→子のサブクエリは1クエリあたりの数(公式ドキュメントでは20)や、ネストの深さにも上限があります。親を第1階層として最大5階層(親から子方向に4段階)まで取得できますが、対応バージョンは経路によって異なります。REST・SOAP APIでは API 58.0以降、Apex では API 61.0(Summer '24)以降が対象です。ただし、Bulk API / Bulk API 2.0、外部オブジェクト、Big Objectでは、この5階層取得は利用できません。


4. SQL → SOQL 読み替えチートシート

fig_04_cheatsheet.png

SQLの感覚で書きたくなるものを、SOQLでどう表現するかを表にしました。

やりたいこと SQL SOQL
親の条件で子を絞る INNER JOIN ... ON + WHERE WHERE Account.Name = 'Acme'
左外部結合(親→子) LEFT JOIN ... ON (SELECT ... FROM Contacts)
存在するものだけ(semi-join) WHERE EXISTS / IN WHERE Id IN (SELECT AccountId ...)
存在しないもの(anti-join) WHERE NOT EXISTS WHERE Id NOT IN (SELECT ...)
全項目取得 SELECT * FIELDS(ALL) など(制約あり)
任意の列同士の結合 JOIN ... ON a.x = b.y 不可(リレーション経由のみ)
更新・削除 UPDATE / DELETE SOQLでは不可(Apex DML / API)

※この表は「やりたいこと」に応じた読み替えの目安です。たとえば親の条件で子レコードを絞ることはできますが、SQLのINNER JOINとSOQLのリレーションクエリは、取得結果の形や表現できる操作が完全に同じではありません。

JOIN以外にも違うところ

JOIN以外にも、SQL経験者が「おや?」となる違いがいくつかあります。

  • SELECT * がない
    • 代わりに FIELDS(ALL) / FIELDS(STANDARD) / FIELDS(CUSTOM) がある
    • FIELDS(STANDARD) はApexでも利用できます。一方、FIELDS(ALL) と FIELDS(CUSTOM) はApexのインラインSOQL・動的SOQLやBulk API 2.0では利用できません。REST/SOAP APIなどで使う場合も、LIMIT 200 以下を指定するなど、取得件数を限定する必要があります。基本は必要な項目を列挙するのが無難です
  • 読み取り専用
    • SOQLは検索専用で、作成・更新・削除は Apex の DML(insert / update / delete)や REST/SOAP API で行う
  • 任意の結合ができない
    • 参照関係のない2オブジェクトを、メールアドレスなどの値で突き合わせることはできない。必要ならクエリを分けてApex側で突き合わせるか、リレーションを設計する
  • 日付リテラルが便利
    • WHERE CreatedDate = LAST_N_DAYS:30 や THIS_MONTH など、SOQLの日付リテラルで期間を表せる
  • ガバナ制限がある
    • Apexの同期処理では1トランザクションあたりSOQL発行は100回まで、取得行数は合計50,000行までなど。ループの中でSOQLを投げる書き方(いわゆる「SOQL in loop」)は避ける
  • 全文検索は別言語
    • 複数オブジェクトをまたいだキーワード検索は SOSL(Salesforce Object Search Language)の担当

5. 試して気づいたこと:「JOINしたい」と思ったらリレーションを確認する

SOQLで「この2つをJOINしたいのに書けない」と感じたときは、次の順で考えると整理しやすいと感じました。

  1. そもそもリレーション(参照関係/主従関係)は定義されているか?
    • 定義されていれば、ドット表記かサブクエリでたどれる
  2. 取りたいのは「親の情報」か「子の一覧」か?
    • 親の情報 → 子起点でドット表記
    • 子の一覧 → 親起点でサブクエリ
  3. 絞り込みたいだけなら semi-join で足りないか?
  4. どうしてもリレーションがないなら、クエリを分けて Apex で Map を使って突き合わせる
// リレーションがない2オブジェクトを Apex 側で突き合わせる例
Map<String, Lead> leadsByEmail = new Map<String, Lead>();
for (Lead l : [SELECT Id, Email FROM Lead WHERE Email != null]) {
    leadsByEmail.put(l.Email.toLowerCase(), l);
}
for (Contact c : [SELECT Id, Email FROM Contact WHERE Email IN :leadsByEmail.keySet()]) {
    Lead matched = leadsByEmail.get(c.Email.toLowerCase());
    // ...
}

※これは説明用の簡略例です。この例で使うEmail項目のSOQLの = / IN 比較は大文字・小文字を区別しません。一方、Apexの通常の Map<String, ...> のキーは区別するため、toLowerCase() で小文字にそろえています。SOQLの文字列比較には例外があり、大文字・小文字を区別する設定の一意項目では区別されます。

同じメールアドレスのLeadが複数存在する場合、Map<String, Lead> には1件しか保持されません。複数件を扱う場合は Map<String, List<Lead>> などを検討します。

また、この例はEmailのあるLeadを全件取得します。実運用では対象を絞り、LeadとContactの取得行数の合計や、Mapに保持するデータのヒープ使用量を考慮してください。SOQL forループを使っていても、1トランザクションあたりの取得行数の上限はなくなりません。

👉 「JOINが書けない」場合は、まず データモデルに適切なリレーションがあるか を確認すると整理しやすそうです。リレーションがないこと自体が、必ずしも設計ミスというわけではありません。


まとめ

  • SQLは「表を ON 句でつなぐ」言語、SOQLは「定義済みのリレーションをたどる」言語
  • 子→親は ドット表記(Account.Name / Invoice__r.Name)
  • 親→子は 子リレーション名を使ったサブクエリ(FROM Contacts / FROM Invoices__r)
  • 存在チェックは semi-join / anti-join(WHERE Id IN (SELECT ...))
  • SELECT *・任意結合・更新系はなく、代わりに FIELDS()・Apex側の突き合わせ・DML を使う

SOQLは、任意の結合条件を書くのではなく、スキーマに定義されたリレーションをたどる言語 と捉えると理解しやすそうです。


参考(公式情報)

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?