Identifiers and stability

Numbers checked against the live data on

This page shows queries.

Other systems hold references to this data, so identifier stability is a contract. A factor's identity is anchored in the publisher's own source: its record id, sheet coordinate or process UUID. Region, unit, boundary and activity text are annotations on top of that identity. They can be corrected and refreshed without re-keying the factor. This page lists every identifier, how it is built, what keeps it stable, and which one to reach for.

Which identifier to use#

you want to use lives on how
Store a durable pointer to a factor: in a mapping, a report, another system slug (ef-…) emission_factor, factors_flat Exact match on slug, then on emission_factor_alias.slug. Never parse it.
Pin the exact published number, for audit or reproducibility source_key (efv1-…) emission_factor_value, factors_flat Exact match. You can recompute it from the source fields.
Say which served row you read, including the price year of a spend factor value_key factors_flat Exact match. It equals source_key on a physical row.
Recognise the same source record across releases, or de-duplicate on import factor_source_id emission_factor Unique per publisher. The loader reuses a factor on this key.
Collect one source activity's factors: its boundaries, and any region or unit its key does not hold apart activity_src_id emission_factor Exact match; it is always a literal prefix of factor_source_id.
Detect that a publisher edited a record in place content_hash (ch1-…) source_row, emission_factor_value Recompute it over the record and compare.

Watch out. factor_id, value_id, release_id and the other UUID columns are join keys inside one database. They are not stable across a rebuild of factor search. Never store or cite one; store the slug or the source_key.

Every token says what it is#

Each identifier that leaves the database starts with a kind prefix, so a pasted token tells you what it is and how to resolve it.

prefix what it is example
ef- factor slug ef-aib-grid_electricity-fr-kwh-generation-86d757e7
efv1- a value's key, recipe version 1: a source_key, or the value_key built from it efv1-a143fd8320cf95bb70520ea954878c16
ch1- content hash, recipe version 1 ch1-497c6c9a166d91c55895797ac9f0bfe2

The recipe version lives in the prefix, never in a column. If a hashing recipe ever changes, newly published values get a new prefix (efv2-), and every existing efv1- key stays valid.

The slug#

ef-<publisher>-<activity>-<region>-<unit>[-<boundary>]-<hex8>

ef-aib-grid_electricity-fr-kwh-generation-86d757e7
   |   |                |  |   |          \ first 8 hex of the factor's own id
   |   |                |  |   \ system_boundary (omitted when unknown)
   |   |                |  \ unit_code (at most 12 characters)
   |   |                \ region_code (at most 24 characters)
   |   \ activity text, folded to ASCII (at most 24 characters)
   \ publisher code

The slug is minted once, when the factor is first loaded, and then frozen. Segments are lowercase ASCII with accents stripped; words inside a segment join with _, and the ISO - of a region code becomes _ (FR-34 becomes fr_34). The readable part is display only: it can be truncated, and it does not follow later corrections. Only the trailing hex makes a slug unique.

Note. Because the slug is frozen, its readable part can go stale. The DESNZ diesel total ef-desnz-liquid_fuels_diesel_100-gb-l-well_to_wheel-99d9d558 has system_boundary = 'well_to_use' today. Read the columns, never the slug.

When factors are merged, split or re-keyed, each old slug gets a row in emission_factor_alias, so a slug that was ever published keeps resolving. Resolve a slug by exact match first, then through the alias table:

Resolve one slug: first as a current slug, then through the alias table, which is where this retired slug is found.

Show the SQL
SELECT ef.slug AS current_slug, 'current' AS matched_as
FROM open_ef.emission_factor ef
WHERE ef.slug = 'ef-ademe-butane_maritime_included-europe-kwh-well_to_wheel-53be0cf1'
UNION ALL
SELECT ef.slug, a.reason
FROM open_ef.emission_factor_alias a
JOIN open_ef.emission_factor ef ON ef.id = a.emission_factor_id
WHERE a.slug = 'ef-ademe-butane_maritime_included-europe-kwh-well_to_wheel-53be0cf1'
current_slug matched_as
ef-ademe-butane_maritime_included-europe-kwh-well_to_use-53be0cf1 key_fix

The old slug is no longer a factor, so only the alias row matches. Guarantees you can rely on counts the aliases per library.

The factor identity: factor_source_id#

A factor is identified by its publisher code joined to its own source parts, under a frozen recipe per source:

factor_source_id = publisher_code "//" canon(part_0) "//" canon(part_1) ...

canon = Unicode NFC, trim, collapse inner whitespace. Case is never folded.
"//"  = the part separator, doubled because a single "/" occurs in source text.
        A part that contains "//" is refused at import.

UNIQUE (publisher_id, factor_source_id) is the only identity constraint. Region, unit, boundary and activity text are not part of it, with a few declared exceptions below.

library parts after the publisher code example factor_source_id
ADEME, a total identifiant ademe//12896
ADEME, a stage the total's parts, then type_poste, nom_poste_francais ademe//twin//Butane//inclus maritime//-//kwh//Europe//-//Combustion//Combustion
AGRIBALYSE, a gate code_agb, gate agribalyse//20052//cradle_to_consumer
AGRIBALYSE, a stage code_agb, contrib, stage agribalyse//20052//contrib//consumption
AIB electricity, country aib//electricity//FR
DESNZ activity path, boundary, unit desnz//Fuels > Liquid fuels > Diesel (100% mineral diesel)//combustion//l
EPA sheet tag, the row's coordinate on that sheet, unit epa//stationary//Biomass Fuels - Gaseous//Landfill Gas//mmBtu
EXIOBASE product code, region exiobase//C_OANP//FR
MITECO sheet tag, the row's coordinate on that sheet, the unit or basis where the sheet prints one miterd//fuel//B100//l
ÖKOBAUDAT process UUID, EN 15804 module okobaudat//a9141bc2-9a57-40cc-9a5d-f6345af7bf4e//A4
Open CEDA sector code, then the ISO 3166-1 alpha-3 country or the literal ROW aggregate openceda//311513//USA
USEEIO BEA commodity code useeio//311513

Where a publisher issues a stable code, that code is the identity: an ADEME identifiant, an ÖKOBAUDAT process UUID, a BEA commodity code. Where it does not, the identity is the row's own coordinate in the published file, and a column that renumbers between editions is kept out of it. That is why DESNZ is keyed on the activity path and not on its ID column. Where one library publishes several sheet shapes (EPA, MITECO), the first part is a sheet tag, so two sheets can never share a key.

Three kinds of recipe copy a code open_ef assigns into the key, and each is declared:

  • A unit where one row is printed on two bases. DESNZ keys on the canonical unit code. So do ADEME's twin keys, which merge the kWh and GJ printings of one fuel into one factor.
  • A folded name where the source ships no id. MITECO's electricity retailers are keyed on a fold of the retailer's name.
  • A total open_ef summed. A derived total adds _sum, region, unit and target boundary to its activity's parts: okobaudat//a9141bc2-9a57-40cc-9a5d-f6345af7bf4e//_sum//DE//m2//cradle_to_grave_embodied.

On those segments a change to the mapping is a declared re-key, with an alias for every old slug. It is never a silent edit.

ADEME and ÖKOBAUDAT also publish some activities as a year series: a new record id for each edition. Those factors are keyed series, on the activity without its year, and the per-year record id moves into the value's source_key. One factor then carries every year.

Activity families: activity_src_id#

activity_src_id is the activity part of factor_source_id: the same key with its region, unit and boundary tail dropped. Use it to collect a family with one filter:

Four activity families across three libraries: how many factors each holds and how many regions, countries, units and boundaries they span.

Show the SQL
SELECT ef.activity_src_id,
  count(*) AS factors,
  count(DISTINCT ef.region_code) AS regions,
  count(DISTINCT ef.region_code) FILTER (WHERE length(ef.region_code) = 2) AS iso_countries,
  count(DISTINCT ef.unit_code) AS units,
  count(DISTINCT ef.system_boundary) AS boundaries
FROM open_ef.emission_factor ef
WHERE ef.activity_src_id IN ('openceda//311513', 'useeio//311513', 'agribalyse//20052', 'ademe//twin//Butane//inclus maritime//-//kwh//Europe//-')
GROUP BY 1
ORDER BY 1
activity_src_id factors regions iso_countries units boundaries
ademe//twin//Butane//inclus maritime//-//kwh//Europe//- 3 1 0 1 3
agribalyse//20052 12 1 1 1 12
openceda//311513 149 149 148 1 1
useeio//311513 1 1 1 1 1

Open CEDA's cheese sector covers 148 countries plus a rest-of-world aggregate, 149 regions in all; AGRIBALYSE's artichoke is one product in 12 gates and stages. activity_src_id always starts with the publisher code, so it never groups two libraries, even when they share a sector code as Open CEDA and USEEIO do here.

Watch out. A family is only as wide as the key. Where a recipe folds the unit or the locality into the activity token itself, as ADEME's twin keys do, they stay inside activity_src_id: kwh and Europe are both part of the butane key above, so that family is three boundaries of one fuel and can never hold a second unit or a second locality, and the same butane for France continentale is a second family, not a second region of this one.

Note. A coarser grouping is a query over these columns, not a stored group id. The only durable pointers are the slug (a factor) and the source_key (a value).

The value keys: source_key and value_key#

source_key = "efv1-" + sha256(NFC(field_0) US NFC(field_1) US ...)[:32]

US     = U+001F, the unit separator
fields = publisher code, release label, the factor's source parts, then the
         value's discriminators (such as calculation approach and grid mix)
NFC    = Unicode NFC. Fields are not case-folded; a missing field is "".
digest = hex, the first 32 characters

Because the factor's parts come first, value identity nests inside factor identity. You can recompute a key yourself. This reproduces the AIB France production value:

import hashlib
import unicodedata

US = "\x1f"

def source_key(publisher: str, release_label: str, parts: list[str | None]) -> str:
    fields = [publisher, release_label, *parts]
    payload = US.join(unicodedata.normalize("NFC", f) if f else "" for f in fields)
    return "efv1-" + hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]

print(source_key("aib", "2025", ["electricity", "FR", "location_based", "production"]))
# efv1-a143fd8320cf95bb70520ea954878c16

The exact field list per library is set by its import and does not change for existing keys.

value_key names the served row. On a physical factor it equals source_key. A spend factor is served once per accounting year at that year's price level, so its value_key appends the year. A source_key contains no underscore, so the split is unambiguous:

A USEEIO spend factor and an AIB electricity factor side by side: the value key equals the source key on one and appends the year on the other.

Show the SQL
SELECT f.library, f.slug, f.source_key, f.value_key, f.applies_from_year, f.grid_mix
FROM open_ef.factors_flat f
WHERE f.library IN ('useeio', 'aib')
  AND f.slug IN ('ef-useeio-cheese_manufacturing-us-usd-cradle_to_gate-9c6d0a5a', 'ef-aib-grid_electricity-fr-kwh-generation-86d757e7')
  AND f.is_latest_activity_year
  AND (f.price_basis = 'purchaser' OR f.grid_mix = 'production')
ORDER BY f.library
library slug source_key value_key applies_from_year grid_mix
aib ef-aib-grid_electricity-fr-kwh-generation-86d757e7 efv1-a143fd8320cf95bb70520ea954878c16 efv1-a143fd8320cf95bb70520ea954878c16 NULL production
useeio ef-useeio-cheese_manufacturing-us-usd-cradle_to_gate-9c6d0a5a efv1-ef09998c37ce579fe15e87d39628973f efv1-ef09998c37ce579fe15e87d39628973f_2025 2025 NULL

A mirror stores the slug and watches the source_key: a source_key that changes under an unchanged slug means the served number changed, through a new release, a new default or a corrected figure. Pin a factor value to a release builds on this.

The content hash#

content_hash = "ch1-" + sha256(canonical_json(record))[:32]

canonical_json = json.dumps(record, sort_keys=True, separators=(",", ":"),
                            ensure_ascii=False, default=str)

source_row.content_hash covers the raw line as read from the file (source_row.raw); emission_factor_value.content_hash covers the value's own source record. The hash is not an identity: it is recomputed on every import, and a changed hash under an unchanged source_key means the publisher edited the record in place. It is not served on factors_flat; read it through source_row_id as in Trace a served value back to its source line.

Suppliers are named, never keyed#

Some factors describe a named company: a Spanish electricity retailer in MITECO, the declaration owner of an ÖKOBAUDAT EPD. The company lives in supplier, and the handles each source ships are mapped to it in supplier_resolution.

  1. The supplier is an annotation. supplier_id is not part of factor_source_id, source_key or the slug, so pointing a factor at the right legal entity never re-keys it. MITECO's electricity sheet is the one place a company name shapes a key, because that sheet publishes one row per retailer.
  2. The supplier code is a fold of the name that keeps legal forms. supplier.code is lowercase and alphanumeric only: case is folded, punctuation is dropped, and an accented Latin letter is reduced to its ASCII base, so none of those three splits one company in two. Letters of a script ASCII cannot represent are kept as they are, so a code is not guaranteed ASCII, although across the ten libraries these docs use for examples every one of them is. The code keeps legal-form suffixes on purpose: ACME S.A. and ACME S.L. can be two companies, and merging them would assign one company's emissions to another.
  3. A rename is an append. A new spelling or id is added to supplier_resolution against the same supplier_id, and no factor moves. Where the source ships an id, as ÖKOBAUDAT does with a contact UUID, a rename needs no new row at all.

Watch out. The fold is not a spelling normaliser. A Latin letter with no ASCII base at all is dropped rather than transliterated, so ÖKOBAUDAT's Betonverband Straße, Landschaft, Garten e.V. folds to betonverbandstraelandschaftgartenev, and the same company spelled Strasse would fold to a different code. Two spellings of one company are joined by a row in supplier_resolution, never by the fold alone.

Today 531 MITECO retailer names and 425 ÖKOBAUDAT contact UUIDs resolve to suppliers; 16 ÖKOBAUDAT suppliers carry more than one UUID.

What stays stable#

  1. A published slug always resolves, directly or through emission_factor_alias.
  2. A source_key never changes. A recipe change mints a new prefix for new values only.
  3. Identity survives better normalization. Improving how a region, unit, boundary or label is derived re-stamps norm_version and can leave the slug's readable part stale, but it never changes factor_source_id, the slug's hex or a source_key. The declared exceptions above are re-keyed with aliases.

You can check the structural rules yourself:

On the public libraries, how many factors break each of three structural rules: none.

Show the SQL
SELECT count(*) AS factors,
  count(*) FILTER (WHERE left(ef.factor_source_id, length(ef.activity_src_id)) <> ef.activity_src_id) AS activity_not_a_prefix,
  count(*) FILTER (WHERE right(ef.slug, 8) <> left(replace(ef.id::text, '-', ''), 8)) AS slug_hex_not_the_id,
  count(*) FILTER (WHERE ef.factor_source_id NOT LIKE p.code || '//%') AS key_without_publisher
FROM open_ef.emission_factor ef
JOIN open_ef.publisher p ON p.id = ef.publisher_id
WHERE p.code IN ('ademe','agribalyse','exiobase','desnz','miterd','aib','epa','useeio','openceda','okobaudat')
factors activity_not_a_prefix slug_hex_not_the_id key_without_publisher
146758 0 0 0

factors counts every factor row of the ten example libraries, stages and earlier editions included, not only the ones served today.