做了六年后端,MySQL用了五年,Redis、MongoDB、Elasticsearch也都在生产环境摸爬滚打过。但每次遇到复杂关联查询、地理空间计算、JSON内检索时,总要在多个数据库之间来回组合,代码层做聚合,维护成本越来越高。
去年开始把一部分业务往PostgreSQL迁移,跑了半年之后,整理出3个让我觉得"迁对了"的核心功能。
之前用MySQL存JSON,只能当字符串,查询靠JSON_EXTRACT,性能捉急且无法建索引。遇到需要从JSON字段内筛选数据的场景,要么改表结构加冗余字段,要么把逻辑搬到代码层过滤。
PostgreSQL的JSONB解决了这个问题。它采用二进制存储格式,配合GIN索引,可以直接在JSON字段内部做高效检索。
建表与索引:
-- 订单表,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);查询实践:
-- 查询购买过商品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%是已完结状态,高频查询只查"待处理"和"处理中"。
-- 部分索引:只索引未完成的订单,索引体积减少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()函数导致索引失效。
-- 按日期(忽略时间)建索引
CREATE INDEX idx_orders_date ON orders (DATE(created_at));
-- 走索引
SELECT COUNT(*) FROM orders WHERE DATE(created_at) = '2025-07-30';部分索引让大表的高频查询维持了亚毫秒级响应,同时索引总大小从8GB降到1.2GB,大幅降低了内存压力。
分布式任务调度是个经典场景。常见方案是Redis做队列 + Worker消费,但需要处理重复消费、任务持久化等问题。
PostgreSQL的SKIP LOCKED配合FOR UPDATE可以实现无锁任务抢占,结合LISTEN/NOTIFY实现实时通知,一套数据库搞定队列+存储+持久化。
任务表结构:
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):
-- 原子抢占:高并发下多个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):
-- 触发器:任务创建时发送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 删除。