sql:SQL Pagination and Limiting Results using PorstgreSQL 18.0

文章大纲

  • 前言
    • 为什么分页在万亿级交易流水里会成为性能瓶颈
    • 本文的演示环境:PostgreSQL 18.6、八域统一交易流水、10 万行造数
    • 阅读路线:从 OFFSET/LIMIT 到键集分页,再到时区与多币种陷阱
  • 一、分页的两种范式:OFFSET/FETCH 与键集分页
    • LIMIT/OFFSET 习惯写法与 SQL 标准 OFFSET ... FETCH NEXT 的语法等价性
    • 深层 OFFSET 的代价来源:扫描行数随页码线性增长
    • 键集(Seek)分页的核心思想:用上一页最后一行的值做游标
  • 二、复合游标与稳定性:解决并列值与页码漂移
    • 为什么单列游标不够:低位宽列大量并列,必须补唯一键做 tie-break
    • 并发补录导致的 OFFSET 漂移与漏读,键集游标为何天然稳定
    • 锚点行排除与结果可重放:两次运行结果一致的验证方法
  • 三、国际化场景的两个硬约束:时区与多币种
    • 游标必须基于 UTC(timestamptz)比较,本地时间字符串会随会话时区漂移
    • 多币种排序必须落在归一化口径 amount_usd 上,混币排序会乱序
    • 生成列 tz 与 create_local_date 在报表与分页中的正确用法
  • 四、执行计划与索引优化:让键集分页真正跑快
    • 覆盖索引 INCLUDE 把 Index Scan 升级为 Index Only Scan
    • VACUUM ANALYZE 与可见性映射对免回表的影响
    • 跨 JOIN 的键集分页仍由索引驱动,单页代价保持恒定
  • 五、工程落地:总页数、深分页与自检体系
    • COUNT(*) 全表扫描的代价,以及无限滚动与估算总数的替代方案
    • 深层 OFFSET 与键集分页的块数比值量化(O(位置) vs O(1/页))
    • V1..V12 自检框架:用真实执行计划与真实数据验证每个结论
  • 总结
    • 分页方案选型清单:数据量、排序稳定性、并发写入、国际化口径
    • 常见陷阱回顾:本地时间游标、混币排序、单列游标、每页重算 COUNT
    • 从演示脚本到生产代码的迁移建议
  • 参考资料
    • PostgreSQL 官方文档:SELECT 的 LIMIT 与 OFFSET/FETCH 子句
    • PostgreSQL 官方文档:索引类型、覆盖索引与仅索引扫描
    • PostgreSQL 官方文档:EXPLAIN 输出与执行计划分析
sql 复制代码
-- =============================================================================
-- 主题: SQL Pagination and Limiting Results
--       (OFFSET/FETCH vs Keyset / Seek pagination in PostgreSQL 18)
-- 引擎: PostgreSQL 18.6  (本机监听 5433, 超级用户 postgres, 库 jewelry_pg)
-- 场景: 国际化珠宝集团 OA/ERP/CRM/HRM/MES/SCM/WMS/KMS 八域统一交易流水
--       万亿级数据高并发、国际时区时间差、国际多币种结算
-- 范式: 建表造数(10万行) + 度量框架 + 多章演示 + V1..V12 自检 + 完全幂等
-- 说明: 本脚本不含任何明文口令; 连接凭据经 PGPASSWORD 环境变量注入。
-- =============================================================================
-- 让执行计划可复现(关闭并行、压低随机IO代价, 使 Index Only Scan 真实免回表)
SET max_parallel_workers_per_gather = 0;
SET random_page_cost = 1.1;
SET seq_page_cost = 1.0;
SET effective_cache_size = '4GB';
-- 0. 幂等清理: 所有对象均 DROP IF EXISTS, 可无限重跑
DROP TABLE IF EXISTS pg_demo_check CASCADE;
DROP TABLE IF EXISTS pg_demo_probe CASCADE;
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;
DROP TABLE IF EXISTS pg_domain CASCADE;
DROP TABLE IF EXISTS pg_stab CASCADE;
DROP FUNCTION IF EXISTS pg_plan_stats(text) CASCADE;
DROP FUNCTION IF EXISTS pg_demo_v(text,int,text) CASCADE;
DROP FUNCTION IF EXISTS pg_demo_err_probe(text,text) CASCADE;
-- 1. 度量框架 (PG 风格: 无会话变量, 全部包进 DO 块; JSON 的 Actual Rows 为浮点串须 ::numeric::bigint)
CREATE TABLE pg_demo_check (
check_name text PRIMARY KEY,
pass_cnt   int,
detail     text
);
CREATE TABLE pg_demo_probe (
label    text PRIMARY KEY,
sql_state text,
msg      text
);
-- 下钻到真正的扫描节点 (顶层常是 Limit/Sort/Aggregate, 须取 Scan 节点类型)
CREATE OR REPLACE FUNCTION pg_find_scan(pl json)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE nt text; cur json := pl;
BEGIN
LOOP
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
cur := cur->'Plans'->0;
ELSE
RETURN nt;
END IF;
END LOOP;
END $$;
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 $$;
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(check_name, pass_cnt, detail)
VALUES (p_name, p_pass, p_detail)
ON CONFLICT (check_name) DO UPDATE SET pass_cnt = EXCLUDED.pass_cnt, detail = EXCLUDED.detail;
END $$;
CREATE OR REPLACE FUNCTION pg_demo_err_probe(p_label text, p_sql text)
RETURNS void LANGUAGE plpgsql AS $$
DECLARE v_state text; v_msg text;
BEGIN
BEGIN
EXECUTE p_sql;
EXCEPTION WHEN OTHERS THEN
v_state := SQLSTATE; v_msg := SQLERRM;
END;
INSERT INTO pg_demo_probe(label, sql_state, msg)
VALUES (p_label, v_state, v_msg)
ON CONFLICT (label) DO UPDATE SET sql_state = EXCLUDED.sql_state, msg = EXCLUDED.msg;
END $$;
-- 2. 八域维度建模 (国际化珠宝集团统一中台)
CREATE TABLE pg_domain (
system_code  text PRIMARY KEY,
domain_name  text,
module       text
);
INSERT INTO pg_domain(system_code, domain_name, module) VALUES
('OA',  '办公自动化',   '协同/审批'),
('ERP', '企业资源计划', '财务/库存'),
('CRM', '客户关系管理', '会员/营销'),
('HRM', '人力资源管理', '薪酬/考勤'),
('MES', '制造执行系统', '工坊/工序'),
('SCM', '供应链管理',   '采购/物流'),
('WMS', '仓储管理系统', '出入库/盘点'),
('KMS', '知识管理',     '文档/工艺');
-- 客户维度 (含时区与会员等级, 用于键集 JOIN 演示)
CREATE TABLE pg_customer (
customer_id  int PRIMARY KEY,
country_code text,
tz           text,
vip_level    int
);
INSERT INTO pg_customer(customer_id, country_code, tz, vip_level)
SELECT n,
(ARRAY['HK','CN','US','JP','DE','GB','FR','SG'])[1 + (n % 8)],
(CASE (ARRAY['HK','CN','US','JP','DE','GB','FR','SG'])[1 + (n % 8)]
WHEN 'HK' THEN 'Asia/Hong_Kong' WHEN 'CN' THEN 'Asia/Shanghai'
WHEN 'US' THEN 'America/New_York' WHEN 'JP' THEN 'Asia/Tokyo'
WHEN 'DE' THEN 'Europe/Berlin' WHEN 'GB' THEN 'Europe/London'
WHEN 'FR' THEN 'Europe/Paris' WHEN 'SG' THEN 'Asia/Singapore' END),
1 + (n % 5)
FROM generate_series(1,10000) s(n);
-- 商品维度 (含 specs jsonb, 供其他专题复用)
CREATE TABLE pg_product (
product_id int PRIMARY KEY,
specs jsonb
);
INSERT INTO pg_product(product_id, specs)
SELECT n, jsonb_build_object(
'carat', round((n % 300)/100.0 + 0.3, 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) s(n);
-- 供应商 / 仓库 / 员工 维度 (八域组织结构)
CREATE TABLE pg_supplier  (supplier_id int PRIMARY KEY, name text);
CREATE TABLE pg_warehouse(wh_id int PRIMARY KEY, region text);
CREATE TABLE pg_employee (emp_id int PRIMARY KEY, dept text, name text);
INSERT INTO pg_supplier  SELECT n, 'SUP'||lpad(n::text,4,'0') FROM generate_series(1,200) s(n);
INSERT INTO pg_warehouse SELECT n, (ARRAY['APAC','EMEA','AMER'])[1 + (n % 3)] FROM generate_series(1,50) s(n);
INSERT INTO pg_employee  SELECT n,
(ARRAY['OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'])[1 + (n % 8)],
'EMP'||lpad(n::text,4,'0')
FROM generate_series(1,500) s(n);
-- 3. 交易事实表 (10 万行, 万亿级高并发的缩影)
--    created_utc 为真·带时区时间; tz/create_local_date 为 STORED 生成列;
--    结算口径统一 amount_usd, 原币 amount_orig 仅展示用。
CREATE TABLE pg_txn (
txn_id       bigint PRIMARY KEY,
system_code  text NOT NULL REFERENCES pg_domain(system_code),
customer_id  int  REFERENCES pg_customer(customer_id),
product_id   int  REFERENCES pg_product(product_id),
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 'HK' THEN 'Asia/Hong_Kong'  WHEN 'CN' THEN 'Asia/Shanghai'
WHEN 'US' THEN 'America/New_York' WHEN 'JP' THEN 'Asia/Tokyo'
WHEN 'DE' THEN 'Europe/Berlin'   WHEN 'GB' THEN 'Europe/London'
WHEN 'FR' THEN 'Europe/Paris'    WHEN 'SG' THEN 'Asia/Singapore'
ELSE 'UTC' END) STORED,
create_local_date date GENERATED ALWAYS AS (
(created_utc AT TIME ZONE (
CASE country_code
WHEN 'HK' THEN 'Asia/Hong_Kong'  WHEN 'CN' THEN 'Asia/Shanghai'
WHEN 'US' THEN 'America/New_York' WHEN 'JP' THEN 'Asia/Tokyo'
WHEN 'DE' THEN 'Europe/Berlin'   WHEN 'GB' THEN 'Europe/London'
WHEN 'FR' THEN 'Europe/Paris'    WHEN 'SG' THEN 'Asia/Singapore'
ELSE 'UTC' END))::date) STORED,
reference_no text,
note         text,
rand_val     double precision
);
INSERT INTO pg_txn
(txn_id, system_code, customer_id, product_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 % 10000),
1 + (n % 2000),
(ARRAY['HK','CN','US','JP','DE','GB','FR','SG'])[1 + (n % 8)],
(ARRAY['HKD','CNY','USD','JPY','EUR','GBP','EUR','SGD'])[1 + (n % 8)],
(((n13) % 9000) + 50)::numeric(14,2),
round(((((n13) % 9000) + 50)::numeric *
(ARRAY[0.128,0.139,1.0,0.0067,1.08,1.27,1.08,0.74])[1 + (n % 8)])::numeric, 4),
('2025-01-01 00:00:00+00'::timestamptz + (n::float/100000.0)interval '365 days'),
'REF' || lpad(n::text, 8, '0'),
'txn ' || n,
(n0.6180339887) % 1
FROM generate_series(1,100000) s(n);
-- 索引 (键集覆盖索引 idx_txn_keyset 在 V8 中动态创建/验证)
CREATE INDEX idx_txn_created ON pg_txn (created_utc);
CREATE INDEX idx_txn_system  ON pg_txn (system_code);
CREATE INDEX idx_txn_amt     ON pg_txn (amount_usd, txn_id);
CREATE UNIQUE INDEX idx_txn_ref ON pg_txn (reference_no);
-- 稳定性漂移演示用的小表 (独立于大表, 干净可控)
CREATE TABLE pg_stab (id int PRIMARY KEY, created_utc timestamptz);
INSERT INTO pg_stab
SELECT n, '2025-01-01 00:00:00+00'::timestamptz + (n || ' hours')::interval
FROM generate_series(1,100) s(n);
CREATE INDEX idx_stab_created ON pg_stab (created_utc);
-- 关键: VACUUM ANALYZE 更新可见性映射, 使 Index Only Scan 真正免回表 (顶层语句, 不在 DO 内)
VACUUM ANALYZE pg_txn;
ANALYZE pg_stab;
-- =============================================================================
-- 4. 数据概览 (示例: 八域 / 多币种 / 时区 / 美元结算口径)
-- =============================================================================
SELECT d.domain_name, count(*) AS txn_cnt,
sum(t.amount_usd) AS gmv_usd
FROM pg_txn t JOIN pg_domain d ON d.system_code = t.system_code
GROUP BY d.domain_name ORDER BY d.domain_name;
SELECT currency_code, count(*) AS cnt,
sum(amount_orig) AS orig_total, sum(amount_usd) AS usd_total
FROM pg_txn GROUP BY currency_code ORDER BY currency_code;
SELECT min(created_utc) AS first_utc, max(created_utc) AS last_utc,
count(*) AS total_rows, sum(amount_usd) AS grand_gmv_usd
FROM pg_txn;
-- =============================================================================
-- 5. 分页演示: OFFSET/LIMIT 习惯写法 与 SQL 标准 OFFSET...FETCH
-- =============================================================================
-- 5.1 第一页 (PG 习惯 LIMIT/OFFSET)
SELECT txn_id, system_code, country_code, currency_code,
amount_orig, amount_usd, created_utc
FROM pg_txn
ORDER BY created_utc DESC, txn_id DESC
LIMIT 10 OFFSET 0;
-- 5.2 等价的标准 SQL 写法 (PG 18 支持 OFFSET ... FETCH NEXT ... ROWS ONLY)
SELECT txn_id, system_code, country_code, currency_code,
amount_orig, amount_usd, created_utc
FROM pg_txn
ORDER BY created_utc DESC, txn_id DESC
OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;
-- 5.3 深层页 (OFFSET 越大越慢, 且会随插入漂移)
SELECT txn_id, created_utc
FROM pg_txn
ORDER BY created_utc DESC, txn_id DESC
LIMIT 10 OFFSET 90000;
-- 5.4 键集(Seek)分页: 用上一页"最后一行"的值做游标, 与位置无关, 稳定且快
--     复合游标 (created_utc, txn_id) 解决并列值无法唯一定位的问题
SELECT txn_id, system_code, country_code, currency_code,
amount_orig, amount_usd, created_utc
FROM pg_txn
WHERE (created_utc, txn_id) < ('2026-06-15 12:00:00+00', 9223372036854775807)
ORDER BY created_utc DESC, txn_id DESC
LIMIT 10;
-- 5.5 多币种列表页: 排序必须在归一化口径 amount_usd 上, 而非混币的 amount_orig
SELECT txn_id, country_code, currency_code, amount_orig, amount_usd
FROM pg_txn
WHERE (amount_usd, txn_id) < (999999.0, 9223372036854775807)
ORDER BY amount_usd DESC, txn_id DESC
LIMIT 10;
-- 5.6 跨域报表分页 (JOIN 客户会员等级), 仍用键集游标驱动
SELECT t.txn_id, d.domain_name, c.vip_level, t.amount_usd, t.created_utc
FROM pg_txn t
JOIN pg_domain d ON d.system_code = t.system_code
JOIN pg_customer c ON c.customer_id = t.customer_id
WHERE (t.created_utc, t.txn_id) < ('2026-06-15 12:00:00+00', 9223372036854775807)
ORDER BY t.created_utc DESC, t.txn_id DESC
LIMIT 10;
-- =============================================================================
-- 6. 自检 V1..V12 (全部用真实执行计划/真实数据量化)
-- =============================================================================
-- V1: PG 习惯 LIMIT/OFFSET 与 SQL 标准 OFFSET...FETCH 生成相同执行节点
DO $$
DECLARE r1 record; r2 record;
BEGIN
SELECT * INTO r1 FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id LIMIT 20 OFFSET 0$q$);
SELECT * INTO r2 FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY$q$);
PERFORM pg_demo_v('V1_std_vs_idiom',
CASE WHEN r1.node_type = r2.node_type THEN 1 ELSE 0 END,
'idiom='||r1.node_type||' standard='||r2.node_type||' (语法等价, 计划相同)');
END $$;
-- V2: 深层 OFFSET 代价随页码线性增长 (仍只取 20 行, 却要扫过 9 万行)
DO $$
DECLARE r1 record; r2 record;
BEGIN
SELECT * INTO r1 FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id LIMIT 20 OFFSET 0$q$);
SELECT * INTO r2 FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id LIMIT 20 OFFSET 90000$q$);
PERFORM pg_demo_v('V2_deep_offset_cost',
CASE WHEN r2.shared_blks > r1.shared_blks AND r2.actual_rows = 20 THEN 1 ELSE 0 END,
'page1_blks='||r1.shared_blks||' deep_blks='||r2.shared_blks||' deep_rows='||r2.actual_rows);
END $$;
-- V3: 同深度的键集分页远比深层 OFFSET 廉价 (索引精准定位, 与位置无关)
DO $$
DECLARE r_off record; r_ks record;
BEGIN
SELECT * INTO r_off FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id LIMIT 20 OFFSET 90000$q$);
SELECT * INTO r_ks FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn WHERE (created_utc, txn_id) < ('2025-10-01 00:00:00+00', 9223372036854775807) ORDER BY created_utc DESC, txn_id DESC LIMIT 20$q$);
PERFORM pg_demo_v('V3_keyset_cheap',
CASE WHEN r_ks.shared_blks < r_off.shared_blks THEN 1 ELSE 0 END,
'offset_blks='||r_off.shared_blks||' keyset_blks='||r_ks.shared_blks);
END $$;
-- V4: 键集游标用"锚点行的完整 (created_utc,txn_id) 对", 排除锚点本身, 且两次运行一致
DO $$
DECLARE arr1 int[]; arr2 int[]; c_created timestamptz; c_txn bigint;
BEGIN
SELECT created_utc, txn_id INTO c_created, c_txn
FROM pg_txn ORDER BY created_utc DESC, txn_id DESC LIMIT 1 OFFSET 70000;
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr1
FROM (SELECT txn_id, created_utc FROM pg_txn
WHERE (created_utc, txn_id) < (c_created, c_txn)
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr2
FROM (SELECT txn_id, created_utc FROM pg_txn
WHERE (created_utc, txn_id) < (c_created, c_txn)
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
PERFORM pg_demo_v('V4_keyset_deterministic',
CASE WHEN arr1 = arr2 AND (c_txn <> ALL(arr1)) THEN 1 ELSE 0 END,
'deterministic='||(CASE WHEN arr1=arr2 THEN 'yes' ELSE 'no' END)
||' anchor_excluded='||(CASE WHEN c_txn <> ALL(arr1) THEN 'yes' ELSE 'no' END));
END $$;
-- V5: 稳定性 --- OFFSET 页码漂移 vs 键集游标稳定
--     背景插入"早于此前已读页"的迟到/补录单据后, 同一 OFFSET 页码返回不同行(漂移/漏读);
--     而键集游标锚定值, 后续页保持确定, 不会与已读页重叠。
DO $$
DECLARE
page2_before int[]; page2_after int[]; ks_next int[]; cur timestamptz;
BEGIN
-- 插入前: 第1页=1..10, 第2页(偏移10)=11..20
SELECT array_agg(id ORDER BY created_utc, id) INTO page2_before
FROM (SELECT id, created_utc FROM pg_stab ORDER BY created_utc, id LIMIT 10 OFFSET 10) t;
-- 并发补录 5 张"时间戳更早"的单据(模拟迟到的门店/工坊回传)
INSERT INTO pg_stab VALUES
(101, '2024-12-31 00:00:00+00'::timestamptz),
(102, '2024-12-31 01:00:00+00'::timestamptz),
(103, '2024-12-31 02:00:00+00'::timestamptz),
(104, '2024-12-31 03:00:00+00'::timestamptz),
(105, '2024-12-31 04:00:00+00'::timestamptz);
-- 插入后: 第2页偏移10 变成 5..14 (用户已读的 1..10 与新单重叠, 11..15 被漏读)
SELECT array_agg(id ORDER BY created_utc, id) INTO page2_after
FROM (SELECT id, created_utc FROM pg_stab ORDER BY created_utc, id LIMIT 10 OFFSET 10) t;
-- 键集: 以第1页最后一行(id=10)的 created_utc 为游标, 取其后 10 行
SELECT created_utc INTO cur FROM pg_stab WHERE id = 10;
SELECT array_agg(id ORDER BY created_utc, id) INTO ks_next
FROM (SELECT id, created_utc FROM pg_stab WHERE created_utc > cur
ORDER BY created_utc, id LIMIT 10) t;
PERFORM pg_demo_v('V5_OFFSET_drift',
CASE WHEN page2_before <> page2_after THEN 1 ELSE 0 END,
'page2_before='||array_to_string(page2_before,',')||' page2_after='||array_to_string(page2_after,','));
PERFORM pg_demo_v('V5_KEYSET_stable',
CASE WHEN ks_next = page2_before THEN 1 ELSE 0 END,
'keyset_next='||array_to_string(ks_next,',')||' (等于插入前第2页, 无漏读/无重叠)');
END $$;
-- V6: 时区 --- 键集游标必须基于 UTC(timestamptz) 比较; 用本地时间字符串会随会话时区漂移
DO $$
DECLARE
c_cursor timestamptz; n_sh int; n_ny int; arr_sh int[]; arr_ny int[];
n_sh2 int; n_ny2 int;
BEGIN
SELECT created_utc INTO c_cursor FROM pg_txn ORDER BY created_utc DESC LIMIT 1 OFFSET 50000;
SET TIME ZONE 'Asia/Shanghai';
SELECT count() INTO n_sh FROM pg_txn WHERE created_utc < c_cursor;
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr_sh
FROM (SELECT txn_id, created_utc FROM pg_txn WHERE created_utc < c_cursor
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
SET TIME ZONE 'America/New_York';
SELECT count() INTO n_ny FROM pg_txn WHERE created_utc < c_cursor;
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr_ny
FROM (SELECT txn_id, created_utc FROM pg_txn WHERE created_utc < c_cursor
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
-- 陷阱: 直接用"本地时间字符串"当游标, 未带时区 -> 边界随会话时区变化
SET TIME ZONE 'Asia/Shanghai';
SELECT count() INTO n_sh2 FROM pg_txn WHERE created_utc < '2025-06-01 00:00:00';
SET TIME ZONE 'America/New_York';
SELECT count() INTO n_ny2 FROM pg_txn WHERE created_utc < '2025-06-01 00:00:00';
SET TIME ZONE 'UTC';
PERFORM pg_demo_v('V6_utc_cursor_stable',
CASE WHEN n_sh = n_ny AND arr_sh = arr_ny THEN 1 ELSE 0 END,
'SH_count='||n_sh||' NY_count='||n_ny||' (UTC 游标跨时区一致)');
PERFORM pg_demo_v('V6_local_cursor_trap',
CASE WHEN n_sh2 <> n_ny2 THEN 1 ELSE 0 END,
'naive_local SH='||n_sh2||' NY='||n_ny2||' (本地串游标随会话时区漂移, 必须用 UTC)');
END $$;
-- V7: 多币种 --- 键集必须按归一化 amount_usd 排序; 按混币 amount_orig 排序会乱序
DO $$
DECLARE r record; prev numeric; mono int := 1; trap int := 1;
BEGIN
prev := NULL;
FOR r IN (SELECT amount_usd FROM pg_txn ORDER BY amount_usd DESC, txn_id DESC LIMIT 50) LOOP
IF prev IS NOT NULL AND r.amount_usd > prev THEN mono := 0; END IF;
prev := r.amount_usd;
END LOOP;
PERFORM pg_demo_v('V7_usd_keyset_monotonic', mono,
'按 (amount_usd,txn_id) 键集: 美元口径严格单调='||(CASE WHEN mono=1 THEN 'yes' ELSE 'no' END));
prev := NULL;
FOR r IN (SELECT amount_usd FROM pg_txn ORDER BY amount_orig DESC LIMIT 50) LOOP
IF prev IS NOT NULL AND r.amount_usd > prev THEN trap := 0; END IF;
prev := r.amount_usd;
END LOOP;
PERFORM pg_demo_v('V7_orig_sort_not_usd',
CASE WHEN trap = 0 THEN 1 ELSE 0 END,
'按 amount_orig 排序: 美元口径非单调 (混币陷阱已证实)');
END $$;
-- V8: 覆盖索引把键集从 Index Scan 升级为 Index Only Scan (少回表, 万亿级更关键)
DO $$
DECLARE r1 record; r2 record;
BEGIN
DROP INDEX IF EXISTS idx_txn_keyset;
SELECT * INTO r1 FROM pg_plan_stats(
$q$SELECT txn_id, created_utc, amount_usd FROM pg_txn
WHERE created_utc < '2025-07-01 00:00:00+00'
ORDER BY created_utc DESC, txn_id DESC LIMIT 20$q$);
CREATE INDEX idx_txn_keyset ON pg_txn (created_utc, txn_id) INCLUDE (amount_usd, currency_code);
ANALYZE pg_txn;
SELECT * INTO r2 FROM pg_plan_stats(
$q$SELECT txn_id, created_utc, amount_usd FROM pg_txn
WHERE created_utc < '2025-07-01 00:00:00+00'
ORDER BY created_utc DESC, txn_id DESC LIMIT 20$q$);
PERFORM pg_demo_v('V8_covering_index_only_scan',
CASE WHEN r2.node_type = 'Index Only Scan' THEN 1 ELSE 0 END,
'before='||r1.node_type||'('||r1.shared_blks||') after='||r2.node_type||'('||r2.shared_blks||')');
END $$;
-- V9: 总页数计数代价 --- COUNT() 全表扫描, 远大于键集单页
DO $$
DECLARE r1 record; r2 record;
BEGIN
SELECT * INTO r1 FROM pg_plan_stats($q$SELECT count() FROM pg_txn$q$);
SELECT * INTO r2 FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn WHERE created_utc < '2025-07-01 00:00:00+00'
ORDER BY created_utc DESC, txn_id DESC LIMIT 20$q$);
PERFORM pg_demo_v('V9_count_cost_vs_keyset',
CASE WHEN r1.shared_blks > r2.shared_blks THEN 1 ELSE 0 END,
'count_blks='||r1.shared_blks||' keyset_blks='||r2.shared_blks
||' (万亿级不应每页重算 COUNT, 用无限滚动/估算)');
END $$;
-- V10: 万亿级深度代价比 --- 深层 OFFSET 块数远超键集 (O(位置) vs O(1/页))
DO $$
DECLARE r_off record; r_ks record; ratio numeric;
BEGIN
SELECT * INTO r_off FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn ORDER BY created_utc, txn_id LIMIT 20 OFFSET 90000$q$);
SELECT * INTO r_ks FROM pg_plan_stats(
$q$SELECT txn_id FROM pg_txn WHERE (created_utc, txn_id) < ('2025-10-01 00:00:00+00', 9223372036854775807)
ORDER BY created_utc DESC, txn_id DESC LIMIT 20$q$);
ratio := r_off.shared_blks::numeric / NULLIF(r_ks.shared_blks,0);
PERFORM pg_demo_v('V10_deep_ratio',
CASE WHEN r_off.shared_blks > r_ks.shared_blks AND ratio > 100 THEN 1 ELSE 0 END,
'offset_blks='||r_off.shared_blks||' keyset_blks='||r_ks.shared_blks||' ratio='||round(ratio,1));
END $$;
-- V11: 复合游标必要性 --- 低位宽列(如 system_code 仅 8 值)天然大量并列, 单列游标无法在组内续翻页;
--      复合 (order_col, unique_id) 游标才能稳定续翻。
DO $$
DECLARE arr1 int[]; arr2 int[]; grp int;
BEGIN
-- 复合游标键集 (created_utc, txn_id) 稳定可重放
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr1
FROM (SELECT txn_id, created_utc FROM pg_txn
WHERE (created_utc, txn_id) < ('2025-10-01 00:00:00+00', 9223372036854775807)
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
SELECT array_agg(txn_id ORDER BY created_utc DESC, txn_id DESC) INTO arr2
FROM (SELECT txn_id, created_utc FROM pg_txn
WHERE (created_utc, txn_id) < ('2025-10-01 00:00:00+00', 9223372036854775807)
ORDER BY created_utc DESC, txn_id DESC LIMIT 20) t;
-- 低位宽列证明单列游标不足: 任一域组内行数远大于页大小
SELECT count(*) INTO grp FROM pg_txn WHERE system_code = 'CRM';
PERFORM pg_demo_v('V11_keyset_deterministic',
CASE WHEN arr1 = arr2 THEN 1 ELSE 0 END,
'composite_cursor same_result_across_runs='||(CASE WHEN arr1=arr2 THEN 'yes' ELSE 'no' END));
PERFORM pg_demo_v('V11_tiebreak_needed',
CASE WHEN grp > 100 THEN 1 ELSE 0 END,
'CRM 组内行数='||grp||' >> 页大小, 单列(system_code)游标无法在组内续翻, 须加唯一 txn_id');
END $$;
-- V12: 键集分页跨 JOIN 仍由索引驱动 (Index Only Scan + Nested Loop), 单页代价恒定
DO $$
DECLARE r record;
BEGIN
SELECT * INTO r FROM pg_plan_stats(
$q$SELECT t.txn_id, c.vip_level, t.amount_usd FROM pg_txn t
JOIN pg_customer c ON c.customer_id = t.customer_id
WHERE (t.created_utc, t.txn_id) < ('2025-10-01 00:00:00+00', 9223372036854775807)
ORDER BY t.created_utc DESC, t.txn_id DESC LIMIT 20$q$);
PERFORM pg_demo_v('V12_keyset_join',
CASE WHEN r.actual_rows <= 20 AND r.shared_blks < 500 THEN 1 ELSE 0 END,
'join_keyset node='||r.node_type||' blks='||r.shared_blks);
END $$;
-- =============================================================================
-- 7. 自检汇总 (失败探针也应登记, 证明"常见记忆"以本机实测为准)
-- =============================================================================
SELECT check_name, pass_cnt, detail FROM pg_demo_check ORDER BY check_name;
SELECT label, sql_state, msg FROM pg_demo_probe ORDER BY label;
DO $$
DECLARE tot int; pas int;
BEGIN
SELECT count() INTO tot FROM pg_demo_check;
SELECT count() INTO pas FROM pg_demo_check WHERE pass_cnt = 1;
IF pas = tot THEN
RAISE NOTICE '=== SELF-CHECK: % / % 全部通过 (ALL PASS) ===', pas, tot;
ELSE
RAISE NOTICE '=== SELF-CHECK: % / % 未全部通过 ===', pas, tot;
END IF;
END $$;
相关推荐
LCG米1 小时前
STM32 DMA 多通道链式传输实测:请求仲裁顺序错位根因、半满中断乒乓与高速并发搬运落地
stm32·单片机·嵌入式·1024程序员节
Shaoshing1 小时前
MySQL日志
1024程序员节
奇树谦1 小时前
朴素贝叶斯(Naive Bayes)详解
机器学习·1024程序员节
Liaiyang661 小时前
空圈容错·极限工程实战下篇——外部供给层怎么升维,内外层怎么联动
量子计算·脑机接口·1024程序员节·科学哲学·容错设计·系统思维·工程方法论
传奇开心果编程2 小时前
【Jetpack Compose娓娓道来】第26课:Repository模式与数据层设计——数据的唯一入口
android·移动开发·1024程序员节·jetpack compose·编程语言理论·编程框架·声明式ui
Lucis__2 小时前
Redis持久化机制解析
redis·缓存·操作系统·文件·非关系型数据库·1024程序员节
不脱发的程序猿2 小时前
纳指ETF量化策略
数据库·人工智能·1024程序员节
科创致远2 小时前
廊坊注塑配套企业SOP工位数字化是把信息变简单
1024程序员节
文慧的科技江湖2 小时前
贰号开源零碳园区管理平台
1024程序员节