Agent Skill · Cockroach Labs

auditing-table-statistics

Audits optimizer table statistics for staleness, missing coverage, and data quality issues using SHOW STATISTICS. Use when diagnosing poor query performance, unexpected plan changes, or after bulk data changes to identify stale statistics requiring refresh via CREATE STATISTICS.

Provider: Cockroach Labs Path in repo: skills/cockroachdb-observability-and-diagnostics/auditing-table-statistics/SKILL.md

Skill body

Auditing Table Statistics

Audits optimizer table statistics for staleness, missing column coverage, and row count drift to diagnose poor query performance caused by outdated or incomplete statistics. Uses SHOW STATISTICS for read-only SQL analysis of table-level and column-level statistics freshness, entirely without requiring DB Console access.

Complement to profiling-statement-fingerprints: This skill diagnoses optimizer statistics issues; for identifying historically slow queries, see profiling-statement-fingerprints.

Prerequisites

Related skills: profiling-statement-fingerprints for historical query analysis, triaging-live-sql-activity for live triage.

Core Concepts

CockroachDB-specific defaults:

See references/statistics-thresholds.md for workload-specific guidance.

Core Diagnostic Queries

Query 1: Identify Tables with Stale or Missing Statistics

Finds tables with outdated statistics or no statistics at all, ranked by staleness.

WITH table_stats AS (
  SELECT
    table_catalog,
    table_schema,
    table_name,
    column_names,
    row_count,
    created,
    now() - created AS stats_age
  FROM [SHOW STATISTICS FOR TABLE database_name.*]  -- Replace database_name
  WHERE column_names = '{}'  -- Table-level stats only (empty array)
)
SELECT
  table_schema || '.' || table_name AS full_table_name,
  row_count,
  created AS stats_created_at,
  stats_age,
  CASE
    WHEN created IS NULL THEN 'Missing statistics'
    WHEN stats_age > INTERVAL '30 days' THEN 'Very stale (>30d)'
    WHEN stats_age > INTERVAL '7 days' THEN 'Stale (>7d)'
    ELSE 'Fresh'
  END AS staleness_status
FROM table_stats
WHERE stats_age > INTERVAL '7 days' OR created IS NULL  -- Adjust threshold
ORDER BY stats_age DESC NULLS FIRST
LIMIT 50;

Customization:

Key columns:

Query 2: Audit Statistics for Specific Table

Shows all statistics for a single table, including table-level and per-column details.

SELECT
  column_names,
  row_count,
  distinct_count,
  null_count,
  created,
  now() - created AS stats_age,
  CASE
    WHEN histogram_id IS NOT NULL THEN 'Yes'
    ELSE 'No'
  END AS has_histogram
FROM [SHOW STATISTICS FOR TABLE database_name.schema_name.table_name]
ORDER BY
  CASE WHEN column_names = '{}' THEN 0 ELSE 1 END,  -- Table-level first
  created DESC;

Customization:

Key columns:

Interpretation:

Query 3: Detect Row Count Drift

Compares current table row count against cached statistics to identify significant drift.

WITH current_count AS (
  SELECT count(*) AS actual_rows
  FROM database_name.schema_name.table_name  -- Replace with target table
),
stats_count AS (
  SELECT row_count, created
  FROM [SHOW STATISTICS FOR TABLE database_name.schema_name.table_name]
  WHERE column_names = '{}'  -- Table-level stats
  ORDER BY created DESC
  LIMIT 1
)
SELECT
  c.actual_rows,
  s.row_count AS stats_rows,
  s.created AS stats_created_at,
  now() - s.created AS stats_age,
  ABS(c.actual_rows - s.row_count) AS drift_absolute,
  ROUND(
    ABS(c.actual_rows - s.row_count)::NUMERIC /
    NULLIF(s.row_count, 0) * 100,
    2
  ) AS drift_pct,
  CASE
    WHEN ABS(c.actual_rows - s.row_count)::NUMERIC / NULLIF(s.row_count, 0) > 0.30 THEN 'High drift (>30%)'
    WHEN ABS(c.actual_rows - s.row_count)::NUMERIC / NULLIF(s.row_count, 0) > 0.20 THEN 'Medium drift (>20%)'
    WHEN ABS(c.actual_rows - s.row_count)::NUMERIC / NULLIF(s.row_count, 0) > 0.10 THEN 'Low drift (>10%)'
    ELSE 'Minimal drift (<10%)'
  END AS drift_status
FROM current_count c, stats_count s;

Customization:

Key columns:

Interpretation:

Query 4: Identify Missing Column-Level Statistics

Finds table columns without statistics, focusing on columns frequently used in WHERE/JOIN clauses.

WITH table_columns AS (
  SELECT column_name
  FROM information_schema.columns
  WHERE table_schema = 'schema_name'  -- Replace
    AND table_name = 'table_name'    -- Replace
    AND is_hidden = 'NO'             -- Exclude internal columns
),
stats_columns AS (
  SELECT UNNEST(column_names) AS column_name
  FROM [SHOW STATISTICS FOR TABLE database_name.schema_name.table_name]
  WHERE column_names != '{}'  -- Exclude table-level stats
)
SELECT
  tc.column_name AS missing_column,
  'No statistics available' AS status
FROM table_columns tc
WHERE tc.column_name NOT IN (SELECT column_name FROM stats_columns)
ORDER BY tc.column_name;

Customization:

Interpretation:

Action: Generate CREATE STATISTICS commands (see Query 7)

Query 5: Histogram Coverage Analysis

Identifies columns with/without histogram data for range query optimization.

SELECT
  UNNEST(column_names) AS column_name,
  created,
  now() - created AS stats_age,
  CASE
    WHEN histogram_id IS NOT NULL THEN 'Has histogram'
    ELSE 'Missing histogram'
  END AS histogram_status
FROM [SHOW STATISTICS FOR TABLE database_name.schema_name.table_name]
WHERE column_names != '{}'  -- Exclude table-level stats
ORDER BY
  CASE WHEN histogram_id IS NULL THEN 0 ELSE 1 END,  -- Missing first
  created DESC;

Customization:

Key columns:

Interpretation:

Query 6: Multi-Column Statistics Detection

Identifies existing multi-column (composite) statistics for correlated columns.

SELECT
  column_names,
  created,
  now() - created AS stats_age,
  row_count,
  ARRAY_LENGTH(column_names, 1) AS column_count
FROM [SHOW STATISTICS FOR TABLE database_name.schema_name.table_name]
WHERE ARRAY_LENGTH(column_names, 1) > 1  -- Multi-column only
ORDER BY created DESC;

Customization:

Key columns:

Interpretation:

See references/create-statistics-examples.md for multi-column creation patterns.

Query 7: Generate CREATE STATISTICS Recommendations

Produces ready-to-run CREATE STATISTICS commands for tables with stale or missing statistics.

WITH stale_tables AS (
  SELECT
    table_schema,
    table_name,
    created,
    now() - created AS stats_age
  FROM [SHOW STATISTICS FOR TABLE database_name.*]
  WHERE column_names = '{}'
    AND (created IS NULL OR now() - created > INTERVAL '7 days')  -- Adjust threshold
)
SELECT
  table_schema || '.' || table_name AS full_table_name,
  stats_age,
  'CREATE STATISTICS __auto__ FROM ' || table_schema || '.' || table_name || ';' AS create_command
FROM stale_tables
ORDER BY stats_age DESC NULLS FIRST
LIMIT 50;

Customization:

Output:

Execution:

Common Workflows

Workflow 1: Post-Bulk-Load Statistics Audit

Scenario: After bulk INSERT/COPY/IMPORT operation, validate statistics are current.

Steps:

  1. Identify affected tables:
    -- List tables modified in last 24 hours
    SELECT DISTINCT table_schema || '.' || table_name AS full_table_name
    FROM [SHOW TABLES]
    WHERE table_schema = 'target_schema';  -- Replace
    
  2. Check row count drift (Query 3): Run drift detection query for each affected table.

  3. Generate and execute refresh commands (Query 7):
    CREATE STATISTICS __auto__ FROM schema_name.table_name;  -- From Query 7 output
    
  4. Monitor collection job:
    SELECT job_id, status, fraction_completed, running_status
    FROM [SHOW JOBS]
    WHERE job_type = 'CREATE STATS'
      AND created > now() - INTERVAL '1 hour'
    ORDER BY created DESC
    LIMIT 10;
    
  5. Verify refresh (Query 2): Re-run statistics audit to confirm created timestamp updated.

Expected outcome: Statistics age <1 hour, drift_pct <5%.

Workflow 2: Diagnose Unexpected Query Plan Changes

Scenario: Query performance suddenly degrades; EXPLAIN shows different plan.

Steps:

  1. Identify affected query from profiling-statement-fingerprints: Find query with latency spike or plan hash change.

  2. Extract table references: Parse query text to identify tables in FROM/JOIN clauses.

  3. Audit statistics for each table (Query 2): Check staleness and row count currency.

  4. Compare historical vs current row counts:
    -- Example: Check if table grew significantly
    SELECT row_count, created
    FROM [SHOW STATISTICS FOR TABLE users]
    WHERE column_names = '{}'
    ORDER BY created DESC
    LIMIT 5;  -- Last 5 collections
    
  5. Refresh stale statistics (Query 7): Execute CREATE STATISTICS for tables with high drift.

  6. Validate plan stability: Re-run EXPLAIN to verify plan returns to expected structure.

Expected outcome: Plan hash stabilizes, latency returns to baseline after statistics refresh.

Workflow 3: Routine Statistics Health Check

Scenario: Periodic audit to proactively identify statistics issues before performance degrades.

Steps:

  1. Run cluster-wide staleness scan (Query 1):
    -- All databases
    SHOW STATISTICS FOR TABLE *.*;  -- Warning: May be slow on large clusters
    
  2. Prioritize critical tables: Focus on high-traffic tables from profiling-statement-fingerprints.

  3. Check automatic collection is enabled:
    SHOW CLUSTER SETTING sql.stats.automatic_collection.enabled;  -- Should be true
    
  4. Review pending auto-collection jobs:
    SELECT job_id, description, status, fraction_completed
    FROM [SHOW JOBS]
    WHERE job_type = 'AUTO CREATE STATS'
      AND status IN ('pending', 'running')
    ORDER BY created DESC;
    
  5. Generate batch refresh script (Query 7): Save output to file for scheduled execution.

  6. Schedule refresh during maintenance window: Execute generated CREATE STATISTICS commands during low-traffic period.

Frequency: Weekly for OLTP, monthly for OLAP.

Safety Considerations

Production-Safe Operations

SHOW STATISTICS:

CREATE STATISTICS:

Resource Consumption

Small tables (<10K rows): Negligible impact, safe anytime

Medium tables (10K-10M rows): Seconds to minutes, minor impact

Large tables (>10M rows): Minutes to hours, plan accordingly:

Cancellation (if needed):

-- Find job ID
SELECT job_id, status, fraction_completed
FROM [SHOW JOBS]
WHERE job_type = 'CREATE STATS' AND status = 'running';

-- Cancel job (non-destructive, existing statistics remain)
CANCEL JOB 123456789012345678;

Batch Collection Best Practices

Avoid overwhelming cluster:

Example staggered script:

# Collect statistics in batches with delays
for table in table1 table2 table3; do
  cockroach sql -e "CREATE STATISTICS __auto__ FROM $table;" &
done
wait  # Wait for batch to complete

sleep 60  # Delay between batches

for table in table4 table5 table6; do
  cockroach sql -e "CREATE STATISTICS __auto__ FROM $table;" &
done
wait

See references/create-statistics-examples.md for detailed batch patterns.

Troubleshooting

Issue Likely Cause Fix
SHOW STATISTICS returns empty No statistics ever collected Run CREATE STATISTICS __auto__ FROM table_name;
row_count shows 0 for non-empty table Statistics out of sync Refresh: CREATE STATISTICS __auto__ FROM table_name;
Permission denied error No privileges on table Grant any privilege: GRANT SELECT ON table_name TO user;
CREATE STATISTICS job stuck Large table with high write volume Check SHOW JOBS status; consider CANCEL JOB and retry during low-traffic period
Automatic collection not triggering Setting disabled or threshold not met Verify sql.stats.automatic_collection.enabled = true and check row count drift
Statistics exist but query plans still poor Stale statistics or missing multi-column stats Refresh existing; create multi-column for correlated columns (see Query 6)
High drift but recent created timestamp Extreme write volume between collections Lower automatic collection threshold or increase manual refresh frequency

Defensive Query Patterns

Handle missing statistics:

-- Use COALESCE for NULL created timestamps
SELECT COALESCE(created, '1970-01-01'::TIMESTAMP) AS stats_created_at
FROM [SHOW STATISTICS FOR TABLE table_name]
WHERE column_names = '{}';

Avoid division by zero in drift calculations:

-- Use NULLIF to prevent divide-by-zero errors
SELECT
  ABS(actual - stats)::NUMERIC / NULLIF(stats, 0) * 100 AS drift_pct
FROM ...;

Key Considerations

See references/create-statistics-examples.md and references/statistics-thresholds.md for detailed guidance.

References

Official CockroachDB Documentation:

Related Skills:

Supplementary References:

Skill frontmatter

compatibility: Requires SQL access with any privilege on target tables (SELECT, INSERT, UPDATE, DELETE, or admin). Uses SHOW STATISTICS (production-safe, read-only). metadata: {"author" => "cockroachdb", "version" => "1.0"}