去年这个时候,我决定深入学习PostgreSQL。
那时候我们项目准备从MySQL迁移到PostgreSQL,领导让我先研究一下。我心想,不就是个数据库吗,SQL都差不多,能有多难。于是我信心满满地开始了PostgreSQL的学习之旅。
没想到,这一学就是一年多。从入门到熟悉,从踩坑到填坑,经历了太多让人想放弃的瞬间。PostgreSQL确实很强大,但它的强大是有代价的——它太复杂了,复杂到有时候你会怀疑人生。
这篇文章,我想分享一下这一年多来学习和使用PostgreSQL 17的真实经历。从安装配置、性能调优到各种奇葩问题,聊聊PostgreSQL的强大和那些让人想放弃的瞬间。
如果你也在学习PostgreSQL,或者准备从其他数据库迁移过来,希望我的经历能给你一些参考,让你少踩一些坑。
先说明一下,我主要用的是PostgreSQL 17,也试过16和18的一些新特性。不同版本的行为可能会有差异,我的经验仅供参考。
入门:看起来很简单
刚开始学PostgreSQL的时候,我觉得很简单。
安装很简单。Windows上下载安装包,下一步下一步就装好了。Linux上用包管理器,一条命令就装好了。Docker更简单,拉个镜像就能跑。
基本操作也很简单。创建数据库、创建表、增删改查,SQL语法和MySQL差不多,只是有一些细节差异。比如,PostgreSQL用SERIAL或者GENERATED ALWAYS AS IDENTITY来做自增主键,用RETURNING来返回插入或更新的数据,这些稍微看一下文档就会了。
我当时心想,PostgreSQL也不过如此嘛,和MySQL差不多,还多了一些好用的功能。比如,支持JSON类型,支持全文搜索,支持CTE和窗口函数,这些在MySQL里要么没有,要么很难用。
那时候我甚至觉得,我们项目迁移到PostgreSQL是分分钟的事情。
但很快,我就被打脸了。
第一个坑:字符集和排序规则
第一个坑,是字符集和排序规则。
我们的项目是中文的,数据库需要存储中文。在MySQL里,默认用utf8mb4字符集,排序规则用utf8mb4unicodeci,中文排序基本没问题。
但在PostgreSQL里,情况复杂多了。
PostgreSQL的字符集和排序规则,是在创建数据库的时候指定的,而且创建之后就不能改了。如果你创建数据库的时候选错了字符集或者排序规则,那对不起,只能重新创建数据库,把数据导出来再导进去。
而且,PostgreSQL的排序规则依赖操作系统的locale。不同的操作系统,locale的名称和行为可能不一样。比如,在Linux上,中文排序用zhCN.UTF-8;在Windows上,可能要用ChinesePRCCIAS或者类似的名称。
我当时在开发环境(Windows)上创建数据库的时候,用了默认的排序规则。结果中文排序完全不对,"张三"排在"李四"前面还是后面,完全没有规律。查了半天文档,才发现是排序规则的问题。
然后我重新创建数据库,指定了正确的排序规则。但这时候,我已经写了一些测试数据,只能导出来再导进去,折腾了好几个小时。
这还没完。到了生产环境(Linux),我发现Linux上的locale名称和Windows不一样,排序的行为也有细微的差异。为了保证开发环境和生产环境的一致性,我又花了不少时间研究怎么在不同操作系统上配置相同的排序规则。
后来我才知道,PostgreSQL 15之后引入了ICU排序规则,可以不依赖操作系统的locale,在不同平台上行为一致。但我们的项目用的是操作系统的locale,迁移起来又要折腾。
这个坑,让我第一次意识到,PostgreSQL的"简单"只是表面的。它的很多功能,都有很多细节和坑,稍不注意就会踩进去。
第二个坑:数据类型的"惊喜"
PostgreSQL的数据类型非常丰富,比MySQL多得多。这是它的优势,但也是坑的来源。
坑2.1:时间类型
PostgreSQL的时间类型,有DATE、TIME、TIMESTAMP、TIMESTAMPTZ、INTERVAL等。其中最容易搞混的是TIMESTAMP和TIMESTAMPTZ。
TIMESTAMP是不带时区的时间戳,TIMESTAMPTZ是带时区的时间戳。听起来很简单,但实际用起来,坑很多。
比如,你在数据库里存了一个TIMESTAMPTZ类型的值,它在内部存储的是UTC时间。但你查询的时候,它会根据数据库的时区设置,转换成当地时间显示。如果你的数据库时区设置不对,查出来的时间就会差几个小时。
我当时就踩了这个坑。我们的数据库时区默认是UTC,应用服务器时区是Asia/Shanghai。存进去的时间,查出来差了8个小时。查了半天,才发现是时区的问题。
后来我学乖了,所有时间类型都用TIMESTAMPTZ,数据库时区统一设为UTC,应用层处理时区转换。这样虽然麻烦一点,但至少不会出错。
坑2.2:数组类型
PostgreSQL支持数组类型,这是一个很强大的功能。你可以在一个字段里存一个整数数组、字符串数组,甚至是自定义类型的数组。
但数组类型用起来,也有很多坑。比如,数组的索引是从1开始的,不是从0开始的。如果你习惯了编程语言里从0开始的数组,在这里就会踩坑。
再比如,数组的操作符和函数很多,&&(重叠)、@>(包含)、<@(被包含)、||(拼接),还有unnest、arrayagg、arrayto_string等函数。刚开始用的时候,经常搞混,查半天文档。
还有,数组类型的索引,需要用GIN索引,不是普通的B-tree索引。如果你不知道这一点,给数组字段建了普通索引,查询的时候根本用不上,性能会很差。
我当时用数组类型存了一个标签字段,查询的时候想找包含某个标签的记录。结果写了个WHERE tags @> ARRAY['tag1'],查询很慢。后来才知道,需要给tags字段建GIN索引。建了索引之后,查询速度快了几百倍。
坑2.3:JSON类型
PostgreSQL的JSON类型,分为JSON和JSONB两种。JSON是存原始文本,JSONB是存解析后的二进制格式。
JSONB功能更强大,支持索引,查询效率更高,是推荐使用的类型。但JSONB也有坑,比如它会去掉键的顺序,会去掉重复的键,会把数字存成numeric类型,可能会有精度问题。
我当时用JSONB存了一些配置数据,里面有一些大整数。结果存进去之后,大整数变成了科学计数法,精度丢了。查了半天,才发现JSONB的数字类型是numeric,有精度限制。后来只能把大整数转成字符串存。
还有,JSONB的查询语法,刚开始用的时候很不适应。->是取JSON对象字段,返回JSON类型;->>是取JSON对象字段,返回文本类型。这两个操作符长得很像,经常用错,导致查询结果不对或者性能很差。
这些数据类型的"惊喜",让我花了很多时间去学习和适应。虽然现在用得比较熟练了,但偶尔还是会踩坑。
第三个坑:索引和性能调优
PostgreSQL的性能调优,是一个很深的话题。我在这上面踩的坑,比其他所有坑加起来都多。
坑3.1:索引不生效
最常见的坑,就是索引不生效。
你给一个字段建了索引,结果查询的时候,PostgreSQL就是不用索引,全表扫描,性能很差。这时候你会很困惑:为什么不用索引?
原因可能有很多:
第一,数据量太小。如果表只有几百条记录,全表扫描可能比走索引还快,PostgreSQL就会选择全表扫描。这是正常的,不用管。
第二,选择性太差。如果你的查询条件,匹配了表中大部分的记录,PostgreSQL也会选择全表扫描。因为走索引需要先读索引,再回表读数据,反而比直接全表扫描慢。
第三,类型不匹配。比如,你给一个varchar字段建了索引,但查询的时候用了整数去比较,PostgreSQL会做隐式类型转换,索引就用不上了。这个坑很隐蔽,很多人都踩过。
第四,函数包装。比如,你给createdat字段建了索引,但查询的时候写了WHERE DATE(createdat) = '2025-01-01',用函数包装了字段,索引就用不上了。这时候需要建表达式索引,或者改写查询条件。
第五,统计信息过时。PostgreSQL是基于统计信息来选择执行计划的。如果统计信息过时了,它可能会做出错误的判断,选择全表扫描而不是走索引。这时候需要运行ANALYZE来更新统计信息。
我当时就遇到过类型不匹配的问题。我们的用户表,phone字段是varchar类型,建了索引。但查询的时候,ORM框架传了一个整数参数,PostgreSQL做了隐式类型转换,索引用不上,查询很慢。查了很久,用EXPLAIN看执行计划,才发现是类型不匹配的问题。
坑3.2:VACUUM和表膨胀
PostgreSQL的MVCC(多版本并发控制)实现,和MySQL很不一样。
在PostgreSQL中,更新和删除操作,不会直接覆盖旧数据,而是标记旧数据为无效,然后插入新数据。这些无效的数据,被称为"死元组"。死元组需要通过VACUUM操作来清理,否则表会越来越大,性能会越来越差。
PostgreSQL有自动VACUUM机制,默认是开启的。但自动VACUUM的参数默认值比较保守,对于写入频繁的表,可能清理不及时,导致表膨胀。
我当时就遇到了这个问题。我们有一张日志表,写入很频繁,每天新增几百万条记录。运行了几个月之后,这张表变得越来越大,查询越来越慢。查了一下,发现死元组占了大部分空间,自动VACUUM根本清理不过来。
后来,我调整了自动VACUUM的参数,让它更积极地清理。同时,对这张表做了分区,按时间分区,老的分区直接删除,不用VACUUM。这样才解决了表膨胀的问题。
还有一个相关的坑,就是VACUUM FULL。VACUUM FULL会把表重新整理一遍,回收空间。但它会锁表,而且需要额外的磁盘空间。如果你在生产环境直接运行VACUUM FULL,可能会导致业务停摆。我当时差点就犯了这个错误,幸好被同事拦住了。
坑3.3:连接数和连接池
PostgreSQL的连接,是进程模型。每个连接,都会创建一个独立的进程。这意味着,PostgreSQL的连接开销比较大,连接数不能太多。
默认情况下,PostgreSQL的最大连接数是100。对于小型应用,这可能够了。但对于大型应用,100个连接可能不够用。
但如果你把最大连接数调得太高,比如几千个,又会出问题。因为每个连接都要占用一定的内存,连接太多会导致内存不足,系统变慢甚至崩溃。
所以,PostgreSQL通常需要配合连接池来使用,比如PgBouncer或者Pgpool-II。连接池可以复用连接,减少连接的创建和销毁开销,同时控制最大连接数。
我们项目刚开始的时候,没有用连接池,应用直接连数据库。结果高峰期的时候,连接数飙升,数据库响应变慢,甚至出现连接被拒绝的情况。后来加了PgBouncer连接池,问题才解决。
这个坑,让我意识到,PostgreSQL的运维,比MySQL复杂得多。很多在MySQL里不用太关心的事情,在PostgreSQL里都需要认真对待。
第四个坑:复制和高可用
PostgreSQL的复制和高可用,功能很强大,但配置也很复杂。
PostgreSQL支持物理复制和逻辑复制。物理复制是把整个实例的数据复制到从库,逻辑复制是按表或者按订阅来复制。
我们项目需要做读写分离,所以配置了一主一从的物理复制。配置的过程,踩了不少坑。
比如,pghba.conf的配置。这个文件控制客户端的认证方式,配置错了,从库就连不上主库。我当时配置了半天,从库一直报认证失败,查了很久才发现是pghba.conf里的IP地址写错了。
再比如,复制槽(replication slot)。复制槽可以防止主库在从库断开的时候删除WAL日志,保证从库重新连接的时候能追上。但复制槽如果不及时清理,也会导致WAL日志堆积,磁盘空间被占满。我当时就遇到过从库挂了,复制槽没清理,WAL日志把磁盘占满的情况。
还有,高可用的切换。主库挂了,怎么把从库提升为主库?怎么让应用自动切换到新主库?这些都需要额外的工具来做,比如Patroni、Repmgr等。这些工具的配置和使用,又是一个大坑。
我们当时用了Patroni来做高可用。配置的过程,看了很多文档,踩了很多坑,花了好几天才搭好。虽然现在运行得比较稳定了,但回想起来,那个过程真的让人想放弃。
相比之下,MySQL的主从复制和高可用,虽然也不简单,但生态更成熟,资料更多,遇到问题更容易找到解决方案。PostgreSQL在这方面,相对来说要小众一些,资料少一些,踩坑了可能要自己摸索。
第五个坑:从MySQL迁移的痛苦
我们项目从MySQL迁移到PostgreSQL,这个过程,是最让人想放弃的。
虽然SQL是标准的,但MySQL和PostgreSQL之间,有很多差异。这些差异,导致迁移的过程非常痛苦。
差异一:数据类型的差异
MySQL和PostgreSQL的数据类型,有很多不一样的地方。
比如,MySQL的TINYINT(1),在PostgreSQL里通常用BOOLEAN。MySQL的DATETIME,在PostgreSQL里对应TIMESTAMP。MySQL的LONGTEXT,在PostgreSQL里用TEXT。MySQL的JSON,在PostgreSQL里推荐用JSONB。
这些类型的映射,看起来简单,但实际迁移的时候,会遇到很多细节问题。比如,MySQL的DATETIME精度到秒,PostgreSQL的TIMESTAMP精度到微秒,迁移的时候要注意精度。MySQL的JSON是文本,PostgreSQL的JSONB是二进制,迁移的时候要注意格式。
差异二:SQL语法的差异
SQL语法的差异,是迁移中最繁琐的部分。
比如,分页查询。MySQL用LIMIT offset, count,PostgreSQL用LIMIT count OFFSET offset。虽然都支持,但写法不一样。
比如,字符串拼接。MySQL用CONCAT()函数或者||操作符(取决于sql_mode),PostgreSQL用||操作符或者CONCAT()函数。看起来差不多,但行为有差异。比如,MySQL的CONCAT会忽略NULL,PostgreSQL的||遇到NULL会返回NULL。
比如,自增主键。MySQL用AUTO_INCREMENT,PostgreSQL用SERIAL或者GENERATED ALWAYS AS IDENTITY。迁移的时候,要注意自增序列的当前值,否则插入的时候会报主键冲突。
比如,GROUP BY的行为。MySQL在某些sqlmode下,GROUP BY可以选择非聚合的非分组列,PostgreSQL不允许,必须用聚合函数或者ANYVALUE(PostgreSQL没有ANY_VALUE,需要用其他方式)。
这些语法差异,导致我们的很多SQL语句都要改写。项目大了,SQL语句成千上万,一条条改,改到崩溃。
差异三:ORM的兼容性
我们项目用了ORM框架。虽然ORM号称屏蔽数据库差异,但实际上,MySQL和PostgreSQL的差异,ORM并不能完全屏蔽。
比如,ORM生成的SQL,可能用了MySQL特有的语法,在PostgreSQL上跑不通。比如,ORM的分页、批量插入、upsert等操作,在不同数据库上的实现不一样,可能会有性能差异或者行为差异。
我们当时用的ORM,对PostgreSQL的支持还可以,但还是有一些地方不兼容。比如,JSON字段的查询,ORM生成的SQL在PostgreSQL上性能很差,需要手写SQL。比如,某些类型的映射,ORM处理得不对,需要自定义类型转换器。
这些ORM的兼容性问题,又花了我们很多时间去调试和修复。
差异四:性能行为的差异
迁移到PostgreSQL之后,我们发现,有些查询的性能行为和MySQL很不一样。
有些在MySQL上很快的查询,在PostgreSQL上很慢。有些在MySQL上很慢的查询,在PostgreSQL上反而很快。
这是因为,两个数据库的查询优化器、索引实现、存储引擎都不一样,执行计划的选择也不一样。同样的SQL,在两个数据库上的性能表现可能完全不同。
我们当时有一个查询,在MySQL上只要几十毫秒,迁移到PostgreSQL之后,变成了几十秒。查了半天,发现是PostgreSQL的执行计划选错了,走了全表扫描。更新了统计信息,调整了查询条件,才解决问题。
还有一个查询,在MySQL上需要建一个联合索引,在PostgreSQL上只需要建一个单列索引就够了,因为PostgreSQL的索引可以组合使用。
这些性能行为的差异,需要我们重新做性能调优,重新建索引,重新优化SQL。这个过程,又花了很多时间。
迁移的过程,真的非常痛苦。好几次,我都想放弃,跟领导说"我们还是用回MySQL吧"。但最终,我们还是坚持下来了,成功完成了迁移。
那些让人想放弃的瞬间
回顾这一年多的经历,有好几个瞬间,我真的想放弃PostgreSQL。
第一个瞬间,是刚接触的时候,被各种复杂的概念和配置搞晕。字符集、排序规则、数据类型、索引类型、MVCC、VACUUM、复制、高可用,每一个都是一个大坑,学都学不完。
第二个瞬间,是生产环境出问题的时候。有一次,数据库突然变得很慢,查询都超时了。我查了半天,发现是自动VACUUM在跑,占用了大量IO。但又不能停,因为停了死元组会越来越多。那种无力感,真的让人想放弃。
第三个瞬间,是迁移MySQL的时候。成千上万的SQL语句要改,各种兼容性问题要处理,性能行为不一样要重新调优。那段时间,我每天加班到很晚,改SQL改到想吐。
第四个瞬间,是遇到问题查不到资料的时候。PostgreSQL虽然很流行,但相比MySQL,中文资料还是少一些。有些比较偏的问题,搜遍了中文社区都找不到答案,只能去看英文文档,或者去Stack Overflow上提问。那种孤独感,真的让人想放弃。
但每次想放弃的时候,我又会想起PostgreSQL的好。它的功能真的很强大,JSONB、全文搜索、CTE、窗口函数、分区表、并行查询,这些功能用起来真的很爽。它的SQL标准兼容性很好,写复杂查询的时候很舒服。它的开源社区很活跃,版本更新很快,新功能层出不穷。
而且,当你解决了一个困扰很久的问题,那种成就感也是无与伦比的。
所以,虽然有很多想放弃的瞬间,但我最终还是坚持下来了。现在,我对PostgreSQL已经比较熟悉了,大部分问题都能自己解决。回头看,这一年多的学习和踩坑,虽然痛苦,但也让我成长了很多。
给初学者的建议
如果你也在学习PostgreSQL,或者准备从其他数据库迁移过来,我有一些建议,希望能帮你少踩一些坑。
第一,不要轻视PostgreSQL。它虽然也是关系型数据库,SQL也差不多,但它的复杂度比MySQL高很多。不要以为会MySQL就会PostgreSQL,很多东西需要重新学习。
第二,认真读官方文档。PostgreSQL的官方文档,写得非常好,非常详细。遇到问题,先查官方文档,大部分问题都能在文档里找到答案。不要只看二手的博客和教程,那些可能过时或者不准确。
第三,从小处开始。不要一上来就搞复杂的架构。先在本地装一个PostgreSQL,写写SQL,熟悉基本操作。然后再慢慢学习高级功能,比如索引、性能调优、复制、高可用等。
第四,做好测试。在生产环境使用之前,一定要在测试环境充分测试。特别是从其他数据库迁移过来的,要测试功能是否正常,性能是否达标,边界情况是否处理正确。
第五,善用工具。PostgreSQL有很多好用的工具,比如psql命令行工具、pgAdmin图形界面、EXPLAIN分析执行计划、pgstatstatements监控慢查询等。学会使用这些工具,能大大提高你的效率。
第六,加入社区。遇到问题,可以去PostgreSQL的中文社区、邮件列表、Stack Overflow等地方提问。社区里有很多热心的大佬,会帮你解决问题。同时,也可以分享自己的经验,帮助别人。
第七,要有耐心。学习PostgreSQL是一个长期的过程,不可能一蹴而就。遇到问题和挫折是正常的,不要因为一时的困难就放弃。坚持下去,你会发现PostgreSQL越来越好用。
写在最后
PostgreSQL 17+从入门到放弃,我经历了很多。
从最开始的"不过如此",到后来的"怎么这么难",再到现在的"确实很强大",我的心态经历了好几次变化。
PostgreSQL不是一个简单的数据库。它功能强大,但也复杂;它性能优秀,但也需要精心调优;它标准兼容,但也有自己的特色。它不是那种拿来就能用、不用怎么管的数据库,它需要你花时间去学习、去理解、去调优。
但如果你愿意花时间去掌握它,它会给你丰厚的回报。它的强大功能,会让你在处理复杂业务的时候游刃有余;它的优秀性能,会让你的应用跑得又快又稳;它的活跃社区,会让你不断学到新东西。
所以,虽然我经历了很多想放弃的瞬间,但我从来没有真正后悔过选择PostgreSQL。它是一个值得深入学习和使用的数据库。
如果你也在学习PostgreSQL,不要被暂时的困难吓倒。坚持下去,你会发现,那些曾经让你想放弃的坑,最终都会变成你宝贵的经验。
最后,用一句话来结束这篇文章:"PostgreSQL不是银弹,但它是一把锋利的瑞士军刀。只要你愿意花时间去掌握它,它能帮你解决很多复杂的问题。"
愿你在PostgreSQL的学习道路上,少踩坑,多成长。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录