LoginSignup
8
6

SOQL Tips

Last updated at Posted at 2022-12-03

SOQLの構文

簡易版
SELECT 項目/サブクエリ
FROM オブジェクト名
[WHERE 検索条件]
[GROUP BY 集計単位の項目 [HAVING 集計結果の検索条件]]
[ORDER BY 項目 {ASC|DESC} [NULLS {FIRST|LAST}]]
[LIMIT 検索結果の上限件数]
[OFFSET 無視する検索結果の行数]
完全版
SELECT fieldList [subquery][...]
[TYPEOF typeOfField whenExpression[...] elseExpression END][...]
FROM objectType[,...] 
    [USING SCOPE filterScope]
[WHERE conditionExpression]
[WITH [DATA CATEGORY] filteringExpression]
[GROUP BY {fieldGroupByList|ROLLUP (fieldSubtotalGroupByList)|CUBE (fieldSubtotalGroupByList)} 
    [HAVING havingConditionExpression] ] 
[ORDER BY fieldOrderByList {ASC|DESC} [NULLS {FIRST|LAST}] ]
[LIMIT numberOfRowsToReturn]
[OFFSET numberOfRowsToSkip]
[FOR {VIEW  | REFERENCE}[,...] ]
      [ UPDATE {TRACKING|VIEWSTAT}[,...] ]

クエリに関する制限

SOQLの制限

  • WHERE 句の文字列は 4,000 文字を超えることができません。
  • 子-親リレーション(SELECT Account.Nameなど)の制限
    • 1回のクエリに指定できる子-親リレーションは、55 個以下です。
    • 指定された各リレーションで、1 つの子-親リレーションに指定できるレベルは 5 つ以下です。たとえば、Contact.Account.Owner.FirstName は 3 レベルです。
  • 親-子リレーション(SELECT (SELECT Id FROM OpportunityLineItems)など)の制限
    • 1回のクエリに指定できる親-子リレーションは、20 個以下です。
    • REST、SOAPの場合、最大 5 レベルの親-子リレーションを照会できます(v58.0以降)。Big Object、外部オブジェクトとApex、Bulk APIでは、5レベルの親-子リレーションをサポートしていません。

リレーションクエリ制限について | SOQL および SOSL リファレンス | Salesforce Developers

トランザクション当たりのApex制限

  • SOQLクエリ回数:同期Apex 100、非同期Apex 200
  • SOQLクエリで取得するレコード件数(合計):50,000

親子リレーションのサブクエリを使用する SOQL クエリでは、各親子リレーションは追加クエリとみなされます。これらのクエリタイプは、最上位クエリ数の 3 倍に制限されています。サブクエリの制限は、Limits.getLimitAggregateQueries() が返す値に対応します。これらのリレーションクエリの行数は、全体のコード実行の行数に加算されます。この制限はカスタムメタデータ型には適用されません。1 つの Apex トランザクション内で、カスタムメタデータレコードの SOQL クエリは無制限です。

Salesforce プラットフォームの制限

  • ユーザごとに同時に開くクエリカーソルの制限
    • SOAP APIクエリカーソル最大数:10
    • メタデータAPIクエリカーソル最大数:10
    • Apexクエリカーソル最大数:50
    • Apex一括処理startメソッドのクエリカーソル最大数:15
    • Apex一括処理executeメソッドのクエリカーソル最大数:5
    • Apex一括処理finishメソッドのクエリカーソル最大数:5
    • Visualforceカーソル最大数:5
  • SOQLクエリの最大実行時間(タイムアウト):120 秒

クエリカーソルについて
クエリカーソルが最大数に達したとき、古いクエリカーソルから削除されます。queryMore()でクエリカーソルにアクセスします。このとき、クエリカーソルが存在していないと、INVALID_QUERY_LOCATORが発生します。
AttachmentのBodyなどbase64型を含むクエリは1件ごとにqueryMoreが発生します。APIを使用してbase64型を含むクエリを発行して繰り返し処理を実装すると長時間クエリカーソルへのアクセスを行うことになるので、上記のエラーが発生しやすくなります。そういった場合はbase64型項目をクエリする場合は1件ずつクエリを発行することをお勧めします。

文字数

SOQLの文法

日付型でフィルターする

yyyy-MM-ddの形式で比較します。TODAYなどの日付リテラルを使用できます。

SELECT Id
FROM Opportunity
WHERE CloseDate > 2022-05-16

日時型でフィルターする

yyyy-MM-ddThh:mm:ss+09:00の形式で比較します。+09:00の箇所はタイムゾーンです。日本時間の時は+09:00にします。TODAYなどの日付リテラルを使用できます。

SELECT Id
FROM Account
WHERE CreatedDate >= 2022-05-16T00:00:00+09:00 

複数選択リストでフィルターする

複数選択リストでフィルターする場合に使用できる演算子は=!=INCLUDESEXCLUDES;です。
公式リファレンスには記述がないですが、一応INも使えるようです。INは完全一致になるため誤用に注意です。

  • AAAのみ:Selectable__c = 'AAA'
  • AAAとBBBのみ:Selectable__c = 'AAA;BBB'
  • AAAを含む:Selectable__c INCLUDES ('AAA')
  • AAAとBBBを含む:Selectable__c INCLUDES ('AAA;BBB')
  • AAAとBBBを含む、またはCCCを含む:Selectable__c INCLUDES ('AAA;BBB', 'CCC')

ApexでINCLUDESにListを使用する場合、バインド変数が使用できないため動的クエリにする必要があります。

List<String> values = new List<String>{'AAA;BBB', 'CCC'};
String valuesAsString = '(\'' + String.join(values, '\', \'') + '\')';
String query = '';
query += 'SELECT Id FROM Account ';
query += 'WHERE Selectable__c INCLUDES ' + valuesAsString);
List<Account> accList = Database.query(query);

項目の値がリストに含まれているかでフィルターする

SELECT Id, Name
FROM Account
WHERE Name IN ('sForce', 'United Oil & Gas Corp.')
Set<String> names = new Set<String>{'sForce', 'United Oil & Gas Corp.'};
List<Account> accountList = [
    SELECT Id, Name 
    FROM Account 
    WHERE Name IN :names
];

項目の値がリストに含まれていないでフィルターする

SELECT Id, Name 
FROM Account 
WHERE Name NOT IN ('sForce', 'United Oil & Gas Corp.')
Set<String> names = new Set<String>{'sForce', 'United Oil & Gas Corp.'};
List<Account> accountList = [
    SELECT Id, Name 
    FROM Account 
    WHERE Name NOT IN :names
];

結果の一部だけ得る(オフセット)

LIMIT 取得件数OFFSET スキップする件数を使用します。
OFFSETは2000まで指定できます。2001以上を指定するとNUMBER_OUTSIDE_VALID_RANGEが発生します。
OFFSETを使用するとLIMITに2001以上指定していても2000件までしか取得できません。

/* 301件目から100件取得する */
SELECT Id
FROM Account
ORDER BY CreatedDate DESC
LIMIT 100
OFFSET 300

重複を除いた件数を取得する

COUNT_DISTINCTを使用します。NULLも除外されます。COUNTとガバナ制限「SOQLクエリで取得するレコード件数」のカウントの仕方が異なるので注意が必要です。COUNT_DISTINCTは集計対象レコードの数がガバナ制限の対象です。

SELECT COUNT(Id), COUNT_DISTINCT(Product2Id)
FROM OpportunityLineItem

WHERE句でサブクエリを使用する

項目 IN (SELECT 比較する項目 FROM ...)または項目 NOT IN (SELECT 比較する項目 FROM ...)のように記述することで、WHERE句でサブクエリを利用できます。
WHERE句でサブクエリを使用する場合、ORは使用できません。

/* OK */
SELECT Name FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE Id = '0065g00000Sr6EwAAJ')
SELECT Phone FROM Account WHERE (Phone != null) AND (Id IN (SELECT AccountId FROM Contact WHERE Phone != null))

/* NG: Semi join sub-selects are not allowed with the 'OR' operator */
SELECT Phone FROM Account WHERE (Phone != null) OR (Id IN (SELECT AccountId FROM Contact WHERE Phone != null))

SOQLをApexで使用する

動的にクエリを組み立てる

動的なクエリを文字列で作成し、Database.query()に渡すことでクエリを実行することができます。
静的なクエリとことなり、バインド変数に.は使用できません。使用した場合、Variable does not existとなります。
ユーザ・API・DBから渡された値は、SOQLインジェクションを避けるためString.escapeSingleQuotes()でエスケープする必要があります。

List<String> fields = new List<String>{'Id', 'Name'};
String name = 'Force';

String query = '';
query += 'SELECT ' + String.join(fields, ',') + ' ';
query += 'FROM Account ';
query += 'WHERE Name LIKE \'%' + String.escapeSingleQuotes(name) + '%\'';
List<SObject> fetched = Database.query(query);

クエリした結果を変数に代入する

SOQLの結果はSObjectのリストで返ります。1件も取得できなかった場合はnullではなく、空のリストが返却されます。

結果が1件の場合は、SObject型に代入できますが、確実に1件だけ取得できるときのみ使用できます。1件もない場合はQueryException: List has no rows for assignment to SObject、結果が複数ある場合はQueryException: List has more than 1 row for assignment to SObjectが発生します。

// SObjectのコレクション
List<Account> accList = [SELECT Id FROM Account];
Set<Account> accSet = new Set<Account>([SELECT Id FROM Account]);
Map<Id, Account> accMap  = new Map<Id, Account>([SELECT Id FROM Account]);

// SObject型
Account acc = [SELECT Name FROM Account WHERE Id = :accId LIMIT 1];

集計関数を使用する

集計関数を使用したSOQLの結果はAggregateResultオブジェクトのリストになります。各項目の値はAggregateResultオブジェクトのgetメソッドで取得します。Apexでも集計関数の別名を使用できます。

集計関数(別名)
for (AggregateResult ar : [
    SELECT AccountId, COUNT(Id) num, MAX(Amount) max, MIN(Amount) min, AVG(Amount) avg
    FROM Opportunity
    GROUP BY AccountId
]) {
    System.debug(LoggingLevel.DEBUG, ar);
    // AggregateResult:{AccountId=0015h00000SrA4lAAF, num=1, max=235000, min=235000, avg=235000.0}
    System.debug(LoggingLevel.DEBUG, 'accountId:' + ar.get('AccountId'));
    System.debug(LoggingLevel.DEBUG, ' max:' + ar.get('max'));
}

別名を使用しない場合、expr0などの暗黙的列名が使用されます。

集計関数(暗黙的列名)
for (AggregateResult ar : [
    SELECT AccountId, COUNT(Id) num, MAX(Amount), MIN(Amount), AVG(Amount)
    FROM Opportunity
    GROUP BY AccountId
]) {
    System.debug(LoggingLevel.DEBUG, ar);
    // AggregateResult:{AccountId=0015h00000SrA4lAAF, num=1, expr0=235000, expr1=235000, expr2=235000.0}
    System.debug(LoggingLevel.DEBUG, 'accountId:' + ar.get('AccountId'));
    System.debug(LoggingLevel.DEBUG, ' max:' + ar.get('expr0'));
}

ApexでSOQLを使用するときの考慮事項

  • クエリは極力セレクティブにします。特にトリガでは非セレクティブなクエリでクエリ対象が200,000件超える場合にはエラーが発生してしまいます。
  • クエリで取得したレコードで繰り返し処理を行うときはSOQL For ループを使用します。リストに詰めるよりヒープサイズを削減することができます。
  • 大量の子レコード(200件以上)を取得する可能性がある場合は、直接アクセスせずにforループで子レコードを反復処理をします。200件以上の子レコードに直接アクセスするとエラーになります。
  • COUNT() および COUNT(fieldname) 以外集計関数を使用するときはガバナ制限(Too many query rows)に注意が必要です。集計に含まれるレコード数もカウントされます。

その他

クエリのパフォーマンスを測定する

クエリプランツールを使用します。また、REST APIのqueryリソースにexplainパラメータを指定することでもパフォーマンスに関するフィードバックが得られます(ベータ機能)。

クエリにかかる時間を短縮したい

Visual Studio Codeでクエリを作成したい

Salesforce公式のVSCode拡張機能に含まれるSOQL Query Builderを使用する。

カスタムメタデータのソート順がおかしい

カスタムメタデータ型のクエリでは、ORDER BYする項目はSELECTしていないとソート順が保証されない、という不具合があります。

参考

8
6
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
8
6