SQL Syntax Reference
Stategraph SQL is the subset of SQL that queries your infrastructure in Stategraph. This page gives its grammar, operators, functions, row limits, and errors. For a guided introduction, see Query. For the command, see SQL commands.
Grammar Overview
query := [WITH cte [, cte ...]] select [UNION [ALL] select ...] [ORDER BY order_exprs] [LIMIT n]
cte := name AS [MATERIALIZED] (query)
select := SELECT select_list FROM from_item [, from_item ...] [join ...] [WHERE expr] [GROUP BY exprs [HAVING expr]]
from_item := table [AS alias] | unnest(expr) AS alias
join := {INNER | LEFT | RIGHT} JOIN table [AS alias] ON expr
Each query needs a FROM clause: SELECT 1 with no table is a parse error. Select scalar values from one of the available tables too.
ORDER BY and LIMIT come after the last UNION term and apply to the whole result. To sort or limit one term, put that term in parentheses. HAVING needs GROUP BY. LIMIT takes an integer, not an expression.
SELECT Statement
Basic Syntax
SELECT columns FROM table
Select All Columns
SELECT * FROM instances
Select Specific Columns
SELECT address, type, provider FROM resources
Column Aliases
SELECT address AS resource_address, type AS resource_type FROM resources
AS is required: SELECT address a is a parse error.
FROM Clause
Single Table
SELECT * FROM instances
Table Aliases
Alias a table with AS, then qualify columns with the alias. AS is required here too:
SELECT r.type, r.provider FROM resources AS r
JOINs
Combine tables with INNER JOIN, LEFT JOIN, or RIGHT JOIN, and match rows with ON:
SELECT i.address, r.type
FROM instances AS i
INNER JOIN resources AS r
ON i.resource_address = r.address
AND i.state_id = r.state_id
WHERE r.type = 'aws_instance'
A query can have more than one join. Write the join type: JOIN alone, LEFT OUTER JOIN, FULL JOIN, and CROSS JOIN are parse errors.
Available Tables
| Table | Description |
|---|---|
instances |
Resource instances: attributes, dependencies, state |
resources |
Resource definitions: type, provider, module |
states |
Terraform states |
providers |
Providers |
outputs |
State outputs |
transactions |
Transaction history |
transaction_logs |
Transaction log entries |
tenants |
Tenants |
users |
Users |
check_results |
Check results |
check_entries |
Check entries |
cost_snapshots |
Cost snapshot summaries. See Cost |
cost_snapshot_resources |
Per-resource cost breakdown |
focus_billing_sources |
FOCUS billing sources for cost |
security_scans, security_scan_findings |
Security scans and their findings |
hcl, hcl_refs |
Stored HCL blocks and the references between them |
files, tfvars |
Data files and variable definitions of a state, with ephemeral tfvar values masked |
transaction_plan_operations, transaction_output_chunks |
Per-resource plan operations and apply output of transactions |
transaction_subgraphs, revision_hashes, revision_tx_hashes |
Transaction and revision bookkeeping |
github_pull_requests, gitlab_pull_requests, pull_request_stacks, plans, gates, gate_approvals, drift_schedules, github_work_manifests, gitlab_work_manifests, work_manifest_results, github_pull_request_latest_unlocks, gitlab_pull_request_latest_unlocks, … |
Orchestration data for GitHub and GitLab of your tenant: runs, pull requests, stacks, gates, and drift schedules. Present when Orchestration is enabled |
For the full list of tables and columns, run stategraph sql schema, or see Query.
WHERE Clause
Basic Filtering
SELECT * FROM resources WHERE type = 'aws_instance'
Multiple Conditions
SELECT * FROM resources
WHERE type = 'aws_instance'
AND provider LIKE '%hashicorp/aws%'
Expressions
Literals
| Type | Examples |
|---|---|
| String | 'hello', 'aws_instance', 'it''s' |
| Integer | 42, -1, 0 |
| Float | 3.14, -0.5, 1e3 |
| Boolean | true, false |
| Null | null |
Identifiers
Table and column names are lowercase and case-sensitive: ADDRESS is an unknown column. Quoted names, such as "address", are not supported. Qualify a column with its table name or alias:
SELECT instances.address, status FROM instances
Operators
| Operator | Description | Example |
|---|---|---|
= |
Equal | type = 'aws_instance' |
<> |
Not equal | type <> 'null_resource' |
< |
Less than | cardinality(dependencies) < 10 |
> |
Greater than | cardinality(dependencies) > 100 |
<= |
Less or equal | cardinality(dependencies) <= 50 |
>= |
Greater or equal | cardinality(dependencies) >= 5 |
AND |
Logical AND | type = 'aws_instance' AND module IS NULL |
OR |
Logical OR | type = 'aws_instance' OR type = 'aws_eip' |
NOT |
Logical NOT | NOT (type = 'null_resource') |
+ |
Addition | (attributes->>'volume_size')::integer + 10 |
- |
Subtraction | cardinality(dependencies) - 1 |
* |
Multiplication | (attributes->>'volume_size')::integer * 2 |
/ |
Division | monthly_cost::real / 30 |
- (unary) |
Negation | -cardinality(dependencies) |
\|\| |
String concatenation | 'hello' \|\| ' world' |
!= and % are not supported. Use <> for not equal.
NULL Handling
| Expression | Description |
|---|---|
IS NULL |
Value is null |
IS NOT NULL |
Value is not null |
IS DISTINCT FROM |
NULL-safe not equal |
IS NOT DISTINCT FROM |
NULL-safe equal |
Resources in the root module:
SELECT * FROM resources WHERE module IS NULL
Resources in modules:
SELECT * FROM resources WHERE module IS NOT NULL
Pattern Matching
IN
SELECT * FROM resources
WHERE type IN ('aws_instance', 'aws_ebs_volume', 'aws_eip')
LIKE / ILIKE
LIKE is case-sensitive, and ILIKE is not. % matches any sequence of characters, and _ matches one character. NOT LIKE and NOT ILIKE negate the match.
Types that start with aws_:
SELECT * FROM resources WHERE type LIKE 'aws_%'
Case-insensitive substring:
SELECT * FROM resources WHERE name ILIKE '%prod%'
Negated match:
SELECT * FROM resources WHERE type NOT LIKE 'null_%'
Type Casting
A column to text:
SELECT state_id::text FROM instances
A JSON value to a number:
SELECT (attributes->>'volume_size')::integer FROM instances
The syntax is expression::type. The target types are text, integer, bigint, smallint, real, boolean/bool, jsonb, timestamptz, and uuid, in lowercase. A cast to another type, such as numeric or interval, returns CAST_ERR. CAST(expression AS type) is not supported.
JSON Operators
These operators read JSON columns, such as attributes.
Arrow Operators
| Operator | Description | Returns |
|---|---|---|
-> |
Get JSON value | JSON |
->> |
Get JSON as text | Text |
#>> |
Get nested path as text | Text |
The path of #>> is a string literal, such as '{tags,Name}'. #> is not supported.
Examples
->:
SELECT attributes->'tags' FROM instances
->>:
SELECT attributes->>'instance_type' FROM instances
#>>:
SELECT attributes#>>'{tags,Name}' FROM instances
JSON Contains
@> checks that a JSON value contains another. Cast the JSON literal on the right to ::jsonb:
SELECT * FROM instances
WHERE attributes->'tags' @> '{"Environment": "production"}'::jsonb
Aggregation
COUNT
All instances:
SELECT COUNT(*) FROM instances
All resources:
SELECT COUNT(*) FROM resources
GROUP BY
The alias cannot be count, a reserved keyword. Use another alias, such as total:
SELECT type, COUNT(*) AS total
FROM resources
GROUP BY type
HAVING
SELECT type, COUNT(*) AS total
FROM resources
GROUP BY type
HAVING COUNT(*) > 10
Ordering
ORDER BY
Ascending, the default:
SELECT * FROM resources ORDER BY type
Ascending, explicit:
SELECT * FROM resources ORDER BY type ASC
Descending:
SELECT * FROM resources ORDER BY type DESC
Multiple Columns
SELECT * FROM resources ORDER BY type ASC, address DESC
Limiting Results
LIMIT
SELECT * FROM instances LIMIT 100
- Without a
LIMIT, a query returns at most 20 rows: the default limit. - An explicit
LIMITis capped at 1000 rows: the maximum. - Over the API, the
mql-default-limit-appliedresponse header marks a result that the default limit truncated. See API reference. stategraph sql schema --format jsonandGET /api/v1/mql/schemareport the two values asdefault_limitandmax_limit.
For a predictable result size, always add a LIMIT.
Common Table Expressions (CTEs)
WITH Clause
WITH aws_resources AS (
SELECT * FROM resources WHERE provider LIKE '%hashicorp/aws%'
)
SELECT type, COUNT(*) FROM aws_resources GROUP BY type
Multiple CTEs
WITH
aws AS (SELECT * FROM resources WHERE provider LIKE '%hashicorp/aws%'),
gcp AS (SELECT * FROM resources WHERE provider LIKE '%hashicorp/google%')
SELECT 'aws' AS cloud, COUNT(*) FROM aws
UNION
SELECT 'gcp' AS cloud, COUNT(*) FROM gcp
Functions
Built-in Functions
Function names are lowercase
Function names are case-sensitive and must be lowercase.
| Function | Description | Example |
|---|---|---|
count(*) |
Count rows | count(*) |
sum(expr) |
Sum values | sum((attributes->>'volume_size')::integer) |
avg(expr) |
Average values | avg((attributes->>'volume_size')::integer) |
nullif(a, b) |
Return null if a = b | nullif(module, '') |
to_char(expr, fmt) |
Format as string | to_char(created_at, 'YYYY-MM-DD') |
json_build_object(...) |
Build JSON object | json_build_object('key', value) |
The allowed functions are below. Any other function returns FUNC_ACCESS_ERR.
| Kind | Functions |
|---|---|
| Conditional | coalesce, nullif, greatest, least |
| Aggregate | count(*), sum, avg, min, max, bool_and, bool_or, string_agg, array_agg, json_agg, jsonb_agg |
| String | lower, upper, length, trim, ltrim, rtrim, substr, replace, split_part, strpos, starts_with, concat, concat_ws, regexp_replace, to_char |
| Numeric | abs, ceil, floor, round, trunc |
| JSON | to_jsonb, json_build_object, jsonb_build_object, json_build_array, jsonb_build_array, json_array_length, jsonb_array_length, json_typeof, jsonb_typeof, jsonb_extract_path_text, jsonb_pretty |
| Array | array_length, cardinality, array_to_string |
| Date and time | now, date_trunc, date_part, age |
Operator Precedence
From highest (binds tightest) to lowest:
.(field access),[](index)::(type cast)-(unary negation),NOT->,->>,#>>(JSON)\|\|(concatenation)*,/+,-=,<>,<,>,<=,>=,@>,IS,IS NOT,IS DISTINCT FROM,IS NOT DISTINCT FROM,IN,LIKE,ILIKE,NOT LIKE,NOT ILIKEANDOR
NOT binds tighter than a comparison, so put the condition after NOT in parentheses: NOT (type = 'null_resource').
Use parentheses to change the order, or when you are not sure:
SELECT * FROM resources WHERE (type = 'aws_instance' OR type = 'aws_eip') AND module IS NULL
Unsupported Constructs
These constructs return an error:
CASE ... WHEN ... THEN ... ENDexpressionsDISTINCT, includingCOUNT(DISTINCT ...)OFFSETNOT INBETWEENINTERSECTandEXCEPTWITH RECURSIVE- Window functions (
OVER (...)) - Derived tables (subqueries in the
FROMclause) - Qualified star (
t.*): list the columns explicitly
Subqueries work only in EXISTS (...) and IN (...). UNION ALL and MATERIALIZED CTEs also work.
Differences from PostgreSQL
Stategraph SQL checks and rewrites each query before PostgreSQL runs it. These rules differ from PostgreSQL:
- A query cannot contain comments,
--or/* */. - A query is one statement. Do not put two statements in one query, and do not end a query with
;. countworks only ascount(*).count(address)andcount(DISTINCT type)are parse errors.- In
ORDER BY, use an alias only when it names a column orcount(*). For another expression, such assum(...)or a function call, repeat the expression:ORDER BY sum(monthly_cost::real) DESC. ORDER BY 1andGROUP BY 1do not refer to a column position. The number is a value. Write the column name.ANY(...)is not allowed. To find a value in an array column, convert the array to JSON:to_jsonb(dependencies) @> '["aws_vpc.main"]'::jsonb. Or useunnestinFROM.- A string literal is
text. Compared with auuidortimestamptzcolumn, it takes the type of the column. Cast it where a function or operator needs another type:attributes->'tags' @> '{"Environment": "production"}'::jsonb. - An integer literal is
bigint. Cast it where a function or operator needsinteger:substr(name, 1::integer, 3::integer),array_length(dependencies, 1::integer),attributes->'ingress'->0::integer. - When one side of a comparison or arithmetic operator is a column, the other side must be a column or a literal that fits the column type. A function call, a cast, arithmetic,
||, or a negative number there returnsTYPE_MISMATCH_ERR. - To compare a column with an expression, cast the column:
created_at::timestamptz > date_trunc('week', now()). - In
GROUP BY, an expression with a literal does not match the same expression in the select list. PostgreSQL rejects the query. For a JSON value, use#>>in both places:GROUP BY attributes#>>'{instance_type}'. - For another such expression, compute it in a CTE, then group by the CTE column.
Reserved Keywords
ALL, AND, AS, ASC, BY, COUNT, DESC, EXISTS, FALSE, FROM, GROUP,
HAVING, ILIKE, IN, INNER, IS, JOIN, LEFT, LIKE, LIMIT, MATERIALIZED, NOT,
NULL, ON, OR, ORDER, RIGHT, SELECT, TRUE, UNION, UNNEST, WHERE, WITH
Write a keyword in all lowercase or all uppercase: select and SELECT work, but Select does not.
Query Examples
Basic Queries
All instances:
SELECT * FROM instances
All resources:
SELECT * FROM resources
One type, from the resources table:
SELECT * FROM resources WHERE type = 'aws_instance'
Multiple conditions:
SELECT * FROM resources
WHERE type = 'aws_instance'
AND module IS NOT NULL
Aggregations
Count by type. Order by the aggregate expression, not by the alias:
SELECT type, COUNT(*) AS total
FROM resources
GROUP BY type
ORDER BY COUNT(*) DESC
With a minimum count:
SELECT type, COUNT(*) AS total
FROM resources
GROUP BY type
HAVING COUNT(*) > 5
ORDER BY COUNT(*) DESC
JSON Queries
One attribute:
SELECT
address,
attributes->>'instance_type' AS instance_type
FROM instances
Filter by attribute:
SELECT address
FROM instances
WHERE attributes->>'instance_type' = 't3.micro'
Check tags:
SELECT address
FROM instances
WHERE attributes->'tags'->>'Environment' = 'production'
Complex Queries
Resources by type with counts:
SELECT type, COUNT(*) AS total
FROM resources
WHERE provider LIKE '%hashicorp/aws%'
GROUP BY type
ORDER BY COUNT(*) DESC
Error Handling
The API returns these errors with HTTP 400, in a JSON body with the error id and data. The CLI prints Error: <id> (<data>), except for an unknown or ambiguous column, where it prints a plain message.
| Error | Cause |
|---|---|
QUERY_ERR |
The query does not parse, or PostgreSQL rejects it. A parse error does not name the wrong token or its column. |
UNKNOWN_COLUMN_ERR |
A column that is not in the tables of the query. The CLI prints Error: Unknown column 'foo'. |
AMBIGUOUS_COLUMN_ERR |
A column name that is in more than one table of the query. Qualify it with the table name or alias. |
TABLE_ACCESS_ERR |
A table that is not in the available tables |
FUNC_ACCESS_ERR |
A function that is not in the allowed functions |
CAST_ERR |
A cast to a type that is not allowed |
TYPE_MISMATCH_ERR |
A column compared with a value that does not fit its type. See Differences from PostgreSQL. |
TIMEOUT_ERR |
The query ran longer than STATEGRAPH_DB_STATEMENT_TIMEOUT, 30s by default |
Next Steps
- Query: queries in the console and the CLI
- SQL commands:
stategraph sql queryandstategraph sql schema - Cost queries: cost tables and example queries