agentleFS
Google CloudKnown publisherGoogleCloudPlatform/data-agent-kit-plugin/skills/bigquery-graph-query/SKILL.md

bigquery-graph-query

Skill240 starsChanged 5 months agoApache-2.0View on GitHub

Provides guidelines and best practices for querying BigQuery property graphs and semantic graphs: native GQL (Graph Query Language) pattern matching, multi-hop and variable-length paths, aggregation, top-N, path extraction, output formatting (graph visualization with TO_JSON vs tabular), GRAPH_TABLE relational SQL integration, and semantic graph querying via the GRAPH_EXPAND TVF with starting-node view selection and AGG() measures. Also covers hybrid semantic search, combining `VECTOR_SEARCH` candidate generation with graph traversal when a node carries an embedding-backed property. Use when querying graph topology, node/edge connections, paths, or semantic measures over an existing BigQuery graph. Don't use for Cypher or non-BigQuery graph databases, or for creating/modifying a graph schema.

The file

Show its rules and text

We split this skill into the rules it gives an agent

22 rules. About 0 are common, 7 are distinctive, and 15 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before writing any query, determine whether you are querying a Property Graph or a Semantic Graph:

    line 20
  2. Distinctive:

    Only read a companion file when the query genuinely needs one of these

    line 48
  3. Distinctive:

    If none of the above clearly applies, this core document is sufficient.

    line 69
  4. Distinctive:

    Before generating any GQL, you MUST:

    line 77
  5. Very specific:

    Default Query Construction (Standalone GQL): Write standalone GQL queries using the RETURN statement natively (GRAPH ...

    line 87
  6. Very specific:

    Strictly Follow Graph Schema: All labels (e.g., :Person, :Account) and properties (e.g., n.id, e.amount) used in the query MUST match the …

    line 96
  7. Very specific:

    Result Uniqueness: Use DISTINCT in your RETURN/COLUMNS clause when the user prompt implies they want unique results.

    line 99
  8. Very specific:

    Graph Path Variables: When a query involves "paths", "path traversal", "path finding", or finding relationships between nodes, you MUST …

    line 101
  9. Very specific:

    Top-N / ranking: add ORDER BY <agg> DESC then LIMIT <n>.

    line 140
  10. Very specific:

    Use GRAPH_TABLE (load the SQL-integration reference) only when you must JOIN graph output with a separate relational table or feed …

    line 159
  11. Very specific:

    Group Variables: when an edge variable is quantified (e.g., e in -[e:Transfers]->{1,3}), e becomes a group variable — an array of the …

    line 224
  12. Very specific:

    When an entire path is bound to a variable (MATCH p = (...)), extract metadata and elements from it:

    line 250
  13. Distinctive:

    Use when the user wants to see relationships, paths, topology, networks, connectivity, or entire entities (nodes/edges) as a whole.

    line 274
  14. Very specific:

    Limit: always append LIMIT 500 unless the user requests a different number, to avoid overwhelming the UI.

    line 283
  15. Distinctive:

    Use when the user focuses on specific attributes, statistics, or metrics.

    line 295
  16. Very specific:

    Always query a semantic graph through the GRAPH_EXPAND table-valued function (TVF).

    line 325
  17. Very specific:

    CRITICAL FALLBACK RULE: If a GRAPH_EXPAND query fails (e.g., syntax errors or system limits), DO NOT fall back to querying the graph or its …

    line 349
  18. Distinctive:

    One fact per query: Prefer answering with a single starting node.

    line 381
  19. Very specific:

    When you fall back to GQL you MUST write the aggregation yourself with standard SQL aggregate functions (SUM, COUNT, AVG, …) over the …

    line 406
  20. Very specific:

    If the data needed for the aggregation is not exposed as a property in the <property_graph> either, tell the user the metric cannot be …

    line 426
  21. Very specific:

    Do not apply SUM, AVG, COUNT, etc.

    line 437
  22. Very specific:

    Example Scenario: If the user asks for the "total number of entities", and the schema provides an Entity_id column as well as a measure …

    line 475

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

BigQuery Graph Query Guidelines

You are querying a BigQuery property graph or semantic graph. You MUST exclusively use the BigQuery GoogleSQL GQL and SQL standards. NEVER, under any circumstances, generate or consider Cypher queries. Follow the instructions below exactly rather than relying on general graph-query knowledge.

Query Paradigm Decision Guide

Before writing any query, determine whether you are querying a Property Graph or a Semantic Graph:

  • Property Graph (topology, paths, connections):
    • Schema in Context: <property_graph> DDL / node & edge tables.
    • Target Query Engine / Mechanism: Native GQL (GRAPH ... MATCH ... RETURN) by default; GRAPH_TABLE if joining relational tables.
  • Semantic Graph (business metrics, dimensions, measures):
    • Schema in Context: <semantic_graph start_node="X"> flattened views.
    • Target Query Engine / Mechanism: GRAPH_EXPAND TVF with start_node => "X" and AGG() measures.
  • Semantic Graph Fallback (unqueryable entity / cyclic path):
    • Schema in Context: Node has no <semantic_graph start_node="X"> view.
    • Target Query Engine / Mechanism: Native GQL fallback via GRAPH_TABLE over the <property_graph> with manual SQL aggregations.

Reference Files (rarely needed — do NOT load for standard queries)

This document is self-sufficient for the vast majority of questions, including matching, multi-hop and variable-length paths, filtering, aggregation / grouping / top-N, existence checks, output formatting, and GRAPH_EXPAND queries. For these, generate the query directly from the rules below — do not open the companion files.

Only read a companion file when the query genuinely needs one of these uncommon capabilities:

  • Joining graph results with a separate relational (non-graph) table, or passing values from an outer SQL table into the graph scope (parameterized GRAPH_TABLE): graph-table-and-sql-integration.md
  • Advanced element-identity functions (SOURCE_NODE_ID, DESTINATION_NODE_ID, ELEMENT_ID, LABELS), or deep performance tuning of a slow/large traversal beyond the optimization rules in this core: gql-advanced-functions-and-perf.md
  • Correlated-subquery edge cases beyond the WHERE-vs-FILTER rule below (e.g. VALUE/ARRAY subquery correlation limits): gql-subqueries.md
  • Hybrid semantic + graph search, when the question is conceptual rather than key-based and a node has an embedding-backed property, so candidates must come from VECTOR_SEARCH before traversal (embedding calls bill per invocation — do not open this otherwise): hybrid-vector-graph-search.md

If none of the above clearly applies, this core document is sufficient.


PART 1: Property Graph Querying (Native GQL)

Pre-generation Checklist

Before generating any GQL, you MUST:

  1. Identify Output Intent: Determine if the user intends to visualize a graph network (requires TO_JSON()) or view tabular data (requires specific properties).
  2. Verify Language Standard: Confirm the query will use BigQuery GoogleSQL GQL. NEVER use Cypher.

Core Directives

  1. Default Query Construction (Standalone GQL): Write standalone GQL queries using the RETURN statement natively (GRAPH ... MATCH ... RETURN). This natively supports aggregation, grouping, and top-N (see below). Only use the GRAPH_TABLE table-valued function when you must join graph results with a separate relational table (see the SQL-integration reference).
  2. Keyword Escaping: You MUST enforce backticks around any reserved SQL and GQL keywords used as identifiers (e.g., order, begin, path used as column, label, or variable names).
  3. Strictly Follow Graph Schema: All labels (e.g., :Person, :Account) and properties (e.g., n.id, e.amount) used in the query MUST match the provided graph schema. Do NOT guess or hallucinate schema elements.
  4. Result Uniqueness: Use DISTINCT in your RETURN/COLUMNS clause when the user prompt implies they want unique results.
  5. Graph Path Variables: When a query involves "paths", "path traversal", "path finding", or finding relationships between nodes, you MUST assign the matched pattern to a path variable (e.g., MATCH p = ...).

Basic Query Construction

A GQL query names the graph, then chains clauses that pipe a "working table" from one to the next: MATCH, LET (define a variable/alias), FILTER (filter intermediate results — this is the GQL clause, used between clauses instead of a trailing WHERE), WITH (project/sort/group into the next scope), ORDER BY / LIMIT / OFFSET, and RETURN (project the final output).

GRAPH <project>.<dataset>.<graph>
MATCH (src:Account)-[t:Transfers]->(dst:Account)
LET transfer_amount = t.amount
FILTER transfer_amount > 1000
WITH src, dst, transfer_amount
ORDER BY transfer_amount DESC
LIMIT 50
RETURN src.id AS source, dst.id AS destination, transfer_amount

Chaining with NEXT: Compose multiple linear statements into one compound query with the NEXT keyword — the results of the statement before NEXT pipe into the statement after it.

Aggregation, Grouping, and Top-N (native standalone GQL)

You do NOT need GRAPH_TABLE for aggregation, grouping, counts, sums, averages, "how many", "most/least", or "top N". Standalone GQL handles these natively — this covers the majority of analytical questions:

  • Aggregate functions go directly in RETURN (or WITH): COUNT(*), COUNT(DISTINCT x), SUM(e.amount), AVG(...), MIN/MAX.
  • Implicit GROUP BY: any non-aggregated expressions in RETURN/WITH become the grouping keys automatically (like standard GQL grouping). There is no separate GROUP BY clause in standalone GQL — group by listing the key columns alongside the aggregate.
  • Top-N / ranking: add ORDER BY <agg> DESC then LIMIT <n>.
-- "Which 3 entities filed the most filings?" (grouping + top-N, no GRAPH_TABLE)
GRAPH <project>.<dataset>.<graph>
MATCH (e:Entity)-[:Filed]->(f:Filing)
RETURN e.name AS entity, COUNT(f) AS filing_count
ORDER BY filing_count DESC
LIMIT 3
-- "Total transferred out per account" (grouped aggregate)
GRAPH <project>.<dataset>.<graph>
MATCH (src:Account)-[t:Transfers]->(:Account)
RETURN src.id AS account, SUM(t.amount) AS total_out
ORDER BY total_out DESC

Use GRAPH_TABLE (load the SQL-integration reference) only when you must JOIN graph output with a separate relational table or feed outer-table values into the graph scope.

Existence Checks and Subqueries (essentials)

For "entities that have / do not have a relationship", "at least one", "where there exists ..." questions, use a subquery. Two rules prevent the common errors (load the subqueries reference only for VALUE/ARRAY correlation edge cases):

  1. Use FILTER, not WHERE, for EXISTS / IN / LIKE subqueries. These throw errors inside a WHERE clause.
  2. Re-declare the graph name inside the subquery block. A subquery { ... } must repeat GRAPH <project>.<dataset>.<graph> even though the outer query already declared it.
GRAPH <project>.<dataset>.<graph>
MATCH (p:Person)
FILTER EXISTS {
  GRAPH <project>.<dataset>.<graph>
  MATCH (p)-[:Owns]->(:Account)
}
RETURN p.name

Graph Pattern Matching

Node patterns use parentheses (); edge patterns use square brackets [] connected with arrows for direction. The BigQuery-specific rules:

Node Patterns

  • MATCH (p:Person|Account): the | label expression matches nodes that have either label (OR). You cannot use & in a label expression.
  • MATCH (p:Person {id: 1}): property-filter shorthand.
  • MATCH (p:Person WHERE p.age > 18): inline WHERE condition on properties.

Edge Patterns

  • MATCH (a)-[e:Transfers]->(b): directed edge (preferred).
  • MATCH (a)-[e:Transfers {amount: 50}]->(b): edge with a property filter.
  • MATCH (a)-[e:Transfers]-(b): undirected (any direction) — avoid; specify explicit direction for better performance.

Pattern Joins

A complex pattern is one or more path patterns separated by commas ,. If two comma-separated patterns share a variable, BigQuery performs an equijoin on it; if they share none, the result is a cross join.

GRAPH <project>.<dataset>.<graph>
MATCH (src:Account)-[t1:Transfers]->(interm:Account),
      (interm)<-[:Owns]-(p:Person)
RETURN src.id AS account_id, p.name AS owner_name

Variable-Length Paths and Quantifiers

Find multi-hop connections by appending a quantifier to an edge pattern. This syntax differs from Cypher — use the BigQuery form:

  • {m, n}: repeat the edge pattern between m and n times, e.g. -[e:Transfers]->{1,3}.
  • Group Variables: when an edge variable is quantified (e.g., e in -[e:Transfers]->{1,3}), e becomes a group variable — an array of the matched edges in the path. Interact with it using array functions (ARRAY_LENGTH(e)) or horizontal aggregation (SUM(e.amount)).

Path Search Prefixes

Variable-length paths can produce exponential combinations and repeating paths. Constrain the search between each source/destination pair with a prefix placed immediately before the path pattern:

  • ANY: exactly one arbitrary matching path per unique source/destination pair.
  • ANY SHORTEST: a single path with the minimum number of edges (hops).
  • ANY CHEAPEST: a single path with the minimum total cost, aggregating COST expressions defined on the edges.
GRAPH <project>.<dataset>.<graph>
MATCH ANY SHORTEST
  (a:Account {id: 123})-[e:Transferred]->{1,3}(b:Account {id: 456})
RETURN e

Path Extraction Functions

When an entire path is bound to a variable (MATCH p = (...)), extract metadata and elements from it:

  • PATH_FIRST(p) / PATH_LAST(p): the starting / terminal node of path p.
  • PATH_LENGTH(p): an INT64 count of the edge hops in path p.
  • NODES(p) / EDGES(p): an array of the node / edge elements, ordered by their sequence in the path.
GRAPH <project>.<dataset>.<graph>
MATCH p = (a:Account)-[t:Transfers]->{1,3}(b:Account)
RETURN PATH_LENGTH(p) AS hops, TO_JSON(NODES(p)) AS path_nodes

For SOURCE_NODE_ID, DESTINATION_NODE_ID, ELEMENT_ID, and LABELS, load the advanced-functions reference.

Output Formatting: Visualization vs. Tabular

Strictly distinguish graph visualization intent from tabular data intent when constructing the RETURN/COLUMNS clause.

1. Graph Visualization Intent

Use when the user wants to see relationships, paths, topology, networks, connectivity, or entire entities (nodes/edges) as a whole. Triggers: "visualize", "show the graph", "network", "connections", "find the path", "relationship between X and Y".

  • Default to TO_JSON(): unless specific properties (e.g., n.name) or path metrics (e.g., PATH_LENGTH(p)) are explicitly requested, you MUST wrap all graph topology outputs (nodes, edges, and path variables) in TO_JSON() so the graphing UI receives full JSON objects.
  • Limit: always append LIMIT 500 unless the user requests a different number, to avoid overwhelming the UI.
GRAPH <project>.<dataset>.<graph>
MATCH p = (:Person)-[:Knows]->(:Person)
RETURN TO_JSON(p) AS full_path
LIMIT 500

2. Tabular or Chart Intent

Use when the user focuses on specific attributes, statistics, or metrics. Triggers: "what is the name", "list", "how many", "count", "average", "top 10", "aggregate". Return ONLY the required properties or aggregates; do NOT use TO_JSON().

  • Example: RETURN account.id, SUM(t.amount) AS total_transfer

Query Optimization

Apply these principles to every traversal (detailed rationale and the bidirectional UNION ALL example are in the advanced-functions/performance reference):

  1. Start from low-cardinality nodes and push specific property filters (e.g., Account {id: 7}) as early as possible in MATCH to prune the search space.
  2. Specify labels explicitly on nodes and edges — omitting known labels forces full scans over the underlying node/edge tables.
  3. Avoid bidirectional/undirected traversals ((a)-[e]-(b)) — they carry a severe performance penalty. Use explicit ->/<-; to match either direction between two nodes, UNION ALL two directed queries.
  4. Prefer a single comprehensive MATCH over chaining several MATCH statements, giving the optimizer a wider global view of the pattern.

PART 2: Semantic Graph Querying (GRAPH_EXPAND TVF)

1. Query the Flattened View with GRAPH_EXPAND

Always query a semantic graph through the GRAPH_EXPAND table-valued function (TVF); never as a table.

  • A graph exposes one or more flattened views, one per starting node. In your context each view is a separate <semantic_graph start_node="X"> block whose <ddl> describes the columns of that specific view. Pick the block that fits the question (see Rule 2) and query it by passing the graph name and the starting node as a named argument:

    SELECT ...
    FROM GRAPH_EXPAND("project_id.dataset_id.property_graph_id",
                      start_node => "X")
    WHERE ...

    The starting node MUST be passed by name (start_node => "X", matching a start_node value from the context); a positional second argument is invalid.

  • CRITICAL RULE: The semantic graph is NOT a regular table, even though its schema is presented using CREATE TABLE. You MUST NEVER query it directly as a table (e.g., FROM my_project.my_dataset.my_graph).
  • CRITICAL FALLBACK RULE: If a GRAPH_EXPAND query fails (e.g., syntax errors or system limits), DO NOT fall back to querying the graph or its source tables as standard tables. Doing so causes a critical NOT_FOUND error.

2. Choosing the Starting Node (Avoid Under-counting)

Each starting-node view is produced by joining outward from that node, and it keeps exactly one row per entity reachable from the starting node. Entities that are not reachable from the starting node are dropped from the view — so the wrong starting node silently under-counts.

  • Rule: To aggregate a measure M that is defined on node N (its column is named N_<measure>, e.g. Department_total_budget), use start_node => "N". You may then GROUP BY or filter on N and on any node reachable from N (e.g. a parent entity). Do not start from a finer-grained node, which would drop rows of N that have no finer-grained children.
  • Worked example. For "total budget per college", where the schema has Department_total_budget INT64 OPTIONS(is_measure=TRUE, ...):

    -- CORRECT: start from Department (the measure's node), then group by college.
    SELECT College_college_name, AGG(Department_total_budget) AS budget
    FROM GRAPH_EXPAND("proj.ds.school_graph", start_node => "Department")
    GROUP BY College_college_name;

    Starting from a finer node such as Course would omit any department that has no courses, so its budget would vanish from the total — a silent under-count. Always root at the node that owns the measure.

  • One fact per query. Prefer answering with a single starting node. If a question needs measures that live on different nodes and no single view can hold them all without dropping rows (e.g. a budget measure on Department and a count measure on Course), compute each with its own start_node in a separate query and combine the results (e.g. join on the shared grouping key).

3. When No Starting Node Fits — Fall Back to GQL

Some nodes are not GRAPH_EXPAND-queryable — for example a node on a cyclic or convergent path (a node reachable by two or more paths). Such a node has no <semantic_graph start_node="X"> block in your context; only the shared <property_graph> describes it. When answering the question requires an entity or measure that has no starting-node view:

  • DO NOT substitute a different starting node to force a GRAPH_EXPAND query — a view rooted elsewhere drops that entity's rows and silently mis-counts (see Rule 2).
  • DO NOT query the graph's source tables directly (see Rule 1).
  • Answer with GQL instead. Query the <property_graph> with Graph Query Language, following the property-graph (GQL) guidelines. GQL pattern-matches regardless of join cardinality, so it is not restricted to GRAPH_EXPAND-queryable starting nodes.
  • Measures are NOT available in GQL. is_measure columns and the AGG() function exist only in the flattened GRAPH_EXPAND views, never in GQL. When you fall back to GQL you MUST write the aggregation yourself with standard SQL aggregate functions (SUM, COUNT, AVG, …) over the underlying node/edge properties, wrapping the pattern match in GRAPH_TABLE(...) so it integrates with SQL grouping.
  • Worked example. In a graph where Office is not queryable (it converges on Company via both Division and Region), "total headcount per company" cannot use any GRAPH_EXPAND view. Match the path in GQL and aggregate the raw headcount property yourself:

    SELECT company_name, SUM(headcount) AS total_headcount
    FROM GRAPH_TABLE(
      `proj.ds.graph`
      MATCH (o:Office)-[:OfficeToDivision]->(:Division)
            -[:DivisionToCompany]->(c:Company)
      COLUMNS (c.name AS company_name, o.headcount AS headcount)
    )
    GROUP BY company_name;
  • If the data needed for the aggregation is not exposed as a property in the <property_graph> either, tell the user the metric cannot be computed for that entity rather than guessing.

4. Querying Measures with AGG()

Columns marked with is_measure=TRUE (e.g., Customer_customer_count INT64 OPTIONS(is_measure=TRUE)) are measure columns. You MUST aggregate them with the AGG() function.

  • Syntax: AGG(<measure_column_name>)
  • Do not apply SUM, AVG, COUNT, etc. directly to a measure column — use AGG(), which applies the measure's own aggregation exactly once per key even across fan-out joins.
  • sql_expression is informational only. A measure column may carry OPTIONS(is_measure=TRUE, sql_expression='SUM(budget)'). The sql_expression tells you what the measure computes so you can pick the right one; it is not something to reproduce. You MUST always reference the measure as AGG(column). NEVER copy the raw expression or the base columns it names (e.g. SUM(budget) / budget) — those base columns are not present in the flattened view.
  • Example:

    -- Given Schema:
    -- CREATE TABLE `my_project.my_dataset.my_graph` (
    --   Customer_name STRING,
    --   Customer_total_orders INT64
    --     OPTIONS(is_measure=TRUE, sql_expression='COUNT(order_id)'),
    --   Product_name STRING
    -- );
    
    SELECT
      Customer_name,
      AGG(Customer_total_orders) AS total_orders
    FROM GRAPH_EXPAND("my_project.my_dataset.my_graph",
                      start_node => "Customer")
    GROUP BY Customer_name;

5. Prefer Measures (AGG) over Standard SQL Aggregations

You MUST prioritize using pre-defined measures (columns with is_measure=TRUE) over writing standard SQL aggregations (like COUNT(DISTINCT ...), SUM, etc.) whenever a relevant measure is available in the schema.

  • Context: Semantic graphs define business logic within measures to ensure accuracy and prevent issues like overcounting. Generating aggregations via standard SQL bypasses this logic.
  • Example Scenario: If the user asks for the "total number of entities", and the schema provides an Entity_id column as well as a measure column Entity_count INT64 OPTIONS(is_measure=TRUE):

    • INCORRECT (Standard SQL):

      SELECT COUNT(DISTINCT Entity_id) AS total_entities ...
    • CORRECT (Measure):

      SELECT AGG(Entity_count) AS total_entities ...

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.