Skip to content

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.