专注,勤学,慎思。戒骄戒躁,谦虚谨慎

just do it

导航

记一次PostgreSQL交叉表crosstab行转列导致的OOM

0,环境

Ubuntu 20,PostgreSQL 17.6

 

1,场景

源数据格式

id|business_id|key|val      |created_at                      |
--+-----------+---+---------+--------------------------------+
 1|          1|aaa|100.00000|2026-03-28 18:20:50.553354 +0800|
 2|          1|bbb|200.00000|2026-03-28 18:20:50.553354 +0800|
 3|          1|ccc|300.00000|2026-03-28 18:20:50.553354 +0800|

目标数据格式

business_id|aaa      |bbb      |ccc      |
-----------+---------+---------+---------+
          1|100.00000|200.00000|300.00000|

 

源表是典型的行标,每一个逻辑id对应多个类似于key-value键值对的数据行,目标是把多行key-value的转换为列显示,这里简化一下逻辑,大概如下

CREATE TABLE my_test (
    id int generated always as identity primary key,
    business_id int,
    key varchar(100),
    val decimal(18,5),
    created_at timestamptz
);

-- 生成1000万条数据,每个 business_id 有3条记录(aaa, bbb, ccc)
-- 总 business_id 数量 = 10,000,000 / 3 = 3,333,333 个
INSERT INTO my_test (business_id, key, val, created_at)
SELECT 
    seq as business_id,
    key,
    val,
    NOW() - interval '60 days' + (seq * interval '0.0005184 second') as created_at
FROM 
    generate_series(1, 3333333) AS seq
    CROSS JOIN LATERAL (
        VALUES 
            ('aaa', 100.00),
            ('bbb', 200.00),
            ('ccc', 300.00)
    ) AS k(key, val)
ORDER BY business_id, key;


--示例数据如下
id|business_id|key|val      |created_at                      |
--+-----------+---+---------+--------------------------------+
 1|          1|aaa|100.00000|2026-03-28 18:20:50.553354 +0800|
 2|          1|bbb|200.00000|2026-03-28 18:20:50.553354 +0800|
 3|          1|ccc|300.00000|2026-03-28 18:20:50.553354 +0800|
 4|          2|aaa|100.00000|2026-03-28 18:20:50.553872 +0800|
 5|          2|bbb|200.00000|2026-03-28 18:20:50.553872 +0800|
 6|          2|ccc|300.00000|2026-03-28 18:20:50.553872 +0800|
 7|          3|aaa|100.00000|2026-03-28 18:20:50.554390 +0800|
 8|          3|bbb|200.00000|2026-03-28 18:20:50.554390 +0800|
 9|          3|ccc|300.00000|2026-03-28 18:20:50.554390 +0800|
10|          4|aaa|100.00000|2026-03-28 18:20:50.554908 +0800|

 

2,crosstab行转列遭遇OOM

通过crosstab可以完美实现行列转换

CREATE EXTENSION IF NOT EXISTS tablefunc;

create index idx_business_id on my_test(business_id);

--利用crosstab行转列,完全没有问题
SELECT *
FROM crosstab
	(
	    'SELECT business_id, key, val 
	     FROM my_test  where business_id = 888888
	     ORDER BY business_id, key',
	    'VALUES (''aaa''), (''bbb''), (''ccc'')'
	) AS ct(business_id INT, aaa DECIMAL(18,5), bbb DECIMAL(18,5), ccc DECIMAL(18,5))

 
--行转列的结果如下,完全没有问题,因为这样完全可以用到索引
business_id|aaa      |bbb      |ccc      |
-----------+---------+---------+---------+
     888888|100.00000|200.00000|300.00000|
     

想尝试屏蔽crosstab的复杂语法,将上述sql语句封装成一个视图,然后通过视图查询,类似如:select * from v_my_test where business_id = 888888;
原本想着where条件可以“下推”到crosstab内部,与原始查询一样,也能用到索引,于是对crosstab的查询,去掉where条件之后,将where条件放到外面,如果可行的话就可以用视图了屏幕crosstab的复杂的语法了

--想尝试屏蔽crosstab的复杂语法,将上述sql语句封装成一个视图
create view v_my_test as
select * from
(
	SELECT *
	FROM crosstab
		(
		    'SELECT business_id, key, val 
		     FROM my_test  --where business_id = 888888
		     ORDER BY business_id, key',
		    'VALUES (''aaa''), (''bbb''), (''ccc'')'
		) AS ct(business_id INT, aaa DECIMAL(18,5), bbb DECIMAL(18,5), ccc DECIMAL(18,5))	
)t

--然后通过视图查询
select * from v_my_test where business_id = 888888

--上述通过视图的查询,等价于如下SQL,也即crosstab内部去掉where条件,将where条件放到外面,原本想着where条件可以“下推”到crosstab,也能用到索引
select * from
(
	SELECT *
	FROM crosstab
		(
		    'SELECT business_id, key, val 
		     FROM my_test  --where business_id = 888888
		     ORDER BY business_id, key',
		    'VALUES (''aaa''), (''bbb''), (''ccc'')'
		) AS ct(business_id INT, aaa DECIMAL(18,5), bbb DECIMAL(18,5), ccc DECIMAL(18,5))	
)t
where business_id = 888888

然后就遇到OOM了

2026-05-27 11:13:49.690 CST [437019] LOG:  checkpoint starting: wal
2026-05-27 11:17:10.218 CST [437017] LOG:  server process (PID 1308788) was terminated by signal 9: Killed
2026-05-27 11:17:10.218 CST [437017] DETAIL:  Failed process was running: select * from
select * from
(
	SELECT *
	FROM crosstab
		(
		    'SELECT business_id, key, val 
		     FROM my_test  --where business_id = 888888
		     ORDER BY business_id, key',
		    'VALUES (''aaa''), (''bbb''), (''ccc'')'
		) AS ct(business_id INT, aaa DECIMAL(18,5), bbb DECIMAL(18,5), ccc DECIMAL(18,5))	
)t
where business_id = 888888
	
2026-05-27 11:17:10.219 CST [437017] LOG:  terminating any other active server processes
2026-05-27 11:17:10.247 CST [1311860] FATAL:  the database system is in recovery mode
2026-05-27 11:17:10.293 CST [1311869] FATAL:  the database system is in recovery mode
2026-05-27 11:17:10.409 CST [437017] LOG:  all server processes terminated; reinitializing
2026-05-27 11:17:10.582 CST [1311898] LOG:  database system was interrupted; last known up at 2026-05-27 11:09:27 CST
2026-05-27 11:17:10.600 CST [1311901] FATAL:  the database system is in recovery mode
2026-05-27 11:17:10.645 CST [1311902] FATAL:  the database system is in recovery mode
2026-05-27 11:17:10.688 CST [1311898] LOG:  database system was not properly shut down; automatic recovery in progress
2026-05-27 11:17:10.692 CST [1311903] FATAL:  the database system is in recovery mode
2026-05-27 11:17:10.716 CST [1311898] LOG:  redo starts at 0/658B1820
2026-05-27 11:17:10.782 CST [1311904] FATAL:  the database system is not yet accepting connections
2026-05-27 11:17:10.782 CST [1311904] DETAIL:  Consistent recovery state has not been yet reached.
2026-05-27 11:17:20.716 CST [1311898] LOG:  redo in progress, elapsed time: 10.00 s, current LSN: 0/931FA9C0
2026-05-27 11:17:21.552 CST [1311898] LOG:  invalid record length at 0/98B0E848: expected at least 24, got 0
2026-05-27 11:17:21.555 CST [1311898] LOG:  redo done at 0/98B0E810 system usage: CPU: user: 5.93 s, system: 1.30 s, elapsed: 10.85 s
2026-05-27 11:17:21.591 CST [1311899] LOG:  checkpoint starting: end-of-recovery immediate wait
2026-05-27 11:17:22.591 CST [1311899] LOG:  checkpoint complete: wrote 50482 buffers (80.2%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.941 s, sync=0.044 s, total=1.003 s; sync files=8, longest=0.044 s, average=0.006 s; distance=838004 kB, estimate=838004 kB; lsn=0/98B0E848, redo lsn=0/98B0E848
2026-05-27 11:17:22.619 CST [437017] LOG:  database system is ready to accept connections

 

3,解决方案

1,废弃视图的想法,直接使用sql语句查询,虽然增加了语法的复杂程度,但至少避免了OOM

2,PostgreSQL的systemctl service文件中,限制单个回话最大使用内存,防止单个查询失控,参考:PostgreSQL 被 OOM 杀库?最佳实践有了!
  # 限制单个进程虚拟内存为 2GB(单位:字节,也可直接写 2G)
  LimitAS=2147483648

此时如果SQL语句受限制之后,session执行失败,会出现如下错误,而不至于导致整个PostgreSQL实例OOM

SQL Error [53200]: ERROR: out of memory
  Detail: Failed on request of size 67108864 in memory context "SPI TupTable".
  Where: SQL statement "****"

 

posted on 2026-05-27 19:43  MSSQL123  阅读(51)  评论(0)    收藏  举报