Pivoting and Unpivoting Data in SQL using postgreSQL 18

sql 复制代码
-- ============================================================================
-- PostgreSQL 18 --- SQL 透视与逆透视 (Pivoting & Unpivoting Data)
-- 行业: 国际化珠宝集团 OA/ERP/CRM/HRM/MES/SCM/WMS/KMS 统一中台
-- 场景: 万亿数据高并发 / 国际时区时间差 / 国际多种货币结算
-- 本文件【自包含·完全幂等】: 重复运行结果逐字节一致 (setseed 保证造数一致)
-- 连接方式: PGPASSWORD=<your_password> psql -U postgres -h localhost -p 5433 -d jewelry_pg -f 本文件
--
-- 说明: PostgreSQL 没有 SQL Server/Oracle 原生的 PIVOT / UNPIVOT 关键字。
--       本专题演示 4 种等价写法:
--         PIVOT  : ① CASE + GROUP BY(静态列) ② tablefunc.crosstab() ③ jsonb_object_agg(动态列)
--         UNPIVOT: ④ UNION ALL(宽→长)       ⑤ LATERAL VALUES(宽→长, 更简洁)
-- ============================================================================

\timing off
SET client_min_messages = warning;         -- 抑制 DROP/CREATE/VACUUM 的 NOTICE, 保证重跑幂等
SET max_parallel_workers_per_gather = 0;   -- 关并行, 使块统计稳定(万亿级可据需开启)
SET random_page_cost = 1.1;
SET seq_page_cost = 1.0;
SET effective_cache_size = '4GB';
SET TIME ZONE 'UTC';   -- 关键: 使 extract(quarter from created_utc) 基于 UTC,
                       -- 与显式 'Asia/Shanghai' 的 create_local_date 形成真正可比的时区透视

-- ----------------------------------------------------------------------------
-- 0. 度量框架 (DROP + CREATE, 幂等)
--    pg_plan_stats     : 抓真实执行计划 (Actual Rows / 共享块 / 扫描节点类型)
--    pg_find_scan      : 下钻到真正的扫描节点 (顶层常是 Aggregate/WindowAgg)
--    pg_plan_text_has  : 检查计划文本是否含某关键字 (如 Sort / Index)
--    pg_demo_v         : 自检登记
--    pg_demo_err_probe : 捕获真实报错
-- ----------------------------------------------------------------------------
DROP FUNCTION IF EXISTS pg_plan_stats(text) CASCADE;
CREATE OR REPLACE FUNCTION pg_plan_stats(
  p_sql text,
  OUT actual_rows bigint,
  OUT shared_blks bigint,
  OUT node_type   text)
RETURNS record LANGUAGE plpgsql AS $$
DECLARE j json;
BEGIN
  EXECUTE 'EXPLAIN (ANALYZE, TIMING OFF, FORMAT JSON) ' || p_sql INTO j;
  actual_rows := (j->0->'Plan'->>'Actual Rows')::numeric::bigint;
  shared_blks := COALESCE((j->0->'Plan'->>'Shared Hit Blocks')::numeric::bigint,0)
              + COALESCE((j->0->'Plan'->>'Shared Read Blocks')::numeric::bigint,0);
  node_type   := pg_find_scan(j->0->'Plan');
END $$;

DROP FUNCTION IF EXISTS pg_find_scan(json) CASCADE;
CREATE OR REPLACE FUNCTION pg_find_scan(cur json) RETURNS text LANGUAGE plpgsql AS $$
DECLARE nt text;
BEGIN
  nt := cur->>'Node Type';
  IF nt LIKE '%Scan' THEN RETURN nt; END IF;
  IF cur->'Plans' IS NOT NULL AND json_array_length(cur->'Plans') > 0 THEN
    RETURN pg_find_scan(cur->'Plans'->0);
  END IF;
  RETURN nt;
END $$;

DROP FUNCTION IF EXISTS pg_plan_text_has(text, text) CASCADE;
CREATE OR REPLACE FUNCTION pg_plan_text_has(p_sql text, p_sub text) RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE r record; t text := '';
BEGIN
  FOR r IN EXECUTE 'EXPLAIN (FORMAT TEXT) ' || p_sql LOOP
    t := t || r."QUERY PLAN";
  END LOOP;
  RETURN position(p_sub in t) > 0;
END $$;

DROP TABLE IF EXISTS pg_demo_check;
CREATE TABLE pg_demo_check (
  name   text PRIMARY KEY,
  pass   int,
  detail text
);
DROP FUNCTION IF EXISTS pg_demo_v(text,int,text) CASCADE;
CREATE OR REPLACE FUNCTION pg_demo_v(p_name text, p_pass int, p_detail text)
RETURNS void LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO pg_demo_check(name,pass,detail) VALUES (p_name,p_pass,p_detail)
  ON CONFLICT (name) DO UPDATE SET pass=EXCLUDED.pass, detail=EXCLUDED.detail;
END $$;

DROP TABLE IF EXISTS pg_demo_probe;
CREATE TABLE pg_demo_probe (
  label    text PRIMARY KEY,
  sqlstate text,
  msg      text
);
DROP FUNCTION IF EXISTS pg_demo_err_probe(text,text) CASCADE;
CREATE OR REPLACE FUNCTION pg_demo_err_probe(p_label text, p_sql text)
RETURNS void LANGUAGE plpgsql AS $$
DECLARE e text; s text;
BEGIN
  BEGIN
    EXECUTE p_sql;
    INSERT INTO pg_demo_probe(label,sqlstate,msg) VALUES (p_label,'OK','执行成功')
    ON CONFLICT (label) DO UPDATE SET sqlstate='OK', msg='执行成功';
  EXCEPTION WHEN OTHERS THEN
    GET STACKED DIAGNOSTICS s=RETURNED_SQLSTATE, e=MESSAGE_TEXT;
    INSERT INTO pg_demo_probe(label,sqlstate,msg) VALUES (p_label,s,e)
    ON CONFLICT (label) DO UPDATE SET sqlstate=EXCLUDED.sqlstate, msg=EXCLUDED.msg;
  END;
END $$;

-- ----------------------------------------------------------------------------
-- 1. 维度表 (八域中台配套维度) ------ DROP + CREATE 保证幂等
-- ----------------------------------------------------------------------------
DROP TABLE IF EXISTS pg_txn CASCADE;
DROP TABLE IF EXISTS pg_customer CASCADE;
DROP TABLE IF EXISTS pg_product CASCADE;
DROP TABLE IF EXISTS pg_supplier CASCADE;
DROP TABLE IF EXISTS pg_warehouse CASCADE;
DROP TABLE IF EXISTS pg_employee CASCADE;

CREATE TABLE pg_customer (
  customer_id int PRIMARY KEY,
  cust_name   text,
  country_code text,
  tz          text,
  vip_level   int,
  email       text
);
INSERT INTO pg_customer
SELECT n,
       (ARRAY['Acme','Globex','Initech','Umbrella','Soylent','Hooli','Stark','Wayne','Wonka','Cyberdyne'])[1+(n%10)] || ' ' || chr(65+(n%26)) || n,
       (ARRAY['CN','HK','US','JP','DE','FR','GB','SG','AU','IN'])[1+(n%10)],
       (ARRAY['Asia/Shanghai','Asia/Hong_Kong','America/New_York','Asia/Tokyo','Europe/Berlin','Europe/Paris','Europe/London','Asia/Singapore','Australia/Sydney','Asia/Kolkata'])[1+(n%10)],
       1+(n%5),
       'cust'||n||'@example.com'
FROM generate_series(1,10000) n;

CREATE TABLE pg_product (
  product_id int PRIMARY KEY,
  sku        text,
  name_en    text,
  name_cn    text,
  desc_en    text,
  desc_cn    text,
  specs      jsonb,
  search_vec tsvector GENERATED ALWAYS AS (
    to_tsvector('english', coalesce(desc_en,'')||' '||coalesce(name_en,''))) STORED
);
INSERT INTO pg_product (product_id, sku, name_en, name_cn, desc_en, desc_cn, specs)
SELECT n,
       'SKU-'||lpad(n::text,6,'0'),
       (ARRAY['Diamond Ring','Gold Necklace','Pearl Earring','Ruby Bracelet','Sapphire Pendant','Emerald Brooch','Platinum Band','Tanzanite Ring','Jade Pendant','Onyx Cufflink'])[1+(n%10)] || ' ' || n,
       '珠宝'||n,
       'A fine '||(ARRAY['diamond','gold','pearl','ruby','sapphire','emerald','platinum','tanzanite','jade','onyx'])[1+(n%10)]||' piece crafted for global markets.',
       '精致'||(ARRAY['钻石','黄金','珍珠','红宝石','蓝宝石','祖母绿','铂金','坦桑石','翡翠','缟玛瑙'])[1+(n%10)]||'饰品。',
       jsonb_build_object('carat', round((0.5+(n%50)/10.0)::numeric,2),
                          'color', (ARRAY['D','E','F','G','H'])[1+(n%5)],
                          'clarity',(ARRAY['VVS1','VVS2','VS1','VS2','SI1'])[1+(n%5)])
FROM generate_series(1,2000) n;

CREATE TABLE pg_supplier (supplier_id int PRIMARY KEY, supplier_name text, country_code text);
INSERT INTO pg_supplier
SELECT n, 'Supplier-'||n, (ARRAY['CN','HK','US','JP','DE','FR','GB','SG','AU','IN'])[1+(n%10)]
FROM generate_series(1,200) n;

CREATE TABLE pg_warehouse (warehouse_id int PRIMARY KEY, warehouse_name text, country_code text);
INSERT INTO pg_warehouse
SELECT n, 'WH-'||n, (ARRAY['CN','HK','US','JP','DE','FR','GB','SG','AU','IN'])[1+(n%10)]
FROM generate_series(1,50) n;

CREATE TABLE pg_employee (emp_id int PRIMARY KEY, emp_name text, dept text, country_code text);
INSERT INTO pg_employee
SELECT n, 'Emp-'||n, (ARRAY['OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'])[1+(n%8)],
       (ARRAY['CN','HK','US','JP','DE','FR','GB','SG','AU','IN'])[1+(n%10)]
FROM generate_series(1,500) n;

-- ----------------------------------------------------------------------------
-- 2. 事实表 pg_txn (10 万行交易, 八域 + 时区生成列 + 本地日生成列 + 多币种)
--    country 用 (n/8)%10 与 system=n%8 独立, 完整覆盖 8×10=80 组合 (避免模耦合)
-- ----------------------------------------------------------------------------
CREATE TABLE pg_txn (
  txn_id        bigint PRIMARY KEY,
  system_code   text NOT NULL,
  customer_id   int,
  product_id    int,
  supplier_id   int,
  warehouse_id  int,
  emp_id        int,
  country_code  text,
  currency_code text,
  amount_orig   numeric(14,2),
  amount_usd    numeric(14,4),
  created_utc   timestamptz,
  tz            text GENERATED ALWAYS AS (
                   CASE country_code
                     WHEN 'CN' THEN 'Asia/Shanghai' WHEN 'HK' THEN 'Asia/Hong_Kong'
                     WHEN 'US' THEN 'America/New_York' WHEN 'JP' THEN 'Asia/Tokyo'
                     WHEN 'DE' THEN 'Europe/Berlin' WHEN 'FR' THEN 'Europe/Paris'
                     WHEN 'GB' THEN 'Europe/London' WHEN 'SG' THEN 'Asia/Singapore'
                     WHEN 'AU' THEN 'Australia/Sydney' WHEN 'IN' THEN 'Asia/Kolkata'
                     ELSE 'UTC' END) STORED,
  create_local_date date GENERATED ALWAYS AS (
                   (created_utc AT TIME ZONE (
                     CASE country_code
                       WHEN 'CN' THEN 'Asia/Shanghai' WHEN 'HK' THEN 'Asia/Hong_Kong'
                       WHEN 'US' THEN 'America/New_York' WHEN 'JP' THEN 'Asia/Tokyo'
                       WHEN 'DE' THEN 'Europe/Berlin' WHEN 'FR' THEN 'Europe/Paris'
                       WHEN 'GB' THEN 'Europe/London' WHEN 'SG' THEN 'Asia/Singapore'
                       WHEN 'AU' THEN 'Australia/Sydney' WHEN 'IN' THEN 'Asia/Kolkata'
                       ELSE 'UTC' END))::date) STORED,
  reference_no  text UNIQUE,
  note          text,
  rand_val      double precision
);

-- 造数: setseed 保证每次运行数据完全一致 -> 幂等
DO $$
BEGIN
  PERFORM setseed(0.20261020);
  INSERT INTO pg_txn (txn_id, system_code, customer_id, product_id, supplier_id, warehouse_id,
                      emp_id, country_code, currency_code, amount_orig, amount_usd,
                      created_utc, reference_no, note, rand_val)
  SELECT n,
         (ARRAY['OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'])[1+(n%8)],
         1+((n+n/8)%10000),
         1+(n%2000),
         1+(n%200),
         1+(n%50),
         1+(n%500),
         (ARRAY['CN','HK','US','JP','DE','FR','GB','SG','AU','IN'])[1+((n/8)%10)],
         (ARRAY['CNY','HKD','USD','JPY','EUR','EUR','GBP','SGD','AUD','INR'])[1+((n/8)%10)],
         round((100 + random()*90000)::numeric,2),
         round(((100 + random()*90000) /
           (CASE (ARRAY['CNY','HKD','USD','JPY','EUR','EUR','GBP','SGD','AUD','INR'])[1+((n/8)%10)]
             WHEN 'CNY' THEN 7.10 WHEN 'HKD' THEN 7.80 WHEN 'USD' THEN 1.00
             WHEN 'JPY' THEN 150.0 WHEN 'EUR' THEN 0.92 WHEN 'GBP' THEN 0.79
             WHEN 'SGD' THEN 1.35 WHEN 'AUD' THEN 1.52 WHEN 'INR' THEN 83.0 END))::numeric,4),
         '2025-01-01 00:00:00+00'::timestamptz + ((random()*300*24*3600)::integer * interval '1 second'),
         'REF-'||lpad(n::text,8,'0'),
         (ARRAY['Diamond','Gold','Pearl','Ruby','Sapphire','Emerald','Platinum','Tanzanite','Jade','Onyx','Zirconia','Topaz'])[1+(n%12)] || ' order for VIP client',
         random()
  FROM generate_series(1,100000) n;
END $$;

CREATE INDEX idx_txn_cover      ON pg_txn (system_code, amount_usd);
CREATE INDEX idx_txn_created    ON pg_txn (created_utc);
CREATE INDEX idx_txn_localday   ON pg_txn (create_local_date);
CREATE INDEX idx_txn_cust       ON pg_txn (customer_id);
CREATE INDEX idx_txn_sys_ctry   ON pg_txn (system_code, country_code);
CREATE INDEX idx_txn_sys_created ON pg_txn (system_code, created_utc);
CREATE INDEX brin_txn_created   ON pg_txn USING brin (created_utc);

VACUUM ANALYZE pg_txn;

-- ============================================================================
-- §1. PIVOT 静态写法: CASE + GROUP BY (域 × 季度 GMV 矩阵, 含总计行)
--     PostgreSQL 无 PIVOT 关键字, 用 CASE 把行值翻成列; ROLLUP 追加总计行
-- ============================================================================
\echo '=== §1 PIVOT(CASE+GROUP BY): 八域 × 季度 GMV(USD) 矩阵 + 总计行 ==='
SELECT COALESCE(system_code,'ALL') AS system_code,
       round(SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END),2) AS q1_usd,
       round(SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END),2) AS q2_usd,
       round(SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END),2) AS q3_usd,
       round(SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END),2) AS q4_usd,
       round(SUM(amount_usd),2)                                       AS total_usd
FROM (SELECT *, 'Q'||extract(quarter from created_utc) AS q FROM pg_txn) t
GROUP BY ROLLUP(system_code)
ORDER BY system_code NULLS LAST;

-- ============================================================================
-- §2. PIVOT 表格函数: tablefunc.crosstab() (域 × 币种 USD 矩阵)
--     crosstab 适合"行分组 × 列分组"的刚性矩阵, 列由 category_sql 固定
-- ============================================================================
\echo '=== §2 PIVOT(crosstab): 八域 × 9 币种 GMV(USD) 矩阵 ==='
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT ct.system_code,
       round(COALESCE("AUD",0),2) AS aud, round(COALESCE("CNY",0),2) AS cny,
       round(COALESCE("EUR",0),2) AS eur, round(COALESCE("GBP",0),2) AS gbp,
       round(COALESCE("HKD",0),2) AS hkd, round(COALESCE("INR",0),2) AS inr,
       round(COALESCE("JPY",0),2) AS jpy, round(COALESCE("SGD",0),2) AS sgd,
       round(COALESCE("USD",0),2) AS usd
FROM crosstab(
  'SELECT system_code, currency_code, SUM(amount_usd)
     FROM pg_txn GROUP BY system_code, currency_code ORDER BY 1,2',
  'SELECT DISTINCT currency_code FROM pg_txn ORDER BY 1'
) AS ct(system_code text, "AUD" numeric, "CNY" numeric, "EUR" numeric, "GBP" numeric,
        "HKD" numeric, "INR" numeric, "JPY" numeric, "SGD" numeric, "USD" numeric)
ORDER BY ct.system_code;

-- ============================================================================
-- §3. PIVOT 动态写法: jsonb_object_agg (每域按币种动态成 JSON, 列数不固定也行)
--     报表列数随数据变化时, 用 jsonb 承载透视结果比硬编码 CASE 更灵活
-- ============================================================================
\echo '=== §3 PIVOT(动态 jsonb): 每域按币种动态聚合成 JSON 对象 ==='
SELECT system_code,
       jsonb_object_agg(currency_code, round(usd,2) ORDER BY currency_code) AS gmv_by_currency
FROM (SELECT system_code, currency_code, SUM(amount_usd) usd
      FROM pg_txn GROUP BY 1,2) t
GROUP BY system_code
ORDER BY system_code;

-- ============================================================================
-- §4. UNPIVOT 经典写法: UNION ALL (把 §1 的季度宽表转回 长表 域,季度,GMV)
--     PostgreSQL 无 UNPIVOT 关键字, 用 UNION ALL 把每列拆成行
-- ============================================================================
\echo '=== §4 UNPIVOT(UNION ALL): 季度宽表 -> (域, 季度, GMV) 长表 (前 10) ==='
WITH pivot_wide AS (
  SELECT system_code,
         SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) AS q1,
         SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) AS q2,
         SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) AS q3,
         SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) AS q4
  FROM (SELECT *, 'Q'||extract(quarter from created_utc) AS q FROM pg_txn) t
  GROUP BY system_code
)
SELECT system_code, 'Q1' AS quarter, q1 AS gmv_usd FROM pivot_wide WHERE system_code IS NOT NULL
UNION ALL SELECT system_code, 'Q2', q2 FROM pivot_wide WHERE system_code IS NOT NULL
UNION ALL SELECT system_code, 'Q3', q3 FROM pivot_wide WHERE system_code IS NOT NULL
UNION ALL SELECT system_code, 'Q4', q4 FROM pivot_wide WHERE system_code IS NOT NULL
ORDER BY system_code, quarter
LIMIT 10;

-- ============================================================================
-- §5. UNPIVOT 现代写法: LATERAL VALUES (宽→长, 一行搞定, 不用多段 UNION ALL)
-- ============================================================================
\echo '=== §5 UNPIVOT(LATERAL VALUES): 同上但更简洁 (前 10) ==='
WITH pivot_wide AS (
  SELECT system_code,
         SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) AS q1,
         SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) AS q2,
         SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) AS q3,
         SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) AS q4
  FROM (SELECT *, 'Q'||extract(quarter from created_utc) AS q FROM pg_txn) t
  GROUP BY system_code
)
SELECT p.system_code, u.quarter, round(u.gmv_usd,2) AS gmv_usd
FROM pivot_wide p
CROSS JOIN LATERAL (VALUES ('Q1',q1),('Q2',q2),('Q3',q3),('Q4',q4)) AS u(quarter, gmv_usd)
WHERE p.system_code IS NOT NULL
ORDER BY p.system_code, u.quarter
LIMIT 10;

-- ============================================================================
-- §6. 时区透视: 同一笔 UTC 交易按 UTC 季度 vs 上海本地季度 归属可能不同列
-- ============================================================================
\echo '=== §6a 时区 PIVOT(UTC 季度): CN 各季度交易笔数 ==='
SELECT COALESCE(system_code,'ALL') AS system_code,
       SUM(CASE WHEN q='Q1' THEN 1 ELSE 0 END) AS q1,
       SUM(CASE WHEN q='Q2' THEN 1 ELSE 0 END) AS q2,
       SUM(CASE WHEN q='Q3' THEN 1 ELSE 0 END) AS q3,
       SUM(CASE WHEN q='Q4' THEN 1 ELSE 0 END) AS q4
FROM (SELECT system_code, 'Q'||extract(quarter from created_utc) AS q
      FROM pg_txn WHERE country_code='CN') t
GROUP BY ROLLUP(system_code) ORDER BY system_code NULLS LAST;

\echo '=== §6b 时区 PIVOT(上海本地季度): CN 各季度交易笔数 (UTC+8 越界致分布不同) ==='
SELECT COALESCE(system_code,'ALL') AS system_code,
       SUM(CASE WHEN lq='Q1' THEN 1 ELSE 0 END) AS q1,
       SUM(CASE WHEN lq='Q2' THEN 1 ELSE 0 END) AS q2,
       SUM(CASE WHEN lq='Q3' THEN 1 ELSE 0 END) AS q3,
       SUM(CASE WHEN lq='Q4' THEN 1 ELSE 0 END) AS q4
FROM (SELECT system_code, 'Q'||extract(quarter from create_local_date) AS lq
      FROM pg_txn WHERE country_code='CN') t
GROUP BY ROLLUP(system_code) ORDER BY system_code NULLS LAST;

-- ============================================================================
-- §7. 多币种透视: 域 × 原币金额; 注意混币原币不可直接相加比较, 须用 amount_usd
-- ============================================================================
\echo '=== §7 多币种 PIVOT(CASE): 八域 × 原币金额 (仅展示, 跨币种比较须用 USD 口径) ==='
SELECT system_code,
       round(SUM(CASE WHEN currency_code='CNY' THEN amount_orig ELSE 0 END),2) AS cny,
       round(SUM(CASE WHEN currency_code='HKD' THEN amount_orig ELSE 0 END),2) AS hkd,
       round(SUM(CASE WHEN currency_code='USD' THEN amount_orig ELSE 0 END),2) AS usd,
       round(SUM(CASE WHEN currency_code='JPY' THEN amount_orig ELSE 0 END),2) AS jpy,
       round(SUM(CASE WHEN currency_code='EUR' THEN amount_orig ELSE 0 END),2) AS eur,
       round(SUM(CASE WHEN currency_code='GBP' THEN amount_orig ELSE 0 END),2) AS gbp,
       round(SUM(CASE WHEN currency_code='SGD' THEN amount_orig ELSE 0 END),2) AS sgd,
       round(SUM(CASE WHEN currency_code='AUD' THEN amount_orig ELSE 0 END),2) AS aud,
       round(SUM(CASE WHEN currency_code='INR' THEN amount_orig ELSE 0 END),2) AS inr
FROM pg_txn
GROUP BY system_code
ORDER BY system_code;

-- ============================================================================
-- §8. 万亿级预演 --- 物化视图预聚合后再透视 (避免每次全表扫描做 CASE 透视)
-- ============================================================================
DROP MATERIALIZED VIEW IF EXISTS mv_pivot_base CASCADE;
CREATE MATERIALIZED VIEW mv_pivot_base AS
  SELECT system_code,
         to_char(created_utc,'YYYY-MM') AS utc_month,
         SUM(amount_usd) gmv_usd, COUNT(*) cnt
  FROM pg_txn
  GROUP BY system_code, to_char(created_utc,'YYYY-MM');
REFRESH MATERIALIZED VIEW mv_pivot_base;

\echo '=== §8 MV 预聚合 + 动态透视: 每域按月 GMV 动态 JSON (前 3 域) ==='
SELECT system_code,
       jsonb_object_agg(utc_month, round(gmv_usd,2) ORDER BY utc_month) AS monthly_gmv
FROM mv_pivot_base
GROUP BY system_code
ORDER BY system_code
LIMIT 3;

-- ============================================================================
-- V1--V18 自检 (全部包进 DO $$ 块; PG 无会话变量, 用 SELECT INTO)
-- ============================================================================
\echo '=== SELF-CHECK V1..V18 ==='

-- V1: §1 PIVOT 行数 = 8 域 + 1 总计 = 9; 总计行 total = 全表 GMV
DO $$
DECLARE n int; all_tot numeric; base numeric;
BEGIN
  SELECT count(*) INTO n FROM (
    SELECT COALESCE(system_code,'ALL') s, SUM(amount_usd) t
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) x
    GROUP BY ROLLUP(system_code)) z;
  SELECT t INTO all_tot FROM (
    SELECT COALESCE(system_code,'ALL') s, SUM(amount_usd) t
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) x
    GROUP BY ROLLUP(system_code)) z WHERE s='ALL';
  SELECT SUM(amount_usd) INTO base FROM pg_txn;
  PERFORM pg_demo_v('V1_pivot_rows_total',
    CASE WHEN n=9 AND abs(all_tot-base) < 0.01 THEN 1 ELSE 0 END,
    'pivot_rows='||n||' all_total='||round(all_tot,2)||' base='||round(base,2));
END $$;

-- V2: §2 crosstab 行数 = 8 域
DO $$
DECLARE n int;
BEGIN
  SELECT count(*) INTO n FROM (
    SELECT ct.system_code FROM crosstab(
      'SELECT system_code, currency_code, SUM(amount_usd)
         FROM pg_txn GROUP BY system_code, currency_code ORDER BY 1,2',
      'SELECT DISTINCT currency_code FROM pg_txn ORDER BY 1'
    ) AS ct(system_code text, "AUD" numeric, "CNY" numeric, "EUR" numeric, "GBP" numeric,
            "HKD" numeric, "INR" numeric, "JPY" numeric, "SGD" numeric, "USD" numeric)
  ) z;
  PERFORM pg_demo_v('V2_crosstab_rows',
    CASE WHEN n=8 THEN 1 ELSE 0 END, 'crosstab_domain_rows='||n);
END $$;

-- V3: §3 动态 jsonb 透视 --- 每域 jsonb 各币种值之和 = 该域 GMV
DO $$
DECLARE n_ok int;
BEGIN
  SELECT count(*) INTO n_ok FROM (
    SELECT system_code,
           (SELECT SUM((value)::numeric) FROM jsonb_each(gmv_by_currency)) AS sum_json,
           (SELECT SUM(amount_usd) FROM pg_txn WHERE system_code=p.system_code) AS base
    FROM (SELECT system_code,
                 jsonb_object_agg(currency_code, usd) AS gmv_by_currency
          FROM (SELECT system_code, currency_code, SUM(amount_usd) usd
                FROM pg_txn GROUP BY 1,2) t GROUP BY system_code) p
  ) z WHERE abs(sum_json - base) < 0.01;
  PERFORM pg_demo_v('V3_dynamic_jsonb_pivot',
    CASE WHEN n_ok=8 THEN 1 ELSE 0 END,
    'domains_reconciled='||n_ok||'/8 (jsonb各币种和=域GMV)');
END $$;

-- V4: §4 UNION ALL 逆透视 行数 = 8 域 × 4 季度 = 32 (每域四季均非空)
DO $$
DECLARE n int;
BEGIN
  WITH pivot_wide AS (
    SELECT system_code,
           SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) q1,
           SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) q2,
           SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) q3,
           SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) q4
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t GROUP BY system_code)
  SELECT count(*) INTO n FROM (
    SELECT system_code,'Q1' q, q1 v FROM pivot_wide WHERE system_code IS NOT NULL
    UNION ALL SELECT system_code,'Q2',q2 FROM pivot_wide WHERE system_code IS NOT NULL
    UNION ALL SELECT system_code,'Q3',q3 FROM pivot_wide WHERE system_code IS NOT NULL
    UNION ALL SELECT system_code,'Q4',q4 FROM pivot_wide WHERE system_code IS NOT NULL
  ) z;
  PERFORM pg_demo_v('V4_union_all_unpivot',
    CASE WHEN n=32 THEN 1 ELSE 0 END, 'unpivot_rows='||n||' (8域 x 4季)');
END $$;

-- V5: §5 LATERAL VALUES 逆透视 与 UNION ALL 完全一致 (行数 & 总和)
DO $$
DECLARE n int; tot numeric;
BEGIN
  WITH pivot_wide AS (
    SELECT system_code,
           SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) q1,
           SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) q2,
           SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) q3,
           SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) q4
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t GROUP BY system_code)
  SELECT count(*), SUM(u.gmv_usd) INTO n, tot FROM pivot_wide p
  CROSS JOIN LATERAL (VALUES ('Q1',q1),('Q2',q2),('Q3',q3),('Q4',q4)) AS u(quarter,gmv_usd)
  WHERE p.system_code IS NOT NULL;
  PERFORM pg_demo_v('V5_lateral_values_unpivot',
    CASE WHEN n=32 AND tot>0 THEN 1 ELSE 0 END,
    'unpivot_rows='||n||' total_usd='||round(tot,2));
END $$;

-- V6: 时区透视差异 --- CN 的 (季度,笔数) 在 UTC 与上海本地下不完全相同
DO $$
DECLARE diff int;
BEGIN
  SELECT count(*) INTO diff FROM (
    SELECT 'Q'||extract(quarter from created_utc) q, count(*) c
    FROM pg_txn WHERE country_code='CN' GROUP BY 1
    EXCEPT
    SELECT 'Q'||extract(quarter from create_local_date) q, count(*) c
    FROM pg_txn WHERE country_code='CN' GROUP BY 1) z;
  PERFORM pg_demo_v('V6_timezone_pivot_diff',
    CASE WHEN diff>0 THEN 1 ELSE 0 END,
    'cn_utc_vs_shanghai_quarter_diff='||diff||' (时区越界致季度归属不同)');
END $$;

-- V7: §7 多币种透视 --- 每域 9 列原币之和 = 该域原币总额
DO $$
DECLARE n_ok int;
BEGIN
  SELECT count(*) INTO n_ok FROM (
    SELECT system_code,
      cny+hkd+usd+jpy+eur+gbp+sgd+aud+inr AS sum_cols,
      (SELECT SUM(amount_orig) FROM pg_txn WHERE system_code=p.system_code) AS base
    FROM (SELECT system_code,
            SUM(CASE WHEN currency_code='CNY' THEN amount_orig ELSE 0 END) cny,
            SUM(CASE WHEN currency_code='HKD' THEN amount_orig ELSE 0 END) hkd,
            SUM(CASE WHEN currency_code='USD' THEN amount_orig ELSE 0 END) usd,
            SUM(CASE WHEN currency_code='JPY' THEN amount_orig ELSE 0 END) jpy,
            SUM(CASE WHEN currency_code='EUR' THEN amount_orig ELSE 0 END) eur,
            SUM(CASE WHEN currency_code='GBP' THEN amount_orig ELSE 0 END) gbp,
            SUM(CASE WHEN currency_code='SGD' THEN amount_orig ELSE 0 END) sgd,
            SUM(CASE WHEN currency_code='AUD' THEN amount_orig ELSE 0 END) aud,
            SUM(CASE WHEN currency_code='INR' THEN amount_orig ELSE 0 END) inr
          FROM pg_txn GROUP BY system_code) p
  ) z WHERE abs(sum_cols - base) < 0.01;
  PERFORM pg_demo_v('V7_multicurrency_pivot',
    CASE WHEN n_ok=8 THEN 1 ELSE 0 END,
    'domains_reconciled='||n_ok||'/8 (9币种原币列和=域原币总额)');
END $$;

-- V8: 透视↔逆透视 往返 --- 逆透视结果汇总回原口径 (总 GMV 与每域 GMV 均一致)
DO $$
DECLARE tot_diff numeric; dom_diff int;
BEGIN
  WITH pivot_wide AS (
    SELECT system_code,
           SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) q1,
           SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) q2,
           SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) q3,
           SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) q4
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t GROUP BY system_code),
  unpiv AS (
    SELECT system_code, gmv_usd FROM pivot_wide p
    CROSS JOIN LATERAL (VALUES ('Q1',q1),('Q2',q2),('Q3',q3),('Q4',q4)) AS u(q,gmv_usd)
    WHERE p.system_code IS NOT NULL)
  SELECT (SELECT SUM(amount_usd) FROM pg_txn) - (SELECT SUM(gmv_usd) FROM unpiv),
         (SELECT count(*) FROM (
            SELECT system_code, SUM(gmv_usd) s FROM unpiv GROUP BY 1
            EXCEPT
            SELECT system_code, SUM(amount_usd) FROM pg_txn GROUP BY 1) z)
  INTO tot_diff, dom_diff;
  PERFORM pg_demo_v('V8_pivot_unpivot_roundtrip',
    CASE WHEN abs(tot_diff) < 0.01 AND dom_diff=0 THEN 1 ELSE 0 END,
    'total_gmv_diff='||round(tot_diff,4)||' domain_mismatch='||dom_diff);
END $$;

-- V9: 总计行 --- §1 的 ALL 行 total 等于全表 GMV
DO $$
DECLARE all_tot numeric; base numeric;
BEGIN
  SELECT t INTO all_tot FROM (
    SELECT COALESCE(system_code,'ALL') s, SUM(amount_usd) t
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) x
    GROUP BY ROLLUP(system_code)) z WHERE s='ALL';
  SELECT SUM(amount_usd) INTO base FROM pg_txn;
  PERFORM pg_demo_v('V9_grand_total_row',
    CASE WHEN abs(all_tot-base) < 0.01 THEN 1 ELSE 0 END,
    'all_row_total='||round(all_tot,2)||' base='||round(base,2));
END $$;

-- V10: crosstab 列对账 --- 每域 9 币种 USD 之和 = 该域 USD 总额
DO $$
DECLARE n_ok int;
BEGIN
  SELECT count(*) INTO n_ok FROM (
    SELECT system_code,
      COALESCE("AUD",0)+COALESCE("CNY",0)+COALESCE("EUR",0)+COALESCE("GBP",0)+
      COALESCE("HKD",0)+COALESCE("INR",0)+COALESCE("JPY",0)+COALESCE("SGD",0)+COALESCE("USD",0) AS sum_cols
    FROM crosstab(
      'SELECT system_code, currency_code, SUM(amount_usd)
         FROM pg_txn GROUP BY system_code, currency_code ORDER BY 1,2',
      'SELECT DISTINCT currency_code FROM pg_txn ORDER BY 1'
    ) AS ct(system_code text, "AUD" numeric, "CNY" numeric, "EUR" numeric, "GBP" numeric,
            "HKD" numeric, "INR" numeric, "JPY" numeric, "SGD" numeric, "USD" numeric)
  ) z JOIN (SELECT system_code, SUM(amount_usd) base FROM pg_txn GROUP BY 1) b USING (system_code)
  WHERE abs(sum_cols - base) < 0.01;
  PERFORM pg_demo_v('V10_crosstab_reconcile',
    CASE WHEN n_ok=8 THEN 1 ELSE 0 END,
    'domains_reconciled='||n_ok||'/8 (crosstab 9列和=域USD总额)');
END $$;

-- V11: 动态 jsonb 透视含恰好 9 个币种键 (无遗漏/无多余)
DO $$
DECLARE n_ok int;
BEGIN
  SELECT count(*) INTO n_ok FROM (
    SELECT system_code, (SELECT count(*) FROM jsonb_each(gmv_by_currency)) AS nkeys
    FROM (SELECT system_code,
                 jsonb_object_agg(currency_code, usd) AS gmv_by_currency
          FROM (SELECT system_code, currency_code, SUM(amount_usd) usd
                FROM pg_txn GROUP BY 1,2) t GROUP BY system_code) p
  ) z WHERE nkeys=9;
  PERFORM pg_demo_v('V11_dynamic_keys_count',
    CASE WHEN n_ok=8 THEN 1 ELSE 0 END,
    'domains_with_9_currency_keys='||n_ok||'/8');
END $$;

-- V12: 单笔 CN 交易时区越界 --- UTC 季度与上海本地季度不同笔数 > 0
DO $$
DECLARE n int;
BEGIN
  SELECT count(*) INTO n FROM pg_txn
  WHERE country_code='CN'
    AND extract(quarter from created_utc) <> extract(quarter from create_local_date);
  PERFORM pg_demo_v('V12_timezone_row_shift',
    CASE WHEN n>0 THEN 1 ELSE 0 END,
    'cn_txns_quarter_shifted='||n||' (UTC+8 致季度归属变化)');
END $$;

-- V13: 全表透视源 Seq Scan 代价 (未索引列 amount_orig 必全扫)
DO $$
DECLARE r record;
BEGIN
  SELECT * INTO r FROM pg_plan_stats('SELECT SUM(amount_orig) FROM pg_txn');
  PERFORM pg_demo_v('V13_fullscan_pivot_source',
    CASE WHEN r.node_type='Seq Scan' AND r.shared_blks > 500 THEN 1 ELSE 0 END,
    'node='||r.node_type||' shared_blks='||r.shared_blks||' (万亿级须分区/物化视图)');
END $$;

-- V14: 覆盖索引支撑透视聚合 (system_code, amount_usd) 走 Index Only Scan 免回表
DO $$
DECLARE r record;
BEGIN
  SELECT * INTO r FROM pg_plan_stats(
    'SELECT system_code, SUM(amount_usd) FROM pg_txn WHERE system_code IN (''ERP'',''CRM'',''SCM'') GROUP BY system_code');
  PERFORM pg_demo_v('V14_covering_pivot',
    CASE WHEN r.node_type='Index Only Scan' THEN 1 ELSE 0 END,
    'node='||r.node_type||' shared_blks='||r.shared_blks);
END $$;

-- V15: 万亿预演 --- MV 透视基 vs 全表透视 块数
DO $$
DECLARE r1 record; r2 record; mvc int;
BEGIN
  SELECT count(*) INTO mvc FROM mv_pivot_base;
  SELECT * INTO r1 FROM pg_plan_stats('SELECT SUM(gmv_usd) FROM mv_pivot_base');
  SELECT * INTO r2 FROM pg_plan_stats('SELECT SUM(amount_orig) FROM pg_txn');
  PERFORM pg_demo_v('V15_materialized_pivot',
    CASE WHEN r1.shared_blks < r2.shared_blks THEN 1 ELSE 0 END,
    'mv_rows='||mvc||' mv_blks='||r1.shared_blks||' base_blks='||r2.shared_blks);
END $$;

-- V16: 幂等佐证 --- 同一透视两次结果一致
DO $$
DECLARE a numeric; b numeric;
BEGIN
  SELECT SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) INTO a
  FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t;
  SELECT SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) INTO b
  FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t;
  PERFORM pg_demo_v('V16_deterministic',
    CASE WHEN a IS NOT NULL AND a = b THEN 1 ELSE 0 END, 'q1_usd='||round(a,2));
END $$;

-- V17: 确定性透视 --- CASE 透视列之和跨两次查询稳定
DO $$
DECLARE s1 numeric; s2 numeric;
BEGIN
  SELECT SUM(q1+q2+q3+q4) INTO s1 FROM (
    SELECT SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) q1,
           SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) q2,
           SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) q3,
           SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) q4
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t GROUP BY system_code) p;
  SELECT SUM(q1+q2+q3+q4) INTO s2 FROM (
    SELECT SUM(CASE WHEN q='Q1' THEN amount_usd ELSE 0 END) q1,
           SUM(CASE WHEN q='Q2' THEN amount_usd ELSE 0 END) q2,
           SUM(CASE WHEN q='Q3' THEN amount_usd ELSE 0 END) q3,
           SUM(CASE WHEN q='Q4' THEN amount_usd ELSE 0 END) q4
    FROM (SELECT *, 'Q'||extract(quarter from created_utc) q FROM pg_txn) t GROUP BY system_code) p;
  PERFORM pg_demo_v('V17_pivot_stable',
    CASE WHEN s1 = s2 THEN 1 ELSE 0 END, 'sum_of_pivot_cells='||round(s1,2));
END $$;

-- V18: 透视 NULL 处理 --- CASE 聚合 ELSE 0 使空组合为 0 而非 NULL (用合成表验证)
DO $$
DECLARE r0 numeric; r1 numeric;
BEGIN
  WITH t(s, v) AS (VALUES ('A',1.0),('A',NULL),('B',3.0))
  SELECT SUM(CASE WHEN s='A' THEN v ELSE 0 END),
         SUM(CASE WHEN s='Z' THEN v ELSE 0 END)   -- 'Z' 不存在 => 全 ELSE 0
  INTO r0, r1 FROM t;
  PERFORM pg_demo_v('V18_null_in_pivot',
    CASE WHEN r0=1 AND r1=0 THEN 1 ELSE 0 END,
    'A_sum_incl_null='||r0||' missing_group_yields_0='||r1||' (ELSE 0 防 NULL)');
END $$;

-- ----------------------------------------------------------------------------
-- 自检汇总
-- ----------------------------------------------------------------------------
SELECT name AS check_name, pass AS pass_cnt, detail
FROM pg_demo_check ORDER BY name;

SELECT CASE WHEN min(pass)=1 THEN '全部通过 (ALL PASS)' ELSE '未全部通过 (SOME FAILED)' END AS result
FROM pg_demo_check;

SELECT count(*) AS total, sum(pass) AS passed FROM pg_demo_check;
相关推荐
我命由我123451 小时前
6-3.Python 异常处理 - 异常处理进阶(else 块与 finally 块、异常传递、自定义异常)
开发语言·前端·数据库·后端·python·学习·java-ee
Elastic 中国社区官方博客1 小时前
Elastic 重大事件功能如何将 100 条日志样本转化为检测规则
大数据·运维·数据库·人工智能·elasticsearch·搜索引擎
adinnet20261 小时前
持续智能问数:多轮对话状态管理、下钻钻取与经营驾驶舱的演进
javascript·数据库·人工智能
柏慧通云报餐1 小时前
政企清廉食堂数字化建设方案:基于一体化报餐平台的全链路管控落地思路
大数据·运维·人工智能·物联网·sass
GlobalInfo1 小时前
2026机器人关节谐波减速器研究报告:市场规模与十五五风向
大数据·人工智能·机器人
fengyangaiGEO2 小时前
外贸B2B GEO优化:为何是平台成功的关键?
大数据·人工智能·python
DeveloperHonor2 小时前
pg_walex让回滚如此丝滑:PostgreSQL WAL 日志解析与数据回滚实战
postgresql·数据恢复·wal日志
系统集成-鹰翔2 小时前
ERP接口月度统计少了零单月份:SQLite分组与最近三个月均值排查
javascript·数据库·经验分享·后端
ClouGence2 小时前
支付机构为什么需要数据同步:从监管报送到跨云业务协作
数据库·数据分析