Syntax:
=QUERY(data, query, [headers])
data is any range or named range (cross-tab references work fine). query is a string written in Google's Visualization Query Language, which supports 11 clauses: select, where, group by, pivot, order by, skipping, limit, offset, label, format, and options. The headers argument tells the function how many rows to treat as column labels - almost always 1.
Reading Multi-Tab Data with QUERY
The function handles cross-tab ranges cleanly, which is where it starts earning its place in a real model:
=QUERY('P&L'!A:G, "select A, B, C where B = 'Revenue' order by A", 1)
Reference the sheet name in single quotes, then the column range. If the sheet name has spaces - 'Q1 2026 Actuals'!A:G, for example - the quotes are required.
For dynamic tab references where the sheet name lives in a cell, build the range string with concatenation:
=QUERY(INDIRECT("'"&Assumptions!$B$2&"'!A:G"),
"select A, C, E where E > 50000",
1)
Assumptions!$B$2 holds the sheet name. INDIRECT resolves it at runtime. Worth noting: INDIRECT is volatile, so lean on it sparingly in large models.
WHERE Clauses: Fungsi QUERY di Spreadsheet as a Filter Engine
The where clause is where QUERY pulls ahead of most alternatives. You can filter on text, numbers, dates, and logical combinations - all in one expression.
Text match:
=QUERY('Revenue'!A:F, "select A, B, D where C = 'Enterprise'", 1)
Numeric threshold - pulling every deal above the $50K threshold:
=QUERY('Pipeline'!A:H,
"select A, B, E where E > 50000 and F = 'Closed Won'",
1)
Date filtering using Google's date literal format (required by the Visualization Query Language specification):
=QUERY('Transactions'!A:E,
"select A, B, D where A >= date '2026-01-01' and A < date '2026-04-01'",
1)
Date literals must be written as date 'YYYY-MM-DD' - not a cell reference, not a serial number. To make the date dynamic, concatenate:
=QUERY('Transactions'!A:E,
"select A, B, D where A >= date '"
&TEXT(Assumptions!$B$3,"YYYY-MM-DD")
&"'",
1)
Combining with a dynamic cross-tab reference:
=QUERY(INDIRECT("'"&Assumptions!$B$2&"'!A:H"),
"select A, C, E where E > "&Assumptions!$C$5
&" and D = '"&Assumptions!$D$5&"'",
1)
Dynamic sheet, dynamic threshold, dynamic category filter - one formula. That's the pattern that handles real model complexity.
GROUP BY Aggregation: Fungsi QUERY di Spreadsheet for Multi-Level Summaries
If you're still building P&L summaries with nested SUMIFS, GROUP BY will change how you work. The clause aggregates rows by one or more dimensions without a helper column or pivot table rebuild.
Revenue by business unit, Q1 2026 actuals:
=QUERY('Transactions'!A:F,
"select C, sum(E)
where A >= date '2026-01-01' and A < date '2026-04-01'
group by C
order by sum(E) desc
label C 'Business Unit', sum(E) 'Q1 Revenue'",
1)
Contribution margin by SKU - the kind of slice a product controller needs weekly:
=QUERY({'Revenue'!A:F; 'COGS'!A:F},
"select Col1, Col3, sum(Col4) - sum(Col6)
where Col2 = 'Product'
group by Col1, Col3
label sum(Col4) - sum(Col6) 'Contribution Margin'",
1)
Note the {range1; range2} vertical concatenation syntax - that's how you union two ranges before querying. Column references switch from A/B/C to Col1/Col2/Col3 when you're querying an inline array rather than a named range.
Multi-level GROUP BY for a summary by region and channel:
=QUERY('Sales'!A:G,
"select B, C, sum(F)
group by B, C
order by B, sum(F) desc
label B 'Region', C 'Channel', sum(F) 'Net Revenue'",
1)
The label clause renames output columns in the result. Without it, you get headers like sum(F), which looks broken in a board pack.
QUERY vs. SUMIFS - When to Use Each
Neither wins unconditionally. The choice depends on what you need the output to look like.
| Criteria | QUERY | SUMIFS |
|---|---|---|
| Returns tabular output (multiple columns) | Yes | No |
| Works on cross-tab arrays | Yes | Limited |
| Handles date range filters natively | Yes (with literals) | Yes (with operators) |
| Supports GROUP BY aggregation | Yes | No |
| Compatible with structured table references | Partial | Full |
| Readable by non-formula users | Low | Medium |
| Performance on 200K+ rows | Slower | Faster |
For a single-cell lookup or SUMIF equivalent, SUMIFS is faster and more readable. For anything that returns a result set - a filtered transaction list, a revenue summary by dimension, a cross-tab aggregation - QUERY wins. On datasets above 200K rows, expect recalculation lag. Google's hard cell limit is 10 million cells per spreadsheet (per Google's own specification), and QUERY on a range approaching that ceiling can grind a model to a crawl.
Cross-Tab and Multi-Sheet QUERY Syntax
The most common pattern in a real financial model is pulling from a structured data tab into a summary tab:
=QUERY('P&L'!A:H,
"select A, B, sum(E)
where C = 'Gross Profit'
group by A, B
order by A",
1)
For a model where the actuals tab name changes each period (stored in Assumptions!$B$1):
=QUERY(INDIRECT("'"&Assumptions!$B$1&"'!A:H"),
"select A, C, D, E where B = 'Operating' and E > 0 order by A",
1)
For combining Q1 2026 and Q2 2026 actuals side-by-side using pivot:
=QUERY('Actuals'!A:D,
"select A, B, sum(D) where C != '' group by A, B pivot C",
1)
The pivot clause reshapes unique values in column C into separate output columns - the equivalent of a pivot table, but formula-driven and auto-refreshing when the source data updates.
Generating Complex QUERY Formulas Without Debugging for 20 Minutes
The practical limit on QUERY isn't the function itself - it's building the query string when you need dynamic dates, dynamic column references, and conditional logic all in the same expression. The string concatenation gets messy fast.
If you're working in ModelMonkey, you can describe what you need in plain language ("filter transactions where close date falls in Q2 2026 and deal size exceeds $50K, then group by rep and return total and count") and get the formula back ready to paste. It won't write your model, but it removes the 15-minute debugging loop that usually comes from a malformed date literal or a misplaced quote.
For anything built from scratch, the safest approach is to construct the query string in a helper cell first:
= "select A, B, sum(E) where A >= date '"
&TEXT(Assumptions!$B$3,"YYYY-MM-DD")
&"' and A < date '"
&TEXT(Assumptions!$B$4,"YYYY-MM-DD")
&"' group by A, B order by A"
Then reference that cell as the query argument. Easier to debug, easier to hand off.