-- ============================================================================
-- 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;