【GAS】スプレッドシートの読み書きが遅い!を高速化するコツ
Google Apps Scriptsでスプレッドシートの読み書きをするとき、何も考えずにプログラムを書くと、動作速度が異常に遅くなります。
動作速度を上げる一番のコツとして、スプレッドシートAPIをできるだけ呼び出さないようにするということが挙げられます。これで、動作速度は劇的に速くなります。
ただ、実は、スプレッドシートAPIの呼び出し回数を減らさないでも、適切にAPIを呼び出せば、呼び出し回数が多くても、まあまあ実用的な速度でプログラムを実行することができます。
そこで、この記事では、「スプレッドシートAPIを呼び出さないようにすればいい」という話から、さらに一歩踏み込んで、「APIを使わざるを得ない場合には、どういうふうにAPIを呼び出せば、実行速度の低下を最小限で防げるのか」について解説をしていきます。
まずは、下記のように、A列・B列に、それぞれ数値が入っているときに、C列にその合計を表示するプログラムを例に解説をしていきます。
↓
「最も遅い方法」と「最も速い方法」
例1: 1セルずつ読み込み、書き込みを行う(最も遅い方法)
動作速度が最も遅くなるのは、次のように、1セルずつ読み込んで、1セルずつ書き込みを行う場合です。
function main1() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
for (let y = 1; y <= 100; y++) {
const a = sheet.getRange(y, 1).getValue();
const b = sheet.getRange(y, 2).getValue();
sheet.getRange(y, 3).setValue(a + b);
}
console.timeEnd('test');
}
このプログラムの実行時間は24,123msでした。
例2: まとめて読み込み、まとめて書き込む(最も速い方法)
一方で、一番実行速度が速くなるのは、次のように処理をするケースです。
- セルの内容を、一気に配列に読み込む
- その配列を加工して出力用の配列を作成
- その配列の内容を、セルに一気に書き込む
たとえば、次のプログラムを見てみてください。
function main2() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
const iValues = sheet.getRange(1, 1, 100, 2).getValues();
const oValues = [];
for (let y = 0; y < 100; y++) {
oValues.push([iValues[y][0] + iValues[y][1]]);
}
sheet.getRange(1, 3, 100, 1).setValues(oValues);
console.timeEnd('test');
}
このプログラムでは、以下のようなことを行っています。
6行目 | getValuesメソッドで、セルの内容を、配列「iValues」に一気に読み込み |
8行目~11行目 | forループで、出力するデータを配列「oValues」に作成 |
13行目 | setValuesメソッドを使って、配列「oValues」の内容を、セルに一気に書き込む |
このプログラムの実行時間は335msでした。
セルの値を一気に読み込み、一気に書き込むことで、「24,123ms」→「335ms」と、約72倍高速になりました。
実行速度が速くなった原因は「スプレッドシートAPI」の呼び出し回数が減ったから?
実行速度が速くなった理由として、よく言われるのがスプレッドシートAPIの呼び出し回数が減ったからという理由です。
「スプレッドシートAPI」とは
「スプレッドシートAPI」というのは、「SpreadsheetApp」に続けて書くメソッドを呼び出すことをいいます。
「SpreadsheetApp」直後に呼び出すメソッドだけでなく、「.」でつないで書いていくメソッドは、すべてスプレッドシートAPIに含まれます。
たとえば、冒頭の「例1」のプログラムで、「スプレッドシートAPI」を何回呼び出しているか考えてみましょう。
「例1」では、どこで「スプレッドシートAPI」を呼び出しているか?
function main1() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
for (let y = 1; y <= 100; y++) {
const a = sheet.getRange(y, 1).getValue();
const b = sheet.getRange(y, 2).getValue();
sheet.getRange(y, 3).setValue(a + b);
}
console.timeEnd('test');
}
この中では、次の箇所で「スプレッドシートAPI」を呼び出しています。
行 | スプレッドシートAPI |
---|---|
4行目 | getActiveSheet() |
6行目 | getRange(y, 1)、getValue() |
7行目 | getRange(y, 2)、getValue() |
8行目 | getRange(y, 3)、setValue(a + b) |
6行目~8行目の文には「SpreadsheetApp」という文が含まれていません。ただ、変数「sheet」には「SpreadsheetApp.getActiveSheet()」の結果が格納されています。
そのため、変数「sheet」を「SpreadsheetApp.getActivesheet()」と同視する必要があります。それ以降「.」でつないで呼び出しているメソッドもすべて「スプレッドシートAPI」の呼び出しと考えることに注意してください。
「例1」で「スプレッドシートAPI」を呼び出した回数は?
今回のプログラムでは、6行目~8行目は、forループで100周分実行されます。
それを加味すると、「スプレッドシートAPI」は、下記のように、合計で「601回」呼び出されることになります。
スプレッドシートAPI | 呼び出し回数 | 補足 |
---|---|---|
getActiveSheet() | 1 | 4行目 |
getRange() | 300 | 6行目~8行目で1回ずつ(=3回)× 100周 |
getValue() | 200 | 6行目、7行目で1回ずつ(=2回)× 100周 |
setValue() | 100 | 8行目で1回 × 100周 |
合計 | 601 |
引数が違っていても、動作速度に大きな影響は与えません。そこで、上の表では、引数の違いを無視して集計をしています。
「例2」で「スプレッドシートAPI」を呼び出した回数は?
同じように、「例2」のプログラムで「スプレッドシートAPI」を呼び出した回数を数えてみましょう。
function main2() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
const iValues = sheet.getRange(1, 1, 100, 2).getValues();
const oValues = [];
for (let y = 0; y < 100; y++) {
oValues.push([iValues[y][0] + iValues[y][1]]);
}
sheet.getRange(1, 3, 100, 1).setValues(oValues);
console.timeEnd('test');
}
このプログラムで、「スプレッドシートAPI」を呼び出しているのは、下記の「5回」だけです。
スプレッドシートAPI | 呼び出し回数 | 補足 |
---|---|---|
getActiveSheet() | 1 | 4行目 |
getRange() | 2 | 6行目、13行目 |
getValues() | 1 | 6行目 |
setValues() | 1 | 13行目 |
合計 | 5 |
「例1」と「例2」を比較すると、「スプレッドシートAPI」の呼び出し回数が「601回」→「5回」に激減しています。
一見、API呼び出し回数が激減したから、実行速度が向上したように見えます。もちろん、それは間違いではありません。でも、実際には、別の要因が、実行速度に大きな影響を与えています。
「スプレッドシートAPI」の呼び出し回数を減らさなくても、動作速度は劇的に速くなる
次のプログラムを見てみてください。
例3: 必要なデータを「getValue」した後に「setValue」を実行する
function main3() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
const oValues = [];
for (let y = 1; y <= 100; y++) {
const a = sheet.getRange(y, 1).getValue();
const b = sheet.getRange(y, 2).getValue();
oValues[y - 1] = a + b;
}
for (let y = 1; y <= 100; y++) {
sheet.getRange(y, 3).setValue(oValues[y - 1]);
}
console.timeEnd('test');
}
このプログラムでは、あえて「getValues」「setValues」を使わないで、次のようなプログラムを組んでいます。
- 「getValue」メソッドで、1セルずつ値を読み込んで、計算結果を配列に格納(6~11行目)
- 「setValue」メソッドで、配列の要素を、1セルずつ書き込み(13~15行目)
このプログラムの実行にかかった時間は、1,581msでした。
「例2」の「335ms」に比べると遅いですが、「例1」の「24,123ms」に比べれば、十分実用的な速度です。
APIの呼び出し回数は「例1」とまったく同じ
実は、このプログラムの「スプレッドシートAPI」の呼び出し回数は、「例1」とまったく同じです。
スプレッドシートAPI | 呼び出し回数 | 補足 |
---|---|---|
getActiveSheet() | 1 | 4行目 |
getRange() | 300 | 6行目~8行目で1回ずつ(=3回)× 100周 |
getValue() | 200 | 6行目、7行目で1回ずつ(=2回)× 100周 |
setValue() | 100 | 8行目で1回 × 100周 |
合計 | 601 |
それにもかかわらず、「例1」に比べると、実行速度は約15倍になっています。
つまり、APIの呼び出し回数が同じでも、速度が大きく異なる場合があるのです。
「API呼び出し回数による速度の差」は、一番実行速度が速かった「例2」と、今回の「例3」との速度の差である、約「1,250ms」と考えられます。
意外に「API呼び出し回数が速度に与える影響」は小さいのです。
ポイントは「setValue」の後に「getValue」を使わないこと
実行速度に大きな影響を与えるポイントは、「getValue」と「setValue」の順番です。
- 最初に(必要な回数だけ)getValueを行う
- その後に(必要な回数だけ)setValueを行う
この順番を守ることで、「スプレッドシートAPI」を大量に呼び出しても、プログラムの実行速度がそれほど落ちなくなるのです。
言い換えると、「setValue」の後に「getValue」を行うと、実行速度が大きく落ちてしまうのです。
「setValue」の後に「getValue」をすると遅くなる理由
※この項の内容は、私の推測です。
「setValue」メソッドを実行したときに、シートがどう変化するかを観察してみてください。
たぶん、「setValue」メソッドが実行された瞬間にセルに値が書き込まれず、少し遅れてセルに書き込まれているのではないかと思います。
このように、「setValue」メソッドによる書き込みが完了するまでには、少し時間がかかります。ちょっとした時間ですが、この書き込みを待ってから、次の行の処理を行うと、処理時間がムダに増えてしまいます。
そこで、GASでは、「setValue」メソッドによるセルへの書き込みが完了していなくても、セルへの書き込み指示を出した段階で、次の行の処理が実行される仕組みになっているのです(いわゆる「ライトバックキャッシュ」が効いた状態)。
このような処理が行われることで、「setValue」メソッドは、比較的短時間で終わり、次の行の処理に移ることができます。
ところが、「setValue」メソッドの後に「getValue」メソッドが出てくる場合には、この手法が使えません。というのは、「setValue」メソッドの結果が反映した状態まで待たないと、「getValue」メソッドで誤った値を読み込む可能性があるからです。
ですから、「setValue」メソッドの後に「getValue」メソッドが呼び出されたときには、「setValue」メソッドの処理を待つことになります。その結果、処理に時間がかかってしまうのです。
実務的に出てきそうなパターン
実務的には、「getValues」を使って一括でデータを読み込むことはできるけれど、「setValues」を使って一括でデータを書き込ものは大変、というケースがたまに出てきます。
その場合には、次のように処理をすることで、大きな速度低下を防ぎつつ、シンプルにプログラムを書くことができます。
- 最初に「getValues」で一気にセルを読み込む
- その後「setValue」で個別に書き込みを行う
実際の例を見てみましょう。
例4: setValuesを使いにくいケース
たとえば、次のような処理をするプログラムを書くことを考えてみます。
- A列・B列に数値が入っている場合には、C列にその合計を表示する
- ただし、D列に何か値が入力されているときには、C列の内容は変更せずに、あらかじめ入力してある内容をそのまま表示する
↓
この場合、最初に、「getValues」メソッドで必要なすべてのセルの値を取得するようにしましょう(6行目)。
あとは、無理に「setValues」メソッドを使わずに、都度「setValue」メソッドで必要なセルに値を書き込む(13行目)だけでも、十分な速度を出すことができます。
function main4() {
console.time('test');
const sheet = SpreadsheetApp.getActiveSheet();
const iValues = sheet.getRange(1, 1, 100, 4).getValues();
for (let y = 1; y <= 100; y++) {
//「y」行「4」列目のセルの値は、iValues[y - 1][4 - 1]に格納されている
if (iValues[y - 1][4 - 1] === '') {
const a = iValues[y - 1][1 - 1];
const b = iValues[y - 1][2 - 1];
sheet.getRange(y, 3).setValue(a + b); // 条件に合うセルだけ値を書き込む
}
}
console.timeEnd('test');
}
このプログラムを実行したときの実行速度は、840msでした。
まとめ
「スプレッドシートAPI」の使用回数を減らすことは重要ですが、だからといって、無理に減らす必要はありません。
それよりも、「getValue(s)」の後に「setValue(s)」を使う、という順番を意識するようにしましょう。
これだけで、十分な速度が出ることが多いです。