关于 PolarDB PostgreSQL 版
PolarDB PostgreSQL 版(简称 PolarDB-PG)是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容 Oracle 语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos 全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。
简介
Prepared Statement(预备语句)是 PostgreSQL 数据库中执行查询或更新操作的方法,支持将 SQL 语句和参数分别传递给数据库。它的使用分为两个阶段:PREPARE
命令创建一个 Prepared Statement,只需要调用一次;EXECUTE
命令执行 Prepared Statement,可以反复调用,且每次可以带不同的参数。
使用方法
下面在 psql 和 JDBC 客户端中使用具体的 SQL 示例来介绍如何使用 Prepared Statement。
首先使用 psql 客户端连接到 PostgreSQL 数据库,准备一张含有 id 和 name 这两个列的 users 表,并向表中插入三行数据。
postgres=# create table users (id int, name text);
CREATE TABLE
postgres=# insert into users values(1, 'Kevin');
INSERT 0 1
postgres=# insert into users values(2, 'Russell');
INSERT 0 1
postgres=# insert into users values(3, 'Stephen');
INSERT 0 1复制
PREPARE 阶段:如下是带有一个参数的 Prepared Statement,我们将其命名为 get_user_by_id,它的语义是将传入的参数值与 users 表中 id 列的值进行比较,查找出满足条件的数据。其中 2、$3 来表示,以此类推。
postgres=# prepare get_user_by_id as select * from users where id = $1;
PREPARE复制
EXECUTE 阶段:使用 execute get_user_by_id(<id>)
的方式执行上一步创建的 Prepared Statement,其中
postgres=# execute get_user_by_id(1);
id | name
----+-------
1 | Kevin
(1 row)
postgres=# execute get_user_by_id(2);
id | name
----+---------
2 | Russell
(1 row)
postgres=# execute get_user_by_id(3);
id | name
----+---------
3 | Stephen
(1 row)复制
DEALLOCATE 命令:使用deallocate get_user_by_id
命令可以删除前面创建的 Prepared Statement。
如果不显式执行 deallocate 操作,Prepared Statement 会在当前数据库会话退出时自动释放。
postgres=# deallocate get_user_by_id;
DEALLOCATE复制
在具体的业务场景中,通常会更多地使用 JDBC 来连接 PostgreSQL 数据库并使用 Prepared Statement,代码示例如下。
PREPARE 阶段:用指定的 SQL 语句新建一个 PreparedStatement 对象,其中 '?' 为参数占位符
设置参数值:通过 PreparedStatement 对象的 set 方法将占位符 '?' 替换为具体的参数值
EXECUTE 阶段:通过 PreparedStatement 对象的 execute/executeQuery/executeUpdate 等方法来执行查询或更新操作
DEALLOCATE 操作:执行 PreparedStatement 对象的 close 方法,就实现了 deallocate 操作
// 连接到数据库
Connection conn = DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "username", "password");
// 准备SQL语句
String sql = "SELECT * FROM users WHERE id = ?";
// 创建预处理语句对象
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数值
int userId = 1;
pstmt.setInt(1, userId);
// 执行查询
ResultSet rs = pstmt.executeQuery();
// 处理结果集
while (rs.next()) {
// do something with the results
}
// 自动DEALLOCATE
pstmt.close();复制
Prepared Statement的优势
与直接执行简单查询(Simple Query)相比,Prepared Statement 的主要优势如下。
性能优势:
节省解析器开销:Prepared Statement 在
PREPARE
阶段将 SQL 语句预编译一次,后续多次执行EXECUTE
的过程中无需再次进入语法解析器相关逻辑,节省了 SQL 解析成本,对于比较复杂的 SQL 尤为显著。节省优化器开销:多次
EXECUTE
过程可以复用 Plan Cache 中缓存的执行计划,节省了优化器生成执行计划的开销,EXECUTE
的次数越多,收益就越明显。节省 Plan Cache 内存占用:如果有大量的连接执行相同的 Prepared Statement,那么不同连接会共享 GPC(Global Plan Cache)中缓存的执行计划,GPC 中需要缓存的执行计划数量就更少。与之相比,如果直接执行 SQL 查询,只要 SQL 中的查询条件稍有变化,就无法共享执行计划,从而增加了 GPC 需要缓存的执行计划数量。连接数越多,连接之间的 Prepared Statement 越相似,收益就越明显(PolarDB-PG 的 1.1.28 和 2.0.14.13.0 之后的版本支持 GPC)。
其他优势:
简化代码:使用 Prepared Statement 可以让代码更加简洁易读,因为预编译的 SQL 语句可以分离出来,不需要写在代码中,提高应用程序的可维护性。
支持多种数据类型:Prepared Statement 支持包括字符串、数字、日期等多种数据类型,可以更加灵活地处理数据。
防止 SQL 注入攻击:使用 Prepared Statement 可以避免 SQL 注入攻击,因为预编译的参数会被当作参数传递,而不是 SQL 语句的一部分。
Prepared Statement使用误区
前面介绍了 Prepared Statement 的基本用法和它的各项优势,然而 Prepared Statement 并非随意使用就能发挥出优势,如果使用不当,可能无法获得预期的性能收益。
从 PolarDB for PostgreSQL 的客户对 Prepared Statement 的使用情况来看,最常见的问题如下:
完全不使用 Prepared Statement,即所有的查询都是简单查询。这样每一次执行查询时都需要进行 SQL 解析,如果 SQL 比较复杂,则有较大的解析代价,解析阶段可能消耗较多内存。
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM users WHERE id = 1");复制
使用了 Prepared Statement,但是没有实现参数化,即没有使用占位符 '?' 来表示可变的参数。比如下面的例子,两次查询时 id 列上的值不同,但是没有使用占位符来代替具体的数字,PostgreSQL 内核会认为两个 SQL 不相同,第二次执行 SQL 时无法复用第一次执行 SQL 生成的语法树和执行计划。
// 第一次使用,id = 1
String sql = "SELECT * FROM users WHERE id = 1";
PreparedStatement pstmt = conn.prepareStatement(sql);
// 第二次使用,id = 2
String sql = "SELECT * FROM users WHERE id = 2";
PreparedStatement pstmt = conn.prepareStatement(sql);复制
使用了参数化的 Prepared Statement,但是参数化不完全。下面的例子相比上面的例子已经有了进步,id 这个列上使用了占位符来表示参数,但是 name 这个列上的不同值没有使用占位符将其定义为可变参数,与上一条同理,这两次执行也会被 PostgreSQL 内核认定为两个不同的 SQL,导致无法复用语法树和执行计划。
// 第一次使用,name = 'Kevin'
String sql = "SELECT * FROM users WHERE id = ? AND name = 'Kevin'";
PreparedStatement pstmt = conn.prepareStatement(sql);
// 第二次使用,id = 2
String sql = "SELECT * FROM users WHERE id = ? AND name = 'Russell'";
PreparedStatement pstmt = conn.prepareStatement(sql);复制
以上例子中的正确 Prepared Statement 用法如下,在 id 和 name 列上都定义参数,每次执行之前通过 set 方法设置参数值,然后再执行查询。
String sql = "SELECT * FROM users WHERE id = ? AND name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数值
pstmt.setInt(1, 1);
pstmt.setString(2, "Kevin");
// 执行查询
ResultSet rs = pstmt.executeQuery();
// 再次设置参数值
pstmt.setInt(1, 2);
pstmt.setString(2, "Russell");
// 再次执行查询
ResultSet rs = pstmt.executeQuery();
// ……复制
Prepared Statement的监控
前文提到了 Prepared Statement 的常见使用误区,那么有没有一种简单快捷的办法确认我们对 Prepared Statement 的使用是否合理呢?PolarDB-PG 从 2.0.14.13.0 版本开始支持polar_feature_utils.polar_advisor_utils_stats_view
视图,通过该视图能够快速发现 Prepared Statement 相关的不恰当使用点,并能够辅助客户优化业务逻辑。
polar_feature_utils.polar_advisor_utils_stats_view
视图有多个指标,我们这里只关心 Prepared Statement 有关的指标。
SimpleProtocolExecCount 表示简单查询的执行次数,如果该指标偏高,说明有较多 SQL 未使用 Prepared Statement,可以考虑改用 Prepared Statement。
UnparameterizedStmtCount 表示未参数化的 Prepared Statement 数量,UnparameterizedStmtExecCount 表示未参数化的 Prepared Statement 执行次数。如果这两个指标有一个偏高,就说明有较多的 Prepared Statement 没有使用参数,应该考虑在其中设置参数。
UnnamedStmtCount 表示未命名的 Prepared Statement 数量,UnnamedStmtExecCount 表示未命名的 Prepared Statement 执行次数。如果这两个指标有一个偏高,则说明每次使用完 Prepared Statement 以后就将其 close,应该考虑减少 Prepared Statement 使用过程中的 close 次数,只有在真正使用结束时才进行 close。
下面给出了一个使用示例:
SimpleProtocolExecCount 的数值为 72,相较于其他几项指标是比较低的,说明可以改造为 Prepared Statement 的简单查询数量不多,基本可以忽略。
UnparameterizedStmtCount 和 UnparameterizedStmtExecCount 这两个指标显著高于其他几个指标,说明使用了较多的非参数化 Prepared Statement,需要重点进行参数化改造。
UnnamedStmtCount 和 UnnamedStmtExecCount 这两个指标的数值为 1000+,相对于简单查询的数量来说比较高,但是相比非参数化指标又不算太高,对于追求极致性能的场景可以考虑进行改造。
postgres=# select name, value from polar_feature_utils.polar_advisor_utils_stats_view;
name | value
--------------------------------------+-------
SimpleProtocolExecCount | 72
UnnamedStmtCount | 1152
UnnamedStmtExecCount | 1152
UnparameterizedStmtCount | 12071
UnparameterizedStmtExecCount | 30614复制