先入为主的PostgreSQL“递归性能问题”优化

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
简介: 收到运维告警,数据库磁盘容量100%,一段时间后又降了下去,使用该数据库的服务是因为人员变动后流转到我手里维护的,当时听说过因为PostgreSQL问题进行了对应优化,优化前的表现也是类似将数据库临时表空间占满,所以我也想着从这方面入手排查,后续排查确实也验证了我的猜想。

起因

收到运维告警,数据库磁盘容量100%,一段时间后又降了下去,使用该数据库的服务是因为人员变动后流转到我手里维护的,当时听说过因为PostgreSQL问题进行了对应优化,优化前的表现也是类似将数据库临时表空间占满,所以我也想着从这方面入手排查,后续排查确实也验证了我的猜想。

问题梳理

该系统是用户关系的服务,用户与用户之间的粉丝关系形成一个树结构,使用PostgreSQL的递归查询返回用户的粉丝关系,查询语句也限制了最多只查询3层粉丝,避免深度问题压爆栈空间。

通过查询系统日志,发现某用户多次查询自己的粉丝列表,都超时了,PostgreSQL默认没有做限制,单个递归查询粉丝总数的语句执行了半小时将磁盘写满后抛出异常,异常抛出后也将临时文件回收了,这也符合运维描述。这就很尴尬,幸好平时该数据库压力较小,如果是其他重要的库就要引起大问题,因为该库只有一个服务在使用,报错的查询都是由同一个用户查询引起的,当即决定先配置语句执行超时时间为10秒,避免扩大影响范围。

日志的异常信息:

ERROR: could not write to file "base/pgsql_tmp/pgsql_tmp2466359.17": No space left on device;


下面通过报错语句与表结构进行分析,表结构设计很简洁明了,关键字段只有user_id和parent_id,这里删减了多余字段

CREATETABLE user_relation ( id bigserial NOTNULL, user_id int8NULL,--用户id parent_id int8NULL,--上级用户id create_time timestamp(0)NULL, update_time timestamp(0)NULL, CONSTRAINT hsrj_user_relation_pkey PRIMARY KEY (id));CREATE INDEX idx_user_relation_parent_id ON public.user_relation USING btree (parent_id);CREATE INDEX idx_user_relation_create_time ON public.user_relation USING btree (create_time);CREATE INDEX idx_user_relation_user_id ON public.user_relation USING btree (user_id);


通过WITH语句进行递归

-- 递归字段WITH RECURSIVE T (user_id, parent_id,deep)-- 递归入口AS(SELECT user_id, parent_id,1as deep FROM user_relation WHERE channel_user_id = #{userId}-- UNION后面的语句就是递归语句,T为上次递归的数据-- 就是通过上次查询到数据的用户id作为当次的上级用户id查询UNION ALL SELECT d.user_id, d.parent_id,T.deep+1as deep FROM user_relation D 
JOIN T ON D.parent_id= T.channel_user_id)-- 递归完成后,筛选想要的数据,这里只统计数据总数SELECTcount(1)FROM T where channel_user_id != #{userId}and is_delete =0and deep<4

通过上面的语句可以看出,出现问题的语句限制了deep小于4,deep从1开始,每次递归加1,也就是3层,看完语句,好像没什么问题(不是),先看看前人做了什么优化吧。

先前优化

之前我是听说过该服务,使用PostgreSQL递归调用的时候也出现过一样的问题,之前的同事从业务和架构设计上进行了优化:

  • 第一是从业务上优化,因为我们服务用户是分成两种角色的,可以简单理解成高级和普通会员,升为高级会员的时候会将下属所有粉丝都打上自己的标签,所以可以通过标签反查粉丝数据的,可以不用递归。
  • 第二不查PostgreSQL,我们的用户数据都会写到ES上,所以这部分高级用户就查ES,剩下的普通会员因为大部分因为粉丝数量比较少无法升级为高级会员,所以性能上也不会有什么问题(大问题)

问题分析

陷入惯性

因为之前的优化,所以高级会员是不会出问题的,只是个ES简单查询,问题就是,异常的用户拥有有1000+粉丝,且是普通用户,为什么没有升级为高级会员,我们咨询了业务,说是有其他限制,和我们这次优化无关,可以忽略。

  • 方案一:那按之前的优化,粉丝只有高级用户的标签的,普通用户没办法做到不递归查询,那我就想到,既然是小部分用户,我们将这部分用户记录下来,缓存他们的粉丝数据,定时更新缓存就行了,那可不美哉,转头又想到不行了,那查询一次不就将临时表压满了吗,这不尬住了吗。
  • 方案二:过了几分钟,立刻又想到优化方案,既然是递归的问题,那我自己循环查询就好了,最多就循环3次,每次查询几毫秒最多十几毫秒,怎么也不会超过1秒,果真大聪明如我啊,新建一个表,将粉丝数超过1000的写到表里,查询的时候这部分用户循环查询,避免影响其他可以递归查询的用户,还想好了顾全大局的方案,公司没我真的不行了(bushi)。

醒悟

想好方案美美地我就下班了,下班路上隐隐觉得有什么不妥,既然分开执行性能是没问题的,为什么递归性能这么拉胯,这明显不符合逻辑啊!还有个致命问题,为什么查询单个用户的1000+个粉丝会压满300GB的数据库磁盘空间,递归3次怎么压栈也不可能压爆,隔天上班仔细看了几遍SQL,问题其实很简单,下面是我修改后的SQL:

-- 递归字段WITH RECURSIVE T (user_id, parent_id,deep)-- 递归入口AS(SELECT user_id, parent_id,1as deep FROM user_relation WHERE channel_user_id = #{userId}-- UNION后面的语句就是递归语句,T为上次递归的数据-- 就是通过上次查询到数据的用户id作为当次的上级用户id查询UNION ALL SELECT d.user_id, d.parent_id,T.deep+1as deep FROM user_relation D 
JOIN T ON D.parent_id= T.channel_user_id)and D.is_delete=0and deep <3-- 递归完成后,筛选想要的数据,这里只统计数据总数SELECTcount(1)FROM T where channel_user_id != #{userId}

变更的部分是and D.is_delete = 0 and deep < 3,原来的SQL是递归完成后筛选统计,会递归完全部粉丝,前面一直提到的递归3次根本就没起效果,会一直递归到查询不到数据为止,如果该用户在树的靠上层,公司亿级的用户数据绝对可以压爆数据库。

复盘

其实问题很简单,多加留心一下就能发现,因为排查的时候知道之前同事已经处理过类似问题了,该同事职级比我高,当时立项开发也有其他更高级的领导跟进过,我相信大佬们一定是都研究过相关方案的了,所以一直没有仔细去看SQL,看了看能走索引就完事了。

处理问题一定要从根源排查,数据说话,互相印证,不能人云亦云,要有独立的思考分析能力,这是这个BUG带来的思考,期望也能带给看到这篇文章的你们一点启发。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
1月前
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
197 9
|
2月前
|
SQL 关系型数据库 MySQL
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
MySQL慢查询优化、索引优化,是必知必备,大厂面试高频,本文深入详解,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验分享。
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
|
15天前
|
SQL 关系型数据库 MySQL
MySQL派生表合并优化的原理和实现
通过本文的详细介绍,希望能帮助您理解和实现MySQL中派生表合并优化,提高数据库查询性能。
55 16
|
16天前
|
SQL 关系型数据库 MySQL
MySQL派生表合并优化的原理和实现
通过本文的详细介绍,希望能帮助您理解和实现MySQL中派生表合并优化,提高数据库查询性能。
33 7
|
3天前
|
缓存 关系型数据库 MySQL
【深入了解MySQL】优化查询性能与数据库设计的深度总结
本文详细介绍了MySQL查询优化和数据库设计技巧,涵盖基础优化、高级技巧及性能监控。
36 0
|
1月前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
81 18
|
1月前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
80 7
|
1月前
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
104 5
|
2月前
|
SQL 关系型数据库 MySQL
MySQL慢查询优化、索引优化、以及表等优化详解
本文详细介绍了MySQL优化方案,包括索引优化、SQL慢查询优化和数据库表优化,帮助提升数据库性能。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
MySQL慢查询优化、索引优化、以及表等优化详解
|
2月前
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
82 2