※本記事はプロモーションを含みます
目次
新規にGoogleスプレッドシートを作成すると、初期状態で1000行目までのセルが自動的に生成されます。なので実際の業務で使うデータが数十行から数百行程度だったとしても、使われていない数百行分の空白セルがそのまま下にぶら下がった状態で放置されているシートは珍しくありません。ただしこの「使っていないけれど存在している末尾のセル」は、単に画面のスクロールバーが細長くなって使いにくくなるだけではありません。というのは、スプレッドシート全体の描画速度やメモリ消費、自動計算の再評価負荷にじわじわと影響を与え続けます。なのでGoogle Apps Script(GAS)を使ってスプレッドシート末尾の余分な空白行をまとめて削除し、常にデータが存在する最小限の行数に保つための具体的な処理と、実務で安全に運用するための考え方を整理しましょう。
1000行の余白をそのまま残す人と、限界まで詰める運用の差
スプレッドシートの末尾に空白行が大量に残っている状態と、データが存在する最後の行でシートをきっちり切り詰めた状態とでは、日々の作業効率や動作のスムーズさに無視できない差が出てきます。初期状態の1000行を残したままにしておくと、最もよく起こるのが「Ctrl+↓」キー(MacならCmd+↓)でデータ末尾に飛ぼうとしたときの事故です。ただし連続するデータ列の途中に誤って1セルだけ空欄が紛れ込んでいた場合、そこを飛び越えて最下段である1000行目まで一気に画面が吹き飛んでしまいます。なので元の作業位置を見失って戻ってくるのに無駄なスクロールを強いられることになります。というのは、マウスのホイールやトラックパッドで少し勢いよくスクロールしただけでも、データが存在しない真っ白なエリアに飛び出してしまい、視覚的なストレスが蓄積していくからです。
見た目や操作性の問題だけではありません。というのは、スプレッドシートの内部的なパフォーマンスにおいても、不要な行の存在は直接的な足枷になります。Googleスプレッドシートはクラウド上で動作し、ブラウザ上で仮想的にDOMを構築して画面を描画しています。なのでセルが1000行、26列(A列からZ列)存在しているだけで、そこには26,000個のセル領域がメモリ上に確保されます。ただしデータが空文字であっても、シート全体のセル総数としてカウントされ、セルの書式情報や条件付き書式の評価対象領域として扱われます。これが1シートだけであれば耐えられたとしても、月別や案件別などでシートが20枚、30枚と増えていった場合、数万から数十万もの無駄な空セルがファイルを圧迫することになります。なのでファイルを開くたびに読み込みインジケータが長く回り続け、文字入力のたびにブラウザが引っかかるような遅延が発生する原因になります。
一方で、末尾の空白行をこまめに削除してデータの実寸大に最適化されたシートは、スクロールバーの可動域がデータの範囲内にピタリと収まります。「Ctrl+↓」キーを押せば確実に最終レコードで止まります。なので下スクロールでどこまでも無駄に滑っていくこともありません。というのは、ファイル全体のセル総数が最小限に抑えられるため、読み込み完了までの時間が体感できるレベルで短縮されるからです。GASなどのバッチ処理を実行する際にも、処理対象外の領域が存在しない状態をあらかじめ担保できます。なのでスクリプトの実行時間節約やエラー防止にも寄与します。余分な行を削るという作業は、単なる見た目の掃除にとどまらず、ファイル全体の健康状態を健全に保つための基本的なメンテナンスです。
末尾の空白行を一撃で削る最小のスクリプトと引数の考え方
スプレッドシートの末尾にある空白行を削除する場合、手動であれば「不要な行の行番号を選択して右クリックし、行を削除」という手順を踏みます。ただしGASを使うことでシート全体の構造から自動的に余剰行数を算出し、一瞬で削除を完了させることができます。この処理を自動化する際に使用するのが、Sheetクラスに用意されているdeleteRowsメソッドです。なのでGAS、スプレッドシート、末尾、空白行、削除という一連の処理を構築するにあたり、最も簡潔かつ確実に動作する基本コードを確認しましょう。
const sheet = SpreadsheetApp.getActiveSheet();
const maxRows = sheet.getMaxRows();
const lastRow = sheet.getLastRow();
if (maxRows > lastRow) sheet.deleteRows(lastRow + 1, maxRows - lastRow);
このスクリプトが何を行っているのか、引数の計算根拠を紐解いていきます。ただしまず理解しておくべきなのは、getMaxRowsメソッドとgetLastRowメソッドの明確な違いです。getMaxRowsは「そのシートに物理的に存在している全体の行数」を返します。なのでたとえば1000行あるシートであれば、どんなにデータが少なかろうと1000という数値を返します。対してgetLastRowは、「シート全体の中で、何らかのデータや数式が入っている最も下の行番号」を返します。というのは、ヘッダーを含めて50行目までデータが入っており、51行目以降がすべて空欄であれば、getLastRowは50を返す仕組みだからです。
したがって、削除すべき対象となる行の開始位置は「データが入っている最終行のすぐ次の行」になります。なので計算式としては「lastRow + 1」行目となります。ただし削除すべき行の総数は「シート全体の総行数から、データが存在する行数を引いた差分」となります。なので「maxRows – lastRow」行となります。deleteRowsメソッドの第1引数には「削除を開始する行番号(1始まり)」を渡します。というのは、第2引数には「そこから何行分を削除するかという行数」を渡す仕様になっているからです。なのでこの2つの数値をそのまま引き渡すことで、末尾の不要領域を過不足なく一網打尽にできます。
ここで極めて重要なのが、直前に入れている「if (maxRows > lastRow)」という条件判定です。ただしもしもシート内の全行にギッシリとデータが埋まっており、maxRowsとlastRowが完全に一致していた場合、maxRows – lastRowの計算結果は「0」になります。deleteRowsメソッドに削除行数として0以下の数値を渡すと、GASは例外エラーをスローしてスクリプトの実行を停止させてしまいます。なので仮に全行が埋まっている状態でlastRow + 1を指定すると、存在しない行番号を指定したことになり、同様にエラーとなります。というのは、このif文を挟んでおくことで、「実際に削るべき余剰行が存在している場合のみ削除を実行する」という安全策が成立するからです。
getLastRow と getMaxRows がずれる現場特有のトラブル
前述のコードは極めてシンプルです。ただし実務の現場にそのまま投入すると「意図した行まで消えない」「データが入っているはずの行まで消えそうになった」「エラーで止まった」といった問題に直面することがあります。というのは、その原因の多くは、getLastRowが認識する「データが存在する」という基準と、人間の肉眼で見た「データが存在する」という感覚のズレに起因しているからです。
典型的なトラブルの筆頭が、空文字を返す数式の存在です。たとえばVLOOKUP関数やIF関数などで、条件に合致しない場合に「””」を出力するように設定されているセルが下方向に向かって大量にコピーされているケースです。ただし人間にとっては画面上何も文字が表示されていないため「空白行」に見えます。なのでGASの仕様上、セル内に数式が設定されている場合はたとえ結果が空文字であっても「データが存在する有効なセル」として扱われます。というのは、getLastRowは数式が入力されている最下段の行番号を返してしまい、末尾の空白行を削除する処理を走らせても1行も削られないという現象が起きるからです。これを避けるためには、単にgetLastRowに依存するのではなく、値が本当に存在するかどうかをRangeのgetValuesなどで配列として取得し、実質的な値の有無を下から順に走査して判定するロジックを検討しましょう。
逆に、シート上の全セルが完全に空っぽの場合にも配慮が必要です。まったく新規に作成して何も書き込んでいないシートや、データを全消去した直後のシートに対してgetLastRowを実行すると、返り値は「0」になります。なのでこの状態で「lastRow + 1」を計算すると「1」になり、「maxRows – lastRow」は「maxRows」そのものになります。ただしGoogleスプレッドシートの基本仕様として「シート内のすべての行を削除して0行にすることはできない(最低でも1行は残さなければならない)」という鉄則が存在します。というのは、全行を削除しようとした瞬間に「シートのすべての行を削除することはできません」というAPIエラーが発生してスクリプトが落ちるからです。なので業務で白紙シートを取り扱う可能性がある場合は、lastRowが0である場合の例外処理を設けるか、最低でも1行を残すようなガード条件を入れておきましょう。
さらに見落としがちなのが、非表示の行やフィルターが適用されているケースです。フィルターによってデータが絞り込まれており、たまたま下部の行が隠れている状態であっても、getLastRowは非表示行を無視せずに物理的な最下段行を返します。ただしこれ自体はデータ損失を防ぐ意味で安全に倒れた挙動です。なので人間が画面を見て「ここまでしかデータがないはずだ」と思い込んでいると、期待した行まで削られないことに違和感を覚えることになります。というのは、セルの結合が行われている場合や、ある特定の列(たとえばA列ではなくH列など)にだけ極端に下の行までデータが入っている場合、getLastRowはシート全体のすべての列の中で最も下にある行を採用するからです。「特定の列を基準にして余白を削りたい」と考えている現場では、シート全体のgetLastRowを使うのではなく、特定列のRangeを指定して下から空白を探すアプローチを取りましょう。
複数シートの一括整理と、行追加スクリプトとの共存設計
末尾の空白行を削除するロジックを日々の運用に落とし込む場合、単一のシートで動かすだけでなく、ブック全体に存在する全シートを一括してクリーンアップしたいという需要が多く存在します。ただし一度末尾の行を極限まで削ってしまうと、その後に新しいデータを追記しようとする別のGASスクリプトや手動入力と衝突するリスクが生じます。なのでこの「削除」と「その後のデータ追加」のライフサイクルを正しく設計しておくことが、現場でのトラブルを防ぐ鍵となります。
まず、ブック内の全シートを走査して末尾空白行を一括削除する場合の設計について考えます。SpreadsheetApp.getActiveSpreadsheet().getSheets()を使ってシートの配列を取得し、ループ処理で各シートに対して行削除を行っていくアプローチが基本です。ただし、この際に注意しなければならないのは、シートごとに入力状況や保護設定が異なるという点です。というのは、特定のシートが閲覧専用に保護されていたり、特定のユーザーしか編集できない権限設定になっている場合、deleteRowsを実行した瞬間に権限エラーが発生し、処理全体が途中で止まってしまうからです。なのでtry-catch構文を用いて各シートの処理を独立させ、エラーが発生したシートがあっても処理をスキップして次のシートへ継続できるように組みましょう。
そして最も深刻な問題になりやすいのが、「データを末尾に追加するスクリプト」との兼ね合いです。よくある実装として、シートの末尾に新しいデータを1行追加するために、以下のようなアプローチを取っているスクリプトがあります。
// 危険な例:行数に余裕がないと失敗する書き方
const nextRow = sheet.getLastRow() + 1;
sheet.getRange(nextRow, 1, 1, data.length).setValues([data]);
末尾の空白行をきれいに削除した直後のシートでは、maxRowsとlastRowが完全に同じ値になっています。なのでシートに存在する行のすべてにデータが詰まっている状態です。ただしこの状態のシートに対して、上記のように「lastRow + 1」行目へ直接setValuesを実行しようとするとどうなるでしょうか。「範囲がシートの境界外です」というエラーが発生して書き込みに失敗します。というのは、物理的な行(maxRows)がもう存在しないからです。
同じシート操作の続きは、スプレッドシートの金額が0の行だけ、非表示にする に書いてあります。

