スプシのセルを1つずつ処理するとどれだけ遅い?GASで1万セルを読み書きして比較してみた

Google Apps Script(GAS)でスプレッドシートを扱うとき、よく聞くのが「セルを1つずつ読み書きすると遅い」という話です。
では、getValue() / setValue() と、getValues() / setValues() では、実際にどのような差が生まれるのでしょうか。
この記事では、2,000行×5列、合計10,000セルのデータセットを用意し、次の4つを比較します。
getValue()で1セルずつ読み取るgetValues()で全セルを一括読み取りするsetValue()で1セルずつ書き込むsetValues()で全セルを一括書き込みする
検証コードをすべて掲載しているので、自分のGoogleアカウントや業務環境でも同じ条件で再測定できます。
この記事で分かること
- 1セルずつの処理と一括処理で、サービス呼び出し回数がどう変わるか
- 読み取りと書き込みを公平に測定する方法
- 検証用データセットの作り方
- 実行時間を比較するときの注意点
- 大量データでは一括処理を選ぶべき理由
先に結論
複数セルを処理するなら、基本的には getValues() と setValues() を使いましょう。
10,000セルを処理する場合、メソッドの呼び出し回数は次のように変わります。
| 検証方法 | スプレッドシートに対する主な呼び出し回数 |
|---|---|
getValue() | 10,000回 |
getValues() | 1回 |
setValue() | 10,000回 |
setValues() | 1回 |
Apps Script内の配列処理に比べ、Googleスプレッドシートのサービスを呼び出す処理には大きなコストがあります。データ量が増えるほど、呼び出し回数の差が実行時間へ表れやすくなります。
なぜ一括処理の方が速いのか
GASのコードとスプレッドシートは、同じJavaScriptの変数を読むように一体化しているわけではありません。
getValue() や setValue() を実行すると、GASからスプレッドシートのサービスへ処理を依頼します。一方、getValues() で一度データを取得した後の配列計算は、GASの実行環境内で行われます。
セル単位の処理では、1セルごとにGASとGoogle Sheetsの往復が発生します。
sequenceDiagram
participant GAS as GAS
participant Sheets as Google Sheets
loop セルごとに繰り返す
GAS->>Sheets: getValue()で1セルを読む
Sheets-->>GAS: 1セルの値を返す
GAS->>GAS: 値を加工する
GAS->>Sheets: setValue()で1セルへ書く
Sheets-->>GAS: 書き込み完了
end一括処理では、読み取りと書き込みがそれぞれ1回で済みます。データの加工中はGoogle Sheetsへアクセスしません。
sequenceDiagram
participant GAS as GAS
participant Sheets as Google Sheets
GAS->>Sheets: getValues()で範囲をまとめて読む
Sheets-->>GAS: 二次元配列を返す
GAS->>GAS: 配列をメモリ上で加工する
GAS->>Sheets: setValues()でまとめて書く
Sheets-->>GAS: 書き込み完了Apps Scriptには先読みや書き込みキャッシュなどの最適化があります。それでもGoogle公式は、読み取りと書き込みを交互に繰り返さず、データを一度に配列へ読み、配列上で処理し、一度に書き戻す方法を推奨しています。
検証条件
今回の検証条件をそろえます。
| 項目 | 条件 |
|---|---|
| データ行数 | 2,000行 |
| 列数 | 5列 |
| 合計セル数 | 10,000セル |
| 読み取り元 | BENCH_SOURCE シート |
| 書き込み先 | BENCH_TARGET シート |
| 結果の保存先 | BENCH_RESULTS シート |
| 測定回数 | 各方式3回以上 |
| 比較値 | 実行時間の中央値 |
実行時間には、Google側の混雑、アカウント、シートの状態などによるばらつきがあります。1回だけの結果で判断せず、同じ条件で複数回測定し、極端な値の影響を受けにくい中央値を比較します。
検証用スプレッドシートを準備する
検証専用の新しいGoogleスプレッドシートを作り、「拡張機能」から「Apps Script」を開きます。
注意点として、次のセットアップコードは BENCH_SOURCE、BENCH_TARGET、BENCH_RESULTS という名前のシートを初期化します。同名の業務シートがあるファイルでは実行せず、必ず記事用の新しいスプレッドシートを使用してください。
検証データセット
データセットは、案件管理を想定したダミーデータです。実在する会社名、案件名、担当者名は使用しません。
| 列 | 内容 | 値の例 |
|---|---|---|
| A列 | 案件ID | P-00001 |
| B列 | 行番号 | 1 |
| C列 | ステータス | 未処理 / 完了 |
| D列 | 金額 | 100、200、300… |
| E列 | 確認フラグ | TRUE / FALSE |
文字列、数値、真偽値を混ぜ、合計10,000セルを作ります。
const BENCHMARK_CONFIG = Object.freeze({
rows: 2000,
columns: 5,
sourceSheetName: 'BENCH_SOURCE',
targetSheetName: 'BENCH_TARGET',
resultSheetName: 'BENCH_RESULTS',
});
/**
* 検証用の3シートと、2,000行×5列のダミーデータを準備する。
* 同名シートの内容は初期化されるため、検証専用ファイルで実行する。
*/
function setupBenchmark() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sourceSheet = getOrCreateSheet_(
spreadsheet,
BENCHMARK_CONFIG.sourceSheetName,
);
const targetSheet = getOrCreateSheet_(
spreadsheet,
BENCHMARK_CONFIG.targetSheetName,
);
const resultSheet = getOrCreateSheet_(
spreadsheet,
BENCHMARK_CONFIG.resultSheetName,
);
// 新規シートの初期行数が2,000未満でも検証できるように拡張する
ensureSheetSize_(
sourceSheet,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
ensureSheetSize_(
targetSheet,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
sourceSheet.clear();
targetSheet.clear();
resultSheet.clear();
const values = createBenchmarkValues_();
// セットアップ時間は測定対象外。一括でデータを作成する。
sourceSheet
.getRange(1, 1, BENCHMARK_CONFIG.rows, BENCHMARK_CONFIG.columns)
.setValues(values);
resultSheet.getRange('A1:H1').setValues([[
'測定日時',
'種別',
'メソッド',
'行数',
'列数',
'セル数',
'実行時間(ms)',
'チェック値',
]]);
SpreadsheetApp.flush();
}
function createBenchmarkValues_() {
return Array.from({ length: BENCHMARK_CONFIG.rows }, (_, index) => {
const rowNumber = index + 1;
return [
`P-${String(rowNumber).padStart(5, '0')}`,
rowNumber,
rowNumber % 2 === 0 ? '完了' : '未処理',
rowNumber * 100,
rowNumber % 3 === 0,
];
});
}
function getOrCreateSheet_(spreadsheet, sheetName) {
return spreadsheet.getSheetByName(sheetName)
?? spreadsheet.insertSheet(sheetName);
}
function ensureSheetSize_(sheet, requiredRows, requiredColumns) {
const currentRows = sheet.getMaxRows();
const currentColumns = sheet.getMaxColumns();
if (currentRows < requiredRows) {
sheet.insertRowsAfter(currentRows, requiredRows - currentRows);
}
if (currentColumns < requiredColumns) {
sheet.insertColumnsAfter(
currentColumns,
requiredColumns - currentColumns,
);
}
}最初に setupBenchmark() を1回実行してください。3つのシートが作成され、BENCH_SOURCE に検証用データが入ります。
読み取り性能を比較する
読み取りテストでは、取得した値を使わずに終わらせないよう、文字数や数値を足して「チェック値」を作ります。両方式で同じ配列走査に近い処理を行い、結果が正しく読み取れたことも確認します。
getValue()で10,000セルを読む
function benchmarkGetValue() {
const sheet = getBenchmarkSheet_(BENCHMARK_CONFIG.sourceSheetName);
const range = sheet.getRange(
1,
1,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
const startedAt = Date.now();
let checksum = 0;
for (let row = 1; row <= BENCHMARK_CONFIG.rows; row++) {
for (let column = 1; column <= BENCHMARK_CONFIG.columns; column++) {
const value = range.getCell(row, column).getValue();
checksum += valueToNumber_(value);
}
}
const elapsedMs = Date.now() - startedAt;
appendBenchmarkResult_('読み取り', 'getValue', elapsedMs, checksum);
}getValue() がループ内にあるため、10,000セルに対して10,000回呼び出します。
getValues()で10,000セルを一括取得する
function benchmarkGetValues() {
const sheet = getBenchmarkSheet_(BENCHMARK_CONFIG.sourceSheetName);
const range = sheet.getRange(
1,
1,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
const startedAt = Date.now();
const values = range.getValues();
let checksum = 0;
for (const rowValues of values) {
for (const value of rowValues) {
checksum += valueToNumber_(value);
}
}
const elapsedMs = Date.now() - startedAt;
appendBenchmarkResult_('読み取り', 'getValues', elapsedMs, checksum);
}こちらは、10,000セルを getValues() 1回で取得します。取得後の二重ループはJavaScript配列上の処理であり、そのたびにスプレッドシートへアクセスするわけではありません。
書き込み性能を比較する
書き込みテストでは、測定前に書き込み先を空にします。また、書き込みが保留されたまま測定を終えないよう、両方式で最後に SpreadsheetApp.flush() を実行します。
データ生成、読み取り、事前のクリア処理は測定時間へ含めません。比較したいのは、同じ10,000個の値を書き込む部分だからです。
setValue()で10,000セルへ書く
function benchmarkSetValue() {
const sourceSheet = getBenchmarkSheet_(BENCHMARK_CONFIG.sourceSheetName);
const targetSheet = getBenchmarkSheet_(BENCHMARK_CONFIG.targetSheetName);
const values = sourceSheet
.getRange(1, 1, BENCHMARK_CONFIG.rows, BENCHMARK_CONFIG.columns)
.getValues();
const targetRange = targetSheet.getRange(
1,
1,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
targetRange.clearContent();
SpreadsheetApp.flush();
const startedAt = Date.now();
for (let row = 1; row <= BENCHMARK_CONFIG.rows; row++) {
for (let column = 1; column <= BENCHMARK_CONFIG.columns; column++) {
targetRange
.getCell(row, column)
.setValue(values[row - 1][column - 1]);
}
}
// 保留中の書き込みを反映してから測定を終了する
SpreadsheetApp.flush();
const elapsedMs = Date.now() - startedAt;
appendBenchmarkResult_('書き込み', 'setValue', elapsedMs, '');
}setValues()で10,000セルへ一括書き込みする
function benchmarkSetValues() {
const sourceSheet = getBenchmarkSheet_(BENCHMARK_CONFIG.sourceSheetName);
const targetSheet = getBenchmarkSheet_(BENCHMARK_CONFIG.targetSheetName);
const values = sourceSheet
.getRange(1, 1, BENCHMARK_CONFIG.rows, BENCHMARK_CONFIG.columns)
.getValues();
const targetRange = targetSheet.getRange(
1,
1,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
);
targetRange.clearContent();
SpreadsheetApp.flush();
const startedAt = Date.now();
targetRange.setValues(values);
// setValue版と同じ条件で、書き込み完了までを測る
SpreadsheetApp.flush();
const elapsedMs = Date.now() - startedAt;
appendBenchmarkResult_('書き込み', 'setValues', elapsedMs, '');
}共通の補助関数
上の4つの関数と一緒に、次の補助関数もApps Scriptへ追加します。
function getBenchmarkSheet_(sheetName) {
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName(sheetName);
if (!sheet) {
throw new Error(
`「${sheetName}」シートがありません。setupBenchmark()を実行してください。`,
);
}
return sheet;
}
function valueToNumber_(value) {
if (typeof value === 'number') {
return value;
}
if (typeof value === 'boolean') {
return value ? 1 : 0;
}
if (value instanceof Date) {
return value.getTime();
}
return String(value).length;
}
function appendBenchmarkResult_(category, method, elapsedMs, checksum) {
const resultSheet = getBenchmarkSheet_(BENCHMARK_CONFIG.resultSheetName);
resultSheet.appendRow([
new Date(),
category,
method,
BENCHMARK_CONFIG.rows,
BENCHMARK_CONFIG.columns,
BENCHMARK_CONFIG.rows * BENCHMARK_CONFIG.columns,
elapsedMs,
checksum,
]);
console.log(`${category} ${method}: ${elapsedMs} ms`);
}読み取りテストのチェック値が getValue() と getValues() で同じなら、両方が同じデータを処理できたことを確認できます。
検証の実行手順
処理時間やキャッシュの影響を偏らせないため、4つのテストを1つの関数で連続実行するのではなく、Apps Scriptエディタから個別に実行します。
- 検証専用スプレッドシートで
setupBenchmark()を1回実行する benchmarkGetValue()を実行するbenchmarkGetValues()を実行するbenchmarkSetValue()を実行するbenchmarkSetValues()を実行する- 順番を入れ替えながら、それぞれ合計3回以上実行する
BENCH_RESULTSシートで実行時間の中央値を比較する
Googleアカウントや実行時刻によって結果が変わるため、次のように順番を入れ替えると、先に実行した方式だけが有利になる影響を減らせます。
1巡目:getValue → getValues → setValue → setValues
2巡目:getValues → getValue → setValues → setValue
3巡目:setValues → setValue → getValues → getValuesetValue() 版が実行時間制限へ到達する場合は、rows を1,000や500へ減らし、4方式を同じセル数で測り直してください。単一セル処理だけが完了しないという事実自体も、大量データに対する重要な検証結果です。
検証結果
| 種別 | メソッド | 1回目 | 2回目 | 3回目 | 中央値 |
|---|---|---|---|---|---|
| 読み取り | getValue() | 30871 | 47512 | 25772 | 30871 |
| 読み取り | getValues() | 92 | 138 | 87 | 92 |
| 書き込み | setValue() | 53643 | 50157 | 34174 | 50157 |
| 書き込み | setValues() | 643 | 734 | 1493 | 734 |
10,000セルを読み取ったところ、 getValue()の中央値は 30871 ms、getValues()は 92 msでした。 一括読み取りは約328倍高速でした。 書き込みでは、setValue()の中央値が 50157 ms、setValues()は 734 msとなり、 一括書き込みは約48倍高速でした。
Google公式が公開している参考結果
独自検証の結果とは別に、Google公式のApps Scriptベストプラクティスには、10,000セルを連続更新する性能比較が掲載されています。
公式例は値ではなくセルの背景色を設定する検証ですが、サービスを何度も呼び出す方法と、一括設定する方法の差を示しています。
| 公式例 | 処理時間 |
|---|---|
| 100×100セルをループで連続更新 | 約70秒 |
| 100×100の配列を作り、一括更新 | 約1秒 |
一括処理は、この公式例では約70倍の差になっています。
ただし、この数値を getValue() / setValue() の検証結果として流用してはいけません。背景色とセルの値では処理内容が異なるためです。あくまで「サービス呼び出しをまとめると大幅に高速化できる」という原則を支える参考結果として紹介します。
結果を読むときの注意点
1回だけの測定で結論を出さない
Apps Scriptはクラウド上で動くため、同じコードでも実行時間が変動します。最低でも3回測り、平均値だけでなく中央値も確認しましょう。
getValue・setValueにも内部最適化が働く
Spreadsheetサービスには先読みや書き込みキャッシュがあります。そのため、単一セルメソッドの呼び出し回数と、実際のネットワーク通信回数が常に完全に一致するとは限りません。
それでも、Google公式は読み書きの回数を減らし、一括操作する方法を推奨しています。内部最適化があることを理由に、ループ内で単一セル処理を繰り返す設計へ戻す必要はありません。
flush()の有無をそろえる
書き込みは内部で保留・一括適用される場合があります。片方だけ SpreadsheetApp.flush() を呼ぶと、公平な比較になりません。今回のコードでは、setValue() と setValues() の両方で、書き込み後に flush() を実行しています。
数式や書式の多いシートでは結果が変わる
数式、条件付き書式、編集トリガー、外部参照などが多いシートでは、再計算や関連処理の影響を受けます。まず空の検証用スプレッドシートでメソッド自体の傾向を測り、その後、必要に応じて実際のシートに近い条件でも測定しましょう。
極端に大きなデータは分割する
getValues() / setValues() も、どれだけ大きなデータでも一度に処理すればよいわけではありません。配列が大きすぎるとメモリや実行時間の問題が起こります。
数万行以上を扱う場合は、たとえば1,000行や5,000行など、処理内容に応じた単位へ分割し、それぞれのまとまりを一括処理する方法を検討します。
どのメソッドを選べばよい?
| 処理内容 | 基本の選択 |
|---|---|
| 1つの設定セルを読む | getValue() |
| 1つの結果セルへ書く | setValue() |
| 複数行・複数列を読む | getValues() |
| 複数行・複数列へ書く | setValues() |
| ループ内でセルを読み書きする | 配列を使った一括処理へ変更する |
| データが大きすぎる | 適切な件数に分割して一括処理する |
getValue() と setValue() が悪いわけではありません。対象が本当に1セルなら、単一セル用のメソッドは簡潔で読みやすい選択です。
問題になるのは、大量データに対して単一セルの操作を何千回、何万回と繰り返すことです。
まとめ
今回の検証で注目すべきなのは、10,000セルというデータ量だけではありません。
単一セル処理では10,000回必要だった読み書きが、一括処理なら1回になることが本質です。
GASで複数行のデータを処理するときは、次の流れを基本にしましょう。
getValues()で必要な範囲をまとめて取得する- JavaScriptの配列上で計算・変換する
- 出力用の二次元配列を作る
setValues()でまとめて書き込む
1セルだけなら getValue() / setValue() でも問題ありません。しかし、複数セルをループで処理しているなら、一括処理へ変更できないかを最初に検討してください。
特別な理由がなければ、大量データの読み書きには getValues() と setValues() を使う。この習慣が、GASの処理を速くし、実行時間制限へ到達しにくいコードにつながります。
参考
この記事をシェアする
合同会社raisexでは一緒に働く仲間を募集中です。
ご興味のある方は以下の採用情報をご確認ください。