在Ruby生态里操作PostgreSQL,pg这个gem几乎是绕不开的选择。它是官方维护的Ruby接口,底层直接封装了C语言编写的libpq库,性能比纯Ruby实现的方案高出一大截。无论你是写数据迁移脚本、做后端服务,还是简单地处理一些数据清洗任务,掌握pg的基本操作都能让工作效率提升不少。这篇文章会从安装开始,把连接数据库、执行SQL、处理结果、防注入和事务控制这些高频用法逐一讲透。

安装与环境准备
安装pg gem之前,系统里必须先有PostgreSQL的客户端库。也就是说,libpq和相关头文件要提前装好,否则gem install pg会直接编译失败。不同系统的安装方式不一样,Linux下可以通过包管理器解决,比如Ubuntu执行apt-get install libpq-dev,CentOS则是yum install postgresql-devel。macOS用Homebrew装了PostgreSQL后,一般自带了开发文件。
装好依赖后,在项目目录执行gem install pg即可。如果使用Bundler管理依赖,在Gemfile中加入gem 'pg'然后运行bundle install。有个常见的坑:系统里同时存在多个版本的PostgreSQL时,gem编译时可能链接到错误的版本,这时可以通过--with-pg-config参数指定pg_config的路径,例如gem install pg -- --with-pg-config=/usr/local/pgsql/bin/pg_config。安装成功后,执行require 'pg'不报错就说明环境就绪了。
建立连接与执行基础查询
pg库的核心入口是PG::Connection类。最简单的连接方式是直接传入连接字符串,格式类似URL,也可以用哈希传参。两种方式各有优劣:连接字符串紧凑,适合写脚本;哈希参数可读性强,方便集中管理配置。下面这段代码演示了基本的连接和查询流程:
require 'pg'
# 方式一:使用连接字符串
conn = PG.connect("host=localhost dbname=testdb user=postgres password=secret")
# 方式二:使用哈希参数
conn = PG.connect(
host: 'localhost',
port: 5432,
dbname: 'testdb',
user: 'postgres',
password: 'secret'
)
# 执行查询并遍历结果
result = conn.exec("SELECT id, name, email FROM users LIMIT 10")
result.each do |row|
puts "#{row['id']} - #{row['name']} (#{row['email']})"
end
conn.close注意查询结果的每一行是一个Hash,字段名是字符串键,值也都是字符串类型(除非用类型映射)。PostgreSQL的整型、时间等类型在默认情况下会被转成字符串返回,如果需要原生Ruby类型,可以使用result.type_map = PG::TextDecoder::CopyRow.new这类类型映射工具,或者在取出后自行转换。此外,连接用完记得关闭,也可以配合块使用,PG.connect(...)传入块时连接会在块结束时自动关闭,这是更Ruby化的写法。
参数化查询:exec_params才是正确姿势
直接用exec拼接SQL是危险的做法,只要字符串里混入了用户输入,SQL注入的风险就来了。正确的方式是使用exec_params,用$1、$2这样的占位符代替直接拼值,参数单独以数组传入。这样数据库驱动会处理好转义,安全且省心:
require 'pg'
conn = PG.connect(dbname: 'testdb', user: 'postgres')
# 错误示范:直接拼接,存在注入风险
# name = "张三'; DROP TABLE users; --"
# conn.exec("SELECT * FROM users WHERE name = '#{name}'")
# 正确做法:使用占位符参数化查询
name = "张三"
result = conn.exec_params(
"SELECT * FROM users WHERE name = $1 AND age > $2",
[name, 18]
)
result.each { |row| puts row.inspect }
# 插入数据同样使用占位符
conn.exec_params(
"INSERT INTO users (name, age) VALUES ($1, $2)",
["李四", 25]
)exec和exec_params还有一个容易被忽视的区别:前者把整个字符串当成单条SQL发给服务端执行,后者使用扩展查询协议,参数和服务端预编译绑定。这意味着exec_params一条调用里不能塞多条用分号分隔的SQL语句。日常开发中,只要涉及任何外部输入,一律使用exec_params,把exec留给执行固定的DDL语句这类场景,比如建表、建索引。
增删改与事务处理
执行INSERT、UPDATE、DELETE时,可以通过结果的cmd_tuples方法拿到受影响的行数,这对于判断操作是否生效很实用。比如更新用户状态后,如果返回0行,说明没有匹配的记录,程序就可以据此做相应处理。另外,exec_params配合RETURNING子句还能直接拿回新插入行的数据,省去再查一次的麻烦:
conn = PG.connect(dbname: 'testdb', user: 'postgres')
# 更新并检查影响行数
result = conn.exec_params(
"UPDATE users SET status = $1 WHERE id = $2",
['active', 42]
)
puts "更新了 #{result.cmd_tuples} 行"
# 插入并返回新记录
row = conn.exec_params(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
["王五", 30]
).first
puts "新用户ID:#{row['id']}"涉及多条写操作时,事务是保证数据一致性的关键手段。pg库提供了块形式的事务接口,块内所有SQL要么全部成功,要么在抛异常时整体回滚。看下面的转账例子:
begin
conn.transaction do |tx|
tx.exec_params("UPDATE accounts SET balance = balance - 100 WHERE id = $1", [1])
tx.exec_params("UPDATE accounts SET balance = balance + 100 WHERE id = $1", [2])
end
puts "转账成功"
rescue PG::Error => e
puts "事务回滚:#{e.message}"
end把transaction包在begin rescue结构里是个好习惯,任何一条SQL失败都会触发回滚,并且异常会向上抛出,由外层统一处理。事务块内部的连接对象既可以用块参数tx,也可以继续用外层的conn,两者指向同一个连接,效果一样。
常见问题与排查技巧
连接失败是新手最常撞上的问题,报错信息通常是PG::ConnectionBad。排查思路基本就三板斧:先确认PostgreSQL服务在跑(pg_isready或systemctl status postgresql),再检查pg_hba.conf里的认证配置是否允许你的IP和认证方式,最后核对密码和端口。如果遇到role不存在或者database不存在的错误,多半是连接参数写错了,默认连接的库名会和系统用户名相同,记得显式指定dbname。
编码问题也偶尔出现,比如中文数据存进去变问号。pg默认按UTF-8处理,只要客户端编码和服务端编码一致一般不会有事。可以通过conn.set_client_encoding('UTF8')显式设置。还有一种情况是大结果集占内存,exec会把所有行一次性加载到内存,处理千万级数据时建议改用send_query配合get_result的异步接口,或者直接上游标分批读取,每批取几千行处理完再取下一批,内存占用会平稳很多。
最后提一句,如果你在做正式的Web项目,一般不会直接裸用pg,而是通过Sequel、ActiveRecord这类ORM间接使用,它们底层同样依赖pg gem。理解了本文这些原生操作,再去读ORM生成的SQL、排查慢查询,会顺畅得多。写脚本和处理数据管道时,直接用pg反而更轻量直接。
PostgreSQLRuby pg gem数据库连接修改时间:2026-09-08 19:59:12