亚马逊AWS官方博客

写给开发者的迁移指南:如何将 Oracle 与 SQL Server 代码转换为 PostgreSQL

Original URL: https://amazonaws-china.com/cn/blogs/database/code-conversion-challenges-while-migrating-from-oracle-or-microsoft-sql-server-to-postgresql/
核子可乐译

PostgreSQL已经成为当下最流行的开源关系数据库系统之一。当客户从Oracle及微软SQL Server等商业数据库向外迁移时,PostgreSQL已经成为一大首选替代方案。目前,AWS为大家提供两种PostgreSQL托管选项:Amazon RDS与Amazon Aurora。

除了提供托管PostgreSQL服务之外,AWS还准备了一系列用于协助迁移的工具与资源。AWS Schema Conversion Tool(SCT)就是一款免费AWS工具,可帮助您转换现有schema,且同时支持多个源数据库与目标数据库。AWS Database Migration Service(数据库迁移服务,简称DMS)则用于在异构数据库与同构数据库之间完成数据的传输与连接复制。当然,AWS还提供迁移指导手册,其中包含关于商业数据库以及开源数据库(包括PostgreSQL)之间的大量功能映射说明。

在今天的文章中,我们将介绍从PL/SQL转换为PL/pgSQL的技巧与最佳实践,希望帮助大家在顺利将代码转换为PostgreSQL形式的同时,获取良好的数据库运行性能。本文主要面向从事数据库迁移的开发人员,并要求您预先掌握关于数据库及PL/SQL的基础知识。

性能考量

本节主要探讨一系列在从商业或传统数据库(例如SQL Server及Oracle)向PostgreSQL迁移时,可能对数据库性能造成影响的具体因素。虽然大部分数据库中包含类似的对象,但其中仍有部分对象可能在迁移之后影响到系统的运作方式。本节将向大家介绍如何通过调整存储流程、函数以及SQL语句获得更好的性能。

数据类型

为了避免不必要的返工,在正式启动迁移项目之前,大家需要将目标数据库中的数据类型与源系统正确映射起来。下表总结了从Oracle到SQL Server、再到PostgreSQL的一系列常见数据类型映射。

Oracle PostgreSQL SQL Server 备注
Number Small Integer Tinyint / Smallint 通常用于对数值受限的表进行查找。
Number Integer / Bigint Integer / Bigint
Number

Double Precision /

Float / Numeric

Double Precision /

Float / Numeric

对于金融等需要在应用内保存高精度值的使用场景,您可以将其配置为数字/小数。而在其他场景下,使用双精度或浮点数即可。
Varchar

Char(n)

Varchar(n)

Varchar

Text

Character varying

Nchar

Nvarchar

Ntext

Timestamp(6) Timestamp without timezone

DateTime2(p)

DateTime

Clob Text
Blob Raw
Bytea Binary, Image, VarBinary
Boolean Boolean Bit
XML XML XML

为什么要使用smallint/integer/bigint,而不直接使用数字?

要在数据库中获取最佳性能,选择最适合的数据类型非常重要。

如果您的表列中最多只能包含四位数字,那么具有2字节(smallint)的列数据类型就足以完成任务,意味着我们不必将其定义为4字节(整数/实数)、8字节(bigint/双精度)或者可变字节(数字)等更占资源的数据类型。

数值是一种可以容纳13万1千个数位的复杂类型,主要用于表示货币金额及其他少数需要极高精度的数量。但与整数类型或者浮点类型相比,数字的运算符处理速度很慢,因此计算速度也相当缓慢。

在下表的示例当中,我们可以看到在分别使用smallint/int/bigint建立无索引非精确列时,表整体大小发生的变化。

数据类型 大小 外部大小 插入值
numericsize 16 KB 8192 bytes 插入numericsize值 (1234678)
smallintsize 8192 bytes 0 bytes 插入numericsize值(1234)
intsize 8192 bytes 0 bytes 插入numericsize值(123457)
bigintsize 8192 bytes 0 bytes 插入numericsize值(123486)

下表使用与上表相同的信息,但包含索引。在此表中,大小指的是表的总体大小,外部大小则代表相关对象(例如索引)的大小。

数据类型 大小 外部大小
numericsize 32 KB 24 KB
smallintsize 24 KB 16 KB
intsize 24 KB 16 KB
bigintsize 24 KB 16 KB

AWS SCT在不了解实际数据大小的情况下,也能够将数字与表中的数字数据类型映射起来。这款工具还提供选项,帮助用户在转换过程中配置/映射正确的数据类型。

存储过程与函数

PostgreSQL 10及较早版本并不支持存储过程。Oracle与SQL Server中的所有存储过程与函数都将被映射为PostgreSQL中的函数。但从版本11开始,PostgreSQL也引入了存储过程支持,其基本原理与Oracle类似。

PostgreSQL支持三种波动函数类别,您需要在迁移当中根据函数特性指定适当的类别,即:VolatileStableImmutable。正确标记函数类别,有望给我们的数据库性能带来显著提升。

Volatile

Volatile类型表示那些即使在单一表扫描中也能够变更的函数值,因此无法做出进一步优化。一般来说,大部分数据库函数并不会选择volatile类型,例如ndom()currval()以及timeofday()。只有那些具有副作用的函数(即使结果具有可预测性)才会被归类为volatile,例如setval()。如果在函数创建期间未提供volatility类型,则默认情况下所有新函数都将被标记为volatile。

下面是一条示例函数,用以说明执行Volatile函数需要花费多长时间。

Create Or Replace Function add_ten_v(num int) Returns integer AS $$
Begin
  Perform pg_sleep(0.01);
  Return num + 10;
End
$$ Language 'plpgsql' Volatile;

执行以下函数即可查看执行成本。

lab=>Explain Analyze Select add_ten_v(10)FROM generate_series(1,100,1); 
Query plan
-----------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..260.00 rows=1000 width=4) (actual time=10.200..1015.461 rows=100 loops=1)
 Planning time: 0.030 ms
 Execution time: 1015.501 ms
(3 rows)
Time: 1016.313 ms

Stable

Stable

类型表示该函数无法修改数据库。此外,Stable还表示在单一表扫描操作当中,它对于相同的参数值将始终返回相同的结果,但具体结果可能会在不同SQL语句之间有所区别。如果需要创建一条结果取决于数据库查找或者参数变量(例如当前时区)的函数,那么Stable类型往往是理想的选择。current_timestamp函数家族就是其中的典型代表,它们的值在事务执行过程中始终保持不变。

下面是一条示例函数,用以显示执行Stable函数需要花费多长时间。

Create Or Replace Function add_ten_s(num int) Returns integer AS $$
Begin
  Perform pg_sleep(0.01);
  Return num + 10;
End
$$ Language 'plpgsql' Stable;

执行以下函数即可查看执行成本。

lab=> Explain Analyze Select add_ten_s(10) From generate_series(1,100,1);
                                                       Query Plan
-------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..260.00 rows=1000 width=4) (actual time=10.153..1013.814 rows=100 loops=1)
 Planning time: 0.031 ms
 Execution time: 1013.846 ms
(3 rows)
Time: 1014.507 ms

Immutable

Immutable类型表示该函数无法修改数据库,而且在给定相同的参数值时将始终返回相同的结果。这意味着其无法执行数据库查找,也无法使用参数列表中未直接存在的信息。如果选定此选项,对该函数的一切全常数参数调用都将被立即替换为该函数的值。

下面是一条示例函数,用以显示执行Immutable函数需要花费多长时间。

Create Or Replace Function add_ten_i(num int) Returns integer AS $$
Begin
  Perform pg_sleep(0.01);
  Return num + 10;
End
$$ Language 'plpgsql' Immutable;

执行以下函数即可查看执行成本。

lab=> Explain Analyze Select Add_Ten_I(10) From Generate_Series(1,100,1);
                                                     Query Plan
--------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..10.00 rows=1000 width=4) (actual time=0.009..0.016 rows=100 loops=1)
 Planning time: 10.185 ms
 Execution time: 0.030 ms
(3 rows)
Time: 10.681 ms
以上所有函数都将返回以下值:
lab=> Select Add_Ten_V(10), Add_Ten_S(10), Add_Ten_I(10);
 add_ten_v | add_ten_s | add_ten_i
-----------+-----------+-----------
        20 |        20 |        20
(1 row)
尽管以上各函数都返回相同的值,但根据函数的不同,我们可能需要从中选择特定一种以获得更好的数据库性能。

对各函数执行的测试结果表明,这些函数的基本功能完全相同,但其中Immutable函数的执行时长最短。这是因为Immutable类别允许优化程序在查询调用期间通过常量参数对该函数进行预评估。

视图与查询中的函数调用

许多应用程序都会使用包含函数调用的视图与查询。如上一节所述,在PostgreSQL当中,函数调用有可能占用大量资源,特别是在未能正确设置函数volatility类别的情况下。此外,函数调用本身也会增加相应的查询成本。

为此,我们需要根据函数功能为函数选择适当类别。如果您的函数更适合Immutable或者Stable条件,那么正确设置以取代默认的Volatile将带来一定性能优势。

以下示例代码,为一条包含Volatile函数调用的查询。

Explain Analyze
Select Empid,
		Empname,
		Getdeptname(Deptid),
		Salary,
		Doj,
		Address
From Emp
Where Deptid=2

其中的getDeptname()函数被标记为volatile。该查询的总运行时长为2秒886毫秒。

下面来看包含Stable函数调用的查询示例。

Explain Analyze 
Select Empid,
		Empname,
		Getdeptnames(Deptid),
		Salary,
		Doj,
		Address
From Emp
Where Deptid=2

其中的getDeptname()函数被标记为stable,其总运行时长为2秒644毫秒。

以下示例代码将函数替换为功能。

Explain Analyze 
Select Empid,
		Empname,
		Deptname,
		Salary,
		Doj,
		Address
From Emp E
Join Dept D On D.Deptid = E.Deptid
Where E.Deptid=2

函数逻辑被成功转换至新查询内,而总运行时长仅为933毫秒。

Exception优化

PostgreSQL允许用户使用ExceptionRaise语句捕捉并触发错误的功能。这项功能虽然具有现实意义,但也要付出一定代价。Raise语句会在PL/pgSQL函数的执行过程中引发错误与异常。在默认情况下,PL/pgSQL函数内部发生的任何错误都会导致执行中止以及变更回滚。为了从错误中正常恢复,PL/pgSQL可以使用Exception子句捕捉具体错误。要实现这项功能,我们需要保证PostgreSQL在输入还有异常处理的代码段之前保存事务状态。这项操作会占用大量资源,因此间接增加了运行成本。

为了避免这部分成本,我们的建议是:要么在应用程序端捕捉异常,要么确保提前进行必要验证、使得函数永远不会发生异常。

以下代码示例展示了在函数调用中纳入异常,会给数据库性能造成怎样的影响。

Create Or Replace Function empsal (eid int)
Returns Integer AS $total$
Declare
	Total Integer;
Begin
   Update Emp Set Salary = Salary * 0.20 Where Empid = Eid;
   Return 1;
End;
$$ Total Language Plpgsql;


Create Or Replace Function Empsalexcep (Eid Int)
Returns Integer AS $Total$
Declare
	Total Integer;
Begin
   Update Emp Set Salary = Salary * 0.20 Where Empid = Eid;
   RETURN 1;
   Exception
   	When Others Then
		Raise Notice 'Salary Update Failed ';
END;
$$ Total Language Plpgsql;


Select * From Empsal(3) 		– 	78ms	-- without exception handling
Select * From Empsalexcep(3)		-	84ms	-- with exception handling

如果大家无法在无异常状况下进行验证,那么异常将不可避免。在以上示例中,我们可以检查诊断结果以跟踪是否存在需要关注的变更。另外,如果可能,请尽量不要使用异常处理机制。

无需提取操作的计数器

不少应用程序需要进行游标循环并获取计数,才能完成记录内容的提取工作。由于提取操作会在无对应记录时返回null,因此最好能用提取状态来替代声明两项变量的传统计数检查方法。如此一来,我们可以避免声明额外变量并对其进行检查,从而减少需要执行的语句数量并获得更好的性能。具体请参见以下代码示例。

Select Count(1) Into Count_Value
    From Tab1
    Where Tab1.A = Value 
Counter = 0
Open Dvscriptcursor For Select Id From Tab1;
While (Counter <     Count_Value)
Loop	
	Fetch Id Into Var_Id

	……..
…….
Counter = Counter +1;
End Loop

我们也可以按照以下步骤重新编写上述代码,其中使用游标本体进行迭代并利用游标状态中断/退出循环,这就有效避免了引入两个新的变量。

OPEN Dvscriptcursor For Select Id From Tab1;
Loop	
	Fetch Id Into Var_Id
	Exit When Not Found
	……..
…….
…….
End Loop

检查EXISTS,而非直接计数

在旧版应用程序当中编写SQL查询时,我们首先需要查找匹配的记录数,而后才能应用所需的业务逻辑。如果表中包含数十亿条记录,那么获取记录数量往往需要占用大量资源。

以下代码示例演示了如何先检查行数,而后更新数据。

Create Or Replace Function Empsal (Eid Int)
Returns Integer As $Total$
Declare
	Total Integer;
Begin
  If (Select Count(*) From Emp Where Empid = Eid) > 0 Then  -- Wrong Usage
   		Update Emp Set Salary = Salary * 0.20 Where Empid = Eid  ;
	End If;
   Return 1;
End;
$Total$ Language plpgsql;

此查询的总运行时长为163毫秒。

我们也可以重新编写代码以检查一列——而非一整行,这样可以节约成本并提高性能。具体请参见以下代码示例。

Create Or Replace Function Empsal (Eid Int)
Returns Integer AS $Total$
Declare
	Total Integer;
Begin
	If Exists (Select 1  From Emp Where Empid = Eid) Then.   – Right Usage
   		Update Emp Set Salary = Salary * 0.20 Where Empid = Eid  ;
	End If;
   RETURN 1;
END;
$$ Total Language plpgsql;

这条查询的总运行时长为104毫秒。

在DML语句后记录计数结果

在大多数旧版应用程序中,我们可以通过记录计数结果来判断数据操作语句是否引发了变更。在PostgreSQL中,这部分信息被保留在统计信息当中,用户可以随时检索以避免在操作之后对值进行计数。我们可以使用诊断程序检索受影响的行数,具体如以下代码示例所示。

Create Or Replace Function Empsal (Eid Int)
Returns Integer AS $Total$
Declare
	Total Integer;
	Rows_Affected Int;
Begin
	If Exists (Select 1 From Emp Where Empid = Eid) Then 
   	   Update Emp Set Salary = Salary * 0.20 Where Empid = Eid  ;
	   Get Diagnostics Rows_Affected = ROW_COUNT;
	End If;
         RETURN 1;
END;
$$ Total Language plpgsql;

模式匹配与搜索

从表中检索数据时,常见的做法是将通配符% _ LIKE表达式配合使用(在进行非敏感搜索时也可以使用ILIKE)。如果通配符位于给定schema的开头,那么即使存在索引,查询规划程序也无法使用该索引。在这种情况下,我们就必须使用顺序扫描,而这是一项相当耗时的操作。为了在处理数百万条记录时获得良好性能,并保证查询规划程序正常使用可用的索引,大家需要在谓词的中间或结尾处(而非开头)使用通配符,从而强制引导规划程序使用索引。

除了LIKE表达式之外,大家也可以使用pg_trgm模块/扩展进行模式匹配。其中pg_trgm模块将为我们提供用于确定字母数字文本相似性的函数与运算符,同时提供支持相似字符串快速搜索的索引运算符类。关于更多详细信息,请参阅PostgreSQL网站上发布的pg_tram说明文档

在Oracle、SQL Server以及PostgreSQL之间进行映射转换

本节主要介绍在Oracle、SQL Server以及PostgreSQL数据库中编写SQL语句方面的不同之处。

默认FROM子句

在Oracle当中,FROM子句具有强制性,因此只能在代码中使用Select 1 from Dual;。而在PostgreSQL与SQL当中,大家可以选择使用代码Select 1;。

生成值集合

通过指定开始数字与结束数字,我们可以生成一个值集合。

在Oracle中,我们不需要起始数字,但可以提供结束数字。具体代码示例如下。

Select Rownum As Rownum_Value From Dual Connect By Level <= 64

在使用起始与结束数字时,使用以下代码。

With t(n) As (
  Select 1 from dual
  Union All
    Select n+1 From t Where n < 64
)
Select * From t;

在PostgreSQL中,使用以下代码。

Select Generate_Series(1,64) AS Rownum_Value

在SQL Server当中,使用以下代码。

;With n(n) As
(
    Select 1
    Union All
    Select n+1 From n Where n < 64
)
Select n From n Order By n

联接(+)运算符

在Oracle中,左联接运算的实现需使用以下代码。

Select b.id, b.title,  b.author, b.year_published, l.name language
From  books b, ibrary.languages l
Where  l.id (+)= b.language_id

Order By  b.id 

要实现右联接,使用以下代码。

Select b.id, b.title,  b.author, b.year_published, l.name language
From  books b, ibrary.languages l
Where  l.id = b.language_id (+)
Order BY  b.id

若需了解更多详细信息,请参阅Oracle数据库网站上的SQL新手指南(第五部分):联接

PostgreSQL与SQL Server上并不存在“+”这种可对表进行左右联接的功能;相反,二者使用以下两项查询。

Select b.id, b.title,  b.author, b.year_published, l.name language
From  books b, 
Left join ibrary.languages l 
On   l.id = b.language_id
Order BY  b.id 

Select b.id, b.title,  b.author, b.year_published, l.name language
From  books b, 
Right join ibrary.languages l
On   l.id = b.language_id 
Order By  b.id

将类型作为函数参数

在SQL Server中,我们可以使用Type数据类型传递多条记录。要在PostgreSQL中实现相同的效果,大家可以通过JSON格式或者数组形式将该类型视为JSON或者文本数据类型。在以下示例代码中,JSON格式的文本数据类型就包含有多条记录。您可以将其插入临时表,并在后续代码中执行进一步处理。

Create Table emptable1
(
    empid integer,
    last_name varchar(100),
    first_name varchar(100),
    deptid integer,
    salary double precision
)

Oracle

以下代码所示,为多条记录如何在Oracle的varchar数据类型中实现传递。

DECLARE 
 StructType	Varchar2(1000) Default '[{"empid" : 1, "last_name":"AccName1", "first_name":"AccName1", "deptid":"1", "salary":"1234.578"}
 					,{"empid" : "2", "last_name":"AccName2", "first_name":"AccName2", "deptid":"2", "salary":"4567.578"}
                    ]';
Begin
Insert Into emptable1 (empid,last_name,first_name,deptid,salary)
With Json As  
( Select StructType  --'[{"firstName": "Tobias", "lastName":"Jellema"},{"firstName": "Anna", "lastName":"Vink"} ]' doc  
  from   dual  
)  
Select empid,last_name,first_name,deptid,salary
From  json_table( (Select StructType from json) , '$[*]'  
                Columns ( empid PATH '$.empid'
                        ,last_name Path '$.last_name'  
                        , first_name Path '$.first_name'  
                        ,deptid Path '$.deptid'
                        ,salary Path '$.salary'
                        )  
               );
               End;

SQL Server

以下代码所示,为多条记录如何在SQL Server的表类型中实现传递。

--Create Type structure

Create Type empTableType as Table
(
   empid integer,
    last_name varchar(100),
    first_name varchar(100),
    deptid integer,
    salary double precision
);

--Create Procedure

Create Procedure InsertEmpTable
@InsertEmpt_TVP empTableType READONLY
As
Insert Into emptable1(empid,last_name,first_name,deptid,salary)
Select * FROM @InsertEmpt_TVP;
Go

--Calling the SP with dynamic block and type

Declare @EmpTVP AS empTableType;
Insert Into @EmpTVP(empid,last_name,first_name,deptid,salary)
Values (1,'FirstName','Last_name',1,1234.566),
(2,'FirstName','Last_name',1,1234.566),
(3,'FirstName','Last_name',1,1234.566),
(4,'FirstName','Last_name',1,1234.566),
(5,'FirstName','Last_name',1,1234.566);
Exec InsertEmpTable @EmpTVP;
Go

PostgreSQL

以下代码所示,为多条记录如何在PostgreSQL中实现与之前Oracle及SQL Server相同的传递效果。

Do $$
Declare 
 StructType	Text Default '[{"empid" : "1", "last_name":"AccName1", "first_name":"AccName1", "deptid":"1", "salary":"1234.578"},
 					{"empid" : "2", "last_name":"AccName2", "first_name":"AccName2", "deptid":"2", "salary":"4567.578"}]';

Begin
Insert Into emptable
Select * From json_to_recordset(StructType::json) 
	as x("empid" Int, "last_name" Varchar, "first_name" Varchar, "deptid" Int, "salary" Double Precision);
End $$

Pivoting转换

在PostgreSQL当中,pivoting功能无法直接启用,需要额外扩展提供支持。tablefunc扩展带来的crosstab函数可用于创建数据pivot表,其功能与SQL Server及Oracle类似。以下是Oracle、SQL Server以及PostgreSQL中的pvioting功能代码。

Create Table crosstabFunc (
  id            Number,
  customer_id   Number,
  product_code  Varchar2(5),
  quantity      Number
);

Insert Into crosstabFunc values (1, 1, 'A', 10);
Insert Into crosstabFunc Values (2, 1, 'B', 20);
Insert Into crosstabFunc Values (3, 1, 'C', 30);
Insert Into crosstabFunc Values (4, 2, 'A', 40);
Insert Into crosstabFunc Values (5, 2, 'C', 50);
Insert Into crosstabFunc Values (6, 3, 'A', 60);
Insert Into crosstabFunc Values (7, 3, 'B', 70);
Insert Into crosstabFunc Values (8, 3, 'C', 80);
Insert Into crosstabFunc Values (9, 3, 'D', 90);
Insert Into crosstabFunc Values (10, 4, 'A', 100);

Oracle

使用以下代码在Oracle中实现pivoting功能。

Select *
From   (Select customer_id, product_code, quantity
        From   crosstabFunc)
Pivot  (Sum(quantity) As sum_quantity For (product_code) In ('A' AS a, 'B' AS b, 'C' AS c))
Order By customer_id;

SQL Server

使用以下代码在SQL Server中实现pivoting功能。

Select * From   
(Select customer_id, product_code, quantity
        From   crosstabFunc) as cf
Pivot  (Sum(quantity) For product_code In (A,B,C))
as cf1
Order By customer_id

PostgreSQL

使用以下代码在PostgreSQL中实现pivoting功能。

Create Extension tablefunc;

Select * From  Crosstab
(' Select customer_id, product_code, quantity
   From   crosstabFunc' )
   as T ( customer_id Int, "A" Int, "B" Int, "C" Int)

对数组进行unpivoting

PostgreSQL同样无法直接提供Unpivot函数。在将SQL Server或者Oracle转换为PostgreSQL时,unpivot函数会被映射为一个数组。具体请参见以下代码示例。

Create Table Students
(
	Id Int Primary Key Identity,
	Student_Name Varchar (50),
	Math_marks Int,
	English_marks Int,
	History_marks Int,
	Science_marks Int
)
Go
 
Insert Into Students Values ('Sally', 87, 56, 78, 91 )
Insert Into Students Values ('Edward', 69, 80, 92, 98)

Oracle

使用以下代码示例在Oracle中实现unpivoting功能。

Select StudentName, course,score
From   Students
Unpivot (score For course In (Math_marks AS 'Maths', English_marks AS 'English', History_marks AS 'History', Science_marks As 'Science'));

SQL Server

使用以下代码示例在SQL Server中实现unpivoting功能。

Select Student_Name, Course, Score
From Students
Unpivot
(
	Score
	For Course in (Math_marks, English_marks, History_marks, Science_marks)
) AS SchoolUnpivot

PostgreSQL

使用以下代码示例在PostgreSQL中实现unpivoting功能。

Select Student_Name, course, score From
 (
 Select   
  Student_Name, 
  Unnest (Array[ 'Math', 'English','History', 'Science']
    ) As course,
         
  Unnest (Array[ Math_marks, English_marks,History_marks,Science_marks] 
    ) As score
 From StudentsP  
 ) AS Unpvt

从单一函数处返回多个结果集

SQL Server可以将多条结果按多行形式直接返回。大家可以使用游标在PostgreSQL与Oracle中实现相同的效果,如以下示例所示。

Oracle

使用以下代码在Oracle中通过单一过程返回多个结果集。

Create Procedure Spgetdept23
(P_Cur Out Sys_Refcursor, P_Cur12 Out Sys_Refcursor)
Is
Begin
Open P_Cur For
Select * From employees;


Open P_Cur12 For
Select * From dept;

End;


  var cur Refcursor
  var cur2 Refcursor
Exec Spgetdept23(:cur,:cur2);
Print cur;
Print cur2;

SQL Server

使用以下代码在SQL Server中通过单一过程返回多个结果集。SQL Server不需要使用额外其他参数。

Create  Procedure Dbo.Multiple_Reseultset
As

Begin 
  
	Select * From HumanResources.Employee
	
	Select * From HumanResources.Department
	
End

To execute the procedure in SQL Server, enter the following code.

Exec Dbo.Multiple_Reseultset

使用以下代码在SQL ServerL中通过单一过程返回多个结果集。

Exec Dbo.Multiple_Reseultset

PostgreSQL

使用以下代码在PostgreSQL中通过单一过程返回多个结果集。

Create Or Replace Function Multiple_Reseultset() 
Returns Setof Refcursor As
$$
Declare
   cur1 Refcursor;
   cur2 Refcursor;
Begin 
   Open cur1 For
	Select * From HumanResources.employee;
	Return Next cur1;
	
	Open cur2 For
	Select * From HumanResources. Department;
	Return Next cur2;
End
$$ Language 'plpgsql';

要在PostgreSQL中执行此过程,请输入以下代码。

Begin
Select * From Public.Multiple_Reseultset( )
Fetch All In "<unnamed portal 1>"
Fetch All In "<unnamed portal 2>"
End

带别名的内联查询

PostgreSQL语义可将内联视图称为Subselect或者Subquery。Oracle支持在内部语句中省略别名,PostgreSQL与SQL Server则要求必须使用别名。

Oracle

使用以下代码在Oracle中执行内联查询演示。

Select a.col1, col2_fromSubquery  -- you can specify the columns directly from the subquery with out any prefix of subquery unless have common columns names.
from emplyee a,
  (select * from salary ) 
where  active=true

SQL Server与PostgreSQL

在Oracle中编写的示例内联查询若要在SQL Server及PostgreSQL中运行,则必须使用别名。

Select a.col1, b.col2_fromSubquery
from emplyee a,
  (select * from salary ) b
where  active=true

数据顺序

将数据从Oracle或SQL Server迁移至PostgreSQL之后,数据的检索顺序也可能发生改变。这种改变可能是受到了插入顺序、列数据类型及具体数值、或者排序规则的影响。

为了保证数据顺序的正确性,我们需要确定业务需求并在查询当中采用Order by子句以匹配数据内容。

dblink与外部数据包装器

dblink是一项负责在同构与异构数据库间实现通信的功能。截至本文撰写之时,Amazon RDS与Aurora PostgreSQL还不提供异构支持,但已经能够支持跨PostgreSQL数据库间的通信。

跨同构数据库通信

PostgreSQL可利用dblink与外部数据包装器(FDW)实现跨数据库间的正常通信。在本节中,我们将具体聊聊dblink与FDW的使用方法。

使用外部数据包装器

  • 使用以下代码创建该扩展。
Create Extension postgres_fdw;
  • 使用以下代码创建服务器并接入外部数据库。
Create Server server_name1 Foreign Data Wrapper
postgres_fdw Options (host abcd.rds.amazonaws.com' dbname abcd, port '5432');
  • 使用以下代码创建用户映射,借此访问外部数据库中的表。
Create User Mapping For Current_User
Server server_name1
Options  (user 'pgar1234', password 'pgar1234'); 
  • 为每一位需要通过FDW通信的用户创建用户映射。
  • 将全部外部表导入本地schema,这样我们就能像访问常规表那样访问外部表中的数据。以下为从外部数据库及schema执行表导入的示例代码。
Create Schema imported_public2 -- created local schema


Import Foreign Schema public From Server server_name1
Into imported_public2; -- This will import all the tables
Select * From imported_public2.emptable

跨异构数据库通信

PostgreSQL不支持跨数据库通信。在实现跨数据库的异构通信方面,Amazon Aurora PostgreSQL确实存在一定局限,但大家可以在源环境(例如Oracle或者SQL Server)上建立指向目标(PostgreSQL)的dblink,而后对数据执行pull或push操作。

若需了解更多详细信息,请参阅 在Compose PostgreSQL上进行跨数据库查询。

使用dblink为外部数据库表创建视图

dblink属于一项PostgreSQLcontrib扩展,可帮助用户在其他数据库中执行简短的即席查询。要使用dblink选项,大家必须以对其他用户可见的明文形式提供并保存密码。除非别无选择,否则我们不建议您使用这一选项。

若需了解更多详细信息,请参阅外部数据包装器postgres_fdw说明文档

选项一:在SQL语句之内提供目标数据库的访问细节

在这种选项中,每一次主机或连接的细节信息发生变化,我们都需要反复在主机连接与数据库凭证处做出多项对应调整。

Create Or Replace View emptable_dblink As
Select emptable.empid, emptable.last_name , emptable.first_name
   	From Dblink('host=abcd.rds.amazonaws.com user=abcd 
		 password=abcd dbname=abcd  port=5432',
		Select empid,last_name,first_name FROM emptable')
		AS emptable(empid Int,last_name Varchar , first_name Text );
Select * From emptable_dblink;

选项二:对访问细节进行拆分,并使用连接对象

在这种选项中,主机与连接细节在同一个位置进行定义,并使用连接名称实现跨数据库连接。

Select Dblink_Connect('conName','dbname=abcd user=abcd 
		password=abcd host= abcd.rds.amazonaws.com ');
	
Create Or Replace View mytabview1 As
Select mytable.* From
    	Dblink('conName', Select empid,last_name,first_name FROM emptable')
As mytable(empid Int,last_name Varchar , first_name Text);
Select * From mytabview1;

使用dblink进行函数调用

以下代码为来自外部PostgreSQL数据库的函数,运行后将返回一个整数。

Select * From 
Dblink('host=abcd.rds.amazonaws.com user=abcd 
		 password=abcd dbname=postgres  port=5432',
		'Select public.add_ten(10)')
		As add_ten(a Int);

以下代码为来自外部PostgreSQL数据库的函数,运行后将返回一个表类型。

Select Dblink_Connect('conName','dbname=pgar1234 user=pgar1234 
		password=pgar1234 host=pgar1234.ctegx79rcs0q.ap-south-1.rds.amazonaws.com');

Select Dblink_Open('conName','foo2',
		'Select * From public.tabletypetest(10)');
	
Select * From Dblink_Fetch('conName','foo2', 5) As (empid Int, last_name Varchar);

从一组数字中找出最大与最小值

在面向PostgreSQL进行迁移时,我们可能需要找出最大与最小值。PostgreSQL中包含一项最大/最小值查找函数,具体参见以下示例代码。

Select Greatest(1,2,3,50,100)
-> 100

Select Least(1,2,3,50,100) 
-> 1

考虑使用自联接以实现更新

当在select语句中的from子句内使用相同的源表(正在更新的表)时,PostgreSQL与SQL Server的具体更新机制将有所区别。与SQL Server不同,PostgreSQL中from子句的第二次引用将独立于第一次引用,且变更将被应用于整个表。

以下示例代码,用于更新部门1中员工的薪水。

Update employee 
		Set salary = employee.salary + employee.salary * 0.10
	From Employee e
	Join dept d on d.deptid = e.deptid
	Where d.deptid=1 

此函数在SQL Server的起效方式并无区别;但在迁移之后,同一SQL语句的更新范围将由当前部门扩展至整个表。这是由于PostgreSQL会假定两个employee表彼此独立,这与SQL Server完全不同。

要更新单一部门中的数据,应将DML转换为以下代码。

Update Employee e
		Set salary = e.salary + e.salary * 0.10
	From dept d  
	Where d.deptid = e.deptid
 		And d.deptid=1 

如果使用Oracle,则将DML转换为以下代码。

Update Employee e
		Set Salary = e.salary + e.salary * 0.10
	Where Exists (Select 1 from dept d where d.deptid = e.deptid
 		And d.deptid=1 )

总结

本文从商业数据库到PostgreSQL的迁移场景出发,向开发者朋友们分享了一些技巧与最佳实践。本文的重点在于介绍迁移过程中需要面对的种种决策,以及决策结果给数据库性能造成怎样的影响。在迁移过程中,请牢记这些性能方面的影响因素,这将帮助大家提前避免随后可能因迁移出现的种种性能问题。

如果您对本文还有任何疑问或者建议,请在下方评论中分享您的看法。

 

本篇作者

Viswanatha Shastry Medipalli

印度AWS ProServe团队顾问。他在SQL数据库迁移领域拥有广泛的专业知识与经验积累。他曾参与设计过众多成功的数据库解决方案,帮助客户克服一系列极具挑战性的业务难题。他曾利用Oracle、SQL Server以及PostgreSQL构建起用于报告、商务智能、应用程序及开发等场景的解决方案。此外,他还拥有丰富的自动化与编排专业知识。目前,他的工作重心是由本地数据库向Amazon RDS/Aurora PostgreSQL的同构与异构迁移。