Postgresql 生产问题一例, 为什么1000行数据这么慢要27秒

本文探讨了PostgreSQL中一个包含大量JSON数据的表导致查询缓慢的原因。通过对不同存储方式的对比测试,揭示了TOAST存储机制的影响,并提出了优化建议。

最近开发部门的测试提出一个问题,在我们某一个项目的postgresql V12的服务器上某个表在查询的时候1000行数据竟然跑出了 27秒的"好成绩".  我大PG 的性能这么差,这不能呀.   好请跟着我们来,走近科学, 剥丝抽茧 1000行数据,select * 竟然要27秒 ?

首先我们验证了一下,的确在PG_ADMIN查询平台上,查询很慢这张表的确要20多秒才能将1000行数据展出,但问题是他真的只有1000行.  那么我们先看看到底是为什么.

从上图看的确是如此,并且pg_admin还因为查询时过载,重新启动了服务

既然这个事情是既定的事实,那么我们先来看看这个表的表结构是什么.

这个表中包含了两个JSON 格式的数据, 问题就从这里开始,

我们通过命令来查询这张表到底有多大,  214MB , 几百行的数据已经到了214MB, 并且还是不包含索引的情况下.那么此时心里已经有底了,  request 和 result  字段必然是这个问题的关键.

我们对result 的字段进行可读的设置,证明一个字段大小在不到400KB.

根据toast中的四种的模式的介绍 plan , extended , external, main 4种模式,

Plan  的方式是阻止toast存储,并且也禁止压缩数据,也就是当你的一个column的数据大于页面的大小,将被禁止存储.

Extended 允许压缩和跨行存储,这个是每个列最常见的存储的模式,首先要压缩然后在toast存储

EXTERNAL 这个方式和上的方式的区别就是压缩,这样的存储是不会对数据进行压缩处理的,直接而这样的方式对于text和bytea存储是可以相对于上的存储方式要快速的.

Main 方式允许压缩存储但不允许使用TOAST的方式进行数据的存储,如果你的一行恰恰在压缩后可以放到一个页面中,那这样的方式是比较适合的.

create table l_request_log_p (

id varchar(50),

trackingid varchar(20),

applicationid varchar(20),

request json,

createdate timestamp,

result json);

create table l_request_log_ed (

id varchar(50),

trackingid varchar(20),

applicationid varchar(20),

request json,

createdate timestamp,

result json);

create table l_request_log_el (

id varchar(50),

trackingid varchar(20),

applicationid varchar(20),

request json,

createdate timestamp,

result json);

create table l_request_log_m (

id varchar(50),

trackingid varchar(20),

applicationid varchar(20),

request json,

createdate timestamp,

result json);

alter table l_request_log_p alter column request set storage plain;

alter table l_request_log_p alter column result set storage plain;

alter table l_request_log_ed alter column request set storage extended;

alter table l_request_log_ed alter column result set storage extended;

alter table l_request_log_el alter column request set storage EXTERNAL;

alter table l_request_log_el alter column result set storage EXTERNAL;

alter table l_request_log_m alter column request set storage main;

alter table l_request_log_m alter column result set storage main;

我们开始对每个表导入原表的数据看错误日志中是否有什么显示

第一个往plain 中插入数据的的情况就失败了, 这也就证明上面的对于plain的解释.

第二个和第四个插入的时间基本类似

第三个数据插入的时间是最短的 2秒

第四个main的方式数据插入的时间明显要高于其他插入的时间普通的插入时间是  2秒左右 1000行, 而使用main的方式插入数据需要14秒 这也就证明了四种模式中虽然main没有使用TOAST的方式,但已经极尽全力的去压缩数据.

而使用extended的方式虽然也是可以使用TOAST的方式但他也是要先去通过main的方式来压缩数据.

我们对相关的数据表进行查询, 三个数据库表同样的数据量,但是不同的数据存储方式,提取数据的时间是相同的27秒.

我们来查看某个表的toast表的信息存储

select relname,relfilenode,reltoastrelid from pg_class where relname='l_request_log_el';

toast表中的信息主要包含 chunk_id 对应的主表的OID,同时还是 chunk_seq对应这段数据与原表存储的位置,后面chunk_data 就是数据了.

这个就是我们toast表中存储的数据

通过上面的分析,在实际生产中我们再次确认TOAST 功能的强大, 在实际应用中可以存储巨量的数据,但付出的代价是提取速度的问题,但如果27秒能提取 215MB 的数据量,这样的速度也不算慢了.

所以我们已经准备修订POSTGRESQL 数据库的规范,对于某些未标明的字段我们需要将字段的STORAGE 类型修改成PLAIN,方式滥用TOAST的功能导致数据库性能降低还不自知.

https://github.com/credativ/toastinfo  另如果想详细了解TOAST 存储的可以使用上面的插件来操作,具体的操作的以及TOAST 详细的说明在

https://cloud.tencent.com/developer/article/1760474

<think>我们正在讨论在PostgreSQL中复制表结构和数据的方法。根据引用内容,我们可以总结几种方法: 方法1(来自引用[1]):使用CREATE TABLE AS SELECT语句,可以同时复制表结构和数据。 方法2(来自引用[1]和引用[3]):先使用CREATE TABLE LIKE语句(注意:PostgreSQL中实际上是使用CREATE TABLE ... (LIKE ...))来复制结构,然后使用INSERT INTO ... SELECT来复制数据。 方法3(来自引用[2]):使用pg_dump工具导出表结构,然后导入,再插入数据。但这种方法更适用于备份或迁移,在数据库内操作可能不是最直接的。 方法4(来自引用[3]):先创建新表(指定结构),然后插入数据。 但是,注意引用[1]中提到了两种方法: 1. CREATE TABLE 新表名 AS SELECT * FROM 旧表名; -- 同时复制结构和数据 2. 使用CREATE TABLE LIKE语句(在PostgreSQL中,我们使用CREATE TABLE ... (LIKE ...))然后插入数据。 另外,引用[4]提到了一种类似CREATE TABLE ... AS的方法,但它是针对SQL Server的,不过PostgreSQL也有类似的语法。 根据用户需求,我们专注于PostgreSQL。 具体到PostgreSQL,常用的复制表的方法有: 1. 使用`CREATE TABLE AS`语句: ```sql CREATE TABLE new_table AS SELECT * FROM old_table; ``` 这种方法会复制表结构和数据,但是不会复制索引、约束等。 2. 使用`CREATE TABLE ... (LIKE ...)`语句复制结构,然后插入数据: ```sql CREATE TABLE new_table (LIKE old_table INCLUDING ALL); INSERT INTO new_table SELECT * FROM old_table; ``` 这里使用`INCLUDING ALL`可以复制包括索引、约束、默认值等在内的所有结构。但是注意,`INCLUDING ALL`可能不会复制所有内容,比如外键约束可能需要额外的选项(`INCLUDING CONSTRAINTS`)或单独处理。 3. 使用`pg_dump`导出表结构,然后导入到新表,再插入数据。这种方法比较重量级,通常用于备份或迁移,不推荐在数据库内直接复制表时使用。 4. 另外,还可以使用`CREATE TABLE ... AS`配合`WITH DATA`或`WITHOUT DATA`来控制是否复制数据。 因此,我们主要推荐前两种方法。 注意:如果原表有索引、约束、触发器、默认值等,第一种方法(CREATE TABLE AS)不会复制这些,它只会复制表的基本结构和数据。而第二种方法使用`LIKE`并加上`INCLUDING ALL`可以复制大部分结构(但也不是100%所有,例如序列可能不会自动关联,需要额外处理)。 所以,如果需要完整复制表(包括索引、约束等)和数据,推荐第二种方法。 但是,如果只需要表结构和数据,而不需要索引等,第一种方法更简单。 此外,如果表很大,复制数据可能会很,可以考虑分批次插入。 下面给出两种方法的详细步骤: **方法一:使用CREATE TABLE AS(简单复制结构和数据)** 步骤: 1. 执行SQL语句: ```sql CREATE TABLE new_table AS SELECT * FROM old_table; ``` 这将创建一个新表`new_table`,其结构基于`SELECT *`的列和数据类型,并复制所有数据。 注意:这种方法不会复制索引、约束、默认值、注释等。如果原表有主键,新表不会有主键;如果有非空约束,新表中列的非空约束会被保留(因为SELECT出来的数据如果列非空,那么新表对应的列也会是非空的?实际上,在PostgreSQL中,CREATE TABLE AS会根据查询结果自动推断列的数据类型和是否允许为空,但不会继承原表的约束。例如,如果原表有NOT NULL约束,而新表在创建时,如果查询的列不包含NULL,那么新表的列可能允许为空?需要验证。实际上,CREATE TABLE AS不会复制约束,除非在查询中显式处理。所以,这种方法只适用于简单的复制。 **方法二:使用CREATE TABLE ... (LIKE ...) 和 INSERT INTO ... SELECT(复制完整结构,然后复制数据)** 步骤: 1. 创建新表,复制原表的结构(包括索引、约束等): ```sql CREATE TABLE new_table (LIKE old_table INCLUDING ALL); ``` 这里`INCLUDING ALL`选项会复制包括:默认值、约束、索引、存储参数、注释等。但是,它不会复制外键约束(因为外键约束涉及其他表,需要单独处理)。另外,序列(如果使用了自增列)不会被关联,新表会使用自己的序列(如果有的话)。 2. 复制数据: ```sql INSERT INTO new_table SELECT * FROM old_table; ``` 注意:如果原表有自增列(使用SERIAL或IDENTITY),在复制结构时,新表会创建自己的序列。在插入数据时,自增列的数据会被复制过去,但是序列的当前值不会更新,所以如果后续插入新数据,序列可能从1开始,导致冲突。因此,复制后需要更新序列的当前值。 更新序列的方法: ```sql -- 假设表有一个自增列id,对应的序列名为new_table_id_seq SELECT setval('new_table_id_seq', max(id)) FROM new_table; ``` 但是,序列的名字可能不是固定的,我们可以通过查询系统表来获取序列名。 因此,在复制有自增列的表时,需要额外注意。 另外,如果表很大,插入数据可能会很,可以考虑使用分批插入。 总结: 根据需求选择方法: - 如果只需要表结构和数据,不需要索引、约束等,使用方法一。 - 如果需要完整复制表结构(包括索引、约束等)和数据,使用方法二,并注意处理自增列序列的问题。 下面给出完整的示例(以方法二为例): 示例:复制表`employees`到`employees_copy`,包括所有结构和数据,并处理自增序列。 1. 创建新表结构: ```sql CREATE TABLE employees_copy (LIKE employees INCLUDING ALL); ``` 2. 复制数据: ```sql INSERT INTO employees_copy SELECT * FROM employees; ``` 3. 如果原表有自增列(例如`id`),那么新表会有一个新的序列,我们需要更新这个序列的当前值为新表中`id`的最大值: ```sql -- 获取新表对应的序列名(假设自增列名为id) -- 在PostgreSQL中,如果原表是使用SERIAL创建的,那么新表会创建一个新的序列,序列名通常为:新表名_列名_seq -- 我们可以通过以下查询获取序列名(注意:如果表中有个自增列,需要分别处理) SELECT pg_get_serial_sequence('employees_copy', 'id'); -- 假设返回的序列名为'employees_copy_id_seq' -- 然后更新序列 SELECT setval('employees_copy_id_seq', COALESCE((SELECT MAX(id)+1 FROM employees_copy), 1), false); ``` 注意:如果原表没有自增列,则不需要第三步。 另外,如果原表有外键约束,在复制结构时,外键约束会被创建,但是外键引用的表还是原来的表,所以通常不需要特殊处理。但是,如果外键约束在新表创建时已经存在,那么在插入数据时,如果数据违反外键约束,插入会失败。因此,确保原表的数据是符合外键约束的。 如果原表有触发器,使用`INCLUDING ALL`也会复制触发器,但是触发器的函数必须已经存在。 因此,方法二在大数情况下是可行的,但需要根据实际情况调整。 最后,我们也可以使用`CREATE TABLE ... AS`来复制,然后再单独添加索引和约束。但那样步骤更。 综上所述,对于PostgreSQL,复制表和数据的方法推荐使用`CREATE TABLE ... (LIKE ... INCLUDING ALL)`和`INSERT INTO ... SELECT`,并注意处理序列问题。 引用说明: - 引用[1]提供了两种通用的SQL方法,其中第一种`CREATE TABLE ... AS`在PostgreSQL中适用,第二种`CREATE TABLE ... LIKE`在PostgreSQL中需要写成`CREATE TABLE ... (LIKE ...)`。 - 引用[2]提到使用pg_dump,但这种方法更适合备份,不推荐在数据库内直接复制表。 - 引用[3]提供了先创建表再插入数据的方法,但创建表需要手动指定结构,而我们可以用`LIKE`来避免手动指定。 因此,我们主要参考了引用[1]和引用[3]的方法,并针对PostgreSQL做了调整。 生成相关问题: 1. 在PostgreSQL中复制表时如何排除某些列? 2. 如何只复制表结构而不复制数据? 3. 在PostgreSQL中复制大表时如何提高性能? 4. 复制表后如何更新自增序列的当前值?</think>在 PostgreSQL 中复制表结构和数据,主要有以下几种可靠方法,根据您的需求选择最合适的方案: ### 📝 方法一:使用 `CREATE TABLE AS`(同时复制结构和数据) ```sql -- 复制表结构和所有数据(不包含索引、约束) CREATE TABLE new_table AS SELECT * FROM original_table; ``` **特点**: - 最简洁的单行命令 - 复制**基本结构+数据**,但不会复制索引、约束、默认值等[^1] - 适合快速创建数据副本进行测试分析 ### 🔧 方法二:使用 `CREATE TABLE ... (LIKE)` + `INSERT`(完整复制结构) ```sql -- 1. 复制完整表结构(含索引、约束等) CREATE TABLE new_table (LIKE original_table INCLUDING ALL); -- 2. 复制数据 INSERT INTO new_table SELECT * FROM original_table; ``` **特点**: - 通过 `INCLUDING ALL` 复制所有依赖对象(索引/约束/默认值/注释等)[^1][^2] - 需要两步操作,但能完整克隆表 - 适合生产环境迁移或精确复制 ### ⚙️ 方法三:使用 `pg_dump`(大型表推荐) ```bash # 导出表结构 pg_dump -t original_table --schema-only -U user -d dbname > structure.sql # 导出数据 pg_dump -t original_table --data-only -U user -d dbname > data.sql ``` **操作流程**: 1. 执行导出的 `structure.sql` 创建新表结构 2. 执行 `data.sql` 导入数据 **优势**: - 处理超大型表时更稳定[^2] - 支持选择性导出(如仅索引或特定数据) ### ⚠️ 关键注意事项 1. **权限问题**:新表需单独授权(原表权限不继承) 2. **自增序列**:若表包含 `SERIAL` 列,需手动更新序列值: ```sql SELECT setval('new_table_id_seq', max(id)) FROM new_table; ``` 3. **外键约束**:复制后需验证外键关联完整性 4. **大表优化**:超过 1GB 的表建议分批插入: ```sql INSERT INTO new_table SELECT * FROM original_table WHERE id BETWEEN 1 AND 100000; -- 分批次操作 ``` ### 性能对比 | 方法 | 速度 | 完整性 | 操作复杂度 | 适用场景 | |--------------|------|--------|------------|------------------| | `CREATE AS` | ⚡️快 | 基础 | 简单 | 快速测试副本 | | `LIKE+INSERT`| 🟢中 | 完整 | 中等 | 生产环境精确克隆 | | `pg_dump` | 🐢 | 完整 | 复杂 | 超大型表迁移 | > 💡 **推荐选择**: > - 日常开发:优先用 **方法二**(`CREATE TABLE ... (LIKE) + INSERT`) > - 数据迁移:超过 10GB 的表用 **方法三**(`pg_dump`)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值