// Sheets · Apps Script

Sort a sheet by multiple columns in Google Sheets.

How to use Apps Script's Range.sort() with an array of column specs to sort a Google Sheet by multiple columns, with the right column-numbering model so the sort actually lands where you expect.

I need to sort a Google Sheet by more than one column from a script, and I keep getting the wrong rows sorted or the wrong order.

The script

copy · paste · trigger
sortByMultipleColumns.gs
Apps Script
// Sort the data range by Department (col B) then Salary (col C) descending
function sortByMultipleColumns() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Employees');

  // lastRow / lastColumn scoped to actual data
  var lastRow = sheet.getLastRow();
  var lastCol = sheet.getLastColumn();

  // Range starts at row 2 to skip the header
  var dataRange = sheet.getRange(2, 1, lastRow - 1, lastCol);

  // Column numbers are sheet-absolute (A=1, B=2, C=3...)
  // NOT relative to the range start
  dataRange.sort([
    { column: 2, ascending: true },
    { column: 3, ascending: false }
  ]);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

What sort() actually wants

Range.sort() accepts either a single column number or an array of sort-spec objects. Each object has two keys: column (an integer) and ascending (a boolean). The array order determines sort priority: the first spec is the primary sort, the second breaks ties, and so on.

The thing that trips people up is that column is always sheet-absolute. Column A is always 1, column B is always 2, no matter where your getRange call starts. If your data starts in column D and you pass column: 1, Apps Script sorts by column A, which is probably empty, and your data comes back in the original order with no error and no warning. I have watched this bite people who copy a range from a template that started in column A into a sheet where the data begins in column D.

Skipping the header row without losing it

The range you pass to sort() is the range that gets reordered. Pass the full sheet and your header row shuffles into the middle. The standard fix is to start the range at row 2: sheet.getRange(2, 1, lastRow - 1, lastCol). The subtraction keeps the row count correct; without it, the range runs one row past your data and you pick up a blank row in the sort.

getLastRow() and getLastColumn() on the sheet object return the extent of all content on the sheet, not just your table. If you have a sidebar chart or a stray note in column Z, lastColumn will be larger than you expect. For tightly defined tables I keep the column count as a named constant rather than pulling it dynamically; it saves a debugging session later.

Adding more sort levels

Extend the array with additional spec objects in priority order. There is no documented hard limit on the number of specs, though in practice anything past four or five levels is difficult to reason about and usually signals the sort logic belongs in a more deliberate data pipeline.

If you need to sort descending on the primary key, set ascending: false on the first object. The UI equivalent in Sheets is Data > Sort range > Add another sort column; the array spec maps directly to those dropdowns, left to right.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Why does my sort do nothing even though the code runs without errors?
Almost always a column number mismatch. Your data starts in column B or later, but you passed column: 1, which points at an empty column A. Apps Script sorts by that empty column and the order doesn't change. Print the column numbers you're passing and cross-check them against the sheet's actual column positions (A=1, B=2, etc.).
Can I sort a named range instead of a range I build manually?
Yes. SpreadsheetApp.getActiveSpreadsheet().getRangeByName('MyNamedRange') returns a Range object and sort() works on it identically. Column numbers still refer to the sheet's absolute column positions, not the named range's internal columns.
Does sort() modify the sheet in place or return a sorted copy?
In place. The method returns the Range object for chaining, but the actual sheet data is reordered immediately. There is no undo in script context, so if you need the original order preserved, copy the values to another sheet or array first.
How do I sort ascending by one column and descending by another in the same call?
Set ascending to different values per spec object: [{column: 2, ascending: true}, {column: 3, ascending: false}]. Each spec is independent. You can mix directions freely across as many columns as you need.