Snowflake cost and warehouse efficiency
Idle and oversized warehouses, auto-suspend settings, expensive query patterns, and storage that nobody meant to keep.
Scope
A read-only cost review of one Snowflake account: where the credits go, which
queries waste compute, and which storage is being paid for by accident. Read from
SNOWFLAKE.ACCOUNT_USAGE (365-day retention; latency runs from 45 minutes to
3 hours depending on the view — about 45 minutes for QUERY_HISTORY, up to 90
for TABLE_STORAGE_METRICS, up to 180 for WAREHOUSE_METERING_HISTORY and
ACCESS_HISTORY) and fall
back to INFORMATION_SCHEMA table functions only when a view is not granted.
Propose changes; never run ALTER, SUSPEND, DROP, or anything else that
mutates the account.
Procedure
- Establish the bill before explaining it. Query
ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORYfor the last 30 days grouped byWAREHOUSE_NAME:SUM(CREDITS_USED_COMPUTE)andSUM(CREDITS_USED_CLOUD_SERVICES), ranked descending. Pull the preceding 30 days the same way for a month-over-month delta. The top three warehouses normally carry most of the bill, and that ranking decides where the remaining steps spend their effort. Cloud-services credits above ~10% of compute credits are themselves a finding — that usually means metadata-heavy work such as thousands of tinySHOW/INFORMATION_SCHEMAcalls or single-row DML. - Idle burn. For each warehouse, join metered hours against query activity in
the same hour from
ACCOUNT_USAGE.QUERY_HISTORY. Any hour with credits and no queries is being paid for by the auto-suspend gap. Read each warehouse'sAUTO_SUSPENDandAUTO_RESUME(SHOW WAREHOUSES, orACCOUNT_USAGE.WAREHOUSES) and compare it against the observed distribution of gaps between consecutive queries on that warehouse. A 600-second auto-suspend on a warehouse whose median inter-query gap is 20 minutes pays 10 idle minutes on every gap; a warehouse withAUTO_SUSPENDunset or set toNULLnever suspends at all and should be treated as the worst case of this finding. - Suspend/resume churn — the counter-finding to step 2. Count
RESUME_WAREHOUSEevents per warehouse per day inACCOUNT_USAGE.WAREHOUSE_EVENTS_HISTORY. Every resume bills a 60-second minimum, so a 60-second auto-suspend on a warehouse that resumes hundreds of times a day can cost more than a longer idle window would. Reconcile steps 2 and 3 before reporting either: recommend the auto-suspend value that minimises (idle seconds x rate) + (resumes x 60s), and say which of the two effects dominates for that warehouse. - Sizing, in both directions. From
QUERY_HISTORYper warehouse, look atBYTES_SPILLED_TO_LOCAL_STORAGEandBYTES_SPILLED_TO_REMOTE_STORAGE. Remote spill is the strong signal that a warehouse is undersized for its workload — it is spilling past local SSD to object storage and every affected query pays for it in wall time. Local spill is the softer version of the same thing. In the other direction, a warehouse whose queries never spill, mostly finish in under a second, and show near-zeroQUEUED_OVERLOAD_TIMEis oversized: each size step doubles the credit rate, so halving it is the single largest lever in this checkup. SustainedQUEUED_OVERLOAD_TIMEmeans concurrency pressure, not size — raiseMAX_CLUSTER_COUNTon a multi-cluster warehouse instead of the size, and say so explicitly so nobody "fixes" queueing by doubling the rate. - Expensive query patterns. Over the same 30 days, rank
QUERY_HISTORYbySUM(TOTAL_ELAPSED_TIME)and separately bySUM(BYTES_SCANNED), grouped byQUERY_PARAMETERIZED_HASH(group by the first ~200 characters ofQUERY_TEXTif that column is not available). For each top pattern checkPARTITIONS_SCANNED / PARTITIONS_TOTAL: a ratio near 1.0 against a large table is a full scan, which usually means a missing or wrong cluster key, or a predicate that cannot prune (a function wrapped around the clustered column, an implicit cast,ORacross unrelated columns). Separately, look for dashboard patterns that never reuse the result cache: a result-cache hit scans nothing at all —BYTES_SCANNED = 0, no warehouse attributed, near-zero elapsed time — so the signal is repeated executions of oneQUERY_PARAMETERIZED_HASHwhere every run does scan and does bill a warehouse. Those normally differ only by an embeddedCURRENT_TIMESTAMPliteral or a non-deterministic function, either of which disables the result cache. Do not readPERCENTAGE_SCANNED_FROM_CACHEas that signal: it is the local-SSD data cache on the warehouse's own compute, so it measures warehouse warmth, not result reuse. Read it alongside step 2 instead — a warehouse with a consistently low percentage is re-fetching from remote storage after every suspend, which is the real cost of an aggressiveAUTO_SUSPENDand belongs in the reconciliation there. Note which role or user submits each pattern — the fix has an owner. - Storage. From
ACCOUNT_USAGE.TABLE_STORAGE_METRICS, compareACTIVE_BYTESagainstTIME_TRAVEL_BYTESandFAILSAFE_BYTESper table. A high-churn table whose time-travel plus fail-safe bytes exceed its active bytes is paying several times over for data it does not need; check itsDATA_RETENTION_TIME_IN_DAYSand whether a transient table (no fail-safe) would have been the right choice. Flag rows withDELETED = TRUEstill holding fail-safe bytes, and tables growing steadily with no reads against them inACCOUNT_USAGE.ACCESS_HISTORYfor 90 days — that view is Enterprise Edition and above, so if it is missing, report an edition gap, not a coverage failure, and skip the no-reads test rather than treating silence as "unused". Check whether any large table is only ever queried with a narrow date predicate but is unclustered. - Attribute and hedge honestly. Every number in the report names the view and
the window it came from. Where
ACCOUNT_USAGElatency could matter (anything touching the last hour), say the window is not settled yet rather than present it as fact. If a view is not granted to the connected role, report that as a gap in coverage instead of silently narrowing the review.
Finding keys
A finding key identifies the underlying issue, not this run. The same idle
warehouse must emit the same key next month so the ledger records "still open"
instead of opening a second row — and so a resolved finding that comes back is
detected as a regression. Use <object-type>:<stable-object-name>:<issue-slug>,
with the object name exactly as Snowflake spells it and a lowercase issue slug.
Never put a date, a credit figure, a percentage, or a run id in the key; those
belong in detail and evidence, which are refreshed on every run.
warehouse:REPORTING_WH:idlewarehouse:ETL_WH:oversizedwarehouse:BI_WH:suspend-thrashquery:a91f3c2e:full-scan— theQUERY_PARAMETERIZED_HASH, never the texttable:ANALYTICS.PUBLIC.EVENTS:time-travel-bloat
Severity rubric
- critical — one warehouse or query pattern burning more than 25% of the account's monthly credits with no owner able to justify it; or storage growth on a trajectory that doubles the storage bill within a quarter.
- high — more than 10% of monthly credits recoverable by a single setting
change (
AUTO_SUSPEND, warehouse size,DATA_RETENTION_TIME_IN_DAYS); or remote spill on a warehouse that serves production or customer-facing workloads. - medium — a clear inefficiency worth a few percent of the bill: local spill, a full-scan pattern on a large table, an auto-suspend materially longer than the observed idle gaps, cloud-services credits above 10% of compute.
- low — small recoverable waste, or a bad default with little current cost — a rarely-used warehouse with no auto-suspend, a transient-shaped table created as permanent.
- info — an observation worth recording with no action attached: workload moved between warehouses, a large new table appeared, month-over-month spend flat.
Output guidance
Open with a one-paragraph executive summary in plain words: the
30-day credit total, the month-over-month direction, and the single largest
recoverable item with its rough credit value. Then one section per area —
Warehouses, Query patterns, Storage — each leading with the numbers and naming the
ACCOUNT_USAGE view behind them. Close with a recommendations table
(Recommendation | Object | Estimated monthly credits saved | Risk | Owner), most
valuable first, and state plainly where an estimate is a rough order of magnitude
rather than a computed figure. Say "nothing worth changing" outright if that is
what the data shows.
Run it against your systems
This checkup is in the desktop app under Checkups. No card, read-only credentials you configure.