如果你曾用 appendRow() 一直寫,一陣子後檔案開始變慢;或你用 getLastRow() 續寫,卻忽然發現資料出現在超遠的行數——別懷疑,你踩到的是同一串坑:合併儲存格、隱藏列、公式回傳空字串、還有過去貼上的殘留內容。
本篇文章盡量說明內容,將 getLastRow() 的行為攤開說:它怎麼判斷「有內容」、跟 getMaxRows()/getDataRange() 差在哪、為什麼單欄續寫不能偷懶。接著給你三種抓「某欄最後一列」的方法、批次 setValues() 的正確打開方式,以及寫入前的容量檢查與尾端清理。希望本篇文章能幫助到需要的您。
目錄
{tocify} $title={目錄}
getLastRow() 是什麼?
簡單說:
Sheet.getLastRow() 會回傳工作表中「有內容」的最後一列列號(1 起算)。
空工作表回傳 0。
只要任何一欄有內容,它都視為那一列「有內容」。
含 公式 的儲存格就算公式結果顯示空字串(""),仍視為「有內容」。
受篩選/隱藏的列 一樣會被計入。
別搞混:
getLastRow():最後「有內容」的列。
getMaxRows():整張表目前總共有幾列(不管有沒有內容)。
getDataRange():整張表目前「有資料」的矩形範圍。
基礎語法與最小可行範例
function demoGetLastRow() {
const sh = SpreadsheetApp.getActive().getSheetByName('工作表1');
const lastRow = sh.getLastRow(); // 例如 37
Logger.log('最後有內容的列:' + lastRow);
}
續寫到下一列(最常用)
function appendAtEnd() {
const sh = SpreadsheetApp.getActiveSheet();
const lastRow = sh.getLastRow();
const nextRow = lastRow + 1 || 1; // 空表時 lastRow=0 → 從第1列開始
const values = [['A001', '王小明', new Date(), 1200]]; // 一筆資料(一列多欄)
sh.getRange(nextRow, 1, values.length, values[0].length).setValues(values);
}
口訣:先抓 lastRow,再 +1 寫入。空表特例要顧到(回傳 0)。
一步步實作:打造「出貨紀錄」自動續寫流程
目標:每次執行,就把一筆資料安全地寫到資料區最後面。
步驟 1|準備試算表
新增一張工作表(例如「出貨紀錄」)。
第 1 列當表頭:訂單編號|客戶名稱|出貨日|金額|備註。
步驟 2|建立 Script
在試算表 → 擴充功能 → Apps Script → 新增檔案,貼上:
function addShipment(orderId, customer, amount, note) {
const sh = SpreadsheetApp.getActive().getSheetByName('出貨紀錄');
if (!sh) throw new Error('找不到工作表:出貨紀錄');
// 1) 取得最後列
const lastRow = sh.getLastRow();
const nextRow = lastRow + 1 || 1;
// 2) 若是空表,先自動補上表頭
if (lastRow === 0) {
sh.getRange(1, 1, 1, 5).setValues([['訂單編號','客戶名稱','出貨日','金額','備註']]);
}
// 3) 寫入一列資料(注意:setValues 需 2D 陣列)
const values = [[orderId, customer, new Date(), amount, note || '']];
sh.getRange(sh.getLastRow() + 1, 1, 1, 5).setValues(values);
// 4) 基本防呆:金額數字格式化
sh.getRange(sh.getLastRow(), 4).setNumberFormat('#,##0.00');
}
步驟 3|試跑
function testAddShipment() {
addShipment('A-20251006-001', '好客戶股份有限公司', 12800, '急件');
}
步驟 4|最佳化(大量寫入)
function bulkAddShipments(rows) {
// rows = [['A001','王小明', new Date(), 1200,''], ...]
const sh = SpreadsheetApp.getActive().getSheetByName('出貨紀錄');
if (!sh) throw new Error('找不到工作表:出貨紀錄');
if (sh.getLastRow() === 0) {
sh.getRange(1, 1, 1, 5).setValues([['訂單編號','客戶名稱','出貨日','金額','備註']]);
}
const insertAt = sh.getLastRow() + 1;
sh.getRange(insertAt, 1, rows.length, 5).setValues(rows);
}
一次寫多筆時,避免逐列 setValues(效能差)。用批次寫入:
只想看某欄的「最後一列」?三種做法
getLastRow() 是看「整張表」。實務上常常只關心某一欄(例如 A 欄)。以下三招由快到慢:
作法 A|getNextDataCell(UP)(快、省記憶體)
function lastRowOfColumn(sheet, col) {
const candidate = sheet.getRange(sheet.getMaxRows(), col)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow();
// 若整欄都空,getNextDataCell 會回到第1列;再確認一下是否真的空
return sheet.getRange(candidate, col).isBlank() ? 0 : candidate;
}
作法 B|向下掃一次,再向上找第一個非空(中)
function lastRowByColumn_scan(sheet, col) {
const max = sheet.getMaxRows();
const values = sheet.getRange(1, col, max, 1).getValues();
for (let i = values.length - 1; i >= 0; i--) {
if (values[i][0] !== '') return i + 1;
}
return 0;
}
作法 C|filter(String).length(簡單但重)
function lastRowByColumn_filter(sheet, colA1) {
const values = sheet.getRange(colA1 + ':' + colA1).getValues(); // 讀進整欄
return values.filter(r => r[0] !== '').length;
}
// 用法:lastRowByColumn_filter(sh, 'A')
大欄位+大量列時,作法 C 會讀超多空白列,不建議在大檔案用。
只計算「有顯示內容」的最後列(忽略公式空字串)
有時你想忽略「結果為空字串的公式列」。做法是取 display values 從底往上找第一列有顯示內容的列:
function lastVisibleRow(sheet) {
const range = sheet.getDataRange().getDisplayValues(); // 只取有資料的矩形
for (let r = range.length - 1; r >= 0; r--) {
if (range[r].some(v => v !== '')) return r + 1;
}
return 0;
}
常見錯誤與雷點
1. 把 getLastRow() 當「某欄最後列」用
它看的是整張表;右邊某欄有字,它就算進去。要看特定欄請用上述 A/B/C 作法。
2. 空表沒處理
空表回 0,直接 lastRow + 1 還好,但如果你先拿 getRange(lastRow, 1, ...) 就炸了。請先判斷 === 0。
3. 表頭算進資料
若第 1 列是表頭,常見寫法會從 lastRow + 1 開始沒錯;但讀取/統計時別把第 1 列當資料列。
4. 公式顯示空字串仍被當成有內容
這是預期行為。若你要「忽略只含公式但結果為空的列」,用 getDisplayValues() 重新判定。
5. 篩選/隱藏讓你以為沒有資料
getLastRow() 不理你有沒有被篩掉或隱藏,它照樣抓到最底的那列。續寫時要注意可能寫到視野外。
6. 合併儲存格
上方合併、下方空白,getLastRow() 仍可能回到很下面(因為合併區域的邊界)。避免在資料區大量合併。
7. 逐列 setValue 導致超慢
續寫多筆時務必組成 2D 陣列,一次 setValues。效能差異巨大。
8. Range 尺寸不符
setValues 的列數、欄數必須完全相等於 Range 定義,否則丟錯:
The number of columns in the data does not match the number of columns in the range.
9. 把 getMaxRows() 誤認為最後資料列
getMaxRows() 是表的總列數,通常比 getLastRow() 大很多。續寫請用 getLastRow()。
10. 留白尾巴拖慢效能
一不小心把資料貼到第 5 萬列,之後每次讀整欄都被迫掃到 5 萬。清尾巴是必要功課(見下一節)。
11. clearContent() 與 deleteRows() 差異沒搞懂
clearContent() 清值但保留列;deleteRows() 直接移除列數。要重置「有效範圍」時,通常要刪列或清尾。
12. 單一欄續寫卻用整表 lastRow
想接在 A 欄最後一筆後面,請抓 A 欄的 lastRow;用整表 lastRow 會被右邊零星資料拉著走。
清理與維護:把「看不到的尾巴」處理掉
清掉尾端空白列(讓表更輕)
function trimTrailingEmptyRows(sheet) {
const lastDataRow = sheet.getLastRow(); // 或用 lastVisibleRow(sheet)
const maxRows = sheet.getMaxRows();
if (maxRows > lastDataRow) {
sheet.deleteRows(lastDataRow + 1, maxRows - lastDataRow);
}
}
續寫前自動擴充列數(避免超出)
function ensureRowsForInsert(sheet, rowsNeeded) {
const needed = (sheet.getLastRow() + rowsNeeded) - sheet.getMaxRows();
if (needed > 0) sheet.insertRowsAfter(sheet.getMaxRows(), needed);
}
實務技巧合集
安全續寫(含空表表頭、一次多筆)
function safeAppend(sheetName, rows2d, header) {
const sh = SpreadsheetApp.getActive().getSheetByName(sheetName);
if (!sh) throw new Error('找不到:' + sheetName);
if (sh.getLastRow() === 0 && header) {
sh.getRange(1, 1, 1, header.length).setValues([header]);
}
const start = sh.getLastRow() + 1;
ensureRowsForInsert(sh, rows2d.length);
sh.getRange(start, 1, rows2d.length, rows2d[0].length).setValues(rows2d);
}
只在某欄續寫(例如 A 欄當索引)
function appendByColumnA(valuesRow) {
const sh = SpreadsheetApp.getActiveSheet();
const lastA = lastRowOfColumn(sh, 1); // A 欄
const next = lastA + 1 || 1;
sh.getRange(next, 1, 1, valuesRow.length).setValues([valuesRow]);
}
找出「任一欄有顯示內容」的最後列(忽略公式空字串)
function lastVisibleRowInRange(sheet, startCol, numCols) {
const maxR = sheet.getMaxRows();
const range = sheet.getRange(1, startCol, maxR, numCols).getDisplayValues();
for (let r = range.length - 1; r >= 0; r--) {
if (range[r].some(v => v !== '')) return r + 1;
}
return 0;
}
日誌與除錯:一鍵檢查三值
function debugRows() {
const sh = SpreadsheetApp.getActiveSheet();
Logger.log({
lastRow: sh.getLastRow(),
lastVisibleRow: lastVisibleRow(sh),
maxRows: sh.getMaxRows()
});
}
效能觀念
1. 少讀多寫、整批操作:
能一次拿 getDataRange().getValues() 就不要多次 getValue();能一次 setValues() 就不要迴圈寫。
2. 避免不必要的整欄讀取:
A:A 會讀到 getMaxRows() 那麼長;先用 getLastRow() 縮小範圍。
3. 快取 lastRow:一段流程內需要多次用到,就放變數,不要一直呼叫。
4. 清尾巴:刪掉多餘空白列,getDataRange() 會更精準、更輕。
5. 少用 onEdit 重度運算:
若每次編輯都掃表,會卡。能延後就延後、能批次就批次。
問題集
Q1:為什麼 getLastRow() 比我眼睛看到的還要大?
多半是右側或下方某欄有值、公式、或被合併影響。用 lastVisibleRow() 檢查「顯示內容」的最後列,並清尾巴。
Q2:公式回傳空字串也算內容嗎?
是。格內有公式就是「有內容」。想忽略請改判斷 getDisplayValues()。
Q3:我只想接在 A 欄的最後一個有值下面,怎麼做?
用 getNextDataCell(UP) 或文中的 lastRowOfColumn() 方法。
Q4:appendRow() 可以嗎?
可以,但它一次只寫一列、且彈性較低。大量資料建議自行組成 2D 陣列搭配 setValues(),效能好很多。
Q5:空表寫入會不會失敗?
getLastRow() 為 0 時,lastRow + 1 是 1,不會有事。但你若直接 getRange(0, …) 就會炸。記得防呆。
