GASが遅い原因はセルの読み書き?getValues・setValuesで一括処理する高速化入門

Google Apps Script(GAS)でスプレッドシートを操作していると、データが少ないうちは問題なく動いていた処理が、行数の増加とともに遅くなることがあります。
よくある原因の一つが、ループの中で getValue() と setValue() を繰り返すコードです。
for (let row = 2; row <= lastRow; row++) {
const amount = sheet.getRange(row, 2).getValue();
sheet.getRange(row, 4).setValue(amount * 1.1);
}このコードは分かりやすい一方、1,000行あれば、スプレッドシートからの読み取りを1,000回、書き込みを1,000回行います。パソコンの中だけで完結する配列計算と違い、GASからGoogleスプレッドシートへアクセスする処理には通信やサービス側の処理が伴います。
そこで基本となるのが、getValues() でまとめて読み取り、JavaScriptの配列として加工し、setValues() でまとめて書き戻す方法です。
この記事では、4つのメソッドの違いから、一括処理の書き方、パフォーマンスの考え方、あえて getValue() / setValue() を選ぶ場面まで、初心者向けに解説します。
この記事で解決できる課題
- 行数が増えるとGASの処理が遅くなる
- ループ内でセルを1つずつ読み書きしている
getValues()が返す二次元配列の扱い方が分からないsetValues()で「列数が一致しない」というエラーが出る- 一括処理と単一セル処理をどう使い分けるか迷う
- GASの実行時間制限へ到達しにくいコードを書きたい
先に結論:シートとの往復回数を減らす
GASで複数行を処理するときの基本形は、次の3段階です。
1. getValues()で必要な範囲を一度に読む
↓
2. JavaScriptの配列をメモリ上で加工する
↓
3. setValues()で結果を一度に書く重要なのは、何個のセルを処理したかではなく、スプレッドシートのサービスを何回呼び出したかです。
Googleの公式ベストプラクティスでも、読み取りと書き込みを交互に繰り返さず、データを一度に配列へ読み込み、配列上で処理し、最後にまとめて書き出す方法が推奨されています。
getValue()とgetValues()の違い
名前はよく似ていますが、戻り値の形が異なります。
| メソッド | 読み取る対象 | 戻り値 |
|---|---|---|
getValue() | Rangeの左上セル1つ | 単一の値 |
getValues() | Range内のすべてのセル | 二次元配列 |
getValue()は単一の値を返す
const value = sheet.getRange('B2').getValue();
console.log(value);B2に 1200 が入っていれば、value は数値の 1200 です。
1200注意したいのは、複数セルのRangeに対して getValue() を使っても、返るのは左上セルの値だけという点です。
const value = sheet.getRange('A2:C10').getValue();この場合に取得できるのはA2の値だけです。A2:C10の全データを読みたい場合は、getValues() を使用します。
getValues()は二次元配列を返す
const values = sheet.getRange('A2:C4').getValues();
console.log(values);シートに次のデータがあるとします。
| 案件ID | 金額 | ステータス |
|---|---|---|
| A001 | 1000 | 未処理 |
| A002 | 2500 | 完了 |
| A003 | 1800 | 未処理 |
取得結果は、次のような「配列の中に配列がある形」になります。
[
['A001', 1000, '未処理'],
['A002', 2500, '完了'],
['A003', 1800, '未処理'],
]外側の配列が行、内側の配列が列です。この形を二次元配列と呼びます。
特定の値へアクセスするときは、values[行][列] と書きます。
console.log(values[0][0]); // A001
console.log(values[0][1]); // 1000
console.log(values[1][2]); // 完了スプレッドシートの行番号と列番号は1から始まりますが、JavaScriptの配列番号は0から始まります。A2:C4を取得した場合、A2は values[0][0] です。ここは最初につまずきやすいポイントです。
getValue() と getValues() が返す値は、セルの内容に応じて文字列、数値、真偽値、日付などになります。空のセルは空文字 '' として取得されます。
setValue()とsetValues()の違い
書き込み側も、単一の値を渡すか、二次元配列を渡すかが大きな違いです。
| メソッド | 渡す値 | 主な用途 |
|---|---|---|
setValue() | 1つの値 | 1セル、またはRange全体へ同じ値を書き込む |
setValues() | 二次元配列 | 複数セルへの一括書き込み |
setValue()で1セルへ書く
sheet.getRange('D2').setValue(1100);D2へ数値の 1100 を書き込みます。文字列、数値、真偽値、日付を指定でき、= から始まる文字列は数式として解釈されます。
複数セルのRangeへ setValue() を使うと、すべてのセルへ同じ値を設定できます。
sheet.getRange('E2:E10').setValue('未処理');セルごとに異なる値を書きたい場合は、setValues() を使用します。
setValues()で範囲へまとめて書く
const output = [
[1100, '未処理'],
[2750, '処理済み'],
[1980, '未処理'],
];
sheet.getRange('D2:E4').setValues(output);D2:E4は3行×2列のRangeです。そのため、渡す配列も3行×2列にします。
書き込み先:3行 × 2列
書き込む配列:3行 × 2列Rangeと配列の大きさが一致していないと、setValues() はエラーになります。
たとえば、3行×2列のRangeへ、3行×1列の配列は書き込めません。
// D2:E4は3行×2列だが、outputは3行×1列なのでエラー
const output = [
[1100],
[2750],
[1980],
];
sheet.getRange('D2:E4').setValues(output);実際のコードでは、配列の長さからRangeの大きさを決めると、ずれを防ぎやすくなります。
sheet
.getRange(2, 4, output.length, output[0].length)
.setValues(output);それぞれの数字は、開始行、開始列、行数、列数を表します。
getRange(2, 4, output.length, output[0].length)
│ │ │ └ 列数
│ │ └ 行数
│ └ 開始列:D列
└ 開始行:2行目実践:案件一覧を一括で読み書きする
ここからは、案件一覧を想定したサンプルで、一括処理の基本形を確認します。
「案件一覧」シートの1行目に、次の見出しがあるとします。
| A列 | B列 | C列 | D列 | E列 |
|---|---|---|---|---|
| 案件ID | 金額 | ステータス | 税込金額 | 処理区分 |
A~C列を読み込み、D列へ税込金額、E列へ処理区分を書き込みます。金額の端数処理は説明用の仮ルールです。
function updateProjectList() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName('案件一覧');
if (!sheet) {
throw new Error('「案件一覧」シートが見つかりません。');
}
const firstDataRow = 2;
const lastRow = sheet.getLastRow();
// データ行がない場合は終了する
if (lastRow < firstDataRow) {
return;
}
const rowCount = lastRow - firstDataRow + 1;
// A~C列を一度に読み取る
const sourceValues = sheet
.getRange(firstDataRow, 1, rowCount, 3)
.getValues();
// シートへアクセスせず、JavaScriptの配列上で加工する
const outputValues = sourceValues.map(([projectId, amount, status]) => {
if (projectId === '') {
return ['', ''];
}
const taxIncludedAmount =
typeof amount === 'number' ? Math.floor(amount * 1.1) : '';
const processType = status === '完了' ? '処理済み' : '未処理';
return [taxIncludedAmount, processType];
});
// D~E列へ一度に書き込む
sheet
.getRange(firstDataRow, 4, outputValues.length, 2)
.setValues(outputValues);
}このコードで、スプレッドシートに対する主なアクセスは次の2回です。
getValues()でA~C列をまとめて読むsetValues()でD~E列をまとめて書く
行ごとの計算は map() の中で行いますが、ここではスプレッドシートへアクセスしていません。GASの実行環境に読み込んだJavaScript配列を加工しているだけです。
map() は、配列の各行を別の形へ変換し、新しい配列を作るメソッドです。今回なら、入力の1行を、出力用の [税込金額, 処理区分] へ変換しています。
遅くなりやすいコードと一括処理を比較する
同じ処理を、セル単位で書くと次のようになります。
function updateProjectListSlowly() {
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('案件一覧');
const lastRow = sheet.getLastRow();
for (let row = 2; row <= lastRow; row++) {
const projectId = sheet.getRange(row, 1).getValue();
const amount = sheet.getRange(row, 2).getValue();
const status = sheet.getRange(row, 3).getValue();
if (projectId === '') {
sheet.getRange(row, 4).setValue('');
sheet.getRange(row, 5).setValue('');
continue;
}
const taxIncludedAmount =
typeof amount === 'number' ? Math.floor(amount * 1.1) : '';
const processType = status === '完了' ? '処理済み' : '未処理';
sheet.getRange(row, 4).setValue(taxIncludedAmount);
sheet.getRange(row, 5).setValue(processType);
}
}1行につき、3回読み取り、2回書き込みます。1,000行なら、合計5,000回の読み書きです。
一方、一括処理では、1,000行でも主な読み書きは1回ずつです。
| 方法 | 1,000行を処理する場合の主なサービス呼び出し |
|---|---|
| セル単位 | 読み取り3,000回+書き込み2,000回 |
| 一括処理 | 読み取り1回+書き込み1回 |
実際の実行時間は、シートの数式、データ量、同時利用状況などにも左右されます。しかし、サービス呼び出しを何千回から数回へ減らせるため、一般に一括処理の方が大幅に高速で、データ増加にも強くなります。
Apps Scriptには先読みや書き込みキャッシュなどの最適化があります。それでも、読み取りと書き込みを交互に繰り返すコードは遅くなりやすいため、キャッシュ任せにせず、最初から一括処理を設計するのが基本です。
getValues・setValuesを使うときの注意点
二次元配列であることを忘れない
1列だけ取得しても、結果は一次元配列ではなく二次元配列です。
const values = sheet.getRange('A2:A4').getValues();
console.log(values);
// [['A001'], ['A002'], ['A003']]['A001', 'A002', 'A003'] ではありません。
1列の値だけを一次元配列にしたい場合は、次のように変換できます。
const projectIds = values.map(([projectId]) => projectId);Rangeと配列の行数・列数を一致させる
setValues() では、Rangeと二次元配列のサイズを必ず一致させます。また、配列の各行で列数をそろえる必要があります。
// 2行目だけ列が1つなので使用できない
const invalidValues = [
['A001', 1000],
['A002'],
['A003', 1800],
];0行のRangeを作らない
データがないときに getRange(2, 1, 0, 3) を実行するとエラーになります。getLastRow() を使う場合は、データ行が存在するか先に確認します。
if (lastRow < 2) {
return;
}必要以上に広い範囲を取得しない
一括処理が速いからといって、毎回シート全体や列全体を取得すればよいわけではありません。数万行の空行まで取得すると、メモリと処理時間を無駄にします。
// 必要な最終行まで取得する
const rowCount = sheet.getLastRow() - 1;
const values = sheet.getRange(2, 1, rowCount, 5).getValues();大量データでは、すべてを一度に扱うと配列が大きくなりすぎる場合もあります。その場合は、1,000行や5,000行など、処理内容に応じた単位に分割し、各まとまりを getValues() / setValues() で一括処理します。
書式や数式は別のメソッドを使い分ける
getValues() が取得するのはセルの実値です。画面に表示されている文字列をそのまま取得したい場合は getDisplayValues()、数式そのものを扱いたい場合は getFormulas() / setFormulas() を使います。
たとえば、数値の 123 が表示形式によって 00123 と見えている場合、getValues() では数値の 123、getDisplayValues() では文字列の 00123 を取得します。用途に合わせて選びましょう。
それでもgetValue・setValueを使う場面
getValue() と setValue() が悪いメソッドというわけではありません。処理対象が本当に1セルだけなら、単一値を扱うメソッドの方がコードの意図を読み取りやすくなります。
厳密には、1セルのRangeに getValues() / setValues() を使うこともできます。そのため、「絶対に getValue() / setValue() でなければ実現できない」場面は多くありません。しかし、[[値]] という二次元配列を扱う必要があるため、単一値の操作ではかえって複雑です。
1. 設定値を1つだけ読む
税率や対象年月など、決まったセルに置いた設定値を1つだけ読む場合です。
const taxRate = sheet.getRange('B1').getValue();取得対象がB1だけなら、getValue() が自然です。
2. 実行結果や更新日時を1セルへ書く
処理の終了時刻やステータスを、管理用セルへ1つだけ書き込む場合です。
sheet.getRange('H1').setValue(new Date());3. 複数セルへ同じ値を設定する
選択したRangeの全セルへ同じ初期値を入れたい場合は、setValue() が簡潔です。
sheet.getRange('E2:E100').setValue('未処理');setValues() でも実現できますが、同じ値を並べた二次元配列を作る必要があります。セルごとに異なる値が必要ないなら、setValue() の方が意図を読み取りやすくなります。
4. onEdit(e)で編集された1セルを扱う
編集トリガーで、ユーザーが操作したセルだけを確認するときは、e.range.getValue() が分かりやすい書き方です。
function onEdit(e) {
if (e.range.getA1Notation() !== 'A2') {
return;
}
const input = e.range.getValue();
e.range.offset(0, 1).setValue(`入力値: ${input}`);
}ただし、複数セルの貼り付けにも対応する必要がある場合は、e.range.getValues() でRange全体を扱う設計へ切り替えます。
5. シートの再計算結果を途中で読む必要がある
書き込んだ値によって数式が再計算され、その結果を読んで次の処理を決めるようなケースです。
sheet.getRange('B2').setValue(inputValue);
SpreadsheetApp.flush();
const calculatedResult = sheet.getRange('C2').getValue();SpreadsheetApp.flush() は、保留中の変更をすぐ反映させるメソッドです。このように処理の途中でシートの再計算結果が必要なら、単一セルの読み書きが避けにくいことがあります。
ただし、これをループで何度も繰り返すと非常に遅くなります。数式と同じ計算をJavaScript側で行えないか、複数件をまとめて計算できないか、先に設計を見直しましょう。
6. 離れた場所にある少数の設定セルを読む
B1、D3、H1のように互いに離れたセルを少数だけ取得する場合も、個別の getValue() の方がコードを理解しやすい場合があります。
const targetMonth = sheet.getRange('B1').getValue();
const taxRate = sheet.getRange('D3').getValue();
const operatorName = sheet.getRange('H1').getValue();ただし、設定項目が増え続けるなら、設定を表形式にまとめて一括取得する、名前付き範囲を使うなど、シート構成自体を見直す方が管理しやすくなります。
判断に迷ったときの目安
| 状況 | 選び方 |
|---|---|
| 1セルだけ読み書きする | getValue() / setValue() でもよい |
| Range全体へ同じ値を書く | setValue() が簡潔 |
| 連続する複数セルを扱う | getValues() / setValues() |
| ループで行ごとに読み書きしている | 一括処理へ変更する |
| 編集された1セルだけを扱う | e.range.getValue() が自然 |
| 複数セルの貼り付けに対応する | e.range.getValues() |
| 途中で数式の再計算結果が必要 | 単一セル処理もあり。ただし設計を再検討する |
| 大量データを扱う | 適切な件数に分割して一括処理する |
判断のポイントは、「メソッド名が単数形か複数形か」だけではありません。シートへ何回アクセスするかを数えてみてください。
ループの中に getValue()、setValue()、getRange() が並んでいたら、一括処理へ変更できないかを検討する合図です。
まとめ:基本はgetValues・setValuesを使おう
GASでスプレッドシートを扱うときは、次の形を基本にしましょう。
- 必要な範囲を
getValues()でまとめて読む - JavaScriptの配列上で計算・変換・判定する
- 結果を二次元配列へまとめる
setValues()で一度に書き込む
getValue() と setValue() は、単一の設定値、実行結果の記録、編集された1セルの処理などで便利です。しかし、複数行のデータ処理でループのたびに呼び出すと、データが増えたときに性能上の問題が表面化します。
まずは「シートとの往復を減らせないか」と考えることが大切です。特別な理由がなければ、複数セルの読み書きには getValues() と setValues() を使う。この習慣だけで、GASの処理は速く、安定し、データ増加に耐えやすくなります。
参考
この記事をシェアする
合同会社raisexでは一緒に働く仲間を募集中です。
ご興味のある方は以下の採用情報をご確認ください。