GASでスプレッドシートの処理を
高速化する方法

GASのスプレッドシート処理が遅い原因のほとんどは「1セルずつの読み書き」です。getValues()/setValues()による一括読み書き(範囲をまとめて処理する書き方)で、通信回数を減らして高速化する方法を解説します。

|対象: GAS / スプレッドシート / パフォーマンス

Table of Contents

なぜGASのスプレッドシート処理は遅くなるのか

GASからスプレッドシートを操作するメソッド(getValue()setValue()など)は、呼び出すたびにスプレッドシート本体との通信(API呼び出し)が発生します。 この1回1回の通信には固定の待ち時間があるため、回数が増えるほど積み上がって遅くなります。

つまり処理が遅い原因の多くは「計算が重い」からではなく「セルへのアクセス回数が多い」からです。 高速化の基本方針はシンプルで、スプレッドシートとの通信回数をできるだけ減らすこと。 具体的には、範囲をまとめて読み書きし、計算はメモリ上のJavaScript配列だけで行います。

遅くなる書き方

行数分だけgetValue/setValueを呼ぶ。通信が行数×回発生する

速くなる書き方

getValuesで一括取得し、配列で計算し、setValuesで一括書き込み

遅いコードと速いコードの違い

単価に消費税を加える処理を例に、遅い書き方と速い書き方を比べます。 まずは1セルずつ読み書きする「遅い例」です。ループの中で毎回getRange().getValue()setValue()を呼んでいるため、行数の2倍の通信が発生します。

// 遅い例: 1セルずつ読み書きしている
function addTaxSlow() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("売上");
  const lastRow = sheet.getLastRow();

  // 2行目から最終行まで、1行ずつ処理
  for (let row = 2; row <= lastRow; row++) {
    const price = sheet.getRange(row, 1).getValue(); // 毎回通信
    const withTax = Math.round(price * 1.1);
    sheet.getRange(row, 2).setValue(withTax);        // 毎回通信
  }
}
// 1000行なら getValue + setValue で約2000回の通信が発生する

これを一括処理に書き換えたのが次の「速い例」です。読み込みと書き込みをそれぞれ1回にまとめ、 間の計算はmap()でJavaScriptの配列だけで行います。処理内容は同じでも、通信回数が行数分から合計2回に減ります。

// 速い例: 一括で読み込み、配列で計算し、一括で書き込む
function addTaxFast() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("売上");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return; // データが無ければ終了

  // A列(単価)を2行目から一括取得: 通信は1回
  const numRows = lastRow - 1;
  const prices = sheet.getRange(2, 1, numRows, 1).getValues();

  // 計算はJavaScriptの配列だけで行う(スプレッドシートに触れない)
  const result = prices.map((r) => [Math.round(r[0] * 1.1)]);

  // B列に一括書き込み: 通信は1回
  sheet.getRange(2, 2, numRows, 1).setValues(result);
}
// 1000行でも通信は合計2回で済む

ポイントはgetRange(行, 列, 行数, 列数)の指定です。開始位置と範囲の大きさを渡すことで、複数セルをひとまとめの範囲として扱えます。

getValuesで二次元配列として読み込む

getValues()の戻り値は「行の配列の配列」=二次元配列です。data[行番号][列番号]でアクセスでき、列番号は0始まりです(A列がindex0、B列がindex1)。 使用範囲全体を取得したいときはgetDataRange()が便利です。

function readTable() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("名簿");

  // 使用範囲全体を二次元配列で取得
  const data = sheet.getDataRange().getValues();
  const header = data[0];  // 1行目はヘッダー
  const rows = data.slice(1);

  rows.forEach((row) => {
    // 列は0始まり。A列=row[0], B列=row[1], C列=row[2]
    const name = row[0];
    const email = row[1];
    Logger.log(name + " / " + email);
  });

  Logger.log("ヘッダー: " + header.join(", "));
}

この時点でスプレッドシートへのアクセスはgetDataRange().getValues()の1回だけです。あとはrowsというただのJavaScript配列を回すだけなので、何万回ループしても通信は増えません。

複数列をまとめて計算し一括書き込みする

実務では複数の入力列から複数の結果列を計算することがよくあります。 その場合も、入力範囲をまとめて読み、結果を「行=配列、全体=配列の配列」の形で組み立ててから、setValues()で一気に書き込みます。書き込む配列の行数・列数は、書き込む範囲の大きさと必ず一致させます。

// 複数列をまとめて計算し、行の配列を組み立ててから一括書き込み
function buildInvoiceAmounts() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("受注");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  const numRows = lastRow - 1;
  // A:単価, B:数量, C:税率 の3列をまとめて取得
  const src = sheet.getRange(2, 1, numRows, 3).getValues();

  // D:小計, E:税込 の2列を配列で組み立てる
  const output = src.map((row) => {
    const [price, qty, taxRate] = row;
    const subtotal = price * qty;
    const total = Math.round(subtotal * (1 + taxRate));
    return [subtotal, total];
  });

  // D2から2列分を一括書き込み
  sheet.getRange(2, 4, numRows, 2).setValues(output);
}

setValues()に渡す配列の形が範囲とずれるとエラーになります。 上の例では2列(小計・税込)を返しているので、書き込み範囲もgetRange(2, 4, numRows, 2)と2列分を指定しています。

ループ内のgetRange呼び出しをなくす

「特定の行だけ更新したい」というケースでも、ループの中でgetRange().setValue()を呼ぶと通信が積み上がります。対象列をまとめて読み込み、メモリ上の配列を書き換えてから一括で戻すと、 更新する行が何行あっても通信は2回で済みます。

// 悪い例: ループの中で毎回getRangeを呼ぶ
function markDoneSlow(rowNumbers) {
  const sheet = SpreadsheetApp.getActive().getSheetByName("タスク");
  rowNumbers.forEach((row) => {
    sheet.getRange(row, 3).setValue("完了"); // 対象行の数だけ通信
  });
}

// 良い例: C列をまとめて読み、配列を書き換えて一括で戻す
function markDoneFast(rowNumbers) {
  const sheet = SpreadsheetApp.getActive().getSheetByName("タスク");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  const numRows = lastRow - 1;
  const status = sheet.getRange(2, 3, numRows, 1).getValues(); // 1回で取得
  const targets = new Set(rowNumbers);

  for (let i = 0; i < numRows; i++) {
    const actualRow = i + 2; // 配列indexを実際の行番号に変換
    if (targets.has(actualRow)) {
      status[i][0] = "完了";
    }
  }

  sheet.getRange(2, 3, numRows, 1).setValues(status); // 1回で書き戻す
}

配列のindexは0始まり、スプレッドシートの行番号は1始まりでヘッダー分もずれます。実際の行番号 = index + 2(2行目開始の場合)の対応を意識すると、行のずれによるバグを防げます。

一括化しても終わらない大量データの分割処理

一括処理でほとんどの遅さは解消しますが、数万行に外部API呼び出しなどが絡むと、 GASの実行時間制限(1回あたり6分)に達することがあります。 その場合は処理する行を一定数(チャンク)に区切り、次回の開始位置をPropertiesService(スクリプトに紐づく簡易的なデータ保存領域)に保存して、続きから再開します。

// 一括化しても終わらない大量データは、行を分割して処理する
function processInChunks() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.getActive().getSheetByName("大量データ");
  const lastRow = sheet.getLastRow();

  const CHUNK = 2000; // 1回で処理する行数
  const start = Number(props.getProperty("startRow") || 2);
  const end = Math.min(start + CHUNK - 1, lastRow);
  if (start > lastRow) {
    props.deleteProperty("startRow");
    return; // 全行処理済み
  }

  const numRows = end - start + 1;
  const data = sheet.getRange(start, 1, numRows, 1).getValues();
  const result = data.map((r) => [String(r[0]).trim()]);
  sheet.getRange(start, 1, numRows, 1).setValues(result);

  // 次回の開始位置を保存。トリガーで再度この関数を呼べば続きから処理される
  props.setProperty("startRow", String(end + 1));
}

この関数を時間主導トリガーで数分おきに呼べば、大量データを少しずつ処理できます。 分割処理の詳しい設計はGASの6分実行時間制限を回避する方法でも解説しています。

実務での注意点

1. 書き込む配列の形を範囲と一致させる

setValues()は、渡した配列の行数・列数が書き込み範囲と一致していないとエラーになります。 「1列だけ書き込む場合も、各要素は要素1つの配列([値])にする」点に注意してください。

2. 空セルの値の扱いを確認する

getValues()では空セルは空文字("")として返ります。数値として計算する列は、 Number()で変換したり、空の行をスキップする条件を入れておくと、意図しない0や文字列連結を防げます。

3. 書式ではなく値の更新が目的か確認する

setValues()は値のみを一括更新します。背景色や罫線などの書式を大量に変える処理は別のボトルネックになりやすいため、 必要な範囲にだけ適用し、ループ内での1セルずつの書式変更は避けます。

まとめ

GASのスプレッドシート処理を速くする鍵は「通信回数を減らす」ことです。 1セルずつのgetValue/setValueをやめ、getValues()で一括取得、配列上で計算、setValues()で一括書き込みという流れに統一するだけで、 多くの処理が劇的に速くなります。 それでも終わらない大量データは、チャンク分割とトリガーを組み合わせて安定運用しましょう。

よくある質問

多くの場合、1セルずつgetValue()やsetValue()を呼び出しているのが原因です。これらのメソッドはそのつどスプレッドシートとの通信(API呼び出し)が発生するため、行数が増えるほど通信回数が積み上がって遅くなります。getValues()やsetValues()で範囲をまとめて読み書きすれば、通信回数を1〜2回に減らせます。

getValue()は1つのセルの値を返し、getValues()は指定した範囲を二次元配列(行の配列の配列)でまとめて返します。100行を1セルずつ読むと100回の通信が発生しますが、getValues()なら1回で済みます。同様にsetValue()(1セル書き込み)とsetValues()(範囲を一括書き込み)も使い分けます。

getValues()の戻り値はdata[行][列]の二次元配列です。列番号は0始まりで、A列がindex0、B列がindex1になります。ループはこの配列に対してJavaScriptだけで行い、スプレッドシートには触れません。計算結果を別の配列に詰めて、最後にsetValues()で一括書き込みするのが基本形です。

処理内容や行数によりますが、1セルずつの読み書きを一括処理に置き換えると、数百〜数千行規模で体感が大きく変わります。通信回数が行数分から1〜2回に減るためで、これまで実行時間制限(6分)に達していた処理が数秒で終わるケースもあります。まず一括化を試すのが定石です。

数万行規模で外部API呼び出しや複雑な処理を伴う場合は、一括化だけでは足りないことがあります。その場合は処理を分割して複数回のトリガー実行に分ける、進捗をPropertiesServiceに保存して続きから再開する、といった設計を組み合わせます。まず一括化でボトルネックを潰し、それでも足りなければ分割を検討します。

GAS開発・業務Webシステムを
相談する。

「処理が遅い」「実行時間制限で止まる」といったGASの性能問題の改善から、 フォーム連携・帳票出力・Gmail自動化まで、業務自動化のGAS開発をご相談いただけます。

GAS開発の対応範囲を見る