SOQL for ループがなぜヒープ制限を超えるリスクを軽減できるのか

:::note warn
強くなりたい駆け出しSalesforceエンジニアが、ChatGptに相談しながら公式ドキュメントも参照して作成したアウトプット記事です。
日本のSalesforce業界を一層盛り上げたい想いと共に、この記事が誰かの助けになることを祈ります。
この記事に誤りがある場合は気軽にコメントで教えていただけますと幸いです。
:::
# ヒープサイズとは
　Apexのヒープサイズは、Apexコードが実行される際にメモリに割り当てられるデータの量を指します。具体的には、以下のような場合にヒープサイズが使用されます。

1. **変数やオブジェクトの格納**: Apexコード内で定義された変数やインスタンス化されたオブジェクトがメモリに格納されます。
2. **クエリの結果の格納**: SOQLやSOSLを実行すると、結果として返されるレコードがメモリに一時的に格納されます。
3. **コレクションの使用**: リストやマップ、セットなどのコレクションにデータを格納する際にもメモリが使用されます。
4. **バッチ処理やデータ処理**: 大量のデータを処理する際、データの一部がメモリに保持されるため、その量がヒープサイズに影響します。

# SOQL for ループとは

**SOQL for ループ**は、ヒープサイズの管理に役立つ特別な構文です。通常のSOQLクエリでは、クエリの結果が一度にすべてメモリにロードされますが、SOQL for ループを使用すると、クエリの結果が少しずつメモリにロードされます。この方法により、大量のレコードを扱う場合でも、ヒープ制限を超えるリスクを軽減できます。

SOQL for ループと通常のSOQLクエリを使用したループには、以下のような違いがあります。特に、パフォーマンスやヒープサイズの利用において重要なポイントがあります。

### 1. 通常のSOQLクエリを使用したループ
```apex
List<Account> accounts = [SELECT Id, Name FROM Account];
for (Account acc : accounts) {
    // 各レコードに対する処理
}
```

**この方法の動作**:
- クエリ `[SELECT Id, Name FROM Account]` が実行されると、結果として返される全ての `Account` レコードがメモリに格納されます。
- その後、`for` ループを使用して、リスト `accounts` の各レコードに対して処理が行われます。

**問題点**:
- 大量のレコード（例えば、数万件以上）を取得する場合、これら全てが一度にメモリに格納されます。
- メモリに格納されたデータが多すぎると、Apexのヒープサイズ制限（通常は6MB）が超過するリスクがあります。これが発生すると、「System.LimitException: Apex heap size too large」というエラーが投げられます。

### 2. SOQL for ループ
```apex
for (Account acc : [SELECT Id, Name FROM Account]) {
    // 各レコードに対する処理
}
```

**この方法の動作**:
- `SOQL for ループ`では、クエリ `[SELECT Id, Name FROM Account]` の結果が一度に全てメモリに格納されるのではなく、Salesforceの仕組みによって、結果が**バッチ（部分的なデータセット）**として少しずつ処理されます。
- Salesforceは内部で結果を適切なサイズのチャンク（デフォルトで200件ずつ）に分け、それぞれのチャンクを順次メモリにロードして、ループ内で処理が行われます。
>sObject リスト形式は for ループの <code_block> を 200 件の sObject のリストごとに 1 回実行します。そのため、多少理解しにくく、使用が難しくなりますが、for ループの本文内で DML ステートメントを使用する必要がある場合に最適です。DML ステートメントは、sObject のリストを一括処理します。
[APEX開発者ガイド: SOQL For ループ](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/langCon_apex_loops_for_SOQL.htm)

**利点**:
- 一度に全てのレコードをメモリにロードしないため、メモリの使用量が抑えられます。これにより、ヒープサイズ制限に達するリスクが大幅に減少します。
- 非常に大規模なデータセットを処理する際に、システムパフォーマンスが向上し、効率的なデータ処理が可能になります。

### まとめ
- **通常のクエリとループ**は、小規模なデータセットでの操作には問題ないですが、ヒープサイズの制限に近づくほどの大規模なデータセットではリスクが高まります。
- **SOQL for ループ**は、大量のデータを扱う際に安全かつ効率的にメモリを使用し、ヒープサイズの超過を防ぎます。

このため、特に多くのレコードを処理する際には、SOQL for ループを使用することが推奨されます。

# 掘り下げ
### 「SOQL for ループは、SOAP API の query メソッドと queryMore メソッドのコールで効率的なチャンクを使用して、すべての sObject を取得します。」とは？
　この記事を作成するにあたって、開発者ドキュメントで気になる用語があったので掘り下げます。
>SOQL for ループは、sObjects を取得するために使用するメソッドが、標準 SOQL ステートメントとは異なります。「SOQL および SOSL クエリ」で説明する標準クエリはクエリの count または多数のオブジェクトレコードを取得できますが、SOQL for ループは、SOAP API の query メソッドと queryMore メソッドのコールで効率的なチャンクを使用して、すべての sObject を取得します。開発者はヒープサイズの制限を回避できます。その場合は、SOQL for ループを使用して、複数のレコードを返すクエリ結果を処理します。ただし、この方法では、使用される CPU サイクルが増加する場合があります。「ヒープの合計サイズ」を参照してください。
[APEX開発者ガイド: SOQL For ループ](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/langCon_apex_loops_for_SOQL.htm)

　SOQL for ループで使用される「チャンク」とは、クエリの結果を部分的に分割して処理する単位を指します。具体的には、Salesforceが一度に大量のデータを処理する際のメモリ負荷を軽減するために、クエリで返されるレコードの全体を小さなグループに分けて、そのグループ（チャンク）ごとにデータを取り出して処理します。

### チャンクの具体的な動作
- **SOAP APIのqueryとqueryMoreメソッド**:
  - SOAP APIでは、`query` メソッドを使用して最初のクエリを実行し、初回の結果（最初のチャンク）を取得します。
  - 結果が全て取得されるまで、`queryMore` メソッドを繰り返し呼び出して、次のチャンクを順次取得します。これにより、一度にすべてのデータをメモリにロードするのではなく、少しずつデータを取り出して処理できるようになります。

- **SOQL for ループの動作**:
  - SOQL for ループは、内部的にこの「query」と「queryMore」の仕組みを活用しています。
  - クエリ結果を200件ごとのチャンクに分割して、各チャンクを順次処理します。この200件というデフォルトのチャンクサイズは、Salesforceが自動的に設定しますが、開発者が変更することはできません。

### チャンクの利点
- **メモリ効率**: 一度に全てのレコードをメモリにロードせず、必要な分だけを順次処理するため、メモリの消費量が減少します。
- **スケーラビリティ**: 大量のデータセットを扱う場合でも、システムが安定してパフォーマンスを維持できるようになります。例えば、100,000件のレコードを取得する場合、500個(500 x 200 = 100,000)のチャンクに分けて、処理します。

要するに、「チャンク」は、大量のデータを一度に処理する代わりに、データを小さな塊に分割して順次処理することで、メモリ効率とパフォーマンスを最適化するための手法です。