一、MySQL 存储过程与 PostgreSQL PL/pgSQL 的核心差异对比
1.1 基础语法结构对比
在开始学习 PostgreSQL 存储过程之前,我们首先需要了解两种语言在基础语法结构上的差异。
创建语法对比
在 MySQL 中,我们使用CREATE PROCEDURE来创建存储过程,使用DELIMITER来改变语句分隔符,避免与过程内的分号冲突(75):
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE sp_user_add( IN p_username VARCHAR(50), OUT p_user_id INT ) BEGIN INSERT INTO users (username) VALUES (p_username); SET p_user_id = LAST_INSERT_ID(); END // DELIMITER ; |
而在 PostgreSQL 中,从 11 版本开始引入了真正的PROCEDURE语法,但在此之前,所有的存储过程都需要通过函数来实现(6):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL 11+版本 CREATE OR REPLACE PROCEDURE sp_user_add( p_username VARCHAR(50), OUT p_user_id INT ) LANGUAGE plpgsql AS $$ BEGIN INSERT INTO users (username) VALUES (p_username); p_user_id := CURRVAL('users_id_seq'); END; $$; -- PostgreSQL 11-版本(使用函数模拟) CREATE OR REPLACE FUNCTION sp_user_add( p_username VARCHAR(50) ) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE p_user_id INT; BEGIN INSERT INTO users (username) VALUES (p_username); p_user_id := CURRVAL('users_id_seq'); RETURN p_user_id; END; $$; |
调用方式对比
MySQL 使用CALL语句来调用存储过程:
|-------------------------------------------------------|
| CALL sp_user_add('Alice', @user_id); SELECT @user_id; |
PostgreSQL 同样使用CALL语句,但需要注意的是,只有在使用PROCEDURE时才能进行事务控制,而函数内部不能使用COMMIT或ROLLBACK:
|--------------------------------------------|
| CALL sp_user_add('Alice', OUTPUT user_id); |
1.2 变量声明与数据类型对比
变量声明是存储过程的基础,两种语言在这方面存在显著差异。
变量声明语法
MySQL 使用DECLARE声明局部变量,需要指定数据类型和可选的默认值:
|----------------------------------------------------------------|
| DECLARE v_total INT DEFAULT 0; DECLARE v_username VARCHAR(50); |
PostgreSQL 的变量声明语法更为严格,支持更多的数据类型修饰符(36):
|-------------------------------------------------------------------------------------------------|
| v_total INTEGER DEFAULT 32; v_username VARCHAR := 'John Doe'; CONSTANT v_user_id INTEGER := 10; |
特殊数据类型对比
PostgreSQL 提供了一些 MySQL 没有的高级数据类型,这些类型在处理复杂业务逻辑时非常有用(41):
|---------|-------|------------|----------------------------|
| 数据类型 | MySQL | PostgreSQL | 说明 |
| 行类型 | 不支持 | %ROWTYPE | 存储表的一行数据 |
| 记录类型 | 不支持 | RECORD | 动态存储任意结构的数据 |
| 数组类型 | 不支持 | INT[] | 支持一维和多维数组 |
| JSON 类型 | JSON | JSONB | PostgreSQL 推荐使用 JSONB,性能更高 |
行类型和记录类型的使用示例
在 PostgreSQL 中,可以使用%ROWTYPE声明与表结构相同的变量(37):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DECLARE user_rec users%ROWTYPE; -- 声明一个与users表结构相同的变量 BEGIN SELECT * INTO user_rec FROM users WHERE id = 1; RAISE NOTICE 'Username: %', user_rec.username; END; |
RECORD类型可以动态存储查询结果,无需预先知道结构(41):
|-------------------------------------------------------------------------------------------------------------------------------------------|
| DECLARE rec RECORD; BEGIN FOR rec IN SELECT id, username FROM users LOOP RAISE NOTICE 'User: % - %', rec.id, rec.username; END LOOP; END; |
1.3 条件判断和循环结构对比
条件判断和循环是实现复杂业务逻辑的基础,两种语言在这方面既有相似之处,也有重要差异。
条件判断语法对比
MySQL 支持IF和CASE两种条件判断结构(46):
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- MySQL IF语句 IF v_level = 'gold' THEN SET v_factor = 0.80; ELSEIF v_level = 'silver' THEN SET v_factor = 0.90; ELSE SET v_factor = 1.00; END IF; -- MySQL CASE语句 SET v_discount = CASE WHEN v_level = 'gold' THEN 0.80 WHEN v_level = 'silver' THEN 0.90 ELSE 1.00 END; |
PostgreSQL 的IF语法与 MySQL 类似,但使用ELSIF而非ELSEIF:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL IF语句 IF v_level = 'gold' THEN v_factor := 0.80; ELSIF v_level = 'silver' THEN v_factor := 0.90; ELSE v_factor := 1.00; END IF; -- PostgreSQL CASE语句(表达式形式) v_discount := CASE WHEN v_level = 'gold' THEN 0.80 WHEN v_level = 'silver' THEN 0.90 ELSE 1.00 END; -- PostgreSQL CASE语句(语句形式) CASE WHEN v_level = 'gold' THEN v_factor := 0.80; WHEN v_level = 'silver' THEN v_factor := 0.90; ELSE v_factor := 1.00; END CASE; |
循环结构对比
MySQL 支持LOOP、WHILE和REPEAT三种循环结构(51):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- MySQL WHILE循环 WHILE v_counter < 10 DO INSERT INTO numbers (value) VALUES (v_counter); SET v_counter = v_counter + 1; END WHILE; -- MySQL LOOP循环(需要EXIT条件) loop_label: LOOP IF v_counter >= 10 THEN LEAVE loop_label; END IF; INSERT INTO numbers (value) VALUES (v_counter); SET v_counter = v_counter + 1; END LOOP; |
PostgreSQL 同样支持这三种循环,但语法略有不同,特别是FOR循环的使用更为灵活(56):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL WHILE循环 WHILE v_counter < 10 LOOP INSERT INTO numbers (value) VALUES (v_counter); v_counter := v_counter + 1; END LOOP; -- PostgreSQL LOOP循环 LOOP IF v_counter >= 10 THEN EXIT; END IF; INSERT INTO numbers (value) VALUES (v_counter); v_counter := v_counter + 1; END LOOP; -- PostgreSQL FOR循环(数字范围) FOR i IN 1..10 LOOP INSERT INTO numbers (value) VALUES (i); END LOOP; -- PostgreSQL FOR循环(查询结果)- 这是PostgreSQL的强大特性 FOR rec IN SELECT id, username FROM users WHERE age < 30 LOOP RAISE NOTICE 'Young user: %', rec.username; END LOOP; |
1.4 异常处理机制对比
异常处理是保证存储过程稳健性的重要机制,两种语言在这方面的差异尤为明显。
MySQL 异常处理
MySQL 使用DECLARE HANDLER来定义异常处理逻辑,支持CONTINUE和EXIT两种处理方式(59):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE sp_transfer( IN p_from INT, IN p_to INT, IN p_amount DECIMAL(10,2) ) BEGIN DECLARE exit HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; -- 重新抛出异常 END; START TRANSACTION; UPDATE accounts SET balance = balance - p_amount WHERE id = p_from; UPDATE accounts SET balance = balance + p_amount WHERE id = p_to; COMMIT; END // DELIMITER ; |
PostgreSQL 异常处理
PostgreSQL 使用BEGIN...EXCEPTION块来处理异常,提供了更精细的异常分类和处理能力(57):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE sp_transfer( p_from INT, p_to INT, p_amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN BEGIN UPDATE accounts SET balance = balance - p_amount WHERE id = p_from; UPDATE accounts SET balance = balance + p_amount WHERE id = p_to; EXCEPTION WHEN SQLSTATE '23502' THEN -- 检查约束违规(如余额不足) RAISE NOTICE 'Insufficient balance for account %', p_from; ROLLBACK; WHEN SQLSTATE '23505' THEN -- 唯一约束违规 RAISE NOTICE 'Account does not exist: % or %', p_from, p_to; ROLLBACK; WHEN OTHERS THEN RAISE NOTICE 'Transfer failed: %', SQLERRM; ROLLBACK; END; END; $$; |
异常处理的重要差异
- 异常分类:PostgreSQL 提供了更丰富的异常类型,每个异常都有对应的 SQLSTATE 错误码
- 自动回滚:在 PostgreSQL 中,当异常被捕获时,会自动回滚当前块内的所有数据库更改
- 错误信息获取 :可以使用GET STACKED DIAGNOSTICS获取详细的错误信息:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS v_message = MESSAGE_TEXT, v_detail = PG_EXCEPTION_DETAIL, v_hint = PG_EXCEPTION_HINT; RAISE NOTICE 'Error: %', v_message; RAISE NOTICE 'Detail: %', v_detail; RAISE NOTICE 'Hint: %', v_hint; |
1.5 游标使用对比
游标是处理结果集的重要工具,两种语言在游标使用上有显著差异。
MySQL 游标使用
MySQL 的游标需要显式声明、打开、获取数据和关闭(65):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE loop_young_users() BEGIN DECLARE finished INT DEFAULT 0; DECLARE uid INT; DECLARE uname VARCHAR(50); DECLARE uage INT; -- 声明游标 DECLARE user_cursor CURSOR FOR SELECT id, username, age FROM users WHERE age < 30; -- 声明游标结束处理 DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1; -- 打开游标 OPEN user_cursor; -- 循环读取数据 read_loop: LOOP FETCH user_cursor INTO uid, uname, uage; IF finished = 1 THEN LEAVE read_loop; END IF; SELECT CONCAT('已向 ', uname, '(年龄:', uage, ')发送系统欢迎消息') AS info; END LOOP; -- 关闭游标 CLOSE user_cursor; END // DELIMITER ; |
PostgreSQL 游标使用
PostgreSQL 的游标使用更为灵活,特别是可以直接使用FOR循环遍历查询结果,无需显式处理游标(67):
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE loop_young_users() LANGUAGE plpgsql AS $$ DECLARE rec RECORD; BEGIN -- 直接使用FOR循环遍历查询结果,PostgreSQL会自动处理游标 FOR rec IN SELECT id, username, age FROM users WHERE age < 30 LOOP RAISE NOTICE '已向 %(年龄:%)发送系统欢迎消息', rec.username, rec.age; END LOOP; END; $$; |
如果需要显式使用游标,PostgreSQL 也提供了相应的语法(69):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE cursor_demo() LANGUAGE plpgsql AS $$ DECLARE cur CURSOR FOR SELECT id, username FROM users; rec RECORD; BEGIN OPEN cur; LOOP FETCH cur INTO rec; EXIT WHEN NOT FOUND; RAISE NOTICE 'User: % - %', rec.id, rec.username; END LOOP; CLOSE cur; END; $$; |
1.6 函数返回值对比
在返回值处理方面,PostgreSQL 提供了比 MySQL 更强大和灵活的机制。
标量函数对比
MySQL 的函数返回单一值(75):
|------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE FUNCTION get_employee_count() RETURNS INT BEGIN DECLARE emp_count INT; SELECT COUNT(*) INTO emp_count FROM employees; RETURN emp_count; END; |
PostgreSQL 的函数同样可以返回单一值,但语法略有不同(75):
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION get_employee_count() RETURNS INT LANGUAGE plpgsql AS $$ DECLARE emp_count INT; BEGIN SELECT COUNT(*) INTO emp_count FROM employees; RETURN emp_count; END; $$; |
多值返回对比
PostgreSQL 支持通过输出参数返回多个值:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION sum_n_product( x INT, y INT, OUT sum INT, OUT product INT ) LANGUAGE plpgsql AS $$ BEGIN sum := x + y; product := x * y; END; $$; -- 调用方式 SELECT * FROM sum_n_product(3, 4); |
集合返回对比
这是 PostgreSQL 的一个强大特性,可以返回结果集(78):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 返回表值函数 CREATE OR REPLACE FUNCTION get_employees_by_dept( dept_id INT ) RETURNS TABLE ( id INT, name TEXT, salary NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT id, name, salary FROM employees WHERE department_id = dept_id; END; $$; -- 调用方式 SELECT * FROM get_employees_by_dept(10); |
使用RETURN NEXT可以逐行返回结果(81):
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION generate_series( start_val INT, end_val INT ) RETURNS SETOF INT LANGUAGE plpgsql AS $$ BEGIN FOR i IN start_val..end_val LOOP RETURN NEXT i; END LOOP; RETURN; END; $$; -- 调用方式 SELECT * FROM generate_series(1, 10); |
1.7 数据类型映射表
在迁移过程中,正确的数据类型映射至关重要。以下是 MySQL 和 PostgreSQL 主要数据类型的对应关系(72):
|------------|------------------|-------------------------------------|
| MySQL 数据类型 | PostgreSQL 数据类型 | 注意事项 |
| INT | INTEGER | 无符号整数需特别处理 |
| BIGINT | BIGINT | 自增主键使用SERIAL |
| TINYINT(1) | BOOLEAN | PostgreSQL 原生支持布尔类型 |
| DATETIME | TIMESTAMP | PostgreSQL 不区分 DATETIME 和 TIMESTAMP |
| TEXT | TEXT | 保持一致 |
| ENUM | TEXT + CHECK | PostgreSQL 没有 ENUM 类型 |
| DOUBLE | DOUBLE PRECISION | 名称不同但功能相同 |
| JSON | JSONB | PostgreSQL 推荐使用 JSONB,性能更高 |
二、迁移过程中的常见陷阱与预警
在从 MySQL 迁移到 PostgreSQL 的过程中,有许多看似相似但实际差异很大的语法和特性,这些 "陷阱" 可能导致程序在表面上能够运行,但实际上存在严重的性能或功能问题。
2.1 LIMIT 子句的使用陷阱
陷阱描述
最常见的陷阱之一是LIMIT子句的使用。在 MySQL 中,可以直接在UPDATE和DELETE语句中使用LIMIT,但在 PostgreSQL 中这会导致语法错误(102):
|---------------------------------------------------------------------------------------|
| -- MySQL:正确 UPDATE users SET status = 'processed' WHERE status = 'pending' LIMIT 100; |
|--------------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL:错误,会抛出语法错误 UPDATE users SET status = 'processed' WHERE status = 'pending' LIMIT 100; -- 错误信息:syntax error at or near "LIMIT" |
问题原因
PostgreSQL 不支持在UPDATE和DELETE语句中直接使用LIMIT,主要基于以下设计理念(102):
- 可预测性问题 :没有ORDER BY的LIMIT结果是不确定的,无法保证更新或删除的是哪 100 条记录
- SQL 标准合规性 :SQL 标准中没有UPDATE LIMIT语法
- 更安全的替代方案:PostgreSQL 鼓励使用更明确和安全的方法
正确的解决方案
PostgreSQL 提供了多种安全且可预测的替代方案:
方案一:使用 CTE(公用表表达式) (102)
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 推荐:使用CTE明确指定更新范围 WITH target_rows AS ( SELECT id FROM users WHERE status = 'pending' ORDER BY created_at DESC -- 确保结果可预测 LIMIT 100 ) UPDATE users SET status = 'processed', processed_at = NOW() WHERE id IN (SELECT id FROM target_rows); |
方案二:使用子查询 (102)
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| UPDATE users SET shipped = true, ship_date = CURRENT_DATE WHERE order_id IN ( SELECT order_id FROM orders WHERE status = 'paid' AND shipped = false LIMIT 500 ); |
方案三:使用 UPDATE ... FROM语法
|----------------------------------------------------------------------------------------------------------------------------------------------|
| UPDATE users u SET status = 'processed' FROM ( SELECT id FROM users WHERE status = 'pending' ORDER BY id LIMIT 100 ) AS t WHERE u.id = t.id; |
2.2 隐式类型转换的陷阱
陷阱描述
MySQL 和 PostgreSQL 在类型转换方面的处理方式截然不同,这是导致性能问题的主要原因之一(92)。
在 MySQL 中,当进行比较或计算时,如果操作数类型不一致,MySQL 会自动进行隐式类型转换(94):
|------------------------------------------------------------------------------------------------------------------------------------------|
| -- MySQL:自动将字符串'123'转换为数字 SELECT * FROM users WHERE user_id = '123'; -- 相当于 SELECT * FROM users WHERE CAST(user_id AS UNSIGNED) = 123; |
在 PostgreSQL 中,这种隐式转换会导致错误或性能问题(97):
|-------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL:会尝试将所有id值转换为字符串进行比较 SELECT * FROM users WHERE id = '123'; -- 错误:operator does not exist: integer = character varying |
性能影响分析
当在 PostgreSQL 中使用错误的类型进行查询时,会导致严重的性能问题:
|----------------------------------------------------------------------------------|
| -- 错误示例:id是bigint类型,但使用字符串列表查询 SELECT * FROM table WHERE id IN ('1', '2', '3'); |
执行计划会显示全表扫描而非索引扫描:
|-------------------------------------------------------------------------------------------------------|
| Seq Scan on table (cost=0.00..12345.67 rows=1000 width=64) Filter: (id = ANY ('{1,2,3}'::bigint[])) |
性能对比表(基于 100 万行数据):
|--------------|--------|--------|--------|
| 方案 | 执行时间 | 索引使用 | CPU 负载 |
| 隐式转换(字符串 IN) | 1200ms | ❌ 全表扫描 | 高 |
| 显式转换(整数数组) | 5ms | ✅ 索引扫描 | 低 |
| 临时表 JOIN | 10ms | ✅ 索引扫描 | 中 |
正确的解决方案
- 强制类型匹配:确保查询条件与列类型严格一致
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 正确:使用整数数组 SELECT * FROM users WHERE id = ANY(ARRAY[1, 2, 3]); -- 或者使用显式转换 SELECT * FROM users WHERE id IN (SELECT unnest(string_to_array('1,2,3', ','))::bigint); |
- 避免在索引列上使用函数或转换 :这会导致索引失效(125)
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 错误:在索引列上使用to_char会导致索引失效 SELECT * FROM orders WHERE to_char(order_date, 'YYYY-MM') = '2023-10'; -- 正确:使用日期范围查询 SELECT * FROM orders WHERE order_date >= '2023-10-01' AND order_date < '2023-11-01'; |
2.3 动态 SQL 的语法陷阱
陷阱描述
PostgreSQL 的EXECUTE语法与 MySQL 和 Oracle 有显著差异,容易导致语法错误(105):
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 错误:PL/pgSQL中不能使用EXECUTE IMMEDIATE EXECUTE IMMEDIATE 'CREATE TABLE test (id INT)'; -- 错误:动态SQL必须使用EXECUTE,不能直接拼接 EXECUTE 'SELECT * FROM ' || table_name || ' WHERE id = ' || id; |
正确的动态 SQL 语法
- 基本语法 (104):
|----------------------------------------------------|
| EXECUTE command_string [INTO [STRICT] target]; |
- 使用 USING参数化 (避免 SQL 注入)(108):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 正确:使用USING传递参数 EXECUTE 'SELECT * FROM users WHERE id = 1' USING p_user_id; -- 批量更新示例 EXECUTE 'UPDATE users SET status = 1 WHERE id IN (SELECT unnest($2::int[]))' USING 'processed', ARRAY[1, 2, 3]; |
- 动态表名和列名 :必须使用quote_ident进行转义(101):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 正确:使用quote_ident转义表名 EXECUTE format('SELECT * FROM %s', quote_ident(table_name)); -- 正确:动态列名示例 EXECUTE format('SELECT %s FROM users', quote_ident(column_name)); |
动态 SQL 安全注意事项
- 永远不要直接拼接用户输入 (101)
- 使用quote_literal转义字符串值
- 使用quote_ident转义表名和列名
- 优先使用参数化查询而非字符串拼接
2.4 存储过程与函数的概念差异
陷阱描述
PostgreSQL 在 11 版本之前没有真正的存储过程概念,所有的存储过程都需要通过函数来实现(6)。即使在 11 版本之后引入了PROCEDURE,两者之间仍有重要差异。
主要差异:
- 事务控制 :
- 函数内部不能使用 COMMIT或 ROLLBACK
- 过程(PROCEDURE)可以进行事务控制,但只能在CALL的顶层或嵌套调用中使用(120)
- 返回值 :
- 函数必须有返回值(可以是VOID)
- 过程没有返回值
- 调用方式 :
- 函数:SELECT function_name()
- 过程:CALL procedure_name()
迁移建议
- 对于查询类操作,继续使用函数
- 对于事务性操作,使用 PostgreSQL 11 + 的PROCEDURE
- 避免在函数中进行事务控制
2.5 事务处理的陷阱
陷阱描述
PostgreSQL 的事务处理机制与 MySQL 存在重要差异,这些差异可能导致数据一致性问题。
自动事务处理
PostgreSQL 的所有操作都在事务中进行,不需要显式使用START TRANSACTION(118):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- PostgreSQL中不需要 START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; -- 直接执行即可 UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- (如果在函数或过程中,需要使用BEGIN...END块) |
异常自动回滚
在 PostgreSQL 中,当异常被捕获时,会自动回滚当前块内的所有数据库更改,无需手动调用ROLLBACK:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| BEGIN UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 如果这里抛出异常,上面的UPDATE会自动回滚 EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'An error occurred'; END; |
保存点不支持
PostgreSQL 不支持SAVEPOINT和ROLLBACK TO SAVEPOINT。可以使用嵌套的BEGIN...EXCEPTION块来模拟保存点的功能:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| BEGIN -- 第一个事务块 UPDATE accounts SET balance = balance - 100 WHERE id = 1; BEGIN -- 第二个事务块 UPDATE accounts SET balance = balance + 100 WHERE id = 2; EXCEPTION WHEN OTHERS THEN -- 只回滚第二个块的操作 RAISE NOTICE 'Second update failed, rolling back'; END; -- 第一个块的操作仍然有效 END; |
2.6 性能优化的陷阱
陷阱 1:循环处理大量数据
在存储过程中使用循环处理大量数据会导致严重的性能问题(178):
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 错误:使用循环逐行更新,O(n)时间复杂度 FOR rec IN SELECT id FROM large_table LOOP UPDATE large_table SET status = 'processed' WHERE id = rec.id; END LOOP; -- 正确:使用集合操作,O(1)时间复杂度 UPDATE large_table SET status = 'processed' WHERE status = 'pending'; |
陷阱 2:未使用正确的函数易变性
PostgreSQL 函数的易变性声明对性能有重要影响(176):
- IMMUTABLE:相同参数总是返回相同结果
- STABLE:在单次查询中对相同参数返回相同结果
- VOLATILE:(默认)可能返回不同结果
错误的声明会导致查询优化器生成次优的执行计划。
陷阱 3:分区表设计不当
过多的分区会导致执行计划生成时间过长(129):
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 错误:过度分区(每月一个分区,一年12个分区) CREATE TABLE transaction_partitioned ( id BIGINT NOT NULL, transaction_date DATE NOT NULL, amount NUMERIC ) PARTITION BY RANGE (transaction_date); -- 正确:合理分区(每季度一个分区) CREATE TABLE transaction_partitioned ( id BIGINT NOT NULL, transaction_date DATE NOT NULL, amount NUMERIC ) PARTITION BY RANGE (transaction_date); |
2.7 其他常见陷阱
日期处理陷阱
- 日期格式差异 :PostgreSQL 的datestyle设置影响日期解析格式(142)
建议在应用中使用标准的YYYY-MM-DD格式。
-
- 美国格式:MDY(月 - 日 - 年)
- 欧洲格式:DMY(日 - 月 - 年)
- 时区处理 :PostgreSQL 使用TIMESTAMP WITH TIME ZONE存储带时区的时间
字符集陷阱
内置函数差异
许多 MySQL 的内置函数在 PostgreSQL 中有不同的名称或行为(111):
|------------|-------------------|------|
| MySQL 函数 | PostgreSQL 对应函数 | 注意事项 |
| NVL() | COALESCE() | 功能相同 |
| NOW() | CURRENT_TIMESTAMP | 功能相同 |
| DATE_ADD() | + INTERVAL | 语法不同 |
| CONCAT() | ` | |
三、从简单到复杂的迁移练习路径
为了帮助您顺利完成从 MySQL 到 PostgreSQL 的迁移,我设计了一个渐进式的练习路径,从最简单的函数开始,逐步过渡到复杂的存储过程。
3.1 初级阶段:基础语法转换(1-2 周)
练习 1:Hello World 级别的函数
MySQL 版本:
|-----------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE FUNCTION hello_world() RETURNS VARCHAR(20) BEGIN RETURN 'Hello, World!'; END // DELIMITER ; |
PostgreSQL 版本:
|----------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION hello_world() RETURNS VARCHAR(20) LANGUAGE plpgsql AS $$ BEGIN RETURN 'Hello, World!'; END; $$; |
练习 2:带参数的简单函数
MySQL 版本:
|-----------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE FUNCTION add_numbers(a INT, b INT) RETURNS INT BEGIN RETURN a + b; END // DELIMITER ; |
PostgreSQL 版本:
|----------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION add_numbers(a INT, b INT) RETURNS INT LANGUAGE plpgsql AS $$ BEGIN RETURN a + b; END; $$; |
练习 3:查询单条记录的函数
MySQL 版本:
|-------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE get_user_by_id(IN p_id INT) BEGIN SELECT id, username, email FROM users WHERE id = p_id; END // DELIMITER ; |
PostgreSQL 版本:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION get_user_by_id(p_id INT) RETURNS TABLE ( id INT, username VARCHAR(50), email VARCHAR(100) ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT id, username, email FROM users WHERE id = p_id; END; $$; |
练习 4:带输出参数的函数
MySQL 版本:
|----------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE get_user_count(OUT p_count INT) BEGIN SELECT COUNT(*) INTO p_count FROM users; END // DELIMITER ; |
PostgreSQL 版本:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION get_user_count() RETURNS INT LANGUAGE plpgsql AS $$ DECLARE p_count INT; BEGIN SELECT COUNT(*) INTO p_count FROM users; RETURN p_count; END; $$; -- 或者使用OUT参数 CREATE OR REPLACE FUNCTION get_user_count(OUT p_count INT) LANGUAGE plpgsql AS $$ BEGIN SELECT COUNT(*) INTO p_count FROM users; END; $$; |
3.2 中级阶段:业务逻辑实现(2-3 周)
练习 5:条件判断和循环
需求:计算 1 到 n 的整数和
MySQL 版本:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE FUNCTION calculate_sum(n INT) RETURNS INT BEGIN DECLARE sum INT DEFAULT 0; DECLARE i INT DEFAULT 1; WHILE i <= n DO SET sum = sum + i; SET i = i + 1; END WHILE; RETURN sum; END // DELIMITER ; |
PostgreSQL 版本:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION calculate_sum(n INT) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE sum INT := 0; i INT := 1; BEGIN WHILE i <= n LOOP sum := sum + i; i := i + 1; END LOOP; RETURN sum; END; $$; -- 或者使用更简洁的FOR循环 CREATE OR REPLACE FUNCTION calculate_sum(n INT) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE sum INT := 0; BEGIN FOR i IN 1..n LOOP sum := sum + i; END LOOP; RETURN sum; END; $$; |
练习 6:根据条件更新数据
需求:根据用户等级计算折扣并应用
MySQL 版本:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE apply_discount(IN p_user_id INT, IN p_rate DECIMAL(5,2)) BEGIN DECLARE v_level VARCHAR(10); DECLARE v_factor DECIMAL(5,2) DEFAULT 1.00; SELECT level INTO v_level FROM members WHERE user_id = p_user_id; IF v_level = 'gold' THEN SET v_factor = 0.80; ELSEIF v_level = 'silver' THEN SET v_factor = 0.90; ELSE SET v_factor = 1.00; END IF; UPDATE orders SET amount = amount * v_factor * (1 - p_rate) WHERE user_id = p_user_id AND status = 'pending'; END // DELIMITER ; |
PostgreSQL 版本:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE apply_discount(p_user_id INT, p_rate NUMERIC) LANGUAGE plpgsql AS $$ DECLARE v_level VARCHAR(10); v_factor NUMERIC := 1.00; BEGIN SELECT level INTO v_level FROM members WHERE user_id = p_user_id; IF v_level = 'gold' THEN v_factor := 0.80; ELSIF v_level = 'silver' THEN v_factor := 0.90; ELSE v_factor := 1.00; END IF; UPDATE orders SET amount = amount * v_factor * (1 - p_rate) WHERE user_id = p_user_id AND status = 'pending'; END; $$; |
练习 7:事务处理
需求:转账操作,包含事务和异常处理
MySQL 版本:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE transfer_money( IN p_from_account INT, IN p_to_account INT, IN p_amount DECIMAL(10,2) ) BEGIN DECLARE exit HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_account; UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_account; COMMIT; END // DELIMITER ; |
PostgreSQL 版本:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE transfer_money( p_from_account INT, p_to_account INT, p_amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN BEGIN UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_account; UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_account; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'Transfer failed: %', SQLERRM; RAISE; -- 重新抛出异常 END; END; $$; |
3.3 高级阶段:复杂业务场景(3-4 周)
练习 8:批量数据处理
需求:批量更新用户状态,每次处理 1000 条
MySQL 版本:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DELIMITER // CREATE PROCEDURE batch_update_users(IN p_status VARCHAR(20)) BEGIN DECLARE done INT DEFAULT 0; DECLARE batch_size INT DEFAULT 1000; DECLARE user_cursor CURSOR FOR SELECT id FROM users WHERE status = 'old'; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN user_cursor; REPEAT FETCH user_cursor INTO @user_id; IF NOT done THEN UPDATE users SET status = p_status WHERE id = @user_id; END IF; UNTIL done END REPEAT; CLOSE user_cursor; END // DELIMITER ; |
PostgreSQL 版本(使用更高效的集合操作):
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE batch_update_users(p_status VARCHAR(20)) LANGUAGE plpgsql AS $$ DECLARE batch_size INT := 1000; total_updated INT := 0; BEGIN LOOP -- 每次更新1000条记录 UPDATE users SET status = p_status WHERE id IN ( SELECT id FROM users WHERE status = 'old' LIMIT batch_size ); GET DIAGNOSTICS total_updated = ROW_COUNT; IF total_updated = 0 THEN EXIT; END IF; RAISE NOTICE 'Updated % records in this batch', total_updated; END LOOP; END; $$; |
练习 9:动态 SQL 应用
需求:根据传入的表名和条件执行动态查询
PostgreSQL 版本:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION dynamic_query( table_name TEXT, where_clause TEXT, OUT result_set REFCURSOR ) LANGUAGE plpgsql AS $$ DECLARE query_string TEXT; BEGIN -- 构建安全的查询语句 query_string := 'SELECT * FROM ' || quote_ident(table_name); IF where_clause IS NOT NULL THEN query_string := query_string || ' WHERE ' || where_clause; END IF; -- 打开游标返回结果集 OPEN result_set FOR EXECUTE query_string; END; $$; -- 调用示例 BEGIN FOR row IN SELECT * FROM dynamic_query('users', 'age > 30') LOOP RAISE NOTICE 'User: %', row.username; END LOOP; END; |
练习 10:复杂业务逻辑
需求:订单处理,包含库存扣减、积分计算、日志记录等
PostgreSQL 版本:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE PROCEDURE process_order( p_order_id INT, p_user_id INT, p_product_id INT, p_quantity INT ) LANGUAGE plpgsql AS $$ DECLARE v_product_price NUMERIC; v_total_amount NUMERIC; v_user_points INT; v_new_points INT; BEGIN -- 1. 获取产品价格 SELECT price INTO v_product_price FROM products WHERE id = p_product_id; -- 2. 计算总金额 v_total_amount := v_product_price * p_quantity; -- 3. 扣减库存 UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id; -- 4. 计算积分(每10元获得1积分) v_user_points := COALESCE((SELECT points FROM users WHERE id = p_user_id), 0); v_new_points := v_user_points + FLOOR(v_total_amount / 10); -- 5. 更新用户积分 UPDATE users SET points = v_new_points WHERE id = p_user_id; -- 6. 记录订单日志 INSERT INTO order_logs ( order_id, user_id, product_id, quantity, amount, points_earned, created_at ) VALUES ( p_order_id, p_user_id, p_product_id, p_quantity, v_total_amount, v_new_points - v_user_points, NOW() ); -- 7. 发送通知(使用PostgreSQL的NOTIFY功能) PERFORM pg_notify('order_processed', p_order_id::TEXT); RAISE NOTICE 'Order % processed successfully', p_order_id; RAISE NOTICE 'Total amount: %', v_total_amount; RAISE NOTICE 'New points for user %: %', p_user_id, v_new_points; END; $$; |
3.4 生产环境 Checklist
在将存储过程部署到生产环境之前,必须遵循以下最佳实践:
安全规范
- search_path 设置
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 在函数开始处设置安全的search_path CREATE OR REPLACE FUNCTION secure_function() LANGUAGE plpgsql AS $$ BEGIN -- 只搜索指定的模式,避免SQL注入 SET search_path = app_schema, pg_catalog; -- 业务逻辑... END; $$; |
- SECURITY DEFINER 使用规范 (172)
- 仅在必要时使用SECURITY DEFINER
- 设置安全的search_path
- 限制可执行角色
- 避免使用动态 SQL
- 权限控制
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 撤销PUBLIC的默认权限 REVOKE CONNECT ON DATABASE appdb FROM PUBLIC; REVOKE ALL ON SCHEMA public FROM PUBLIC; -- 只授予特定用户执行权限 GRANT EXECUTE ON FUNCTION secure_function TO app_user; |
性能优化
- 索引优化
- 确保所有查询都使用索引
- 创建覆盖索引
- 定期分析表以更新统计信息
- 避免循环 (178)
- 优先使用集合操作而非循环
- 批量处理数据(每次 100-1000 条)
- 使用RETURN QUERY替代游标
- 执行计划优化 (179)
|----------------------------------------------------------------------|
| -- 强制使用通用执行计划(适用于参数变化大的情况) SET plan_cache_mode = force_generic_plan; |
代码规范
- 命名规范
- 函数名:fn_模块名_功能
- 过程名:sp_模块名_功能
- 参数名:p_参数名
- 变量名:v_变量名
- 注释规范
- 每个函数 / 过程必须有功能描述
- 参数说明
- 异常说明
- 修改记录
- 事务控制
- 事务要简短
- 避免长时间持有锁
- 使用适当的隔离级别
监控和调试
- 日志记录 (182)
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 使用RAISE NOTICE记录关键信息 RAISE NOTICE 'Function started with parameters: %', p_params; RAISE NOTICE 'Data processed: %', v_count; -- 记录到服务器日志 -- 在postgresql.conf中设置log_min_messages = notice |
- 性能监控
- 使用EXPLAIN ANALYZE分析执行计划
- 监控函数执行时间
- 设置慢查询日志
- 错误处理
- 捕获所有异常
- 记录详细的错误信息
- 提供友好的错误提示
部署规范
- 版本管理
- 使用数据库迁移工具(如 Flyway、Liquibase)
- 每个变更都有版本号
- 保持迁移脚本的幂等性
- 测试规范
- 单元测试覆盖主要逻辑
- 集成测试验证业务流程
- 性能测试确保生产环境稳定
- 回滚策略
- 每个变更都要有回滚脚本
- 部署前备份数据库
- 逐步发布,灰度测试
四、高级特性与最佳实践
4.1 PostgreSQL 独有的强大特性
复合类型支持
PostgreSQL 支持返回复合类型,这在处理复杂数据结构时非常有用(79):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 定义复合类型 CREATE TYPE user_info AS ( id INT, username VARCHAR(50), email VARCHAR(100), registration_date DATE ); -- 返回复合类型的函数 CREATE OR REPLACE FUNCTION get_user_info(p_user_id INT) RETURNS user_info LANGUAGE plpgsql AS $$ DECLARE result user_info; BEGIN SELECT id, username, email, registration_date INTO result FROM users WHERE id = p_user_id; RETURN result; END; $$; -- 调用方式 SELECT * FROM get_user_info(1); |
数组类型操作
PostgreSQL 支持在存储过程中直接操作数组:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION process_array(p_array INT[]) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE total INT := 0; i INT; BEGIN -- 遍历数组 FOREACH i IN ARRAY p_array LOOP total := total + i; END LOOP; RETURN total; END; $$; -- 调用示例 SELECT process_array(ARRAY[1, 2, 3, 4, 5]); |
JSONB 数据类型
PostgreSQL 的 JSONB 类型提供了高效的 JSON 存储和查询能力:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CREATE OR REPLACE FUNCTION update_json_field( p_id INT, p_field TEXT, p_value JSONB ) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN UPDATE users SET metadata = metadata || jsonb_build_object(p_field, p_value) WHERE id = p_id; END; $$; |
4.2 性能优化最佳实践
避免全表扫描
- 使用索引
- 确保查询条件使用索引列
- 避免在索引列上使用函数
- 创建覆盖索引
- 优化查询语句
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -- 错误:使用函数导致索引失效 SELECT * FROM orders WHERE to_char(order_date, 'YYYY') = '2023'; -- 正确:使用范围查询 SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01'; |
批量操作优化
- 使用 COPY FROM:批量导入数据时使用 COPY 命令,比逐条 INSERT 快 100 倍以上
- 批量更新:使用集合操作而非循环
- 临时表优化:使用临时表存储中间结果
执行计划优化
- 了解执行计划
|-------------------------------------------------------|
| EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30; |
- 使用合适的 JOIN 策略
- 小表 JOIN 大表时,将小表放在前面
- 使用JOIN ... USING简化语法
- 避免笛卡尔积
内存优化
- 设置合适的 work_mem:用于排序和哈希 JOIN
- 使用连接池:减少连接开销
- 合理设置 shared_buffers:建议为物理内存的 25-40%
4.3 与 MySQL 存储过程的对比优势
功能对比总结表
|------|--------------|-------------------------|
| 特性 | MySQL 存储过程 | PostgreSQL PL/pgSQL |
| 变量类型 | 仅基础类型 | 支持复合类型、数组、JSONB |
| 游标 | 需要显式打开 / 关闭 | 可直接用 FOR 循环遍历查询 |
| 异常处理 | 简单 HANDLER | 精细的异常分类和处理 |
| 性能 | 简单操作高效 | 复杂逻辑性能高 2-3 倍 |
| 调试 | 依赖 SELECT 输出 | 支持断点调试 |
| 安全性 | 基础安全 | 完善的 SECURITY DEFINER 机制 |
迁移建议
- 评估现有存储过程
- 简单查询类:直接转换为函数
- 事务类操作:使用 PostgreSQL 11 + 的 PROCEDURE
- 复杂业务逻辑:充分利用 PostgreSQL 的高级特性
- 分阶段迁移
- 第一阶段:简单函数
- 第二阶段:复杂过程
- 第三阶段:性能优化
- 性能提升点
- 使用集合操作替代循环
- 利用 PostgreSQL 的高级数据类型
- 合理使用索引和执行计划
五、总结与行动建议
通过本文的详细对比和练习,您已经掌握了从 MySQL 存储过程迁移到 PostgreSQL PL/pgSQL 的核心知识。让我总结一下关键要点并给出行动建议。
核心差异回顾
- 语法差异 :PostgreSQL 使用CREATE OR REPLACE FUNCTION,需要指定LANGUAGE plpgsql
- 数据类型:PostgreSQL 支持更多高级类型(复合类型、数组、JSONB)
- 异常处理:PostgreSQL 提供更精细的异常分类和自动回滚机制
- 游标使用 :PostgreSQL 可以直接用FOR循环遍历查询结果
- 事务控制 :PostgreSQL 默认自动事务,函数中不能使用COMMIT
常见陷阱总结
- LIMIT 陷阱 :不能在UPDATE/DELETE中直接使用,需用 CTE 或子查询替代
- 类型转换:PostgreSQL 不支持隐式类型转换,必须显式转换
- 动态 SQL :必须使用quote_ident和quote_literal防止 SQL 注入
- 事务处理 :函数中不能使用事务控制,需用PROCEDURE
学习路径建议
- 初级阶段(1-2 周):掌握基础语法,完成简单函数转换
- 中级阶段(2-3 周):学习复杂逻辑实现,包括条件、循环、异常处理
- 高级阶段(3-4 周):掌握高级特性,优化性能,编写生产级代码
下一步行动
- 搭建测试环境:在本地安装 PostgreSQL,创建测试数据库
- 从简单开始:先转换几个简单的存储过程,熟悉语法差异
- 逐步深入:从查询类函数开始,逐步过渡到事务类过程
- 性能优化 :使用EXPLAIN ANALYZE分析执行计划,不断优化
- 生产部署:严格遵循生产环境 Checklist,确保安全和性能
PostgreSQL 的 PL/pgSQL 为您提供了比 MySQL 更强大的功能和更好的性能。虽然学习曲线较陡,但掌握之后将大大提升您的技术能力和工作效率。建议您在实际项目中逐步应用所学知识,遇到问题及时查阅官方文档或社区资源。相信通过持续学习和实践,您一定能够顺利完成从 MySQL 到 PostgreSQL 的技术迁移。