【GAS】「起動時間の最大値を超えました」エラー時の対策 プログラム高速化編

GAS(Google Apps Script)の実行時に「起動時間の最大値を超えました」というエラーが出て対応に悩まされた経験はありませんでしょうか。

今回はプログラムの無駄を減らして高速化することで、起動時間の最大値を超えないようにする方法をご紹介します。

参考:処理継続以外の他の対処方法

もしプログラムに無駄がなく高速化ができない場合は、処理の並行化、もしくは処理の継続によって起動時間の制限を迂回する方法もあります。

詳しくは以下の記事を参照してください。

起動時間の最大値とは?

エラーへの対処方法を解説する前に、このエラーメッセージがどんな意味なのか見ていきましょう。

表示された「起動時間の最大値を超えました」のメッセージの通り、GASで処理を実行できる時間には上限が設定されています。上限時間はエラーが発生したときの実行時間を確認するとわかります。

期間(実行時間)の欄を確認すると360.14秒と記載されています。

3601.4秒 ≒ 6分なので、上限時間は6分であることがわかります。

そのため、 「時間の最大値を超えました」 エラーを解消するためにはGASの一回の実行を6分以内に収める必要があります。

今回は実行時間の上限を確認しましたが、ほかにもGASには様々な制限が設定されています。詳しくは以下のURLを参照してください。

https://developers.google.com/apps-script/guides/services/quotas

プログラムの高速化

エラーを解消するためには、スクリプトの起動時間を6分以内に収める必要があります。限られた起動時間を有効に使うため、プログラムが無駄なく効率的に動作するように改良をしていきます。

スクリプトの高速化、効率化はプログラマーの永遠のテーマ

スクリプトの高速化、効率化はプログラムを書く人にとっては永遠のテーマとも言える内容です。その方法は多岐にわたり、今回でそのすべてをご紹介することはできません。

そのため今回はよくある非効率なプログラムの例を使って、基本的なプログラムの高速化、効率化の方法を解説します。

高速化の具体例

高速化前のコード

function main() {
  const spreadsheet = SpreadsheetApp.openById("スプレッドシートID")
  const dataSheet = spreadsheet.getSheetByName("データ")

  const rowSize = 10000
  console.log("処理するデータ行数:" + rowSize)

  // データの読み込み
  const dataArray = dataSheet.getRange(2, 1, rowSize, 5).getValues()

  // データ書き込み
  const targetSheet = spreadsheet.getSheetByName("書き込み先")
  for(let i = 0; i < dataArray.length; i++) {
    targetSheet.getRange(i + 2, 1).setValue(dataArray[i][0])  // 社員番号
    targetSheet.getRange(i + 2, 2).setValue(dataArray[i][1])  // 社員名
    targetSheet.getRange(i + 2, 3).setValue(dataArray[i][2])  // 所属1
    targetSheet.getRange(i + 2, 4).setValue(dataArray[i][3])  // 所属2
    targetSheet.getRange(i + 2, 5).setValue(dataArray[i][4])  // 所属3
  }
}

動作解説

このようなスプレッドシートがあるときに、[データ]シートの値をそのまま[書き込み先]シートに転記するシンプルなスクリプトです。

データ数と処理時間の傾向

読み込むデータ数をそれぞれ変えて計測した処理時間は以下の通りです。

実行するタイミングや環境で処理時間は前後します。

データ数処理時間
6000件126秒
7000件99秒
8000件247秒
9000件346秒
10000件タイムアウト(360秒以上)

多少ブレがあるものの、おおむね読み込むデータ数が増えるにつれて処理時間が増えています。

今回のスクリプトで、データが増えて処理の量が増える箇所は、for文で繰り返しデータ書き込みをしている部分です。

for(let i = 0; i < dataArray.length; i++) {
  targetSheet.getRange(i + 2, 1).setValue(dataArray[i][0])  // 社員番号
  targetSheet.getRange(i + 2, 2).setValue(dataArray[i][1])  // 社員名
  targetSheet.getRange(i + 2, 3).setValue(dataArray[i][2])  // 所属1
  targetSheet.getRange(i + 2, 4).setValue(dataArray[i][3])  // 所属2
  targetSheet.getRange(i + 2, 5).setValue(dataArray[i][4])  // 所属3
}

プログラム内の「dataArray」にはスプレッドシートから読み込んだデータが入っているため、読み込むデータ数が増えれば増えるだけ、for文の繰り返しの回数が増えていきます。

また、for文内で実行されているsetValueによる書き込みは重い処理であり、その分時間がかかります。このようにGASではスプレッドシートからデータを読み込む処理や書き込む処理などファイルにアクセスする必要がある処理は純粋なJavaScriptの処理と比べると時間がかかる傾向にあります。

このことから、以下の点を最適化すればスクリプトの動作が高速化できそうです。

  • 繰り返し処理をしている箇所
  • setValue関数などによる書き込みをしている箇所

書き込み処理の最適化

繰り返し処理、setValue関数の複数呼び出し部分を減らすようにプログラムを最適化していきます。

最適化後のプログラム

function main2() {
  const spreadsheet = SpreadsheetApp.openById("シートID")
  const dataSheet = spreadsheet.getSheetByName("データ")

  const rowSize = 10000
  console.log("処理するデータ行数:" + rowSize)

  // データの読み込み
  const dataArray = dataSheet.getRange(2, 1, rowSize, 5).getValues()

  // データ書き込み
  const targetSheet = spreadsheet.getSheetByName("書き込み先")
  targetSheet.getRange(2, 1, dataArray.length, dataArray[0].length).setValues(dataArray)
}

各データ件数ごとの処理時間を最適化前後で比較します。

データ数最適化前書き込み処理最適化
6000件126秒2秒
7000件99秒3秒
8000件247秒3秒
9000件346秒4秒
10000件タイムアウト(360秒以上)4秒

データ数が増えてもほぼ処理時間が増えていないことがわかると思います。

スクリプトの変更点

変更前後を比較するとfor文による繰り返し、setValueの複数呼び出しがなくなっていることに気が付くと思います。

変更前

for(let i = 0; i < dataArray.length; i++) {
  targetSheet.getRange(i + 2, 1).setValue(dataArray[i][0])  // 社員番号
  targetSheet.getRange(i + 2, 2).setValue(dataArray[i][1])  // 社員名
  targetSheet.getRange(i + 2, 3).setValue(dataArray[i][2])  // 所属1
  targetSheet.getRange(i + 2, 4).setValue(dataArray[i][3])  // 所属2
  targetSheet.getRange(i + 2, 5).setValue(dataArray[i][4])  // 所属3
}

変更後

targetSheet.getRange(2, 1, dataArray.length, dataArray[0].length).setValues(dataArray)

その代わりにsetValuesが使われています。

setValuesとは

setValuesとは、getRangeで指定した範囲にデータを書き込む関数です。

今回の場合はdataArray(読み込んだデータ)の行列の件数分だけgetRangeで範囲を指定し、データを書き込んでいます。

https://developers.google.com/apps-script/reference/spreadsheet/range?hl=en#setValues(Object)

変更前では、データ数が1万件のとき書き込み処理setValueが5万回も呼び出されていました。

10000件 × 5回(社員名~所属3) = 50000回

対して、変更後は書き込み処理setValuesは1回しか呼び出されていません。

スプレッドシートへの読み込み書き込みは一般的には重い処理であるため、呼び出しの回数は少なければ少ないほど、GASの起動時間は短くなります。

そのため、for文の中などでGASの関数が繰り返し呼び出されている場合は、そこで処理時間がかかっている可能性が高く最適化・高速化できる余地があるといえます。

まとめ

今回はプログラムの処理を高速化をすることで、GASの起動時間の最大値を超えないようにする方法をご紹介しました。

プログラム内に読み込みや書き込みなど重い処理を繰り返し実行しているような部分が含まれている場合、今回のような考え方で呼び出す回数を減らせないか検討してみると良いでしょう。

「起動時間の最大値を超えました」というエラーが出た場合は是非試してみてください。

処理の最適化・高速化というテーマは、どんな熟練したプログラマーでも頭を悩ます奥深いテーマです。

最適化・高速化に興味があり、もっと詳しく調べたい場合はこの本をオススメします。

問題解決力を鍛える!アルゴリズムとデータ構造

https://bookclub.kodansha.co.jp/product?item=0000275430

プログラムを効率的に各ための定石があり、その定石を丁寧に解説した本です。

GASとは違うプログラム言語や多少の数学の知識が必要になりますが、覚えておいて損はない内容ですので是非読んでみてください。