【GAS】実行時間6分の壁を突破!再帰処理で大量のデータ処理を自動化する方法

基本解説

「Googleスプレッドシートで大量のデータを処理していると、途中でエラーになって止まってしまう…」
日々の業務でGoogleスプレッドシートを活用している中で、このようなお悩みを抱えてはいないでしょうか。

実は、GAS(Google Apps Script)には「スクリプトの実行時間は最大6分まで」という制限があります。データ量が多く、処理に6分以上かかってしまう場合、GASは途中で強制的に処理を終了してしまいます。これが、多くの担当者を悩ませる「6分の壁」です。

しかし、ご安心ください。この「6分の壁」は、「再帰処理」というプログラミングの考え方を使えば乗り越えることができます。

この記事では、GASの基本的な使い方を理解している方を対象に、以下の内容を解説します。

  • GASの「6分の壁」とは何か
  • 「再帰処理(さいきしょり)」を使った解決策の仕組み
  • 再帰処理の仕組みを理解するための具体的なサンプルコード

この記事を最後まで読めば、今まで諦めていた大量のデータ処理も自動化できるようになります。

閲覧の前に

この記事は、GASの基本的な使い方(スクリプトエディタの開き方、関数の実行方法など)を理解している方を対象としています。もしGASを初めてお使いになる場合は、まずはこちらの記事で基本を学んでからお読みいただくことをお勧めします。

そもそもGASの「6分の壁」とは?

GASの「6分の壁」とは、GASによるプログラムの実行が6分を超えるとタイムアウトエラーとなり、処理が強制終了されてしまう制限のことを指します。

これはGoogleが提供するサーバーに過度な負荷がかかるのを防ぐための仕組みです。

どんな時に「6分の壁」にぶつかるのか?

例えば、以下のような業務で「6分の壁」に直面する可能性があります。

  • 全従業員の1年分の勤怠打刻データから、月ごとの労働時間を集計する
  • 数千件以上の顧客リストの住所データを、都道府県ごとに分割・整形する
  • Webサイトから大量の情報を取得(スクレイピング)し、スプレッドシートに転記する
  • 膨大な売上データの中から、特定の条件に合致するデータを抽出してレポートを作成する

最初は問題なく動いていたGASも、事業の成長と共にデータが増え、ある日突然タイムアウトエラーで止まってしまう、というケースは少なくありません。

解決策は「再帰処理」という考え方

では、どうすれば6分以上かかる処理を実行できるのでしょうか。その答えが「再帰処理(さいきしょり)」です。

再帰処理と聞くと難しく感じるかもしれませんが、考え方は非常にシンプルです。

「大きな処理を、6分以内に終わる小さな単位に分割し、自分自身を呼び出しながら順番に処理していく」

これが再帰処理の基本的な考え方です。

具体的には、GASで以下のような仕組みを作ります。

  1. 処理を少しだけ実行する(例:10000件のデータのうち、最初の100件だけ処理する)
  2. 「次は101件目から」という続きの場所を記録しておく
  3. 一旦スクリプトを終了させる
  4. 「1分後に、記録した場所から処理を再開せよ」という予約(トリガー)を設定する
  5. 1分後、予約通りにスクリプトが再開し、次の100件を処理する(101件目〜200件目)

この1〜5の流れをデータがなくなるまで繰り返すことで、1回あたりの実行時間を6分以内に収めつつ、全体としては6分を超えるような大量のデータ処理を完了させることができるのです。

再帰処理の具体例:大量の行データを分割処理する

再帰処理の仕組みを理解するために、特定の業務内容には依存しない、汎用的なサンプルコードを見ていきましょう。

このコードは、「指定したシートの2行目から最終行までを、一定の行数(CHUNK_SIZE)ごとに分割して処理を進める」というシンプルなものです。

サンプルコード

// ====== 設定項目 ======

// 処理対象のシート名
const TARGET_SHEET_NAME = '打刻データ'; 
// 一度に処理する行数(サーバーの状況に応じて調整してください)
const CHUNK_SIZE = 100;

// ====== 設定項目ここまで ======

/**
 * メイン関数:処理の起点となります。
 * この関数を最初に一度だけ実行します。
 */
function main() {
  // 以前の実行情報が残っている可能性があるので、一度リセットする
  deleteTrigger_();
  PropertiesService.getScriptProperties().deleteAllProperties();

  // 処理の開始行をプロパティに保存(2行目から開始)
  PropertiesService.getScriptProperties().setProperty('startRow', '2');
  
  // 最初の処理を実行
  processChunk();
}

/**
 * データをチャンク(塊)単位で処理する関数
 */
function processChunk() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(TARGET_SHEET_NAME);
  const properties = PropertiesService.getScriptProperties();

  // 前回の続きがどこからかをプロパティから取得
  const startRow = parseInt(properties.getProperty('startRow'));
  
  // シートの最終行を取得
  const lastRow = sheet.getLastRow();

  // もし開始行が最終行を超えていたら、処理は完了
  if (startRow > lastRow) {
    console.log('すべての処理が完了しました。');
    deleteTrigger_(); // 不要になったトリガーを削除
    properties.deleteAllProperties(); // プロパティを削除
    return; // ここで処理を終了
  }
  
  // 今回処理する範囲を決定
  const endRow = Math.min(startRow + CHUNK_SIZE - 1, lastRow);
  
  // ▼▼▼ 本来の処理をここに記述 ▼▼▼
  // このサンプルでは、何行目を処理したかをログに出力するだけ
  console.log(`${startRow}行目から${endRow}行目までのデータを処理しました。`);
  // ▲▲▲ 本来の処理はここまで ▲▲▲

  // 次の処理の開始行をプロパティに保存
  properties.setProperty('startRow', (endRow + 1).toString());
  
  // 次の実行のためにトリガーを設定
  createNextTrigger_();
}

/**
 * 次の処理を実行するためのトリガーを作成する関数
 */
function createNextTrigger_() {
  // 既存のトリガーを削除
  deleteTrigger_();

  // 1分後に processChunk 関数を実行するトリガーを設定
  ScriptApp.newTrigger('processChunk')
    .timeBased()
    .after(60 * 1000) // 60秒後 = 1分後
    .create();
  
  console.log('次の処理のトリガーを設定しました。');
}

/**
 * このスクリプトで作成されたトリガーをすべて削除する関数
 */
function deleteTrigger_() {
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'processChunk') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

スクリプトの解説

このコードのポイントは、processChunk関数が自分自身の処理を予約し、PropertiesServiceを介して次の処理範囲を伝達している点です。

main() 関数
 一番最初に手動で実行する、処理の起点となる関数です。
 PropertiesService(スクリプト専用のメモ帳のような機能)とトリガーを初期化し、
 「処理は2行目から開始する」という情報をPropertiesServiceに記録しています。

processChunk() 関数
 実際のデータ処理を行う中心的な関数です。

  1. PropertiesService から「何行目から処理を始めるか」を読み込みます。
  2. CHUNK_SIZE で指定した行数分だけデータを処理します。(このコードではログ出力のみ)
  3. 終了条件のチェック:もし開始行がシートの最終行を超えていたら、すべての処理が完了したとみなし、トリガーとプロパティを削除して処理を完全に終了させます。
  4. 処理が終わったら、次の開始行(endRow + 1)を PropertiesService に上書き保存します。
  5. createNextTrigger_() を呼び出し、1分後に再度このprocessChunk関数自身を実行するよう予約(トリガー設定)します。

・createNextTrigger_() / deleteTrigger_() 関数
 ScriptApp.newTrigger() という命令を使い、次の処理を実行する予約(トリガー)を
 作成・削除するための関数です。

スクリプトの実行方法

  1. 上記のコードをスクリプトエディタに貼り付け、TARGET_SHEET_NAMEを処理したいシート名に変更します。
  2. 関数選択メニューでmainを選択し、実行します。
  3. 実行ログに「〇行目から〇行目までのデータを処理しました。」と1分ごとに出力され、最後に「すべての処理が完了しました。」と表示されれば成功です。

再帰処理を実装する際の5つの注意点

再帰処理は非常に便利ですが、正しく使わないと思わぬトラブルの原因になることもあります。以下の点に注意してください。

1. 終了条件を必ず設定する

もし処理の終了条件を設け忘れると、スクリプトが自分自身を無限に呼び出し続ける「無限ループ」に陥ってしまいます。今回のコードでは if (startRow > lastRow) の部分が終了条件にあたります。処理が完了したら、必ずトリガーとプロパティを削除する処理を入れましょう。

2. トリガーの上限を意識する

GASで設定できるトリガーの数には、1ユーザーあたり20個という上限があります。そのため、createNextTrigger_() で新しいトリガーを作成する前に、必ず deleteTrigger_() で古いトリガーを削除する処理を入れることが重要です。

3. 一度に処理する件数を調整する

CHUNK_SIZE で設定している「一度に処理する行数」は、処理内容の重さによって調整が必要です。もし1回の処理が5分を大きく超えるようであれば、この数値を小さくしてください。逆に、すぐに処理が終わってしまうようであれば、数値を大きくすることで全体の処理時間を短縮できます。最初は100件程度の少ない数から試してみることをお勧めします。

4. PropertiesServiceの制限を理解する

PropertiesServiceは手軽にデータを保存できて便利ですが、保存できるデータサイズに上限があります。今回のサンプルのように処理の開始行だけを保存する場合は問題になりませんが、処理の途中のデータ(例えば集計途中の複雑なオブジェクトなど)を文字列に変換して保存し、次の処理に引き継ごうとすると、この上限を超える可能性があります。大きなデータを引き継ぐ場合は、PropertiesServiceではなく、一時的な作業用シートに書き出すなどの工夫が必要です。

5. トリガーの合計実行時間に注意する

1回あたりの実行時間を6分以内に収めても、トリガーによる実行時間の合計が1日あたりの上限に達すると、それ以降のトリガーは実行されなくなります。非常に膨大なデータを処理する場合、この合計時間も考慮に入れる必要があります。もし上限に達してしまった場合は、翌日まで待つか、処理を複数の日に分けるなどの対策が必要です。

まとめ

今回は、GASの「6分の壁」を乗り越えるための「再帰処理」という手法について、具体的なコードを交えながら解説しました。

  • GASには実行時間6分の上限があり、大量のデータを一度に処理しようとするとエラーになる。
  • 「再帰処理」を使い、処理を小さく分割して少しずつ実行することで、この「6分の壁」は突破できる。
  • 「PropertiesService(どこまで処理したかの記録)」と「トリガー(次の処理の予約)」が再帰処理の重要な要素。

この考え方を応用すれば、大量の顧客リストのクレンジングや、売上データの詳細な分析など、これまで手作業で長時間かかっていた様々な業務を自動化できます。

まずはあなたの身の回りの業務で、「時間がかかっている単純作業」がないか探してみてください。そして、この記事を参考に、スモールスタートで自動化に挑戦してみてはいかがでしょうか。

※Googleサービスは、Google LLC の商標であり、この記事はGoogleによって承認されたり、Google と提携したりするものではありません。