Summary: Learn what version sharding is and why it occurs as FOCUS versions and data generator adoption advances. Practitioners can visualize mock scenarios of how FOCUS versions stack across disparate data generators. Use the three-step process outlined to begin managing multiple versioned datasets.
The FinOps Open Cost and Usage Specification (FOCUS) unifies cost and usage data from many data generators into one consistent dataset. Data generators are the cloud, SaaS, and other technology providers that produce billing data.
FOCUS is also an open specification that grows with its community and data generator adoption, and a new version is ratified roughly every six months. Since each data generator upgrades on its own schedule, most practitioners will eventually hold FOCUS data from several versions in the same data warehouse.
We call this version sharding.
Version sharding means practitioners having to build and manage one unified view, combining data generator exports that use different versions of the specification. It can look like the undoing of a specification meant to simplify, with different columns and rules in each dataset and more time spent reconciling data than doing FinOps.
Managing version sharding isn’t as complicated as it seems. FOCUS versions nest, and changes between them are classified and documented. FinOps practitioners can learn and use the following approach: record the data generator and FOCUS version on every row, query the columns all generators share, add the rest on your own schedule, and document a decision for every gap.
A common assumption is that FOCUS versions behave like operating system releases: different schemas, potentially major changes, and a reconciliation project between you and various data generator exports. Instead, each FOCUS version contains nearly everything in the one before it, like the layers of an onion expanding outward.
Of the 43 columns defined in v1.0, 41 are still present as of v1.4. Both of the removals were deprecated a full release before v1.4 so that users could plan ahead.
Here are three features of how FOCUS manages change to keep its shared core stable.
FOCUS contributors classify each change in each release into one of three categories:
The project defined this classification in v1.2 as a governance commitment and first applied it in the v1.3 changelog. It lives in the project’s specification change guidelines and is applied release by release. Both the v1.3 and v1.4 changelogs record no Incompatible changes. The record is short, and anyone can check it.
The classification also shows where to focus: Migration Compatible changes. Compatible changes can still require work, as the section on definitions below explains.
No column was removed between v1.0 and v1.3, so all 43 columns defined in v1.0 are present in every version through v1.3. Version 1.4 is the first to remove any, and it removes two.
Version 1.4 removed ProviderName and PublisherName because their definitions conflicted with each other. Both were formally deprecated in v1.3, using the Deprecated and PreviousColumnName metadata properties added in v1.2. Their use cases moved to ServiceProviderName, HostProviderName, InvoiceIssuerName, and DataGenerator metadata. Generators had a full release cycle, roughly six months, with old and new columns present together and a documented mapping between them. The removal affects practitioners only when a data generator upgrades to v1.4.
The project settled its vocabulary up to and including v1.0. Since that baseline, the column record has been additive, with one deprecate-then-remove cycle across four releases. Version 1.4 also retired three attributes and two glossary entries without a deprecation cycle, all classified Migration Compatible.
The walkthrough below follows three data generators, each exporting FOCUS datasets on a different version. The counter under the grid tracks the columns you can query across all three. It holds steady as the generators drift four releases apart, drops once when the v1.4 removal lands, and then flags two columns that kept their names but changed their meaning.
A and B both emit FOCUS 1.0. Same column names, same definitions, no mapping layer in between. A cross-generator total works immediately.
C does not emit FOCUS itself. Its proprietary export is converted to the 1.0 shape, history included, because you kept the native data. That is backfill outcome two: re-derived from the native export.
Two columns you have never had arrive on A. Nothing breaks: every 1.0 column is still there, because no column was removed between 1.0 and 1.3. But the new columns have no history behind them.
One new column derives from columns you already hold, so you backfill it deterministically. The other was never computed upstream, so it is null forever. Write the reason down beside the column: the failure is not choosing badly, it is letting nulls happen to you.
Generators do not move in lockstep, and they do not move one release at a time. B lands four new columns at once and is now a release ahead of A.
Three generators, three different ceilings, and the floor is still every column FOCUS 1.0 defined. Cross-generator queries run against the floor and cover the full original specification. This is the crawl tier, and it costs you nothing to be on it.
A and B now share a ceiling. Per-generator dashboards can use 1.2 columns for both, while cross-generator totals still run at the floor, because C is on 1.0. That is the walk tier: two clearly labelled scopes.
Four more columns at once, and B is now two releases beyond A and four beyond C. This is the spread the whole article is about, and so far every column you already had is still exactly where it was.
ProviderName and PublisherName are gone from B, their definitions having conflicted with each other. Both were deprecated a release earlier in 1.3, with their use cases moved to ServiceProviderName, HostProviderName, InvoiceIssuerName and DataGenerator metadata. A full release cycle of notice.
A cross-generator query on ProviderName now returns B as null. Your floor is the minimum declared version minus anything a higher generator removed. History keeps the old columns and the successors carry the meaning forward, so repoint the query.
BilledCost and EffectiveCost were both heavily revised at 1.4. Same names, same positions, different definitions. Nesting protects your queries, not your numbers, and nothing in the schema will tell you which is which. Read the changed-columns section of the changelog for every version you cross.
A keeps ProviderName and PublisherName, because they are deprecated at 1.3 rather than removed. Two generators, same two column names, different status. This is exactly why version belongs in your warehouse as a column.
C is capped at 1.0 by its converter, A is on 1.3, and B is on 1.4 with one column retired and two definitions changed. Four releases of spread and not one silent failure: every gap has a reason you can name. The change still happened, but you saw it coming.
Column counts are illustrative, not to scale. Generators are composites, not specific vendors.
If you want to begin managing multiple versions of FOCUS across your data generators, the highest-leverage step costs two lines in your ingest process.
FOCUS rows do not carry version information, so hard-code Generator and FocusVersion at ingest time for each generator and each export. Take the version from the generator’s published FOCUS metadata where it exists. Once these are ordinary columns, each of the following questions becomes a simple GROUP BY:
Before: Versions never recorded
-- Ingest
INSERT INTO focus_costs
SELECT * FROM read_parquet('s3://billing/gen_a/cost_and_usage/*.parquet');
-- What is my cross-generator spend?
SELECT SUM(EffectiveCost)
FROM focus_costs;
-- 4,182,930.11: one number, with no way to know it spans
-- two different specification versions
-- Which generators are on which version?
-- Not answerable. Go ask each vendor.
After: Two columns hard-coded on every row
-- Ingest (hard-coded values for this generator and export)
INSERT INTO focus_costs
SELECT d.*,
'gen_a' AS Generator, -- hard-coded
'1.3' AS FocusVersion -- hard-coded
FROM read_parquet('s3://billing/gen_a/cost_and_usage/*.parquet') d;
-- Repeat this pattern for every other dataset the generator emits
-- What is my cross-generator spend?
SELECT FocusVersion, SUM(EffectiveCost)
FROM focus_costs
GROUP BY 1;
-- 1.2 3,004,117.55
-- 1.3 1,178,812.56
-- Same total, with the version boundary now visible
-- Which generators are on which version?
SELECT FocusVersion, Generator, COUNT(*)
FROM focus_costs
GROUP BY 1, 2;
Follow each step one at a time and apply them to one question: how much of the version difference does your data warehouse absorb before a query runs?
Find the lowest FOCUS version across your generators, then subtract any columns that a generator on a higher version has removed. The result is your floor. Run every cross-generator query against it.
Because no column was removed between v1.0 and v1.3, the subtraction matters only when a dataset is on v1.4. Within the floor, you get a defensible cross-generator total from day one, with known exclusions.
Use the identified floor columns to aggregate across generators, and use ceiling columns, the full column set of each generator’s version, to go deeper on one generator at a time. Label the two dashboard tiers clearly so each is explicit about its scope.
Bring older-version data up to newer versions. Declare a backfill policy for each column, using the four outcomes in the next section.
When generators begin delivering recency metadata, DatasetInstanceComplete and TimeSectorComplete will show whether a period is final. Until then, record your own load dates so data consumers can tell a true zero from data that has not arrived yet.
Pro tip: Practitioners often get caught here. A column defined in a version is not necessarily populated by every generator. Conditional and Recommended columns can be legitimately null, depending on a generator’s business model. The version number tells you the schema. Where the generator publishes them, two documents tell you the rest:
A column that is correctly null because its condition never applies appears in the Column Mapping Reference, not in the Conformance Gap Report. Read both.
When a data generator moves up a version, you inherit new columns with no history behind them. Each column has one of four outcomes. Choose one explicitly for each column and record the decision.
ListCost must equal ListUnitPrice multiplied by PricingQuantity when both are non-null, and the same relationship holds for ContractedCost and ContractedUnitPrice. This category is smaller than it looks. All five of these columns date to v1.0, so they arrive together rather than at a version boundary. That makes derivation the path for a generator that starts emitting FOCUS partway through your history, not for one that moves up a version. Watch for the near miss: PricingCurrencyEffectiveCost must be the PricingCurrency-denominated equivalent of EffectiveCost. That reads like arithmetic, but the specification carries no exchange rate, so this column belongs in the next category.A mature FOCUS practice still has gaps. It knows exactly which cells are empty and why.
A column can keep its name across a version boundary and change what it measures. That makes a version move a definition change as well as a schema change, and a diagram of column names cannot show it.
Version 1.4 substantially revised the normative requirements for BilledCost (see the v1.3 and v1.4 definitions) and EffectiveCost (see the v1.3 and v1.4 definitions), including rules for amortizing covering charges and a cross-record sum validation. A covering charge is a purchase whose cost offsets other charges, such as a commitment discount purchase. As a result, SUM(EffectiveCost) across generators on either side of the boundary compares two differently defined quantities, even though both datasets use the same column name.
Watch null rates too. InvoiceId moved from Recommended to Conditional in v1.4, a change the specification classifies as Compatible. Your queries keep parsing, but your alerts may not stay quiet: on a dashboard, a shift in population rate looks identical to a data quality regression.
When a generator moves versions, read the changed-columns section of the FOCUS changelog for every version you are crossing, alongside that generator’s Conformance Gap Report. This matters most as the first v1.4 generators ship. With a written policy in place, you can safely ingest data from generators on mixed versions. To query the specification without reading the full changelog, use the FOCUS Model Context Protocol (MCP) server with your AI assistant.
The last question is easy to skip. FOCUS certification applies per version and per generator, and under the program rules, “No, those are different versions” is a legitimate answer. It just needs to be stated. Coverage is a separate question from version: the currently certifiable version defines a single dataset, so a certificate says nothing about a generator’s Contract Commitment, Invoice Detail, or Billing Period data.
The fear was that six-month releases would leave practitioners reconciling versions instead of doing FinOps. The record shows two columns removed since v1.0, both with a full cycle of notice and named successors, and no Incompatible changes recorded since the classification began.
Carrying several specification versions still costs more than carrying one, but the cost is bounded, visible, and documented. Before FOCUS, the alternative was maintaining hand-built bridges between many vendors’ private schemas and finding out one had broken from a board deck.
Get started now by stamping the generator and version on every row. Then compute your floor, query against it, and climb on your own schedule.