Google Ads Query Language (GAQL)¶
GAQL is the SQL-like language you send in the query field of googleAds:searchStream
and googleAds:search. One query reads from one resource (campaign, ad_group, …)
and returns rows made of attributes of that resource (and of its parents),
optional segments (such as the date) and optional metrics.
SELECT
segments.date,
campaign.id,
campaign.name,
metrics.impressions,
metrics.cost_micros
FROM campaign
WHERE segments.date BETWEEN '2026-10-01' AND '2026-10-29'
AND campaign.status = 'ENABLED'
ORDER BY segments.date DESC, metrics.cost_micros DESC
LIMIT 500
Grammar¶
query := SELECT field (, field)*
FROM resource
[WHERE condition (AND condition)*]
[ORDER BY field [ASC | DESC] (, field [ASC | DESC])*]
[LIMIT positive_integer]
condition := field = value
| field != value
| field > value | field >= value | field < value | field <= value
| field IN (value, value, ...)
| field NOT IN (value, value, ...)
| field LIKE 'pattern'
| field BETWEEN value AND value
| field DURING date_range_name
value := 'string' | "string" | number | ENUM_CONSTANT
Rules that follow from the grammar:
| Rule | Detail |
|---|---|
| Clause order is fixed | SELECT, FROM, WHERE, ORDER BY, LIMIT — in that order, each at most once. |
| Keywords are case-insensitive | select … from … where … works. |
| Field names are case-sensitive | campaign.id is a field; Campaign.id is UNRECOGNIZED_FIELD. |
Only AND |
Conditions are combined with AND only. OR, NOT (other than NOT IN) and parentheses around conditions are not supported (UNEXPECTED_INPUT). |
No *, no aliases, no functions |
List each field explicitly, with its full resource.field name. |
No PARAMETERS clause |
PARAMETERS include_drafts=true is rejected with UNEXPECTED_INPUT. |
ORDER BY fields must be selected |
Ordering by a field that is not in SELECT returns PROHIBITED_FIELD_IN_ORDER_BY_CLAUSE. |
LIMIT must be a positive integer |
LIMIT 0 returns BAD_LIMIT_VALUE. LIMIT is applied after ORDER BY. |
| Strings use single or double quotes | 'ENABLED' and "ENABLED" are equivalent. Enum constants may also be written bare: campaign.status = ENABLED. |
| Dates are quoted strings | '2026-10-25' (ISO YYYY-MM-DD). An unquoted date is tokenised as arithmetic and rejected. |
| Numbers may be bare | campaign.id = 22245080429 and campaign.id = '22245080429' are equivalent. |
| Whitespace and newlines are free | Format the query as you like inside the JSON string. |
SELECT¶
List one or more fields. The set of fields you may select depends on the FROM
resource (see attributed resources). Three kinds of
fields exist:
| Kind | Prefix | Examples | Effect on the result |
|---|---|---|---|
| Attribute | customer., campaign., campaign_budget., ad_group., ad_group_ad., geo_target_constant. |
campaign.name |
Describes the entity; one row per entity |
| Segment | segments. |
segments.date, segments.geo_target_country |
Splits each entity's metrics into one row per distinct segment value |
| Metric | metrics. |
metrics.cost_micros |
Aggregated performance numbers for the row's entity × segment combination |
The complete list is in the Fields reference.
FROM and attributed resources¶
FROM names exactly one of five resources. From each resource you may select the
resource's own attributes plus the attributes of its attributed resources (its
parents in the hierarchy). Selecting anything else returns
PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE.
FROM resource |
Attribute prefixes you may select / filter on | Metrics & segments allowed? |
|---|---|---|
customer |
customer. |
Yes |
campaign |
customer., campaign., campaign_budget. |
Yes |
ad_group |
customer., campaign., ad_group. |
Yes |
ad_group_ad |
customer., campaign., ad_group., ad_group_ad. |
Yes |
geo_target_constant |
geo_target_constant. |
No — PROHIBITED_METRIC_IN_SELECT_OR_WHERE_CLAUSE / PROHIBITED_SEGMENT_IN_SELECT_OR_WHERE_CLAUSE |
The resource determines the granularity of the metrics: FROM customer gives one
row per account (per segment), FROM campaign one per campaign, and so on. Selecting
campaign.id from ad_group_ad does not aggregate to campaign level; it simply adds
the parent's id to each ad-level row.
WHERE¶
Zero or more conditions joined by AND. A condition may reference an attribute, a
metric or a segment; each has different rules.
Operators¶
| Operator | Syntax | Works on | Notes |
|---|---|---|---|
= |
field = value |
attributes, metrics, segments.date, segments.geo_target_country |
String comparison for attributes (ids compare as their decimal text). |
!= |
field != value |
attributes, metrics, segments.geo_target_country |
|
> >= < <= |
field > value |
numeric attributes, metrics, segments.date |
Both sides must be numeric for attributes/metrics, otherwise OPERATOR_FIELD_MISMATCH. For segments.date, the value is a date. |
IN |
field IN (v1, v2, …) |
attributes, segments.geo_target_country |
|
NOT IN |
field NOT IN (v1, v2, …) |
attributes, segments.geo_target_country |
|
LIKE |
field LIKE 'pattern' |
string attributes | % is the only wildcard and only at the start and/or end: '%INSTALL%' (contains), 'DMS_%' (starts with), '%_202610' (ends with). No % means exact match. Case-sensitive. |
BETWEEN |
segments.date BETWEEN 'a' AND 'b' |
segments.date |
Inclusive on both ends. b must not be before a (INVALID_VALUE_WITH_BETWEEN_OPERATOR). |
DURING |
segments.date DURING RANGE_NAME |
segments.date |
See named date ranges. |
Filtering on attributes¶
Attribute filters select which entities are returned, before any metric aggregation:
SELECT campaign.id, campaign.name, campaign.status
FROM campaign
WHERE campaign.name LIKE '%INSTALL%' AND campaign.status = 'ENABLED'
ORDER BY campaign.name DESC
[
{
"results": [
{ "campaign": { "id": "28585815842", "name": "DMS_GG_AND_SEA_INSTALL_202608", "status": "ENABLED" } },
{ "campaign": { "id": "28342646724", "name": "DMS_GG_AND_LATAM_INSTALL_202609", "status": "ENABLED" } },
{ "campaign": { "id": "26359584284", "name": "DMS_GG_AND_EA_INSTALL_202610", "status": "ENABLED" } }
],
"fieldMask": "campaign.id,campaign.name,campaign.status",
"requestId": "829df5f6219441e3a433"
}
]
Filtering on segments.date¶
segments.date is the only field that accepts BETWEEN and DURING, and the only
date field you may filter on (segments.week, segments.month and
segments.day_of_week are not allowed in WHERE: PROHIBITED_FIELD_IN_WHERE_CLAUSE).
Several date conditions may be combined; the result is their intersection.
| Condition | Report dates included |
|---|---|
segments.date = '2026-10-25' |
2026-10-25 only |
segments.date BETWEEN '2026-10-01' AND '2026-10-07' |
2026-10-01 … 2026-10-07 inclusive |
segments.date >= '2026-10-27' AND segments.date <= '2026-10-28' |
2026-10-27, 2026-10-28 |
segments.date > '2026-10-27' AND segments.date < '2026-10-30' |
2026-10-28, 2026-10-29 |
segments.date DURING LAST_7_DAYS |
the 7 days ending yesterday (UTC) |
Whatever range you ask for, it is clipped to the available data: nothing before
2026-06-30 (the first report date) and nothing after yesterday (UTC) — see
Reporting › Data freshness.
Asking for a range entirely in the future returns zero rows, not an error.
A date filter is mandatory when you select a date segment. Selecting
segments.date, segments.week, segments.month or segments.day_of_week without
a WHERE condition on segments.date returns EXPECTED_FILTERS_ON_DATE_RANGE:
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.ads.googleads.v18.errors.GoogleAdsFailure",
"errors": [
{
"errorCode": { "queryError": "EXPECTED_FILTERS_ON_DATE_RANGE" },
"message": "Error in query: a filter on segments.date is required when selecting date segments (e.g. WHERE segments.date BETWEEN '2026-10-01' AND '2026-10-07' or DURING LAST_7_DAYS)."
}
],
"requestId": "f0fc0de729c542799bd6"
}
]
}
}
Metrics without a date segment do not require a date filter, but then they are aggregated over the entire history (2026-06-30 to yesterday). Always add a date filter to metric queries unless you really want lifetime totals.
Named date ranges¶
DURING accepts exactly these names (upper- or lower-case). "Today" is the current
UTC date; because a report date becomes available only the next day, TODAY always
returns zero rows and the THIS_* ranges effectively end yesterday.
| Name | Start | End |
|---|---|---|
TODAY |
today | today (always empty) |
YESTERDAY |
yesterday | yesterday |
LAST_7_DAYS |
yesterday − 6 days | yesterday |
LAST_14_DAYS |
yesterday − 13 days | yesterday |
LAST_30_DAYS |
yesterday − 29 days | yesterday |
THIS_MONTH |
1st of the current month | today |
LAST_MONTH |
1st of the previous month | last day of the previous month |
THIS_WEEK_MON_TODAY |
Monday of the current week | today |
THIS_WEEK_SUN_TODAY |
Sunday starting the current week | today |
LAST_WEEK_MON_SUN |
Monday of the previous week | Sunday of the previous week |
LAST_WEEK_SUN_SAT |
Sunday of the previous week | Saturday of the previous week |
LAST_BUSINESS_WEEK |
Monday of the previous week | Friday of the previous week |
Any other name (for example LAST_90_DAYS) returns INVALID_VALUE_WITH_DURING_OPERATOR.
For arbitrary ranges use BETWEEN.
Filtering on segments.geo_target_country¶
Use =, !=, IN or NOT IN with full resource names such as
'geoTargetConstants/2840' (see Geo targets). Other operators
return OPERATOR_FIELD_MISMATCH. The filter restricts which countries' traffic is
counted, and works whether or not segments.geo_target_country is selected.
SELECT segments.date, segments.geo_target_country, metrics.cost_micros
FROM customer
WHERE segments.date = '2026-10-25'
AND segments.geo_target_country IN ('geoTargetConstants/2392', 'geoTargetConstants/2410')
Filtering on metrics¶
A metric condition is applied after aggregation, so it filters rows, not raw events. The metric does not need to be selected.
SELECT campaign.id, metrics.cost_micros
FROM campaign
WHERE segments.date DURING LAST_7_DAYS AND metrics.cost_micros > 1000000000
Returns the campaigns that spent more than $1,000 over the last 7 days.
ORDER BY¶
One or more fields, each optionally followed by ASC (default) or DESC. Every
field used in ORDER BY must also appear in SELECT. Numeric fields (int64,
double) sort numerically even though int64 values are strings in the JSON; all
other fields sort as text. Rows with a missing value sort first.
SELECT segments.date, campaign.id, campaign.name, metrics.cost_micros, metrics.conversions
FROM campaign
WHERE segments.date DURING LAST_7_DAYS
ORDER BY metrics.conversions DESC, segments.date
LIMIT 3
[
{
"results": [
{ "segments": { "date": "2026-10-25" }, "campaign": { "id": "22245080429", "name": "DMS_GG_AND_SEA_VALUE_202512" }, "metrics": { "costMicros": "473030000", "conversions": 379.0 } },
{ "segments": { "date": "2026-10-24" }, "campaign": { "id": "22245080429", "name": "DMS_GG_AND_SEA_VALUE_202512" }, "metrics": { "costMicros": "461900000", "conversions": 369.0 } },
{ "segments": { "date": "2026-10-27" }, "campaign": { "id": "22245080429", "name": "DMS_GG_AND_SEA_VALUE_202512" }, "metrics": { "costMicros": "354970000", "conversions": 319.0 } }
],
"fieldMask": "segments.date,campaign.id,campaign.name,metrics.costMicros,metrics.conversions",
"requestId": "…"
}
]
Without ORDER BY, rows come back sorted by the selected segments (in SELECT
order) and then by the first selected attribute. Do not rely on this; add an explicit
ORDER BY when order matters.
LIMIT¶
LIMIT n keeps the first n rows after sorting. It is applied to the whole result,
before searchStream splits it into batches or search splits it into pages.
Query cookbook for daily reporting¶
Daily campaign performance for one day:
SELECT segments.date, campaign.id, campaign.name,
campaign.app_campaign_setting.app_id,
metrics.impressions, metrics.clicks, metrics.cost_micros,
metrics.conversions, metrics.conversions_value
FROM campaign
WHERE segments.date = '2026-10-29'
Daily ad-level performance split by country (the finest grain available):
SELECT segments.date, segments.geo_target_country,
campaign.id, ad_group.id, ad_group_ad.ad.id, ad_group_ad.ad.name,
metrics.impressions, metrics.clicks, metrics.video_views, metrics.cost_micros,
metrics.conversions, metrics.conversions_value
FROM ad_group_ad
WHERE segments.date BETWEEN '2026-10-27' AND '2026-10-29'
Backfill one month of account-level totals per day:
SELECT segments.date, customer.id, metrics.cost_micros, metrics.conversions
FROM customer
WHERE segments.date BETWEEN '2026-09-01' AND '2026-09-30'
Entity dimensions only (no metrics, no date filter needed):
SELECT campaign.id, campaign.name, campaign.status, campaign.serving_status,
campaign.start_date, campaign.end_date, campaign_budget.amount_micros
FROM campaign
Query errors¶
Every GAQL problem is returned as HTTP 400 INVALID_ARGUMENT with
details[0].errors[0].errorCode.queryError set to one of the codes below. The
message pinpoints the offending token or field.
queryError |
When |
|---|---|
EXPECTED_SELECT |
Query empty or does not start with SELECT |
EXPECTED_FROM |
No FROM after the field list |
EXPECTED_BY |
ORDER not followed by BY |
BAD_FIELD_NAME |
A field was expected but something else was found (e.g. a quoted string) |
BAD_RESOURCE_TYPE_IN_FROM_CLAUSE |
FROM names an unknown resource; the message lists the valid ones |
UNRECOGNIZED_FIELD |
A field in SELECT, WHERE or ORDER BY does not exist (watch the case) |
PROHIBITED_RESOURCE_TYPE_IN_SELECT_CLAUSE |
Attribute of a resource that is not attributed to the FROM resource |
PROHIBITED_METRIC_IN_SELECT_OR_WHERE_CLAUSE |
Metric used with FROM geo_target_constant |
PROHIBITED_SEGMENT_IN_SELECT_OR_WHERE_CLAUSE |
Segment used with FROM geo_target_constant |
EXPECTED_FILTERS_ON_DATE_RANGE |
Date segment selected without a segments.date filter |
PROHIBITED_FIELD_IN_ORDER_BY_CLAUSE |
ORDER BY field not in SELECT |
PROHIBITED_FIELD_IN_WHERE_CLAUSE |
segments.week, segments.month or segments.day_of_week in WHERE |
INVALID_VALUE_WITH_DURING_OPERATOR |
Unknown date range name, or DURING followed by a non-identifier |
INVALID_DATE_FORMAT |
Date not in YYYY-MM-DD |
INVALID_VALUE_WITH_BETWEEN_OPERATOR |
BETWEEN upper bound before lower bound |
OPERATOR_FIELD_MISMATCH |
Operator not valid for the field (e.g. LIKE on segments.date, > on a string attribute) |
BAD_OPERATOR |
Unknown token where an operator was expected (e.g. NOT not followed by IN) |
BAD_VALUE |
Something other than a string, number or identifier where a value was expected |
EXPECTED_LIST |
IN / NOT IN not followed by a parenthesised list |
EXPECTED_AND |
BETWEEN a not followed by AND b |
BAD_LIMIT_VALUE |
LIMIT not a positive integer |
BAD_SYMBOL |
Character the lexer does not understand (e.g. ;) |
UNEXPECTED_INPUT |
Extra tokens after a complete query (OR, PARAMETERS, a second WHERE, …) |
UNEXPECTED_END_OF_QUERY |
Query ends in the middle of a clause |
Full example bodies are in Errors & quotas.