Below are truly BigQuery style :o)
Both versions for BigQuery Standard SQL
##standardSQL
SELECT
projID,
([2001, 2002, 2003])[SAFE_OFFSET(pos)] year,
cost
FROM `project.dataset.table`,
UNNEST([Cost2001,Cost2002,Cost2003]) cost WITH OFFSET pos
you can test / play wit above using dummy data from your question as below
##standardSQL
WITH `project.dataset.table` AS (
SELECT 'projectA' projID, 10 Cost2001, 32 Cost2002, 30 Cost2003 UNION ALL
SELECT 'projectB', 42, 22, 122
)
SELECT
projID,
([2001, 2002, 2003])[SAFE_OFFSET(pos)] year,
cost
FROM `project.dataset.table`,
UNNEST([Cost2001,Cost2002,Cost2003]) cost WITH OFFSET pos
with result as
Row projID year cost
1 projectA 2001 10
2 projectA 2002 32
3 projectA 2003 30
4 projectB 2001 42
5 projectB 2002 22
6 projectB 2003 122
as you can see in above query you had to pre-set values for respective years in below line
([2001, 2002, 2003])[SAFE_OFFSET(pos)] year
If for whatever reason you want to be more generic and be able to derive those values off of original columns' names - you can use below generic approach
##standardSQL
WITH `project.dataset.table` AS (
SELECT 'projectA' projID, 10 Cost2001, 32 Cost2002, 30 Cost2003 UNION ALL
SELECT 'projectB', 42, 22, 122
)
SELECT
projID,
SPLIT(x,':')[SAFE_OFFSET(0)] year,
SPLIT(x,':')[SAFE_OFFSET(1)] cost
FROM `project.dataset.table` t,
UNNEST(SPLIT(REGEXP_REPLACE(TO_JSON_STRING(t), r'[{}"]', ''))) x
WHERE SPLIT(x,':')[OFFSET(0)] != 'projID'
obviously, with same result
Row projID year cost
1 projectA Cost2001 10
2 projectA Cost2002 32
3 projectA Cost2003 30
4 projectB Cost2001 42
5 projectB Cost2002 22
6 projectB Cost2003 122