はじめに
Apexの単体テストは通るのに、本番の大量データで「Apex heap size too large」が出た、という経験はないでしょうか。
Winter '27で同期10MB・非同期25MBに上限が広がりましたが、ヒープ超過はコードの持ち方次第で今も起こります。
しかもこのエラーはtry-catchで拾えません。
この記事では、SOQL forループとBatch Apexによる実装の直し方と、サンドボックスで旧上限を再現して検証する手順をまとめます。
まず確認:ヒープ上限はいくつに変わった?
ヒープとは、Apexが処理の途中でメモリ上に持っておけるデータの置き場のことです。
Winter '27のリリースノートでは、次のように変わると説明されています。
- 同期トランザクション:6MBから10MBへ
- 非同期トランザクション:12MBから25MBへ
Winter '27のリリーススケジュールに合わせて、すべてのorgで自動的に有効になります。自分のorgの日程は、Trust Statusでインスタンスを検索して、メンテナンスのタブから確認できます。
いま自分のorgでどの値が効いているかは、次のメソッドで確認できます。
System.debug(Limits.getLimitHeapSize());
なお、Batch Apexやfuture、Queueableは非同期側の上限です。ちなみにスケジュールApexは非同期の仲間に見えますが、同期の上限が適用されると公式の制限一覧に書かれています。
つまり、「上限は増えたけれど、無限になったわけではない」という理解でOKです!
上限が上がってもエラーが出る、よくある原因
ヒープ超過の原因は、ほとんどが「メモリに乗せすぎ」です。公式ドキュメントの記述をもとに、代表的なパターンを整理します。
-
全件を一気にリストへ入れている
Account[] accts = [SELECT Id FROM Account];のように、大量の結果を1つの変数に受けると、結果が大きいときに実行時エラーになります。 -
使わない項目まで取得している
必要な項目だけを
SELECTに書くのが基本です。項目が多いほど、1件あたりのメモリが増えます。 -
処理が終わったコレクションを持ち続けている
SOQL forループのようにバッチ単位で処理すると、リスト変数は200件分だけになり、バッチごとに作り直されます。公式のコード例でも、この点が説明されています。
-
大きなHTTPレスポンスを受け取っている
コールアウトのリクエストとレスポンスのサイズは、ヒープにも数えられます。
もうひとつ大事なのが、ヒープ超過は try-catch で処理できないという点です。上限を超えると、拾えない System.LimitException が投げられます。「エラーを握りつぶして続行」という逃げ道はありません。
実務では、開発中のテストデータが数十件だったので気づかず、本番で数万件を流した瞬間に落ちるパターンが本当に多いです。
つまり、原因の大半は「一度に持つデータ量が多すぎる」ことだ、という理解でOKです!
出典:Apex開発者ガイド(実行時ガバナ制限)、Apex開発者ガイド(Running Apex within Governor Execution Limits)
直し方:SOQL forループとBatch Apexに切り替える
一番効くのは、SOQLの結果を「200件ずつ」処理する書き方への変更です。洗濯物を1枚ずつ洗うのではなく、数回に分けてまとめて洗うイメージです。
- エラーメッセージが指しているクラスとメソッドを特定します。
- 全件を受けているSOQLを、SOQL forループに書き換えます。
-
SELECTの項目を、処理に必要なものだけに絞ります。 - ループの中で更新まで行う場合は、リスト形式のforループを使います。
- 更新対象が1万件を超える処理(DMLで処理できるレコード数の上限)や、取得が5万件を超える処理(SOQLの取得行数の上限)は、QueryLocatorを使ったBatch Apexに切り替えます。
まず、修正前と修正後の書き方です。
// 修正前:結果が大きいとヒープを使い切る
List<Account> accts = [SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%'];
// 修正後:200件ずつ処理する(DMLをループ内で行う場合はこの形)
for (List<Account> batch : [SELECT Id, Name FROM Account WHERE Name LIKE 'Acme%']) {
for (Account a : batch) {
// ここで処理
}
update batch;
}
SOQL forループは、内部でqueryとqueryMoreを呼び出して、複数のバッチに分けて処理します。ただし、守れるのはヒープだけです。SOQLで取得できるレコードは合計5万件まで、DMLで処理できるレコードは合計1万件まで、DML文は150回までと、他の制限は別に数えられます。
リスト形式のループ内でupdateすると、200件ごとにDML文が1回使われます。大量レコードの一括更新には、Batch Apexが推奨されています。Batch Apexでは、これらの制限がexecuteメソッドの実行ごとにリセットされます。
つまり、「一度に持つ量を減らす」のが基本で、規模が大きければBatch Apexに任せる、という理解でOKです!
出典:Apex開発者ガイド(大量のSOQLクエリの扱い)、Apex開発者ガイド(SOQL For Loops)、Apex開発者ガイド(実行時ガバナ制限)
サンドボックスで旧上限を再現して確認する
もうひとつ、地味に大事な手順があります。Winter '27のサンドボックスは新しい上限で動きますが、本番がまだSummer '26のままだと、本番だけ旧上限(6MB/12MB)で動きます。サンドボックスで通ったコードが本番で落ちる、という事故が起こり得ます。
これを防ぐために、非本番orgには旧上限に戻すための設定が用意されています。
- 設定を開きます。
- クイック検索に「Apex 設定」と入力して、表示された項目を選びます。
- 「Summer '26 の Apex ヒープ制限を適用」をオンにします。
- 大量データのテストを流して、旧上限でも成功するかを確認します。
この設定が使えるのは、サンドボックス、Developer Edition、スクラッチ組織といった本番以外のorgです。リリースノートには、旧上限へ「一時的に」制限できる設定と書かれています。恒常的な切り替えではないので、移行期間の検証用と考えてください。
つまり、「本番のバージョンに合わせて、サンドボックス側で上限を戻して試す」という理解でOKです!
ここは注意!つまずきポイント
- 本番が未アップグレードのタイミングに注意です。 サンドボックスだけ先にWinter '27になっている期間は、新上限で通ったコードが本番の旧上限で落ちることがあります。デプロイ前に、上の設定で旧上限を試しておくと安心です。
-
ヒープ超過は拾えません。
try-catchでは処理できないので、そもそも超えない設計にするしかありません。 -
ヒープ確認用のデバッグ文は、調査が終わったら消してください。
Limits.getHeapSize()をデバッグ文に残したままにすると、不要な処理が本番に残ります。コメントアウトしておきましょう。 - ヒープとCPUは関連しています。 公式の制限一覧の注記に、ヒープとCPUの両方が上限に近いコードは、ヒープサイズの計算コストでCPU制限に当たりやすくなる可能性がある、と書かれています。「上限いっぱいまで使う設計」は避けた方が良さそうです。(出典:Apex開発者ガイド(実行時ガバナ制限)
- SOQL forループにも制約があります。 集計関数を含むクエリはqueryMoreに対応していません。また、200件以上の子レコードをループ内で扱うと、例外になる場合があります。(出典:Apex開発者ガイド(SOQL For Loops))
- コールアウトのサイズは別の行に書かれています。 同じ制限一覧のページには、コールアウトのリクエスト・レスポンスサイズの上限が6MB(同期)と12MB(非同期)と記載されています。そのうえで、これらのサイズはヒープにも数えられます。
- 追加費用について。 リリースノート上では、Apexが動くすべてのエディションが対象と書かれています。追加のアドオン購入が必要という記載はありませんでした。
実務で使うなら、ヒープの上限は「目標」ではなく「保険」だと考えておいた方が、あとで泣かずに済みます。
ヒープの次に確認:CPU時間とガバナ制限
ヒープ超過を直したあとに、CPU時間やDML・SOQLの上限で別のエラーが出ることがあります。主な上限は次のとおりです(Winter '27時点の公式の制限一覧より、同期/非同期の順)。
- ヒープ:10MB / 25MB
- CPU時間:10,000ミリ秒 / 60,000ミリ秒
- SOQLで取得できる合計レコード数:5万件(どちらも同じ)
- DMLで処理できる合計レコード数:1万件(どちらも同じ)
- DML文の回数:150回(どちらも同じ)
- SOQLクエリの回数:100回 / 200回
CPU時間は、DB内での待ち時間やコールアウトの待ち時間は数えられません。一方、DMLで使うアプリケーションサーバ側のCPU時間は数えられます。Batch Apexでは、これらの制限がexecuteメソッドの実行ごとにリセットされます。
症状別の切り分けは、次のとおりです。
- ヒープ超過なら、SOQL forループで一度に持つ量を減らします。
- CPU時間の超過なら、
Limits.getCpuTime()で消費量を測り、どの処理が重いかを切り分けます。 - 取得5万件・DML1万件の超過なら、Batch Apexに切り替えます。QueryLocatorなら、最大5,000万件のレコードを扱えます。
- Batch Apex以外で
Database.getQueryLocatorを使うと、取得できるのは合計1万件までです。
測定用のデバッグ文は、調査が終わったら消しておきましょう。
まとめ
- Winter '27で、Apexのヒープ上限は同期10MB・非同期25MBに拡大されました。
- 上限が増えても、全件をリストに受ける書き方はエラーの原因になり続けます。
- 直し方は、SOQL forループで200件ずつ処理することと、更新が1万件超・取得が5万件超のような大量処理は Batch Apex に任せることです。
- サンドボックスでは「Enforce the Summer '26 Apex heap limit」で旧上限を再現して、本番との差を確認しておきましょう。
ヒープ制限は地味ですが、一度ハマると原因探しが大変です。これを知っているといないとでは検証スピードも変わってくると思います。
