Generated banner for Querying AWS Athena
Generated logo for Querying AWS Athena

Querying AWS Athena

AWSOfficialPublished by AWS

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

Runs SQL queries on Amazon Athena query history exported as an Apache Iceberg table in S3 Tables. The query_executions system table captures per-query metadata (query text, IAM principal, work group, statement type, data scanned, timing breakdown, error details, tables accessed, result reuse, engine version) for every completed query. Applies when analyzing Athena usage and cost, finding who ran a query, auditing table access, tracking data scanned by user or work group, investigating slow or failed queries, detecting performance regressions, reporting query history beyond the 45-day API window, or enabling Athena system tables. Prefers the system table over the ListQueryExecutions and GetQueryExecution APIs at scale. Trigger phrases: Athena query history, who ran this query, data scanned by user, most queried tables, slowest queries, failed query analysis, Athena cost by work group, query audit trail, enable Athena system tables.
— the description by AWS

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides guidance and SQL patterns for querying the Amazon Athena query_executions system table. This table, stored as an Apache Iceberg table in S3 Tables, captures comprehensive metadata for every completed query, including query text, IAM principals, data scanned, and timing breakdowns. It serves as a powerful alternative to standard Athena APIs for large-scale auditing, cost analysis, and performance troubleshooting, overcoming the 45-day API retention limit and enabling complex SQL-based analysis.

What it does

  • Enables and configures the Athena query_executions system table.
  • Provides SQL templates for analyzing query volume, data scanned, and DPU usage.
  • Facilitates auditing by tracking which IAM principals ran specific queries.
  • Supports performance tuning by identifying slow queries and analyzing timing breakdowns.
  • Enables lineage analysis by querying the tables_accessed metadata.
  • Offers patterns for investigating failed queries and error trends.

When to use it

  • When you need to analyze Athena usage, costs, or performance trends.
  • When you need to audit query history beyond the 45-day API window.
  • When you need to identify which users or work groups are scanning the most data.
  • When you need to perform lineage analysis to see which tables are queried most frequently.
  • When you need to debug failed queries or identify performance regressions.

Advantages

  • Allows complex SQL analysis (joins, window functions) on query history.
  • Retains query history beyond the standard 45-day API limit.
  • Includes metadata like principal_arn and tables_accessed not available in standard APIs.
  • Enables efficient aggregation across thousands of queries with a single SQL statement.
  • Supports granular cost attribution by work group and user.

Usage examples

“Show me the top 10 users who scanned the most data in Athena over the last 30 days.”
The agent generates a SQL query using the query_executions table, grouping by principal_arn and summing data_scanned_size to identify the top data consumers.
“Which tables are being queried the most in our Athena environment?”
The agent provides a SQL query that uses CROSS JOIN UNNEST on the tables_accessed column to aggregate access counts per table.
“Find the slowest queries from last week and break down their execution time.”
The agent provides a SQL query that filters by submission_date_time and selects various duration columns to highlight performance bottlenecks.
“How do I enable the Athena system table for my account?”
The agent provides the necessary AWS CLI commands to configure the system table, including guidance on KMS encryption and Glue Data Catalog integration.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/aws/querying-aws-athena/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip querying-aws-athena.zip -d .agents/skills

Documentation by AWS

Query Amazon Athena System Tables

Overview

Works best with the AWS MCP server for sandboxed execution and audit logging. All commands below use the AWS CLI and work in any environment with configured AWS credentials. Use IAM roles or temporary credentials; avoid long-lived access keys.

Amazon Athena system tables are Apache Iceberg tables that Athena automatically creates and populates with account usage data. The query_executions system table records one row per completed query with the metadata from GetQueryExecution plus additional fields that GetQueryExecution does not return. The tables are read-only and stored as fully managed Amazon S3 Tables in the service-managed aws-athena S3 table bucket in your account and Region, and are queryable with Athena SQL (the focus of this skill) or any other Iceberg-compatible engine, such as Amazon Redshift.

The system table is preferred over the raw Athena APIs (ListQueryExecutions, GetQueryExecution) because:

  • ListQueryExecutions returns only query IDs and paginates; GetQueryExecution must then be called once per ID. Reconstructing "how much did each user scan last month" means thousands of sequential API calls. The system table answers it with one GROUP BY.

  • The APIs offer no SQL, no aggregation, and no joins, and cover only a rolling 45-day window. The system table retains data for as long as you configure through S3 Tables record expiration and supports arbitrary SQL, including window functions and cross-table joins.

  • principal_arn (the IAM user or role that ran the query) is not included in the GetQueryExecution response — obtaining it otherwise requires a CloudTrail join. It is a first-class column here.

  • tables_accessed (the tables and views referenced by each query) is likewise not included in the GetQueryExecution response, enabling lineage and "who queried what table" analysis directly.

Decision Tree

User intentUse this skill?Column / approachAlternative
How many queries ran, and cost/data-scanned trendsYesdata_scanned_size, submission_date_time—
Who ran a specific query, or who scans the mostYesprincipal_arn—
Which tables are queried most / by whomYestables_accessed (UNNEST)—
Slowest queries / time breakdown / regressionsYestiming *_duration columns—
Failed-query analysis, error patternsYesstate_enum, error_* columns—
Query history older than 45 daysYessystem table (API window is 45 days)—
Status of a single in-flight/running queryNo—GetQueryExecution (system table has completed queries only)
Look up one known query by ID, ad hocMaybeone-row WHERE query_execution_id =GetQueryExecution (no setup)
Enable / disable the system tableYessee Enable section—
Query the data inside S3 tablesNo—querying-data-lake

Common Tasks

1. Check If Configured

Before querying, confirm the system table is enabled for the account and Region.

aws athena get-system-table-configuration --table-name ALL --region <REGION>
aws athena list-system-table-statuses --region <REGION>

Interpret the response:

  • get-system-table-configuration returns a configuration record (with CreatedAt) → enabled.

  • ResourceNotFoundException → not enabled. See Enable section below.

  • list-system-table-statuses returns items with Enabled: true → publishing is on for that physical table (QUERY_EXECUTIONS).

  • Enabled: false on every item → the account is not opted in.

The physical query_executions table is created by the ingestion pipeline on the first data delivery after enabling, so it may not exist immediately after opt-in even though the configuration shows enabled.

2. Enable (if not configured)

System tables are an account- and Region-level switch. The TableName parameter is always ALL; there is no per-work-group opt-in.

Before enabling — this captures sensitive data account-wide. Enabling records the verbatim query_string (which may contain PII, secrets, or business logic) and the principal_arn for every completed query in every work group, including ones created later. Review Data Sensitivity and decide on a customer-managed KMS key and Lake Formation column controls before turning it on.

Enable with default encryption (SSE-S3) — acceptable only for non-sensitive usage data. Because this table captures query_string and principal_arn verbatim, SSE-S3 is not recommended for production; prefer the customer-managed SSE-KMS form below whenever the query history may contain sensitive data:

aws athena create-system-table-configuration --table-name ALL --region <REGION>

Enable with a customer-managed KMS key (SSE-KMS):

aws athena create-system-table-configuration \
  --table-name ALL \
  --kms-key-arn arn:aws:kms:<REGION>:<ACCOUNT>:key/<KEY_ID> \
  --region <REGION>

The key policy must grant kms:GenerateDataKey, kms:Decrypt, and kms:DescribeKey to both systemtables.athena.amazonaws.com (writes) and maintenance.s3tables.amazonaws.com (S3 Tables maintenance). Scope each grant with a confused-deputy guard — aws:SourceAccount for the Athena principal and the S3 Tables encryption-context ARN for the maintenance principal:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableAthenaSystemTableKeyUsage",
      "Effect": "Allow",
      "Principal": { "Service": "systemtables.athena.amazonaws.com" },
      "Action": ["kms:GenerateDataKey", "kms:Decrypt", "kms:DescribeKey"],
      "Resource": "arn:aws:kms:<REGION>:<ACCOUNT>:key/<KEY_ID>",
      "Condition": { "StringEquals": { "aws:SourceAccount": "<ACCOUNT>" } }
    },
    {
      "Sid": "EnableS3TableMaintenanceKeyUsage",
      "Effect": "Allow",
      "Principal": { "Service": "maintenance.s3tables.amazonaws.com" },
      "Action": ["kms:GenerateDataKey", "kms:Decrypt", "kms:DescribeKey"],
      "Resource": "arn:aws:kms:<REGION>:<ACCOUNT>:key/<KEY_ID>",
      "Condition": {
        "StringLike": {
          "kms:EncryptionContext:aws:s3:arn": "arn:aws:s3tables:<REGION>:<ACCOUNT>:bucket/aws-athena/*"
        }
      }
    }
  ]
}

A customer-managed key is recommended when the usage data is sensitive. Decide on the key before first enabling — the key is immutable once the table has been created by the ingestion pipeline, so it cannot be changed on an existing table. If the query history may contain sensitive data, pass --kms-key-arn at enable time as shown above; if you enable with the default SSE-S3 and later need a customer-managed key, see Configuring encryption in the Athena documentation.

Integrate the S3 table bucket with AWS Glue Data Catalog — a one-time step per account and Region, required before querying. If S3 Tables is already integrated with the Glue Data Catalog, no additional action is needed. See Integrating Amazon S3 Tables with AWS Glue Data Catalog. To register the federated catalog directly:

aws glue create-catalog --region <REGION> --cli-input-json '{
  "Name": "s3tablescatalog",
  "CatalogInput": {
    "FederatedCatalog": {
      "Identifier": "arn:aws:s3tables:<REGION>:<ACCOUNT>:bucket/*",
      "ConnectionName": "aws:s3tables"
    }
  }
}'

Registering the catalog is account-wide S3 Tables federation, not an access grant — it exposes every S3 table bucket to Glue/Athena. Effective read access is still governed by the scoped IAM policy in Security Considerations.

Disable (stops publishing; retains the table and all published data):

aws athena delete-system-table-configuration --table-name ALL --region <REGION>

Disabling makes no S3 Tables API calls — the query_executions table and every row already written remain queryable. Re-enabling resumes publishing going forward with no backfill of the gap. Ingestion stops immediately on disable, so queries that completed shortly before disabling but were not yet delivered may be missing from the table.

3. Verify Permissions

Querying requires:

  • Athena execution permissions (athena:StartQueryExecution, athena:GetQueryResults)

  • S3 Tables read permissions on the aws-athena S3 table bucket (see least-privilege policy in Security Considerations)

  • The S3 Tables federated catalog registered in Glue (s3tablescatalog)

  • An Athena work group with a result location — either an explicit output location or Athena-managed query results — that SHOULD enforce SSE-KMS encryption, and MUST be encrypted and access-controlled for any query projecting query_string or principal_arn

Enabling and managing the configuration requires:

  • Config APIs — athena:CreateSystemTableConfiguration, athena:UpdateSystemTableConfiguration, athena:DeleteSystemTableConfiguration, and athena:GetSystemTableConfiguration on arn:<PARTITION>:athena:<REGION>:<ACCOUNT>:system-table/ALL; athena:ListSystemTableStatuses on arn:<PARTITION>:athena:<REGION>:<ACCOUNT>:system-table/*.

  • S3 Tables (Athena uses the caller's credentials to create the aws-athena S3 table bucket and attach its bucket policy) — s3tables:CreateTableBucket and s3tables:PutTableBucketPolicy on arn:<PARTITION>:s3tables:<REGION>:<ACCOUNT>:bucket/aws-athena.

  • KMS (only when enabling with a customer managed key) — kms:DescribeKey on that key.

  • Glue (to register the bucket with the Data Catalog) — glue:CreateCatalog, glue:PassConnection. Grant these least-privilege: querying users need the read pair (GetSystemTableConfiguration, ListSystemTableStatuses) and the S3 Tables read policy below, and also need AWS Glue Data Catalog read access to the s3tablescatalog catalog and lakeformation:GetDataAccess — plus kms:Decrypt and kms:DescribeKey on the key if system tables use a customer managed key. See IAM permissions in the Athena documentation. The mutating Create/Update/Delete actions enable, reconfigure, or disable capture account-wide, so limit them to a separate setup/admin role and scope them to the system-table resource ARN arn:<PARTITION>:athena:<REGION>:<ACCOUNT>:system-table/ALL rather than *.

If CATALOG_NOT_FOUND errors occur, the Glue integration is not registered — see step 2.

4. Identify the Target Table

The following system table is available:

TableWhat it capturesGrain
query_executionsMetadata for every completed Athena query: text, principal, work group, statement type, data scanned, timing breakdown, error detail, tables referenced, result reuse, engine versionone row per completed query

Query path:

"s3tablescatalog/aws-athena"."metadata"."query_executions"
  • S3 table bucket: aws-athena (one per account per Region) — namespace: metadata.

  • Only completed queries are written: SUCCEEDED, FAILED, CANCELLED. QUEUED and RUNNING queries never appear — use GetQueryExecution for in-flight status.

  • Data is delivered in batches at a fixed frequency; each completed query appears exactly once (no duplicates).

  • Rows from different work groups are distinguished by the work_group_name column.

Schema: query_executions has 39 columns (query text, principal, work group, timing breakdown, error detail, tables accessed, result reuse, engine version, result config). Athena creates and manages the table automatically when system tables are enabled. See references/schema.md for the full column reference, or run aws glue get-table --catalog-id "<ACCOUNT>:s3tablescatalog/aws-athena" --database-name "metadata" --name "query_executions" --region <REGION> to confirm the live schema.

5. Query

Constraints:

  • You MUST confirm the work group and its result location (an explicit output location, or Athena-managed query results) before executing.

  • You SHOULD prefer a work group whose configuration enforces SSE-KMS encryption on query results. When a query projects query_string or principal_arn, you MUST confirm the result location is SSE-KMS-encrypted and access-controlled first — the results copy that sensitive data out of the governed table into the result bucket.

  • You MUST always filter on submission_date_time to enable partition pruning and control cost; an unbounded scan of query history can itself be expensive.

  • You SHOULD also filter on work_group_name when the question is about specific work groups — it is the second partition column, so this prunes further.

  • You MUST wrap total_execution_duration, query_queue_time_duration, service_pre_processing_duration, service_processing_duration, and query_planning_duration in COALESCE(col, 0) before averaging, taking percentiles, filtering on thresholds, or adding them together: a 0 ms duration is stored as NULL, and aggregates skip NULLs (so AVG would average only the non-zero values and overstate the result).

  • You MUST warn the user that the table is read-only: INSERT, UPDATE, and DELETE against it fail (no write access to the service-managed table).

  • When substituting an untrusted value (a query_execution_id, database, or table name) into a query, validate its shape or bind it as an Athena execution parameter (? placeholder) instead of string-interpolating it — the table holds sensitive text, so injection here is high-impact.

  • Every query you run against the system table is itself recorded in it; keep exploratory queries scoped.

The queries below are organized by intent. All reference the table by its short name query_executions; qualify it with the full path above when running.

These queries cover the core idioms — time-window filtering, principal_arn cost attribution, tables_accessed lineage, the timing breakdown, error triage, and the audit trail. For the full library of 38 queries organized by use case, read references/queries.md.

Query volume over time — How many queries did we run last week:

SELECT
  COUNT(*) AS total_queries_last_week
FROM query_executions
WHERE submission_date_time >= DATE_TRUNC('week', CURRENT_TIMESTAMP) - INTERVAL '7' DAY
AND submission_date_time < DATE_TRUNC('week', CURRENT_TIMESTAMP)

Top data scanners — Identify which principals (users) scan the most data over the past 30 days:

SELECT
  principal_arn,
  COUNT(*) AS query_count,
  SUM(data_scanned_size) AS total_bytes_scanned,
  SUM(data_scanned_size) / POWER(1024.0, 3) AS total_gb_scanned,
  AVG(data_scanned_size) / POWER(1024.0, 2) AS avg_mb_per_query
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY principal_arn
ORDER BY total_bytes_scanned DESC
LIMIT 25

Most queried tables — Which tables are accessed most frequently:

SELECT
  accessed_table.catalog_name,
  accessed_table.database_name,
  accessed_table.table_name,
  COUNT(*) AS access_count,
  COUNT(DISTINCT principal_arn) AS distinct_principals
FROM query_executions
CROSS JOIN UNNEST(tables_accessed) AS t(accessed_table)
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY 1, 2, 3
ORDER BY 4 DESC
LIMIT 50

Slowest queries with time breakdown — Top queries by total execution time. total_sec = queue + pre_processing + engine + processing; planning_sec is part of engine_sec, so do not add it separately:

SELECT
  query_execution_id,
  work_group_name,
  principal_arn,
  submission_date_time,
  COALESCE(total_execution_duration, 0) / 1000.0 AS total_sec,
  COALESCE(query_queue_time_duration, 0) / 1000.0 AS queue_sec,
  COALESCE(query_planning_duration, 0) / 1000.0 AS planning_sec, -- subset of engine_sec, not additive
  engine_execution_duration / 1000.0 AS engine_sec,
  COALESCE(service_pre_processing_duration, 0) / 1000.0 AS pre_processing_sec,
  COALESCE(service_processing_duration, 0) / 1000.0 AS processing_sec,
  data_scanned_size / POWER(1024.0, 3) AS gb_scanned,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
ORDER BY total_execution_duration DESC
LIMIT 25

Failed query patterns — Most common error categories and messages grouped by work group:

SELECT
  work_group_name,
  error_category_enum,
  error_type_enum,
  ANY_VALUE(error_message) AS sample_error_message,
  COUNT(*) AS failure_count
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'FAILED'
GROUP BY 1, 2, 3
ORDER BY failure_count DESC
LIMIT 25

Query audit trail — All queries with who ran them, when, and on which tables:

SELECT 
  submission_date_time,
  principal_arn,
  work_group_name,
  statement_type_enum,
  statement_subtype_enum,
  state_enum,
  tables_accessed,
  SUBSTR(query_string, 1, 500) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
ORDER BY submission_date_time DESC

The full library of 38 queries — usage analytics, debugging and performance, and auditing and governance — is in references/queries.md. Read it when the user needs an analysis beyond the examples above.

Routing: Athena SQL vs Direct API
ScenarioUse
Aggregate/trend/ranking across many queriesAthena on query_executions
"Who ran what against which table" / lineageAthena with UNNEST(tables_accessed)
History older than 45 daysAthena (the API window is 45 days)
Status of a single running query right nowDirect API: GetQueryExecution (table has completed queries only)
One-off lookup of a known query ID with no setupEither — GetQueryExecution needs no configuration
Write/modify a rowNot possible — the table is read-only

Key Behaviors

  • Supported within a single AWS account and Region; cross-account and cross-Region consolidation is not supported.

  • Only completed activity is recorded; in-flight queries appear only after reaching a final state.

  • Delivered data is immutable — no INSERT, UPDATE, or DELETE.

  • No data is backfilled: only activity after the feature is enabled is captured, and disabling then re-enabling does not fill the gap.

  • No default record expiration — configure it through S3 Tables to manage storage cost.

  • Querying requires the S3 table bucket to be integrated with AWS Glue Data Catalog.

Troubleshooting

Error / symptomCauseFix
CATALOG_NOT_FOUNDS3 Tables catalog not registered in GlueRegister s3tablescatalog (Enable, step 2), or S3 console > Table buckets > Enable integration
ResourceNotFoundException on get-system-table-configurationAccount not opted inRun create-system-table-configuration --table-name ALL
Table does not exist shortly after enablingTable is created lazily on first data deliveryRun an Athena query, then wait for the next delivery batch
Empty results though enabledData is batched; recent queries not yet delivered, or the time filter predates opt-inWait for the next delivery batch; widen submission_date_time; note there is no backfill before opt-in
Running/queued query missingOnly completed queries are writtenUse GetQueryExecution for in-flight status
INSERT/UPDATE/DELETE fails with access deniedTable is read-only to the customer identityExpected — the table is not writable
AccessDenied querying the tableMissing s3tables:GetTable / GetTableDataSee Security Considerations
TABLE_NOT_FOUND although system tables are enabled and the table existsSystem tables use a customer managed key and the querying principal lacks kms:Decrypt / kms:DescribeKey on it (Athena reports a missing table, not access denied)Grant kms:Decrypt and kms:DescribeKey on the key to the querying principal
KMS change rejectedEncryption is immutable once the table has been createdExpected — the key cannot be changed on an existing table. Set it at enable time with --kms-key-arn (see Enable); for the encryption options, see Configuring encryption

Security Considerations

Least-Privilege IAM Policy

Scope read permissions to the aws-athena S3 table bucket ARN rather than using wildcards:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3tables:GetTable",
        "s3tables:GetTableMetadataLocation",
        "s3tables:GetTableData",
        "s3tables:GetNamespace",
        "s3tables:ListTables",
        "s3tables:ListNamespaces",
        "s3tables:GetTableBucket"
      ],
      "Resource": [
        "arn:aws:s3tables:<REGION>:<ACCOUNT>:bucket/aws-athena",
        "arn:aws:s3tables:<REGION>:<ACCOUNT>:bucket/aws-athena/*"
      ]
    }
  ]
}

The least-privilege read policy above still grants full, unmasked read of query_string and principal_arn. Issue it only to trusted audit/analytics principals; for broader audiences, restrict those columns with AWS Lake Formation column-level permissions rather than widening the policy.

Data Sensitivity

The system table captures query text and identities verbatim, so it is itself a sensitive asset:

  • query_string is stored in full and unredacted. Literals in the SQL — PII in WHERE clauses, hardcoded credentials, business logic — are captured as written. execution_parameters likewise stores bound parameter values.

  • principal_arn reveals which IAM users and roles run which queries.

Access to this data is customer-governed: Athena does not apply read permissions on the caller's behalf and does not mask data. After the S3 table bucket is registered with AWS Glue Data Catalog, read access is controlled with IAM or AWS Lake Formation, including table-, column-, and row-level permissions — for example, restricting the query_string and principal_arn columns to specific principals. Grant access on a least-privilege basis, treat the table as containing the most sensitive text that flows through any query, and steer users toward parameterized queries so secrets are not embedded in query text. Store query results in encrypted, access-controlled locations and avoid logging raw output containing these fields.

Encryption for Query Results

The system table data is encrypted at rest with SSE-S3 or a customer-managed KMS key chosen at enable time (see Enable). All access is over TLS in transit by default; for defense in depth, enforce it with an aws:SecureTransport condition on the relevant resource policies. Separately, configure the Athena work group to encrypt query results at rest with SSE-KMS:

{
  "ResultConfiguration": {
    "EncryptionConfiguration": {
      "EncryptionOption": "SSE_KMS",
      "KmsKey": "arn:aws:kms:<REGION>:<ACCOUNT>:key/<KEY_ID>"
    }
  }
}
Audit Trail

Enable CloudTrail for Athena (StartQueryExecution, GetQueryResults, and the *SystemTableConfiguration calls) and for S3 Tables (s3tables:GetTableData) to retain a record of who queried the system table and who changed its configuration. Encrypt CloudTrail logs with SSE-KMS and store them in an access-logged bucket. If any Athena query metadata or results flow into CloudWatch Logs, encrypt those log groups with a customer-managed KMS key (aws logs associate-kms-key), since query_string and principal_arn are sensitive. Consider a CloudWatch/EventBridge alarm on the *SystemTableConfiguration events so enabling, reconfiguring, or disabling capture — or a KMS-key change — notifies the owning team. Encrypt that SNS topic with a customer-managed KMS key and restrict its subscriptions to authorized recipients.

Additional Resources

Files

references/queries.md

Athena query_executions — Query Library

A library of example SQL queries against the Amazon Athena query_executions system table, organized by use case. Read this file when a user needs a specific analysis; the parent SKILL.md carries the core patterns inline.

Query path: "s3tablescatalog/aws-athena"."metadata"."query_executions" (referenced below as query_executions; qualify with the full path when running).

Reminders: always filter on submission_date_time for partition pruning; the table is read-only (no INSERT/UPDATE/DELETE); only completed queries (SUCCEEDED, FAILED, CANCELLED) are present. Queries projecting query_string or principal_arn output sensitive data — confirm the query result location is SSE-KMS-encrypted and access-controlled before running. Pass untrusted values (query execution IDs, database/table names) as Athena execution parameters (? placeholders) rather than string-interpolating them, to avoid SQL injection into a table that holds sensitive text.

Usage Analytics

Query volume over time — How many queries did we run last week:

SELECT
  COUNT(*) AS total_queries_last_week
FROM query_executions
WHERE submission_date_time >= DATE_TRUNC('week', CURRENT_TIMESTAMP) - INTERVAL '7' DAY
AND submission_date_time < DATE_TRUNC('week', CURRENT_TIMESTAMP)

Average query volume — How many queries do we run on average per day:

SELECT
  ROUND(CAST(COUNT(*) AS DOUBLE) / 30, 2) AS avg_queries_per_day
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY

Average query data scanned — How much data do queries scan on average:

SELECT
  AVG(data_scanned_size) / POWER(1024.0, 2) AS avg_mb_scanned,
  AVG(data_scanned_size) / POWER(1024.0, 3) AS avg_gb_scanned
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'

What is the distribution of data scanned by queries? — Bucket queries into size ranges to understand the scan distribution. This histogram pattern (CASE size buckets, or GROUP BY a discrete numeric column, plus SUM(COUNT(*)) OVER () for percent of total) generalizes to any numeric column — for example, GROUP BY dpu_count for a DPU-usage distribution, or bucket LENGTH(query_string) for query complexity:

SELECT
    CASE
        WHEN data_scanned_size = 0 THEN 'A 0 bytes (metadata only)'
        WHEN data_scanned_size < 1048576 THEN 'B < 1 MB'
        WHEN data_scanned_size < 104857600 THEN 'C 1 MB - 100 MB'
        WHEN data_scanned_size < 1073741824 THEN 'D 100 MB - 1 GB'
        WHEN data_scanned_size < 10737418240 THEN 'E 1 GB - 10 GB'
        WHEN data_scanned_size < 107374182400 THEN 'F 10 GB - 100 GB'
        ELSE 'G > 100 GB'
    END AS scan_size_bucket,
    COUNT(*) AS query_count,
    ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS pct_of_total
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY 1
ORDER BY 1

Top data scanners — Identify which principals (users) scan the most data over the past 30 days:

SELECT
  principal_arn,
  COUNT(*) AS query_count,
  SUM(data_scanned_size) AS total_bytes_scanned,
  SUM(data_scanned_size) / POWER(1024.0, 3) AS total_gb_scanned,
  AVG(data_scanned_size) / POWER(1024.0, 2) AS avg_mb_per_query
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY principal_arn
ORDER BY total_bytes_scanned DESC
LIMIT 25

Query volume, data scanned, and DPU usage trends by work group — Daily query count and total data scanned per work group. Is our usage increasing or decreasing? What work groups are increasing in query volume? What work groups are increasing in DPU usage:

SELECT
  work_group_name,
  CAST(submission_date_time AS DATE) AS query_date,
  COUNT(*) AS query_count,
  SUM(data_scanned_size) / POWER(1024.0, 3) AS total_gb_scanned,
  SUM(dpu_count) AS total_dpus_used
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '90' DAY
GROUP BY work_group_name, CAST(submission_date_time AS DATE)
ORDER BY work_group_name, query_date

Peak usage hours — Query volume by hour of day and day of week to identify peak periods. Using 28 days to capture exactly four weeks. At what time of day do we run the most queries:

SELECT
  DAY_OF_WEEK(submission_date_time) AS dow,
  HOUR(submission_date_time) AS hour_of_day,
  COUNT(*) AS query_count,
  SUM(data_scanned_size) / POWER(1024.0, 3) AS total_gb_scanned
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '28' DAY
GROUP BY DAY_OF_WEEK(submission_date_time), HOUR(submission_date_time)
ORDER BY query_count DESC

Result reuse savings — How much data scanned was avoided thanks to result reuse:

SELECT
  work_group_name,
  COUNT(*) AS total_count,
  COUNT(*) FILTER (WHERE reused_previous_result) AS reused_count,
  ROUND(100.0 * COUNT(*) FILTER (WHERE reused_previous_result) / COUNT(*), 2) AS reuse_percentage,
  SUM(data_scanned_size) FILTER (WHERE NOT reused_previous_result) / POWER(1024.0, 3) AS gb_scanned_without_reuse,
  AVG(data_scanned_size) FILTER (WHERE NOT reused_previous_result) * COUNT(*) FILTER (WHERE reused_previous_result) / POWER(1024.0, 3) AS estimated_gb_saved
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY work_group_name
ORDER BY estimated_gb_saved DESC

DPU consumption by work_group — Total DPU-hours consumed per work_group for capacity analysis:

SELECT
  work_group_name,
  DATE_TRUNC('week', submission_date_time) AS week_start,
  SUM(dpu_count) AS total_dpus_allocated,
  SUM(dpu_count * COALESCE(total_execution_duration, 0) / 3600000.0) AS dpu_hours,
  COUNT(*) AS query_count
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '90' DAY
GROUP BY work_group_name, DATE_TRUNC('week', submission_date_time)
ORDER BY work_group_name, week_start

Most queried tables — Which tables are accessed most frequently:

SELECT
  accessed_table.catalog_name,
  accessed_table.database_name,
  accessed_table.table_name,
  COUNT(*) AS access_count,
  COUNT(DISTINCT principal_arn) AS distinct_principals
FROM query_executions
CROSS JOIN UNNEST(tables_accessed) AS t(accessed_table)
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY 1, 2, 3
ORDER BY 4 DESC
LIMIT 50

Work group comparison — Side-by-side comparison of work groups on key metrics: volume, average data scanned, failure rate, and average duration:

SELECT
  work_group_name,
  COUNT(*) AS total_queries,
  SUM(data_scanned_size) / POWER(1024, 3) AS total_gb_scanned,
  AVG(data_scanned_size) / POWER(1024, 2) AS avg_mb_per_query,
  ROUND(100.0 * COUNT(*) FILTER (WHERE state_enum = 'FAILED') / COUNT(*), 2) AS failure_rate_pct,
  AVG(COALESCE(total_execution_duration, 0)) / 1000.0 AS avg_duration_sec,
  APPROX_PERCENTILE(COALESCE(total_execution_duration, 0), 0.95) / 1000.0 AS p95_duration_sec,
  COUNT(DISTINCT principal_arn) AS distinct_principals
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY work_group_name
ORDER BY total_queries DESC

Query complexity — Histogram of query text length of SELECT queries. This can be used to identify generated or overly complex SQL:

SELECT
  CASE
    WHEN LENGTH(query_string) < 100 THEN 'A < 100 chars'
    WHEN LENGTH(query_string) < 500 THEN 'B 100-500 chars'
    WHEN LENGTH(query_string) < 2000 THEN 'C 500-2000 chars'
    WHEN LENGTH(query_string) < 10000 THEN 'D 2000-10000 chars'
    ELSE 'E > 10000 chars'
    END AS query_length_bucket,
  COUNT(*) AS query_count,
  AVG(COALESCE(total_execution_duration, 0)) / 1000.0 AS avg_duration_sec,
  AVG(data_scanned_size) / POWER(1024.0, 2) AS avg_mb_scanned
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND statement_subtype_enum = 'SELECT'
GROUP BY 1
ORDER BY 1

Statement type breakdown — Proportion of statement type (DDL, DML, UTILITY) and substatement type (SELECT, ALTER TABLE ADD PARTITION, INSERT) per work group:

SELECT
  work_group_name,
  statement_type_enum,
  statement_subtype_enum,
  COUNT(*) AS query_count,
  ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (PARTITION BY work_group_name), 2) AS pct_of_work_group
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY work_group_name, statement_type_enum, statement_subtype_enum
ORDER BY work_group_name, query_count DESC

Data scanned percentiles by work group — P50/P90/P99 data scanned per query to understand tail cost drivers:

SELECT
  work_group_name,
  COUNT(*) AS query_count,
  APPROX_PERCENTILE(data_scanned_size, 0.5) / POWER(1024, 3) AS p50_gb,
  APPROX_PERCENTILE(data_scanned_size, 0.9) / POWER(1024, 3) AS p90_gb,
  APPROX_PERCENTILE(data_scanned_size, 0.99) / POWER(1024, 3) AS p99_gb,
  MAX(data_scanned_size) / POWER(1024, 3) AS max_gb
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum <> 'FAILED' -- no charge for bytes scanned on FAILED queries
AND statement_type_enum = 'DML'
GROUP BY work_group_name
ORDER BY 5 DESC

Work group growth rate — Month-over-month growth in query count per work group:

WITH monthly AS (
  SELECT
    work_group_name,
    DATE_TRUNC('month', submission_date_time) AS month,
    COUNT(*) AS query_count
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '6' MONTH
  GROUP BY work_group_name, DATE_TRUNC('month', submission_date_time)
)
SELECT
  work_group_name,
  month,
  query_count,
  LAG(query_count) OVER (PARTITION BY work_group_name ORDER BY month) AS prev_month_count,
  ROUND(100.0 * (query_count - LAG(query_count) OVER (PARTITION BY work_group_name ORDER BY month)) / LAG(query_count) OVER (PARTITION BY work_group_name ORDER BY month), 2) AS growth_pct
FROM monthly
ORDER BY work_group_name, month

Managed query results adoption over time — Track adoption of managed query results week over week:

SELECT
  DATE_TRUNC('week', submission_date_time) AS week_start,
  work_group_name,
  COUNT(*) AS total_queries,
  COUNT(*) FILTER (WHERE managed_query_results_enabled) AS managed_results_count,
  ROUND(100.0 * COUNT(*) FILTER (WHERE managed_query_results_enabled) / COUNT(*), 2) AS managed_results_pct
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '90' DAY
GROUP BY DATE_TRUNC('week', submission_date_time), work_group_name
ORDER BY work_group_name, week_start

Substatement type distribution — Breakdown of substatement types to understand actual workload composition. Substatement type examples are SELECT, CREATE_TABLE_AS_SELECT, UNLOAD, CREATE_TABLE, ALTER_TABLE_ADD_PARTITION:

SELECT
  work_group_name,
  statement_type_enum,
  statement_subtype_enum,
  COUNT(*) AS query_count,
  AVG(data_scanned_size) / POWER(1024.0, 2) AS avg_mb_scanned,
  AVG(COALESCE(total_execution_duration, 0)) / 1000.0 AS avg_duration_sec
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY work_group_name, statement_type_enum, statement_subtype_enum
ORDER BY work_group_name, query_count DESC

Debugging & Performance

Slowest queries with time breakdown — Top queries by total execution time. total_sec = queue + pre_processing + engine + processing; planning_sec is part of engine_sec, so do not add it separately:

SELECT
  query_execution_id,
  work_group_name,
  principal_arn,
  submission_date_time,
  COALESCE(total_execution_duration, 0) / 1000.0 AS total_sec,
  COALESCE(query_queue_time_duration, 0) / 1000.0 AS queue_sec,
  COALESCE(query_planning_duration, 0) / 1000.0 AS planning_sec, -- subset of engine_sec, not additive
  engine_execution_duration / 1000.0 AS engine_sec,
  COALESCE(service_pre_processing_duration, 0) / 1000.0 AS pre_processing_sec,
  COALESCE(service_processing_duration, 0) / 1000.0 AS processing_sec,
  data_scanned_size / POWER(1024.0, 3) AS gb_scanned,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
ORDER BY total_execution_duration DESC
LIMIT 25

Which queries scanned the most data? — Top queries ranked by data scanned:

SELECT
  query_execution_id,
  work_group_name,
  principal_arn,
  submission_date_time,
  data_scanned_size / POWER(1024, 3) AS gb_scanned,
  COALESCE(total_execution_duration, 0) / 1000.0 AS duration_sec,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
ORDER BY data_scanned_size DESC
LIMIT 25

Which tables result in the most failed queries? — Tables most frequently accessed by queries that fail:

SELECT
  accessed_table.catalog_name,
  accessed_table.database_name,
  accessed_table.table_name,
  COUNT(*) AS failed_query_count
FROM query_executions
CROSS JOIN UNNEST(tables_accessed) AS t(accessed_table)
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'FAILED'
GROUP BY 1, 2, 3
ORDER BY 4 DESC
LIMIT 25

Planning time outliers — Queries with the longest planning time. This can be caused by queries that need to list many partitions on S3:

SELECT
  query_execution_id,
  work_group_name,
  submission_date_time,
  COALESCE(query_planning_duration, 0) / 1000.0 AS planning_sec,
  COALESCE(query_queue_time_duration, 0) / 1000.0 AS queue_sec,
  COALESCE(total_execution_duration, 0) / 1000.0 AS total_sec,
  ROUND(100.0 * COALESCE(query_planning_duration, 0) / NULLIF(COALESCE(total_execution_duration, 0), 0), 2) AS planning_pct_of_total,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
ORDER BY query_planning_duration DESC
LIMIT 25

Failed query patterns — Most common error categories and messages grouped by work group:

SELECT
  work_group_name,
  error_category_enum,
  error_type_enum,
  ANY_VALUE(error_message) AS sample_error_message,
  COUNT(*) AS failure_count
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'FAILED'
GROUP BY 1, 2, 3
ORDER BY failure_count DESC
LIMIT 25

Average execution time trend by work group — Detect performance regressions by tracking weekly avg/p95 execution time:

SELECT
  work_group_name,
  DATE_TRUNC('week', submission_date_time) AS week_start,
  COUNT(*) AS query_count,
  AVG(COALESCE(total_execution_duration, 0)) / 1000.0 AS avg_duration_sec,
  APPROX_PERCENTILE(COALESCE(total_execution_duration, 0), 0.95) / 1000.0 AS p95_duration_sec,
  AVG(data_scanned_size) / POWER(1024, 2) AS avg_mb_scanned
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '90' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY 1, 2
ORDER BY 1, 2

Queue spike detection — Hours where average queue time exceeds 2x the rolling average. This indicates periods where the full account quota is used:

WITH hourly_queue AS (
  SELECT
    DATE_TRUNC('hour', submission_date_time) AS hour,
    AVG(COALESCE(query_queue_time_duration, 0)) AS avg_queue_ms,
    COUNT(*) AS query_count
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '14' DAY
  GROUP BY 1
),
with_baseline AS (
  SELECT
    *,
    AVG(avg_queue_ms) OVER (
      ORDER BY hour
      RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND INTERVAL '1' HOUR PRECEDING
    ) AS rolling_avg_queue_ms
    FROM hourly_queue
)
SELECT
  hour,
  query_count,
  avg_queue_ms / 1000.0 AS avg_queue_sec,
  rolling_avg_queue_ms / 1000.0 AS rolling_avg_queue_sec,
  ROUND(avg_queue_ms / rolling_avg_queue_ms, 2) AS spike_ratio
FROM with_baseline
WHERE avg_queue_ms > 2 * rolling_avg_queue_ms
AND rolling_avg_queue_ms > 0
ORDER BY spike_ratio DESC
LIMIT 25

Error spike detection — Hours where failure rate exceeds 2x the rolling average:

WITH hourly_errors AS (
  SELECT
    work_group_name,
    DATE_TRUNC('hour', submission_date_time) AS hour,
    COUNT(*) AS total_queries,
    COUNT(*) FILTER (WHERE state_enum = 'FAILED') AS failed_queries,
    100.0 * COUNT(*) FILTER (WHERE state_enum = 'FAILED') / COUNT(*) AS failure_rate
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '14' DAY
  GROUP BY work_group_name, DATE_TRUNC('hour', submission_date_time)
),
with_baseline AS (
  SELECT
    *,
    AVG(failure_rate) OVER (
      PARTITION BY work_group_name
      ORDER BY hour
      RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND INTERVAL '1' HOUR PRECEDING
    ) AS rolling_avg_failure_rate
  FROM hourly_errors
)
SELECT
  work_group_name,
  hour,
  total_queries,
  failed_queries,
  ROUND(failure_rate, 2) AS failure_rate_pct,
  ROUND(rolling_avg_failure_rate, 2) AS rolling_avg_pct,
  ROUND(failure_rate / rolling_avg_failure_rate, 2) AS spike_ratio
FROM with_baseline
WHERE failure_rate > 2 * rolling_avg_failure_rate
AND rolling_avg_failure_rate > 0
AND total_queries >= 10  -- ignore low-volume hours
ORDER BY spike_ratio DESC
LIMIT 25

Multi-table join complexity — Queries accessing 5+ tables in a single execution — candidates for optimization or materialization:

SELECT
  query_execution_id,
  work_group_name,
  principal_arn,
  submission_date_time,
  CARDINALITY(tables_accessed) AS tables_count,
  COALESCE(total_execution_duration, 0) / 1000.0 AS duration_sec,
  data_scanned_size / POWER(1024, 3) AS gb_scanned,
  SUBSTR(query_string, 1, 300) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
AND CARDINALITY(tables_accessed) >= 5
ORDER BY CARDINALITY(tables_accessed) DESC, total_execution_duration DESC
LIMIT 25

DPU efficiency — Data scanned per DPU-second — low ratios may indicate over-provisioned queries:

SELECT
  work_group_name,
  query_execution_id,
  principal_arn,
  submission_date_time,
  dpu_count,
  data_scanned_size / POWER(1024.0, 2) AS mb_scanned,
  engine_execution_duration / 1000.0 AS engine_sec,
  data_scanned_size / POWER(1024.0, 2) / (dpu_count * engine_execution_duration / 1000.0) AS mb_per_dpu_second,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
AND state_enum = 'SUCCEEDED'
AND dpu_count > 0
AND data_scanned_size > 0
AND engine_execution_duration > 0
ORDER BY mb_per_dpu_second ASC
LIMIT 25

Repeated identical queries without result reuse — Same query text run multiple times in a short window without result reuse enabled. Either query result reuse was not enabled on these queries, or the time window was too short:

WITH query_groups AS (
  SELECT
    work_group_name,
    query_string,
    COUNT(*) AS repetition_count,
    MIN(submission_date_time) AS first_run,
    MAX(submission_date_time) AS last_run,
    SUM(data_scanned_size) / POWER(1024.0, 2) AS total_mb_scanned
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
  AND state_enum = 'SUCCEEDED'
  AND statement_type_enum = 'DML'
  AND NOT result_reuse_enabled
  AND NOT reused_previous_result
  GROUP BY work_group_name, query_string
  HAVING COUNT(*) >= 3
)
SELECT
  work_group_name,
  repetition_count,
  total_mb_scanned,
  DATE_DIFF('minute', first_run, last_run) AS window_minutes,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_groups
ORDER BY repetition_count DESC
LIMIT 25

Auditing & Governance

Query audit trail — All queries with who ran them, when, and on which tables:

SELECT 
  submission_date_time,
  principal_arn,
  work_group_name,
  statement_type_enum,
  statement_subtype_enum,
  state_enum,
  tables_accessed,
  SUBSTR(query_string, 1, 500) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
ORDER BY submission_date_time DESC

Find the principal that ran a specific query — Look up a specific query execution by ID and find out who ran it, and in what work group:

SELECT
  query_execution_id,
  principal_arn,
  work_group_name,
  submission_date_time,
  completion_date_time,
  state_enum,
  statement_type_enum,
  data_scanned_size / POWER(1024.0, 2) AS mb_scanned,
  COALESCE(total_execution_duration, 0) / 1000.0 AS duration_sec,
  error_message,
  tables_accessed,
  query_string
FROM query_executions
WHERE query_execution_id = '<query-execution-id>' -- replace with the actual ID

Sensitive table access log — All queries touching specific sensitive tables. Replace the filter values with the target database and table names:

SELECT
  query_execution_id,
  submission_date_time,
  principal_arn,
  work_group_name,
  statement_type_enum,
  accessed_table.catalog_name,
  accessed_table.database_name,
  accessed_table.table_name,
  SUBSTR(query_string, 1, 300) AS query_preview
FROM query_executions
CROSS JOIN UNNEST(tables_accessed) AS t(accessed_table)
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
  AND accessed_table.database_name = 'sensitive_db' -- replace with the database
  AND accessed_table.table_name IN ('pii_table', 'financial_data') -- replace with the tables
ORDER BY submission_date_time DESC

After-hours activity — Queries submitted outside business hours by human principals (not service roles):

SELECT
  query_execution_id,
  submission_date_time,
  principal_arn,
  work_group_name,
  statement_type_enum,
  SUBSTR(query_string, 1, 200) AS query_preview
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
  AND (
    HOUR(submission_date_time) < 8 -- time is in UTC, change for local time zone
    OR HOUR(submission_date_time) >= 20 -- time is in UTC, change for local time zone
    OR DAY_OF_WEEK(submission_date_time) IN (6, 7)  -- Saturday, Sunday
  )
  AND principal_arn NOT LIKE '%:assumed-role/service-%' -- exclude service roles
  AND principal_arn NOT LIKE '%:assumed-role/lambda-%' -- exclude Lambda
ORDER BY submission_date_time DESC

New principal detection — First-ever query per principal (user) — surface new identities appearing in work groups. This query intentionally omits the submission_date_time partition filter in the inner CTE — finding a principal's true first appearance requires scanning all history, so it is the deliberate exception to the partition-pruning rule and can be expensive on large tables:

WITH first_seen AS (
  SELECT
    principal_arn,
    work_group_name,
    MIN(submission_date_time) AS first_query_time,
    COUNT(*) AS total_queries_since
  FROM query_executions
  GROUP BY principal_arn, work_group_name
)
SELECT
  principal_arn,
  work_group_name,
  first_query_time,
  total_queries_since
FROM first_seen
WHERE first_query_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
ORDER BY first_query_time DESC

Work group access patterns — Which principals are active in which work groups — detect unexpected cross-work group usage:

WITH principal_work_groups AS (
  SELECT
    principal_arn,
    ARRAY_AGG(DISTINCT work_group_name) AS work_groups_used,
    CARDINALITY(ARRAY_AGG(DISTINCT work_group_name)) AS work_group_count
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
  GROUP BY principal_arn
)
SELECT
  principal_arn,
  work_group_count,
  work_groups_used
FROM principal_work_groups
WHERE work_group_count > 1
ORDER BY work_group_count DESC

Query submission burst detection — Principals submitting more than N queries within a 1-minute window, possibly indicating runaway automation:

WITH minutely AS (
  SELECT
    principal_arn,
    work_group_name,
    DATE_TRUNC('minute', submission_date_time) AS minute,
    COUNT(*) AS queries_in_minute
  FROM query_executions
  WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '7' DAY
  GROUP BY principal_arn, work_group_name, DATE_TRUNC('minute', submission_date_time)
)
SELECT
  principal_arn,
  work_group_name,
  minute,
  queries_in_minute
FROM minutely
WHERE queries_in_minute > 50 -- adjust threshold as needed
ORDER BY queries_in_minute DESC
LIMIT 25

Table access breadth per principal — Number of distinct tables accessed per principal over 30 days — unusually high breadth may warrant review:

SELECT
  principal_arn,
  COUNT(DISTINCT accessed_table.catalog_name || '.' || accessed_table.database_name || '.' || accessed_table.table_name) AS distinct_tables,
  COUNT(*) AS total_queries,
  COUNT(DISTINCT accessed_table.database_name) AS distinct_databases
FROM query_executions
CROSS JOIN UNNEST(tables_accessed) AS t(accessed_table)
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY principal_arn
ORDER BY distinct_tables DESC
LIMIT 25

Failed query ratio by principal — Principals with high failure rates may indicate misconfigured tooling or permissions issues:

SELECT
  principal_arn,
  work_group_name,
  COUNT(*) AS total_queries,
  COUNT(*) FILTER (WHERE state_enum = 'FAILED') AS failed_queries,
  ROUND(100.0 * COUNT(*) FILTER (WHERE state_enum = 'FAILED') / COUNT(*), 2) AS failure_rate_pct
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY principal_arn, work_group_name
HAVING COUNT(*) >= 10  -- only principals with meaningful volume
ORDER BY failure_rate_pct DESC
LIMIT 25

Work group SLA compliance — Percentage of queries completing within target thresholds per work group:

SELECT
  work_group_name,
  COUNT(*) AS total_queries,
  ROUND(100.0 * COUNT(*) FILTER (WHERE COALESCE(total_execution_duration, 0) <= 5000) / COUNT(*), 2) AS pct_under_5s,
  ROUND(100.0 * COUNT(*) FILTER (WHERE COALESCE(total_execution_duration, 0) <= 30000) / COUNT(*), 2) AS pct_under_30s,
  ROUND(100.0 * COUNT(*) FILTER (WHERE COALESCE(total_execution_duration, 0) <= 60000) / COUNT(*), 2) AS pct_under_60s,
  ROUND(100.0 * COUNT(*) FILTER (WHERE COALESCE(total_execution_duration, 0) <= 300000) / COUNT(*), 2) AS pct_under_5m,
  APPROX_PERCENTILE(COALESCE(total_execution_duration, 0), 0.5) / 1000.0 AS p50_sec,
  APPROX_PERCENTILE(COALESCE(total_execution_duration, 0), 0.99) / 1000.0 AS p99_sec
FROM query_executions
WHERE submission_date_time >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
AND state_enum = 'SUCCEEDED'
GROUP BY work_group_name
ORDER BY pct_under_30s ASC

Requirements and compatibility

Written for: Claude Code

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Claude Code frontmatter, Needs an MCP server, Needs a command-line tool

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
AWS
Publisher
AWS (Official)
Source
aws/agent-toolkit-for-aws · skills/specialized-skills/system-table-skills/querying-aws-athena · commit 0a218f8
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • NOTICE (added by AutoGineer from NOTICE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by AWS.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download querying-aws-athena.zip