Snowflake Cost Per Query: How to Find the Queries Quietly Draining Your Budget
Right-sizing and auto-suspend fix the warehouse. But eventually every Snowflake cost program hits the same wall: you know which warehouse is expensive, but not which query made it expensive. And until you know that, you're optimizing in the dark — resizing a warehouse when the real problem is one badly-written query that runs four thousand times a day.
This is Leak #3 in our breakdown of why your Snowflake bill is so high: cost that's invisible because it's measured at the wrong level. The reason per-query cost was historically so hard in Snowflake is structural — credits are metered against the warehouse, by the hour, not against the individual queries that ran on it. A warehouse burns 8 credits in an hour; Snowflake never told you how those 8 credits split across the 600 queries that ran in that hour.
That changed with QUERY_ATTRIBUTION_HISTORY. This post shows you how to use it to get true cost-per-query, find the repeat offenders, and build chargeback your finance team will actually trust.
Why cost-per-query used to be guesswork
Before attribution existed, the common hack was to take a warehouse's hourly credits and divide by the number of queries, or weight by execution time. Both are wrong. Execution time isn't credit consumption — a query that holds the warehouse while mostly waiting doesn't cost the same as one that pins every core. And dividing evenly treats a 2-millisecond lookup the same as a 9-minute join. Every "cost per query" dashboard built this way was an approximation nobody fully believed.
QUERY_ATTRIBUTION_HISTORY fixes this at the source. Snowflake now computes, per query, the actual compute credits that query consumed, in a column called credits_attributed_compute. Critically, that number excludes warehouse idle time — it's the credits for the query's own execution, not its share of a warehouse sitting around waiting. (Idle is a separate problem, and one you solve with AUTO_SUSPEND tuning.) For the first time, the number is real.
Two things to know before you rely on it. Data is available from roughly mid-August 2024 onward, so very long lookbacks won't work yet. And attribution covers queries that consumed warehouse compute — trivial metadata-only operations won't appear. Within those bounds, it's the ground truth for query cost.
Step 1: Find the repeat offenders
The single most useful query you can run groups cost by the parameterized query hash. query_parameterized_hash is identical for queries that are structurally the same but differ only in their literal values — so the same dashboard query run with a thousand different date filters collapses into one row. That's what you want: it surfaces the pattern that's expensive, not a thousand near-identical one-offs.
WITH attrib AS (
SELECT
query_parameterized_hash,
COUNT(*) AS executions,
SUM(credits_attributed_compute) AS total_credits,
AVG(credits_attributed_compute) AS avg_credits_per_run
FROM snowflake.account_usage.query_attribution_history
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY 1
),
sample1 AS (
SELECT
query_parameterized_hash,
ANY_VALUE(query_text) AS sample_query,
ANY_VALUE(user_name) AS user_name,
ANY_VALUE(warehouse_name) AS warehouse_name,
ANY_VALUE(database_name) AS database_name
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
AND query_parameterized_hash IS NOT NULL
GROUP BY 1
)
SELECT
ROUND(a.total_credits, 2) AS total_credits_30d,
a.executions,
ROUND(a.avg_credits_per_run, 4) AS avg_credits_per_run,
s.user_name,
s.warehouse_name,
s.database_name,
LEFT(s.sample_query, 150) AS sample_query
FROM attrib a
LEFT JOIN sample1 s ON a.query_parameterized_hash = s.query_parameterized_hash
ORDER BY total_credits_30d DESC
LIMIT 25;The first row is almost always a surprise. In most accounts the top 10–20 query patterns account for a large majority of compute, and at least one of them is something nobody knew was running — a backfill that never got turned off, a BI tool re-querying raw tables instead of an aggregate, a SELECT * feeding an extract. The sample_query column tells you exactly what to go fix, and user_name and warehouse_name tell you who owns it and where it runs.
Before you act on the list, sanity-check that each pattern is genuinely recurring and not a one-week anomaly. Widen the window to 60 or 90 days and confirm the execution count holds — attribution data goes back to mid-August 2024, so you have enough history to tell a permanent fixture from a temporary spike. The patterns that show up expensive across two or three months are the ones worth engineering time. A backfill that ran hard for a week and then stopped will age out on its own and isn't worth tuning. This one check keeps your team focused on durable savings instead of chasing ghosts.
Step 2: Attribute cost to people and teams (chargeback)
Once you can cost a query, you can cost a person — and that's what turns optimization from a central chore into a shared responsibility. This query rolls attributed credits up by user, which you can map to teams through your own user-naming or role conventions.
SELECT
qh.user_name,
COUNT(*) AS query_count,
ROUND(SUM(qah.credits_attributed_compute), 2) AS total_credits_30d
FROM snowflake.account_usage.query_attribution_history qah
JOIN snowflake.account_usage.query_history qh
ON qah.query_id = qh.query_id
WHERE qah.start_time >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY 1
ORDER BY total_credits_30d DESC
LIMIT 25;Showback like this changes behavior on its own. When a team sees its own name against a number, the expensive nightly job that "had to" run on an X-Large suddenly gets a second look. Pair it with your effective credit rate to turn credits into dollars, and you have a chargeback model finance will trust because it's built on Snowflake's own attribution, not a heuristic.
If you run separate warehouses per team or per environment — a common and healthy pattern — you can cross-check this user-level rollup against warehouse-level spend to make sure the two stories agree. Where they diverge, you've usually found a shared warehouse serving more teams than anyone realized, which is its own optimization: split it, and suddenly each team's cost is visible and ownable instead of pooled into one anonymous bill nobody feels responsible for.
Step 3: Catch the frequency tax
The most overlooked pattern in Snowflake cost isn't the single giant query — it's the cheap one that runs constantly. A query that costs a fraction of a credit per run looks harmless in isolation, but multiplied by ten thousand executions a day it can outspend your heaviest batch job. Dashboards on aggressive auto-refresh, health-check pings, and reverse-ETL syncs are the usual culprits.
WITH attrib AS (
SELECT
query_parameterized_hash,
COUNT(*) AS executions,
SUM(credits_attributed_compute) AS total_credits,
AVG(credits_attributed_compute) AS avg_credits_per_run
FROM snowflake.account_usage.query_attribution_history
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY 1
)
SELECT
a.query_parameterized_hash,
a.executions,
ROUND(a.avg_credits_per_run, 5) AS avg_credits_per_run,
ROUND(a.total_credits, 2) AS total_credits_30d,
ANY_VALUE(LEFT(qh.query_text, 150)) AS sample_query
FROM attrib a
JOIN snowflake.account_usage.query_history qh
ON a.query_parameterized_hash = qh.query_parameterized_hash
WHERE a.executions > 1000
GROUP BY 1, 2, 3, 4
ORDER BY total_credits_30d DESC
LIMIT 25;The filter executions > 1000 isolates high-frequency patterns. Sort by total credits and you'll find the queries where the fix isn't "make it faster" but "make it run less" — cache the result, widen the refresh interval, or materialize the aggregate the dashboard keeps recomputing from scratch.
What to do once you've found them
Finding the expensive queries is the work; fixing them follows a short playbook.
For the heavy single queries from Step 1, go after the usual suspects: a missing filter causing a full-table scan, a SELECT * pulling columns nobody uses, a join that explodes before it aggregates, or repeated computation that belongs in a materialized view or a scheduled table. These are ordinary SQL tuning wins — but now you know exactly which queries are worth the effort, ranked by dollars.
For the frequency-tax queries from Step 3, the fix is architectural, not SQL. Widen dashboard refresh intervals to something the business actually needs. Add a caching layer or a pre-aggregated table so the dashboard reads a small summary instead of recomputing over raw data. Move chatty reverse-ETL syncs to a schedule instead of a tight loop. None of this makes a query faster — it makes it run less, which is where the savings are.
For chargeback, publish the Step 2 numbers on a cadence. A monthly per-team credit summary, even just emailed around, creates the feedback loop that keeps cost from creeping back up after you've cleaned it.
| Pattern you found | Typical cause | The fix |
|---|---|---|
| One query, huge total cost | Full scan, SELECT *, exploding join | SQL tuning; materialize repeated work |
| Cheap query, massive run count | Dashboard refresh, health pings, reverse-ETL | Cache, widen interval, pre-aggregate |
| One user/team dominating | Heavy job on oversized warehouse | Showback + right-size their warehouse |
| Expensive query on shared warehouse | Mixed workload contention | Isolate onto its own right-sized warehouse |
Where teams get query attribution wrong
Optimizing by execution time instead of credits. The slowest query and the most expensive query are often not the same query. Attribution is the only signal that ranks by actual cost — use it, not duration, to pick what to fix.
Chasing one-off queries. A giant ad-hoc query that ran once is rarely worth tuning. Grouping by query_parameterized_hash keeps you focused on recurring patterns, which is where sustained spend lives.
Forgetting the warehouse underneath. Attribution tells you a query is expensive; it doesn't tell you it's running on a warehouse that's two sizes too big. Use query attribution and warehouse right-sizing together — fix the query, then make sure it's running on appropriately-sized compute.
Treating attribution as exact to the penny. It's compute-credit attribution, accurate and far better than any heuristic, but it excludes idle and doesn't cover non-warehouse serverless features. Use it to rank and prioritize, not to reconcile a bill to the cent.
Looking once and moving on. Query patterns drift — a new dashboard ships, a pipeline changes, someone adds a join to a model. The expensive list you pull this month won't be the same one next quarter. Re-run these queries monthly so new offenders surface while they're still cheap to fix, instead of discovering them after a quarter of quiet overspend.
Bottom line
You can't optimize what you can't measure, and for years Snowflake made per-query cost almost impossible to measure honestly. QUERY_ATTRIBUTION_HISTORY ends that. Group cost by parameterized hash to find the recurring offenders, roll it up by user for chargeback, and filter for high execution counts to catch the frequency tax. Then fix the heavy queries with SQL tuning and the frequent ones with caching and scheduling. It's the difference between guessing which query is expensive and knowing — and once you know, the fixes tend to be small, specific, and permanent, which is the best kind of cost work there is.
Want the whole account analyzed — every expensive query pattern ranked, owners identified, and a prioritized fix list with the dollars attached? That's part of a free Snowflake cost assessment. We hand you the list; you decide what to tune.
---
This is part of our Snowflake cost-optimization series. Start with the pillar: Why Is Your Snowflake Bill So High? The 4 Biggest Leaks, and read the companion pieces on warehouse right-sizing and idle warehouses, or explore our Snowflake cost optimization services.
Follow Insignyx on LinkedIn
More on data, cloud & AI cost optimization.