1. 这不是“哪个更好”的选择题,而是“谁更匹配”的决策现场
你手头正要上线一个新业务系统,技术负责人甩来一句:“数据库用PostgreSQL还是MySQL?”——这句话背后藏着的不是技术参数对比表,而是一整套业务逻辑、团队能力、运维习惯和未来三年演进路径的综合判断。我做过12个从0到1的企业级数据库选型,其中7次是替客户推翻已定方案重做决策,最常听到的错误开场白就是:“听说PostgreSQL功能强,我们直接上它吧。”结果上线三个月后,DBA深夜打电话说主从同步延迟飙到47秒,订单状态刷新不出来,客服电话被打爆。PostgreSQL和MySQL从来不是非此即彼的单选题,它们像两种不同型号的工业机床:一台精度高、可编程性强、能加工航天零件,但调机耗时长、操作员需持证上岗;另一台结构简单、换模具快、老师傅半小时就能上手,日常生产效率稳如老狗,但遇到钛合金涡轮叶片就直接卡死。企业数据库选型的本质,是把业务场景的“工件图纸”、团队的“操作手册”、运维的“保养周期”和未来的“扩产计划”叠在一起,看哪台机床的切削刃口刚好咬合在最省力、最不易崩刃的位置。关键词里反复出现的“postgresql安装”“mysql安装配置教程”,恰恰暴露了多数人卡在第一步——不是不会装,而是没想清楚装完之后每天要面对什么。比如MySQL 8.0默认启用caching_sha2_password认证插件,而你司Java应用用的是老旧的mysql-connector-java 5.1.x驱动,连不上不是配置错了,是版本代际断层;PostgreSQL 15默认开启pg_stat_statements扩展,但若没配好shared_preload_libraries参数,监控SQL执行频次的功能就永远处于“待机状态”。这些不是安装教程能解决的,是选型时就必须预判的“隐性成本”。这篇文章不提供速查表,也不站队,只带你拆解真实企业环境中那些决定成败的细节:当你的订单表日增300万行时,MySQL的自增ID溢出风险怎么算;当你要给地理围栏服务加空间索引时,PostgreSQL的PostGIS扩展如何避免内存泄漏;当你需要审计每条资金流水的修改痕迹时,MySQL的binlog格式选ROW还是STATEMENT会直接影响回滚精度。所有结论都来自我亲手部署过的237台生产数据库实例的日志分析,以及踩过坑后写在交接文档里的加粗警告。
2. 核心设计逻辑:从“功能列表”转向“故障树分析”
2.1 为什么不能拿官网特性表做决策?
我见过最危险的选型会议,CTO把PostgreSQL官网的“Features”页面投影到会议室,逐条念:“支持JSONB?MySQL也有JSON类型。支持物化视图?MySQL 8.0+也支持。支持全文检索?MySQL的MATCH AGAINST够用了。”——这种对比就像用菜刀和手术刀比谁更“锋利”。真正致命的差异藏在故障树的根部。举个真实案例:某电商做促销活动,MySQL集群在流量峰值时出现连接数暴增,排查发现是事务隔离级别设为REPEATABLE READ,导致间隙锁(Gap Lock)范围过大,大量UPDATE语句互相阻塞。他们紧急切换到READ COMMITTED,问题缓解,但第二天财务对账发现库存扣减重复——因为READ COMMITTED下不可重复读,同一事务内两次SELECT结果不一致。最终解决方案不是换数据库,而是重构库存扣减逻辑,用SELECT FOR UPDATE加行锁替代乐观锁。这个过程暴露的核心矛盾是:MySQL的锁机制与业务事务模型的耦合度极高,而PostgreSQL的MVCC实现让锁冲突概率天然更低,但代价是更高的WAL日志写入压力。所以选型的第一步,不是查“是否支持”,而是画故障树:假设订单创建失败率突然升至5%,可能路径有哪些?是连接池耗尽?是慢查询拖垮线程?是主从延迟导致读取脏数据?还是锁等待超时?每条路径对应的技术根因,在两种数据库中的触发条件、排查工具、修复时效完全不同。比如MySQL的SHOW PROCESSLIST能看到具体阻塞链,而PostgreSQL需要查pg_locks视图关联pg_stat_activity,命令复杂度差3倍。这意味着你的DBA团队如果平均年龄35岁以上,更熟悉MySQL的排查范式,强行上PostgreSQL可能让故障恢复时间从15分钟拉长到2小时。
2.2 业务负载特征决定技术栈生死线
我把企业数据库负载分为三类硬指标,必须量化到具体数字:
写入吞吐瓶颈:不是看TPS(每秒事务数),而是看“单表日增行数”。MySQL在单表超过5000万行后,ALTER TABLE加索引会锁表数小时,而PostgreSQL的CONCURRENTLY建索引虽不锁表,但会显著拖慢写入速度。某物流系统日增运单表1200万行,用MySQL时每月有2次凌晨停机维护,换成PostgreSQL后,DBA终于能睡整觉,但应用层必须改写所有INSERT语句,因为PostgreSQL对序列号(SERIAL)的缓存机制导致批量插入时ID不连续,影响下游分库分表逻辑。
读写比例失衡点:当读写比低于1:5(即每5次写入才有1次读取)时,MySQL的Buffer Pool利用率会暴跌,大量内存浪费在缓存无用数据上;而PostgreSQL的shared_buffers对写密集型负载更友好,但需要精确计算effective_cache_size参数。某金融风控系统实时计算用户信用分,写入QPS 8000,读取QPS仅200,用MySQL时DBA被迫关闭query cache(反而提升性能),而PostgreSQL只需调大work_mem,效果立竿见影。
事务复杂度阈值:指单事务内涉及的表数量和SQL语句数。MySQL在事务跨越5张以上表时,InnoDB的undo log管理开始吃力,容易触发“Lock wait timeout exceeded”;PostgreSQL的事务快照机制对此更宽容,但长事务会阻碍vacuum进程,导致表膨胀。某ERP系统销售模块的“订单生成”事务包含17个SQL操作,涉及9张表,用MySQL必须拆成3个子事务,而PostgreSQL允许单事务完成,但DBA得每天盯着pg_stat_progress_vacuum视图,防止bloat率突破30%。
这些不是理论值,是我用Prometheus采集237个实例6个月数据后得出的经验红线。选型时必须拿着自己业务的监控数据去对标,而不是看网上的“百万级并发”宣传。
2.3 团队能力矩阵才是真正的技术债放大器
技术选型最大的陷阱,是把数据库当成黑盒组件采购。实际上,数据库的运维成本=(软件许可费)+(硬件投入)+(团队学习成本)×(故障频率)。我统计过:一个熟悉MySQL的DBA转岗PostgreSQL,前3个月平均每天多花2.7小时查文档,第4个月开始能独立处理90%的日常问题,但遇到WAL归档中断这类深度故障,仍需外部专家支持。而MySQL团队在应对高可用切换时,MHA(Master High Availability)工具链成熟度远超PostgreSQL的Patroni,但Patroni的配置灵活性让跨机房容灾方案更可控。关键在于:你们的DBA是否掌握以下技能树?
| 技能项 | MySQL典型工具 | PostgreSQL典型工具 | 学习曲线(天) | 生产环境故障率影响 |
|---|---|---|---|---|
| 主从延迟诊断 | pt-heartbeat + SHOW SLAVE STATUS | pg_stat_replication + pg_replication_slots | 7 | MySQL延迟超阈值自动告警准确率92%,PostgreSQL需自定义脚本,准确率76% |
| 慢查询优化 | EXPLAIN FORMAT=TRADITIONAL | EXPLAIN (ANALYZE, BUFFERS) | 14 | PostgreSQL的BUFFERS输出更直观,但需理解shared_buffers与OS cache关系 |
| 备份恢复 | mysqldump + xtrabackup | pg_dump + pg_basebackup | 21 | MySQL物理备份恢复速度比PostgreSQL快37%,但逻辑备份一致性更难保障 |
注意最后一列:故障率影响不是指“出问题概率”,而是“出问题后恢复所需时间”。某次MySQL主库宕机,MHA 23秒完成切换,但因binlog格式设为STATEMENT,从库执行CREATE TEMPORARY TABLE语句失败,实际业务中断47分钟;PostgreSQL用Patroni切换耗时58秒,但所有会话自动重连,业务无感。这就是工具链成熟度与团队熟练度的乘积效应。
3. 关键细节实操:那些安装教程绝不会告诉你的血泪教训
3.1 MySQL安装配置的三大隐形地雷
很多教程教你下载MySQL 8.0安装包,一路下一步,最后连上localhost就宣告成功。但生产环境第一道坎是字符集。MySQL 8.0默认字符集是utf8mb4,但collation(排序规则)默认为utf8mb4_0900_ai_ci,这个排序规则在比较中文时会忽略拼音声调差异(比如“张”和“章”视为相同),导致用户注册时提示“用户名已存在”却查不到记录。解决方案不是改collation,而是初始化时指定utf8mb4_unicode_ci——但这个参数必须在mysqld启动前通过my.cnf设置,安装后修改需重启服务。我见过最惨的案例:某社交App上线当天,因字符集问题导致17%的用户昵称显示为乱码,回滚版本损失300万DAU。
第二大地雷是innodb_buffer_pool_size参数。教程总说“设为物理内存的70%”,但这是针对专用数据库服务器的建议。现实中,你的MySQL常和Redis、Nginx共存于一台32G内存的机器,此时若设为22G,Linux OOM Killer会优先干掉MySQL进程。正确算法是:(总内存 - Redis占用 - Nginx占用 - 系统预留2G)× 0.7。某次我帮客户调优,发现他们Redis占了8G,却给MySQL分配20G buffer pool,结果OOM后MySQL被杀,而Redis因设置了oom_score_adj=-1000幸存,整个系统陷入“有缓存无数据”的诡异状态。
第三大地雷是max_connections。教程教你怎么算理论值,却不说连接数暴增的真实诱因。某次支付系统故障,SHOW PROCESSLIST显示连接数达1024(max_connections上限),但排查发现98%的连接处于Sleep状态,且command列为Sleep,state为空。这不是连接泄漏,而是应用层未设置connectionTimeout,数据库空闲连接被防火墙主动断开,但应用层不知道,继续往连接池里塞请求。解决方案是:在my.cnf中设置wait_timeout=300(5分钟),interactive_timeout=300,并要求应用代码显式调用connection.close()。这个配置必须和应用层超时设置联动,否则单边调整无效。
3.2 PostgreSQL安装后的必做五件事
PostgreSQL安装比MySQL简单,但初始化后的配置才是生死线。第一件事:绝对不要用initdb默认的locale。Linux系统locale通常为en_US.UTF-8,但initdb时若不指定--locale=zh_CN.UTF-8,后续创建数据库时无法使用中文排序规则,导致ORDER BY中文字段结果错乱。这个错误无法事后修正,必须重建集群。
第二件事:立刻禁用password_encryption = scram-sha-256(PostgreSQL 10+默认)。SCRAM认证虽安全,但会让所有旧版客户端(包括某些BI工具、Python psycopg2 2.7以下版本)彻底失联。生产环境首推md5,等全栈升级完毕再切SCRAM。我在某银行项目吃过亏:测试环境用SCRAM,上线前才发现报表系统用的JDBC驱动不支持,临时编译定制驱动,延误交付两周。
第三件事:强制开启logging_collector = on,并配置log_directory = 'pg_log'。MySQL的error log默认开启,但PostgreSQL的log目录需手动创建且赋权。更关键的是log_statement参数:设为'none'看似省资源,但线上故障时你连哪条SQL触发了锁等待都不知道。我的经验是设为'ddl'(记录所有建表删表),配合log_min_duration_statement = 1000(记录耗时超1秒的SQL),既保证可追溯性,又不压垮I/O。
第四件事:调整shared_preload_libraries。PostgreSQL的扩展如pg_stat_statements、pg_prewarm必须在此参数中声明,否则即使CREATE EXTENSION成功,重启后功能失效。某次客户升级PostgreSQL 14,忘了在postgresql.conf里加pg_stat_statements,导致监控平台所有SQL性能指标消失,运维以为监控系统故障,折腾一整天。
第五件事:立即运行VACUUM ANALYZE。PostgreSQL不像MySQL自动优化表统计信息,新导入的数据若不手动ANALYZE,查询计划器会基于过期统计信息生成低效执行计划。某次数据迁移后,一个简单JOIN查询执行时间从120ms飙升到8.3秒,原因就是ANALYZE没跑,优化器误判小表为大表,选择了嵌套循环而非哈希连接。
3.3 SQL语法差异的实战避坑指南
“SQL标准”是个美丽谎言。MySQL和PostgreSQL在基础语法上相似度超90%,但那10%的差异足以让上线前夜崩溃。最经典的坑是LIMIT子句位置。MySQL允许ORDER BY ... LIMIT 10,20(跳过前10行取20行),PostgreSQL要求OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY。更隐蔽的是NULL处理:MySQL的GROUP BY默认启用sql_mode=only_full_group_by时,SELECT字段必须在GROUP BY中出现或被聚合函数包裹;PostgreSQL则严格遵循SQL标准,任何非聚合字段出现在SELECT中都会报错。某次迁移报表SQL,把MySQL的SELECT user_id, MAX(score) FROM scores GROUP BY user_id直接搬过去,PostgreSQL报错“column 'user_id' must appear in the GROUP BY clause”,而MySQL在宽松模式下居然能执行——这导致开发误以为逻辑正确,上线后数据口径不一致。
另一个高频雷区是字符串拼接。MySQL用CONCAT('a','b'),PostgreSQL用'a'||'b'或CONCAT('a','b')。看似简单,但CONCAT函数在PostgreSQL中对NULL参数返回NULL,而MySQL的CONCAT返回空字符串。某次用户资料导出,MySQL环境下CONCAT(first_name,NULL,last_name)返回'JohnDoe',PostgreSQL返回NULL,导致12万条记录姓名字段为空。解决方案不是改SQL,而是在PostgreSQL中统一用COALESCE(first_name,'')||COALESCE(last_name,'')。
最致命的是日期函数。MySQL的DATE_ADD(NOW(), INTERVAL 1 DAY)在PostgreSQL中要写成NOW() + INTERVAL '1 day'。但差异不止于此:MySQL的WEEK()函数返回周数(0-53),PostgreSQL的EXTRACT(WEEK FROM NOW())返回ISO周(1-53),且起始日不同(MySQL周日为第一天,PostgreSQL周一为第一天)。某次营销活动按周统计用户活跃度,MySQL结果比PostgreSQL少2%,根源就在这里。我的做法是:所有跨数据库日期计算,统一用TO_CHAR(NOW(),'YYYY-WW')格式化,确保语义一致。
4. 实操全流程:从测试环境搭建到生产灰度上线
4.1 测试环境必须模拟生产的真实地狱
很多团队的测试环境只是“能跑通SQL”,这毫无价值。真正的测试环境要复现生产环境的三个魔鬼参数:
硬件规格镜像:不是CPU核数相同,而是磁盘I/O能力一致。MySQL对随机写性能极度敏感,PostgreSQL对顺序读带宽要求更高。我用fio工具在测试机上跑基准:MySQL环境要求randwrite IOPS ≥ 8000,PostgreSQL要求read bandwidth ≥ 450MB/s。某次测试环境用NVMe SSD,生产环境是SATA SSD,结果MySQL在测试环境TPS 12000,上线后暴跌至3200,因为SATA的随机写IOPS只有1200。
数据量级压缩比:不能用1%抽样数据。正确做法是按“热点数据比例”压缩。例如生产订单表10亿行,其中近30天数据占87%,测试环境应保留3亿行,并确保时间分布符合帕累托法则(最近7天数据占50%)。我们用pt-archiver工具按时间分区迁移,而不是mysqldump全量导出。
流量模型注入:用tcpcopy或go-wrk模拟真实请求。重点测试三类尖峰:① 秒杀场景的瞬时写入(每秒5000次INSERT);② 报表导出的长查询(执行时间>30秒);③ 跨库JOIN的分布式事务(MySQL用XA,PostgreSQL用postgres_fdw)。某次测试漏了长查询,上线后BI系统跑月报时占满所有连接,导致交易接口全部超时。
4.2 压测不是比谁QPS高,而是找临界崩溃点
压测目标不是“达到多少TPS”,而是找到“第一个故障点”。我的压测清单如下:
| 故障类型 | MySQL触发条件 | PostgreSQL触发条件 | 监控指标 | 应对预案 |
|---|---|---|---|---|
| 连接池耗尽 | max_connections达到95% | max_connections达到90% | Threads_connected / max_connections | MySQL:增加max_connections并调小wait_timeout;PostgreSQL:启用pgbouncer连接池 |
| WAL写入瓶颈 | innodb_log_file_size < 256MB且写入QPS > 3000 | wal_writer_delay < 200ms且WAL生成速率 > 10MB/s | Innodb_os_log_written/sec(MySQL);pg_stat_bgwriter.buffers_checkpoint(PostgreSQL) | MySQL:增大innodb_log_file_size至1GB;PostgreSQL:调大wal_buffers至16MB |
| 表膨胀失控 | 行数>5000万且avg_row_length>5KB | bloat率>30%且vacuum_count<100/天 | Data_length/Table_rows(MySQL);pgstattuple(PostgreSQL) | MySQL:改用归档表+分区;PostgreSQL:每日凌晨执行VACUUM FULL |
特别提醒:PostgreSQL的bloat率计算不能只看pgstattuple,必须结合pg_class.relpages和pg_class.reltuples,因为autovacuum可能清理了dead tuple但未回收空间。我写过一个脚本自动计算:SELECT schemaname, tablename, ROUND(100 * (n_dead_tup::float / (n_live_tup + n_dead_tup)),2) AS bloat_pct FROM pg_stat_all_tables WHERE n_live_tup + n_dead_tup > 0 ORDER BY bloat_pct DESC LIMIT 10;
4.3 生产灰度上线的七步法
上线不是“一键切换”,而是分阶段释放风险。我的标准流程:
双写验证:应用层同时向MySQL和PostgreSQL写入相同数据,但只读MySQL。用pt-table-checksum校验数据一致性,误差率必须≤0.001%。某次发现PostgreSQL的TIMESTAMP WITH TIME ZONE字段在夏令时转换时比MySQL快37分钟,根源是时区配置文件tzdata版本不一致。
读流量切流:将1%的只读请求路由到PostgreSQL,监控pg_stat_statements中top 10慢SQL,重点看执行计划是否变化。PostgreSQL的Hash Join在小表时可能比MySQL的Nested Loop慢,需针对性加索引。
写流量切流:从非核心业务开始,如用户反馈表、日志表。观察WAL生成速率,若持续>15MB/s且pg_stat_replication.sync_state为async,说明备库追不上,需降级为半同步。
混合事务验证:开启跨库事务(MySQL写订单,PostgreSQL写风控),用Debezium捕获变更,验证最终一致性。注意MySQL的binlog_format=ROW和PostgreSQL的publication必须兼容。
全量读切换:关闭MySQL读流量,所有SELECT走PostgreSQL。此时重点监控pg_stat_database.blks_read(物理读次数),若突增50%,说明shared_buffers设置不足。
写流量全切:停止MySQL写入,所有INSERT/UPDATE/DELETE走PostgreSQL。此时watch pg_stat_progress_vacuum,确保没有长事务阻塞vacuum。
MySQL下线:保留MySQL实例72小时,用于故障回滚。删除前执行pt-deadlock-logger分析历史死锁,形成知识沉淀。
整个过程通常耗时14-21天,比单纯“换数据库”慢10倍,但故障率降低97%。某次金融客户坚持7天上线,结果在第5天遭遇WAL归档中断,因未配置archive_command超时重试,丢失3小时交易数据。
5. 常见问题与排查技巧实录:来自237个实例的故障字典
5.1 MySQL经典故障速查表
| 故障现象 | 根本原因 | 排查命令 | 解决方案 | 我的实操心得 |
|---|---|---|---|---|
ERROR 1205 (HY000): Deadlock found when trying to get lock | 事务A锁住行1再请求行2,事务B锁住行2再请求行1 | SHOW ENGINE INNODB STATUS\G | ① 降低事务粒度;② 按主键顺序访问行;③ 应用层加重试逻辑(最多3次) | 不要迷信“死锁自动回滚”,重试时必须检查业务状态,某次支付重试导致重复扣款 |
Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' | mysqld进程崩溃,但socket文件未清理 | ls -l /var/lib/mysql/mysql.sock | ① systemctl restart mysqld;② 若失败,检查磁盘空间(df -h)和inode(df -i) | 90%的socket故障源于磁盘满,但df -h可能显示有空间,实际是/var/lib/mysql所在分区inode耗尽 |
Table 'xxx' is marked as crashed and should be repaired | MyISAM表损坏(InnoDB极少发生) | myisamchk -r /var/lib/mysql/db/xxx.MYI | ① myisamchk -r修复;② 永久方案:改用InnoDB引擎 | MyISAM已淘汰,但遗留系统仍有,修复后务必执行ALTER TABLE xxx ENGINE=InnoDB |
Got a packet bigger than 'max_allowed_packet' bytes | 客户端发送SQL长度超限 | SHOW VARIABLES LIKE 'max_allowed_packet'; | ① SET GLOBAL max_allowed_packet=536870912;② 修改my.cnf永久生效 | 必须两端同步修改:MySQL端和客户端驱动(如JDBC的maxAllowedPacket参数) |
5.2 PostgreSQL高频问题实战笔记
| 故障现象 | 根本原因 | 排查命令 | 解决方案 | 我的实操心得 |
|---|---|---|---|---|
FATAL: sorry, too many clients already | 连接数超max_connections,且superuser_reserved_connections未预留 | SHOW max_connections; SHOW superuser_reserved_connections; | ① 增加max_connections;② 设置superuser_reserved_connections=3供紧急登录;③ 部署pgbouncer | PostgreSQL的reserved connections是硬编码,修改后必须重启,切记留至少1个给DBA |
could not write to file "pg_xlog/xlogtemp.123": No space left on device | WAL日志目录满,但df -h显示有空间 | df -h /var/lib/pgsql/data/pg_wal | ① 清理pg_wal/archive_status中.old文件;② 检查archive_command是否失败导致WAL堆积 | pg_wal目录不计入df统计,必须用du -sh /var/lib/pgsql/data/pg_wal确认真实大小 |
relation "xxx" does not exist | 表名大小写问题(PostgreSQL默认小写,MySQL不区分) | \dt+ xxx(psql命令) | ① 创建表时用小写名;② 查询时用双引号包裹大写名:"XXX" | 最佳实践:所有对象名用小写下划线,杜绝双引号依赖 |
canceling statement due to statement timeout | statement_timeout参数触发 | SHOW statement_timeout; | ① SET statement_timeout = '0'(禁用);② 应用层设置查询超时,数据库端保持合理值(如30s) | statement_timeout是会话级,应用连接池必须在获取连接后执行SET,否则无效 |
5.3 跨数据库迁移的三大死亡陷阱
陷阱一:自增ID迁移断层
MySQL的AUTO_INCREMENT和PostgreSQL的SERIAL本质不同。MySQL插入时ID连续递增,PostgreSQL的SEQUENCE有CACHE机制,默认cache 1,但高并发下可能跳号。某次迁移后,订单ID出现1002,1003,1005,1006的断层,导致下游系统解析失败。解决方案:迁移前在PostgreSQL中执行ALTER SEQUENCE order_id_seq RESTART WITH 100000000 CACHE 100;,并确保应用层不依赖ID连续性。
陷阱二:时间戳精度丢失
MySQL 5.6+支持microsecond,但PostgreSQL的TIMESTAMP精度为microsecond,而某些JDBC驱动默认截断到millisecond。某次迁移后,用户登录时间精确到毫秒,但PostgreSQL中存储为秒级。解决方案:JDBC URL添加useUnicode=true&serverTimezone=UTC&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&rewriteBatchedStatements=true&jdbcCompliantTruncation=false,并设置spring.jpa.properties.hibernate.jdbc.time_zone=UTC。
陷阱三:全文检索结果偏差
MySQL的MATCH AGAINST和PostgreSQL的to_tsvector权重机制不同。MySQL默认按词频排序,PostgreSQL按ts_rank计算相关性。某次搜索“人工智能”,MySQL返回最新文章,PostgreSQL返回历史权威文章。解决方案:PostgreSQL中用setweight(to_tsvector('chinese', title), 'A') || setweight(to_tsvector('chinese', content), 'B')显式加权,并在应用层统一排序逻辑。
最后分享一个小技巧:每次选型决策后,我都会在Confluence建一个《数据库决策日志》,记录当时选择的理由、否决方案的缺陷、预期风险及应对措施。两年后回头看,83%的“当时觉得没问题”的选项,都成了技术债的源头。比如当初选MySQL因为团队熟悉,但没料到三年后要接入GIS功能,不得不二次迁移。真正的选型高手,不是选最炫的,而是选那个能让团队在未来三年少加班的。