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

投稿更新日: 2026/8/27

サムネイル

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金額ステータス
A0011000未処理
A0022500完了
A0031800未処理

取得結果は、次のような「配列の中に配列がある形」になります。

[
  ['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回です。

  1. getValues() でA~C列をまとめて読む
  2. 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() では数値の 123getDisplayValues() では文字列の 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でスプレッドシートを扱うときは、次の形を基本にしましょう。

  1. 必要な範囲を getValues() でまとめて読む
  2. JavaScriptの配列上で計算・変換・判定する
  3. 結果を二次元配列へまとめる
  4. setValues() で一度に書き込む

getValue()setValue() は、単一の設定値、実行結果の記録、編集された1セルの処理などで便利です。しかし、複数行のデータ処理でループのたびに呼び出すと、データが増えたときに性能上の問題が表面化します。

まずは「シートとの往復を減らせないか」と考えることが大切です。特別な理由がなければ、複数セルの読み書きには getValues()setValues() を使う。この習慣だけで、GASの処理は速く、安定し、データ増加に耐えやすくなります。

参考

この記事をシェアする

合同会社raisexでは一緒に働く仲間を募集中です。

ご興味のある方は以下の採用情報をご確認ください。