首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从MySQL到PostgreSQL:那些让我决定"迁库"的硬核功能

从MySQL到PostgreSQL:那些让我决定"迁库"的硬核功能

原创
作者头像
用户12658083
修改2026-07-30 11:39:00
修改2026-07-30 11:39:00
2080
举报

从MySQL到PostgreSQL:那些让我决定"迁库"的硬核功能

做了六年后端,MySQL用了五年,Redis、MongoDB、Elasticsearch也都在生产环境摸爬滚打过。但每次遇到复杂关联查询、地理空间计算、JSON内检索时,总要在多个数据库之间来回组合,代码层做聚合,维护成本越来越高。

去年开始把一部分业务往PostgreSQL迁移,跑了半年之后,整理出3个让我觉得"迁对了"的核心功能。

一、JSONB:把"文档型"能力收归关系型数据库

之前用MySQL存JSON,只能当字符串,查询靠JSON_EXTRACT,性能捉急且无法建索引。遇到需要从JSON字段内筛选数据的场景,要么改表结构加冗余字段,要么把逻辑搬到代码层过滤。

PostgreSQL的JSONB解决了这个问题。它采用二进制存储格式,配合GIN索引,可以直接在JSON字段内部做高效检索。

建表与索引:

代码语言:javascript
复制
-- 订单表,items存储商品列表(JSON数组)
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    items JSONB NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- GIN索引加速JSONB查询
CREATE INDEX idx_orders_items ON orders USING GIN (items);

查询实践:

代码语言:javascript
复制
-- 查询购买过商品ID为1001的订单
SELECT * FROM orders 
WHERE items @> '[{"product_id": 1001}]'::jsonb;

-- 查询订单总金额超过200的订单(数值提取)
SELECT * FROM orders 
WHERE (items -> 'total')::numeric > 200;

-- 提取每个订单的商品名称列表
SELECT id, jsonb_path_query_array(items, '$.items[*].name') AS product_names
FROM orders;

实际压测结果:100万条订单数据,JSONB条件查询响应时间在15-30ms,而MySQL的JSON_EXTRACT在同样数据量下基本在200ms+,差距超过10倍。

二、部分索引 + 表达式索引:用更小的索引覆盖更多的查询

MySQL的索引相对"死板",无法对索引行做条件过滤,也无法对函数结果建索引。PostgreSQL的部分索引表达式索引在大表场景下非常实用。

场景: 订单表1亿行,80%是已完结状态,高频查询只查"待处理"和"处理中"。

代码语言:javascript
复制
-- 部分索引:只索引未完成的订单,索引体积减少80%
CREATE INDEX idx_orders_pending ON orders (created_at, priority) 
WHERE status IN ('pending', 'processing');

-- 查询直接走这个小型索引
SELECT * FROM orders 
WHERE status IN ('pending', 'processing') 
ORDER BY priority DESC, created_at 
LIMIT 50;

表达式索引: 按日期范围统计,无需在SQL里套DATE()函数导致索引失效。

代码语言:javascript
复制
-- 按日期(忽略时间)建索引
CREATE INDEX idx_orders_date ON orders (DATE(created_at));

-- 走索引
SELECT COUNT(*) FROM orders WHERE DATE(created_at) = '2025-07-30';

部分索引让大表的高频查询维持了亚毫秒级响应,同时索引总大小从8GB降到1.2GB,大幅降低了内存压力。

三、SKIP LOCKED + LISTEN/NOTIFY:用SQL实现轻量级任务队列

分布式任务调度是个经典场景。常见方案是Redis做队列 + Worker消费,但需要处理重复消费、任务持久化等问题。

PostgreSQL的SKIP LOCKED配合FOR UPDATE可以实现无锁任务抢占,结合LISTEN/NOTIFY实现实时通知,一套数据库搞定队列+存储+持久化。

任务表结构:

代码语言:javascript
复制
CREATE TABLE tasks (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    payload JSONB,
    status VARCHAR(20) DEFAULT 'pending',
    scheduled_at TIMESTAMP DEFAULT NOW(),
    started_at TIMESTAMP,
    completed_at TIMESTAMP,
    retry_count INT DEFAULT 0
);

CREATE INDEX idx_tasks_pending ON tasks (scheduled_at) 
WHERE status = 'pending';

Worker抢占任务(核心SQL):

代码语言:javascript
复制
-- 原子抢占:高并发下多个Worker同时执行,每个只抢到一条
WITH picked AS (
    SELECT id FROM tasks
    WHERE status = 'pending' AND scheduled_at <= NOW()
    ORDER BY scheduled_at
    FOR UPDATE SKIP LOCKED
    LIMIT 1
)
UPDATE tasks SET 
    status = 'running', 
    started_at = NOW()
FROM picked 
WHERE tasks.id = picked.id
RETURNING tasks.*;

SKIP LOCKED的作用是:如果某行被其他Worker锁定,直接跳过,不会阻塞等待。100个Worker同时抢任务,不会发生锁竞争,吞吐量线性扩展。

实时通知(任务创建后立刻唤醒Worker):

代码语言:javascript
复制
-- 触发器:任务创建时发送NOTIFY
CREATE OR REPLACE FUNCTION notify_task_created()
RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify('task_created', NEW.id::text);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_task_created
AFTER INSERT ON tasks
FOR EACH ROW
EXECUTE FUNCTION notify_task_created();

Worker侧用LISTEN task_created监听,收到通知后立即执行抢占SQL,避免轮询带来的数据库压力。

这套方案在生产环境运行了半年,每天处理50万+任务,从未出现任务重复消费或丢失的情况。

写在最后

从MySQL迁到PostgreSQL,最核心的感受是:以前需要在应用层解决的问题,现在可以推到数据库层解决,而且数据库层做得更好、更稳定。

JSONB让文档型数据不再需要额外引入MongoDB,部分索引让大表查询维持高性能,SKIP LOCKED让任务队列不再强依赖Redis。

当然,迁移决策也要看场景。如果业务只有简单的CRUD且数据量不大,MySQL依然够用且生态更成熟。但如果你的业务开始出现复杂查询、混合数据类型、高并发写入+一致性要求,PostgreSQL值得花时间深入了解。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 从MySQL到PostgreSQL:那些让我决定"迁库"的硬核功能
    • 一、JSONB:把"文档型"能力收归关系型数据库
    • 二、部分索引 + 表达式索引:用更小的索引覆盖更多的查询
    • 三、SKIP LOCKED + LISTEN/NOTIFY:用SQL实现轻量级任务队列
    • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档