top of page

BigQuery graph measures are GA and stop double counting after joins

7 hours ago
8 min read
BigQuery graph measures are GA and stop double counting after joins, title over a teal city skyline, Data Studios original cover

Google Cloud has made measures in BigQuery Graph generally available. The BigQuery release notes entry dated November 10, 2026 describes graph measures as a way to "lock an aggregation to a key in order to perform complex aggregations without overcounting", and it closes with the statement that these features are generally available (GA). Google followed with a product blog post dated November 11, 2026 that presents measures as the place where BI metric definitions and graph relationships live together in one governed model.


For analysts and BI developers the change is concrete. A measure is an aggregate, such as a sum of budgets or a count of accounts, declared once inside the property graph definition and tied to the key of the table where it is defined. When a query later joins that table to others and rows get duplicated, the measure still aggregates each key once. That removes one of the most common sources of inflated numbers in SQL reporting, the fan-out that happens when a dimension is joined to a fact table with many rows per key.


The GA release also adds multi-fact querying through the GRAPH_EXPAND function, business metadata on measures, automatic cataloging in Knowledge Catalog and support in Data Studio through the BigQuery connector. Google has not announced a separate price for measures, and the BigQuery documentation ties GQL queries to on-demand pricing or to Enterprise and Enterprise Plus edition reservations.


··········


⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣

WHAT GOOGLE MADE GENERALLY AVAILABLE IN BIGQUERY MEASURES.


According to the release notes, a measure expression can contain aggregate function calls, expressions inside aggregates, conditional logic and conditional distinct counting, scalar operations outside aggregates, and combinations of other measures wrapped in the AGG function. The documentation lists the supported aggregate functions as SUM, AVG, COUNT, MIN and MAX, plus AGG for derived measures, and all of them except AGG accept DISTINCT. Every measure needs an alias, and measures are declared in the PROPERTIES clause of a node or edge table inside CREATE PROPERTY GRAPH.


Querying works in two steps. The GRAPH_EXPAND table-valued function flattens the graph into a table, and the AGG function then evaluates each measure. The release notes state that GRAPH_EXPAND now supports multiple fact tables, and that the BQ.SHOW_GRAPH_EXPAND_SCHEMA system procedure returns the output schema of an expansion. Graph labels and properties can carry extensions in their OPTIONS clause, which are string pairs for custom metadata used in natural language querying.


........


Capability

Status on November 10, 2026

What it does

Grain-locked measures

GA

Aggregate once per KEY of the defining table

Derived measures with AGG

GA

Build ratios and differences from other measures

Multi-fact GRAPH_EXPAND

GA

Expand each fact table from its own root node

BQ.SHOW_GRAPH_EXPAND_SCHEMA

GA

Return the expanded schema as JSON

OPTIONS extensions

GA

Attach descriptions, synonyms and key-value metadata

Data Studio connector

Supported

Builds charts on graph measures

Knowledge Catalog

Supported

Catalogs graphs and measures for search


........


Measures sit on top of BigQuery Graph, which became usable under on-demand pricing in an announcement dated November 6, 2026, covered in our article on BigQuery Graph running GQL queries on on-demand pricing. The measures documentation adds that the feature "may not be available when using reservations that are created with certain BigQuery editions", so teams on Standard edition reservations should confirm support before planning a rollout.


··········


HOW A BIGQUERY MEASURE IS DEFINED IN CREATE PROPERTY GRAPH.


The documentation builds its examples on a small university graph. A department node declares its budget column and a measure over it, and the measure can carry a description and extensions in the same statement:


university.Department
  KEY(dept_id)
  PROPERTIES(dept_id, dept_name, college_id,
    budget OPTIONS(description="Department budget in USD"),
    MEASURE(SUM(budget)) AS total_budget,
    MEASURE(SUM(budget) * 1.10) AS budget_with_overhead OPTIONS(
      description="Total department budget including 10% overhead",
      extensions=[("unit", "USD"), ("tier", "planning")]
    ))

Derived measures reuse other measures on the same element table. The documented example computes an average from two measures and protects against division by zero:


MEASURE(SAFE_DIVIDE(AGG(total_credits), NULLIF(AGG(course_count), 0))) AS computed_avg_credits

Google's blog post states that BigQuery validates these dependencies when the schema compiles, so circular references or invalid aggregations fail at definition time instead of producing a wrong number at query time. The documentation confirms that derived measures can reference only measures on the same element table and that circular references return an error.


··········


QUERYING MEASURES WITH GRAPH_EXPAND AND AGG.


A GQL query cannot reference a measure property directly, and a measure column cannot be selected on its own. Measures are read through GRAPH_EXPAND and wrapped in AGG, which performs the measure's aggregation once per key. The documented query rolls department budgets and course counts up to colleges:


SELECT
  College_college_name,
  AGG(Department_total_budget) AS college_budget,
  AGG(Course_course_count) AS total_courses
FROM
  GRAPH_EXPAND("university.SchoolGraph")
GROUP BY
  College_college_name;

Flattened output names columns as element plus property, such as Department_total_budget. Setting flatten_results to FALSE returns root properties as plain columns and joined neighbors as nested STRUCT columns. When a graph has several root node tables, the start_node argument is required and picks the subgraph to expand. Google describes this as the answer to the chasm trap, where joining two fact tables through a shared dimension multiplies the rows of both and inflates each metric. With start_node, each fact table is expanded from its own root, its measures are evaluated at their native grain, and the separate expansions are then joined on the shared key.


··········


[[ADPLUS_300x250_1]]

WORKED EXAMPLE: THE DOUBLE COUNTING THAT BIGQUERY MEASURES PREVENT.


The following dataset is illustrative and small enough to check by hand. It contains three accounts in two cities, six transactions and three payment cards.


........


Account

City

Transactions (USD)

Cards

A1

Prague

1,200 + 800 + 500

2

A2

Prague

9,000

0

A3

Brno

300 + 700

1


........


A plain SQL rollup that joins transactions to accounts and districts and counts account IDs looks reasonable but counts each account once per transaction:


SELECT d.city, SUM(t.amount) AS total_volume, COUNT(a.account_id) AS accounts_involved
FROM trans t JOIN account a ON t.account_id = a.account_id
JOIN district d ON a.district_id = d.district_id
GROUP BY d.city ORDER BY d.city;

We executed this query and a corrected version on the illustrative data in SQLite. The naive query returns 4 accounts for Prague and 2 for Brno, while the true counts are 2 and 1. The transaction volume is correct in this case, 11,500 USD for Prague and 1,000 USD for Brno, because transactions are the table at the bottom of the join.


The chasm trap appears when cards are joined as well. Joining accounts to both transactions and cards produces six rows for account A1, three transactions times two cards, so its volume doubles and its card count triples.


........


Account

Naive volume (USD)

Correct volume (USD)

Naive cards

Correct cards

A1

5,000

2,500

6

2

A2

9,000

9,000

0

0

A3

1,000

1,000

2

1

Total

15,000

12,500

8

3


........


In a BigQuery graph, an account_count measure defined as MEASURE(COUNT(account_id)) on the account node and a total_trans_amount measure defined as MEASURE(SUM(amount)) on the transaction node are each locked to their own key. Following the pattern in Google's documentation and blog, the per-city query reads the measures through AGG, and the per-account query expands transactions and cards from separate roots:


SELECT
  ET.Account_account_id,
  AGG(ET.Trans_total_trans_amount) AS total_volume,
  AGG(EC.Card_card_count) AS total_cards_issued
FROM
  GRAPH_EXPAND("retail_banking.BankingGraph", start_node => "Trans") AS ET
LEFT JOIN
  GRAPH_EXPAND("retail_banking.BankingGraph", start_node => "Card") AS EC
ON ET.Account_account_id = EC.Account_account_id
GROUP BY ET.Account_account_id;

The results to expect from a correctly modeled graph on this data are the "correct" columns above: 2,500, 9,000 and 1,000 USD of volume, 12,500 USD in total, and 2, 0 and 1 cards. Any output that shows 15,000 USD or 8 cards means a measure was replaced by a plain aggregate or the graph was modeled with an ambiguous edge. The same fan-out pattern is the first thing to test in AI-written SQL, as our guide on how to write SQL with Claude and catch fan-out and date errors shows with a different dataset.


··········


LIMITS AND MODELING RULES IN THE BIGQUERY MEASURES DOCUMENTATION.


The documentation lists several constraints that affect how a graph should be built. Data that cannot be reached from the root node table through joins is left out of flattened output. Edges that are ambiguous, for example a many-to-many junction table modeled as a plain edge, are ignored, and if all edges are ignored both GRAPH_EXPAND and BQ.SHOW_GRAPH_EXPAND_SCHEMA fail. The recommended pattern for many-to-many relationships is to promote the junction table to a node table with two many-to-one edges. Google also recommends using NO PROPERTIES on edge tables that add no new properties, because redundant properties can make GRAPH_EXPAND treat the edge as a separate entity.


Two behaviors can produce surprising numbers. Changes to the graph definition do not invalidate cached results for GRAPH_EXPAND queries, and the documentation tells users to disable cached results when calling it. For unmatched rows, an inline expression inside AGG is evaluated on all expanded rows, including rows where the joined table is NULL, while an expression stored as a named property is evaluated only on node table rows. A measure written as AVG(COALESCE(budget, 0)) can therefore return a different value from one built on a stored budget_or_zero property. The flatten_results FALSE option is also unavailable when an element table has a property with the same name as an adjacent element table.


··········


⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁣⁤

BIGQUERY MEASURES IN DATA STUDIO, LOOKER AND CONVERSATIONAL ANALYTICS.


Google's blog post states that the updated BigQuery connector in Data Studio can connect directly to BigQuery graphs and build charts on native measures, and that Data Studio honors the aggregation logic in the graph model to prevent accidental client-side re-aggregation. Teams that already model metrics in Looker can use LookML-based derived analytic models, which compile Explores, joins and measures into native BigQuery Graph models. Conversational Analytics supports graph and measure definitions, and Data Agent Kit includes graph skills that scaffold and validate measure definitions from natural language prompts. The blog post does not label each of these integrations individually as GA, so the GA statement in the release notes is the firm reference for the core measures features.


Measures follow a direction other platforms have also taken. Snowflake added level-of-detail metrics to semantic views, described in our piece on Snowflake semantic views with FIXED, INCLUDE and EXCLUDE metrics, and Databricks introduced relationships between Unity Catalog metric views. In those cases the features were in preview or beta when announced, while BigQuery measures are now GA.


··········


BIGQUERY GRAPH MEASURES: WHAT TO CHECK BEFORE MOVING METRICS INTO THE GRAPH.


Google confirms in the release notes that grain-locked measures, derived measures, multi-fact GRAPH_EXPAND, BQ.SHOW_GRAPH_EXPAND_SCHEMA and OPTIONS extensions are generally available as of November 10, 2026, and that measures are supported in Data Studio and cataloged in Knowledge Catalog. Running them depends on the pricing model: GQL queries require on-demand pricing or an Enterprise or Enterprise Plus reservation, and the documentation warns that some edition reservations may not support the feature. Google has not published a separate charge for measures, and the documentation page does not list the IAM roles required to define or query them.


Before moving a metric from a BI tool into the graph, the practical checks are the edition of any reservation that will run the queries, whether every relationship in the model is unambiguous, whether cached results are disabled for GRAPH_EXPAND, and whether derived measures use inline or property-based expressions for rows that may be unmatched. A small reconciliation dataset like the one above, with known correct totals, is the quickest way to confirm that a measure returns 12,500 and not 15,000.


··········


FOLLOW US FOR MORE.


·····


DATA STUDIOS


·····


datastudios.org


bottom of page