Why bytes scanned is the wrong yardstick for spend, and what to measure instead
A few weeks ago a story made the rounds in Snowflake circles with a great headline: a 149 GB query that Snowflake reported as having scanned negative 1.5 GB. The cause was a quiet technical one. When you unload QUERY_HISTORY to Parquet, a handful of columns, BYTES_SCANNED among them, come out as 32-bit integers and overflow. Wrap the column in any expression (even an identity cast) and the problem goes away.
The bug is worth knowing about, and I’ll come back to it. But what stuck with me was a bigger question. Plenty of teams build their Snowflake cost reporting on bytes scanned. Even when the number is correct, is it the right number?
My answer, after years of looking at data cloud bills: mostly no.
The bug is the easy part
Here’s the practical takeaway first. If you copy Snowflake’s account usage views into a lake or a separate warehouse for FinOps reporting, check the types on the way out. Nine QUERY_HISTORY columns can land as INT32 in a Parquet unload, including TRANSACTION_ID and SESSION_ID. Those last two are identifiers, not statistics, so wrong values there don’t just skew a chart. They break joins.
The scary part isn’t the negative numbers. You’ll notice those. The scary part is the positive numbers that look plausible and are simply wrong. Worksheets and CSV exports show the true value, so nobody suspects the pipeline until someone sums a week of data and gets something absurd.
So: cast explicitly, reconcile a sample against a worksheet, and move on. Then ask the harder question.
Snowflake doesn’t bill you for bytes
Snowflake’s own documentation is clear on how compute is charged: credits are based on how many warehouses you run, how big they are, and how long they run. Billing is per second, with a 60-second minimum every time a warehouse starts or resumes. Each size step up roughly doubles both compute power and credits per hour.
Notice what’s missing from that list: bytes. A query that scans 2 TB on a busy Medium warehouse for 40 seconds can cost less than a 5 MB lookup that wakes an idle X-Large warehouse, pays the one-minute minimum, and then keeps it running until auto-suspend kicks in.
Bytes scanned is a useful performance signal. It tells you whether pruning is working, whether a clustering key earns its keep, whether someone is running SELECT * on a fact table. But it’s a weak proxy for cost, for at least three reasons:
- Idle time is invisible to it. Warehouses burn credits while running, whether or not queries are executing. No query “scanned” that idle time, so it never shows up in a bytes-based view.
- Concurrency changes the math. Ten queries sharing a warehouse share its cost. Bytes per query doesn’t capture that.
- Caching breaks the link. Snowflake can return persisted query results without using a warehouse at all, and a warm warehouse cache changes how much remote storage is read. The same query can show very different bytes on different runs.
What credit-based attribution actually tells you
The good news is that Snowflake now gives you a much better starting point. The QUERY_ATTRIBUTION_HISTORY view in ACCOUNT_USAGE attributes warehouse credits to individual queries, going back 365 days. When queries run concurrently, cost is split by a weighted average of the resources each one consumed. Credits used by the Query Acceleration Service get their own column.
Just as important is what the docs say the view leaves out:
- Warehouse idle time
- Cloud services, storage, and data transfer costs
- Serverless features and AI token costs
- Very short queries (roughly 100 ms or less)
Data in the view can also lag by up to eight hours.
So even the “right” metric needs context. If you sum attributed credits by user and compare it to your warehouse metering, the gap is mostly idle time. That gap is often where the easiest savings are hiding: oversized warehouses, auto-suspend settings that are too generous, or a BI tool that pings a warehouse every few minutes.
Grouping by QUERY_PARAMETERIZED_HASH is another trick I like. It rolls up the “same” query with different literal values, so you see that a single dashboard tile run 20,000 times a month is your real cost driver, not the one scary ad hoc query everyone talks about.
Monthly warehouse credits: attributed to queries vs. idle time
A better scorecard for your warehouse
If I were building a Snowflake cost view from scratch today, I’d structure it in layers:
- Start with metered credits per warehouse. This is what you actually pay for. Everything else should reconcile back to it.
- Attribute credits to queries, users, and tags. Use QUERY_ATTRIBUTION_HISTORY and query tags so that spend maps to teams and data products, not just warehouse names.
- Track the unattributed remainder. Treat idle time as its own line item with an owner.
- Use bytes scanned as a diagnostic, not a KPI. When a query’s credits look high, check bytes scanned, partitions scanned versus total, and spillage to understand why.
- Validate your pipeline. If your usage data leaves Snowflake, cast your numeric columns and spot-check them. The negative-bytes bug is a good reminder that cost data needs the same testing as any other data.
The shift sounds small, but it changes the conversation. “Your team scanned 40 TB last month” invites debate. “Your team’s workloads used 1,800 credits, 30% of which was an idle warehouse” invites action.
Metered credits at the base, attributed credits in the middle, bytes scanned and pruning on top
Where Seemore fits in
This is a problem we spend a lot of time on at Seemore. Rather than leaning on any single metric, Seemore looks at actual warehouse usage and ties cost back to the users, jobs, domains, and data products behind it, using lineage across the pipeline. It also looks at warehouse behavior itself, like right-sizing, auto-suspend, queueing, and spillage, so idle time and misconfiguration show up as their own findings instead of disappearing between query-level numbers. The goal is a cost view your engineers trust enough to act on.
The bottom line
A metric that can quietly turn 149 GB into -1.5 GB is a good reason to test your cost pipeline. But even when bytes scanned is correct, it answers a performance question, not a financial one. Measure what Snowflake bills: credits, by warehouse, attributed to the work that consumed them, with idle time called out explicitly. Then use bytes scanned to figure out why something is expensive.
Frequently asked questions
Why does Snowflake's BYTES_SCANNED show negative values?
When QUERY_HISTORY is unloaded to Parquet and the column passes through untouched, it can be written as a 32-bit integer and overflow, producing negative or wrong positive values. Casting it explicitly, for example CAST(bytes_scanned AS NUMBER(38,0)), writes the correct value. Worksheets and CSV exports aren’t affected.
Is bytes scanned a good measure of Snowflake cost?
Not on its own. Snowflake bills compute in credits based on warehouse size and running time (per second, with a 60-second minimum per resume), not on bytes read. Bytes scanned is better used as a performance diagnostic, for example to check partition pruning.
How can I see the credit cost of an individual Snowflake query?
Use the SNOWFLAKE.ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY view. Its CREDITS_ATTRIBUTED_COMPUTE column shows the warehouse credits attributed to each query over the last 365 days, with concurrent queries split by a weighted average of resource use.
What does QUERY_ATTRIBUTION_HISTORY not include?
It excludes warehouse idle time, cloud services, storage, data transfer, serverless features, AI token costs, and queries shorter than about 100 ms. Data can lag by up to eight hours.
How do I find idle warehouse costs in Snowflake?
Compare metered warehouse credits with the sum of attributed query credits for the same warehouse and period. Most of the difference is idle time, which you can often reduce with tighter auto-suspend settings, right-sizing, or consolidating low-utilization warehouses.
Does caching affect bytes scanned?
Yes. Persisted query results can be returned without running a warehouse, and warehouse caching changes how much remote storage a query reads, so identical queries can report different bytes scanned on different runs.


