儲存程序和函數實現了資料庫的業務邏輯。在將 SQL Server 資料庫遷移到 PostgreSQL 時,您需要正確轉換儲存程序和函數,特別注意參數處理、行集檢索以及其他特定的語法結構。
SQL Server 使用一種稱為 Transact-SQL(或 T-SQL)的 SQL 方言來編寫儲存程序和函數,而 PostgreSQL 則使用程序語言/PostgreSQL(或 PL/pgSQL)來實現相同的功能。這些語言的語法和功能有顯著的不同,因此儲存程序和函數必須仔細分析和轉換。
此外,一些 T-SQL 特性在 PL/pgSQL 中沒有直接的對應,因此在這些情況下需要替代實現。最後,儲存程序和函數必須針對 PostgreSQL 引擎進行優化,以確保它們能有效執行。
返回行集
SQL Server 和PostgreSQL允許從儲存過程或函數返回行集,通常是 SELECT
查詢的結果,但語法有所區別。如果 T-SQL 中的儲存過程包含 SELECT
作為主體的最後語句,這意味著它返回行集。PL/pgSQL 需要提前聲明返回行集為一個表,或通過 refcursor
獲取數據。
當返回的行集只有幾個明確類型的列時,可以使用 PostgreSQL 的 RETURNS TABLE
功能。
在 T-SQL 中:
CREATE PROCEDURE GetCustomerOrders @CustomerID INT
AS
SELECT OrderID, OrderDate, Amount FROM Orders WHERE CustomerID = @CustomerID;
GO
在 PL/pgSQL 中,類似的寫法可能是這樣:
CREATE OR REPLACE FUNCTION GetCustomerOrders(CustomerID INT)
RETURNS TABLE(OrderID INT, OrderDate TIMESTAMP, Amount DECIMAL)
AS $$
BEGIN
RETURN QUERY SELECT OrderID, OrderDate, Amount FROM Orders WHERE CustomerID = GetCustomerOrders.CustomerID;
END;
$$ LANGUAGE plpgsql;
而呼叫者的 PostgreSQL 代碼可能是這樣:
SELECT * FROM GetCustomerOrders(5);
如果返回的行集更複雜,且難以確定每列的數據類型,則上述方法可能無法使用。對於這些情況,替代方法是使用 refcursor
。
在 T-SQL 中:
CREATE PROCEDURE GetSalesByRange
@DateFrom DATETIME,
@DateTo DATETIME
AS
SELECT C.CustomerID, C.Name AS CustomerName, C.FirstName, C.LastName,
C.Email AS CustomerEmail, C.Mobile, C.AddressOne, C.AddressTwo, C.City,
C.ZipCode, CY.Name AS Country, ST.TicketID, TT.TicketTypeID,
TT.Name AS TicketType, PZ.PriceZoneID, PZ.Name AS PriceZone,
ST.FinalPrice AS Price, ST.Created, ST.TransactionType,
COALESCE(VME.ExternalEventID, IIF(E.ExternalID = '', NULL, E.ExternalID), '0') AS ExternalID,
E.EventID, ES.[Name] AS Section, ST.RowName, ST.SeatName
FROM [Event] E WITH (NOLOCK)
INNER JOIN EventCache EC WITH (NOLOCK) ON E.EventID = EC.EventID
INNER JOIN SaleTicket ST WITH (NOLOCK) ON E.EventID = ST.EventID
INNER JOIN EventSection ES WITH (NOLOCK) ON ST.EventSectionID = ES.EventSectionID
INNER JOIN Customer C WITH (NOLOCK) ON ST.CustomerID = C.CustomerID
INNER JOIN Country CY WITH (NOLOCK) ON C.CountryID = CY.CountryID
INNER JOIN TicketType TT WITH (NOLOCK) ON ST.TicketTypeID = TT.TicketTypeID
INNER JOIN PriceZone PZ WITH (NOLOCK) ON ST.PriceZoneID = PZ.PriceZoneID
LEFT OUTER JOIN VenueManagementEvent VME ON VME.EventID = E.EventID
WHERE ST.Created BETWEEN @DateFrom AND @DateTo
ORDER BY ST.Created
GO
在 PL/pgSQL 中:
CREATE OR REPLACE FUNCTION GetSalesByRange (
V_DateFrom TIMESTAMP(3),
V_DateTo TIMESTAMP(3),
V_rc refcursor
)
RETURNS refcursor
AS $$
BEGIN
OPEN V_rc FOR
SELECT C.CustomerID, C.Name AS CustomerName, C.FirstName, C.LastName,
C.Email AS CustomerEmail, C.Mobile, C.AddressOne, C.AddressTwo, C.City,
C.ZipCode, CY.Name AS Country, ST.TicketID, TT.TicketTypeID,
TT.Name AS TicketType, PZ.PriceZoneID, PZ.Name AS PriceZone,
ST.FinalPrice AS Price, ST.Created, ST.TransactionType,
COALESCE(
VME.ExternalEventID,
(CASE WHEN E.ExternalID = '' THEN NULL ELSE E.ExternalID END),
'0') AS ExternalID,
E.EventID, ES.Name AS Section, ST.RowName, ST.SeatName
FROM Event E
INNER JOIN EventCache EC ON E.EventID = EC.EventID
INNER JOIN SaleTicket ST ON E.EventID = ST.EventID
INNER JOIN EventSection ES ON ST.EventSectionID = ES.EventSectionID
INNER JOIN Customer C ON ST.CustomerID = C.CustomerID
INNER JOIN Country CY ON C.CountryID = CY.CountryID
INNER JOIN TicketType TT ON ST.TicketTypeID = TT.TicketTypeID
INNER JOIN PriceZone PZ ON ST.PriceZoneID = PZ.PriceZoneID
LEFT OUTER JOIN VenueManagementEvent VME ON VME.EventID = E.EventID
WHERE ST.Created BETWEEN V_DateFrom AND V_DateTo
ORDER BY ST.Created;
RETURN V_rc;
END;
$$ LANGUAGE plpgsql;
而呼叫者的 PostgreSQL 代碼可能是這樣:
BEGIN;
SELECT GetSalesByRange(
'2024-01-01'::TIMESTAMP(3),
'2025-01-01'::TIMESTAMP(3),
'mycursorname'
);
FETCH 4 FROM mycursorname;
COMMIT;
局部變數的聲明
T-SQL 允許在儲存過程或函數主體內的任何地方聲明局部變數。PL/pgSQL 要求所有局部變數在 BEGIN
關鍵字之前聲明:
CREATE OR REPLACE FUNCTION CreateEvent(…)
AS $$
DECLARE
v_EventID INT;
v_EventGroupID INT;
BEGIN
…
END;
$$ LANGUAGE plpgsql;
在 SQL Server 中,可以如下宣告資料表變數:
DECLARE @Products TABLE
(
ProductID int,
ProductTitle varchar(100),
ProductPrice decimal (8,2)
)
PostgreSQL 不支援此功能;應使用臨時表來替代:
CREATE TEMP TABLE Products
(
ProductID int,
ProductTitle varchar(100),
ProductPrice decimal (8,2)
)
請記住,臨時表會在會話或當前事務結束時自動刪除。如果您需要明確管理表的生命週期,請使用 DROP TABLE IF EXISTS
語句。
注意適當的 SQL Server 到 PostgreSQL 的類型 映射 在轉換變數宣告時。
自動增量欄位的最後值
在執行 INSERT
查詢後,您可能需要獲取自動增量欄位生成的值。在 T-SQL 中,可以這樣獲得:
CREATE TABLE aitest (id int identity, val varchar(20));
INSERT INTO aitest(val) VALUES ('one'),('two'),('three');
SELECT @LastID = SCOPE_IDENTITY();
PostgreSQL 允許通過一個自動生成的序列訪問最後插入的值,該序列的名稱始終為 {tablename}_{columnname}_seq
::
CREATE TABLE aitest (id serial, val varchar(20));
INSERT INTO aitest(val) VALUES ('one'),('two'),('three');
LastID := currval('aitest_id_seq’);
內建函數
在將儲存過程和函數從 SQL Server 遷移到 PostgreSQL 時,所有特定的內建函數和運算子必須根據以下規則轉換為等效的:
- 函數
CHARINDEX
必須被 PostgreSQL 的等效函數POSITION
取代。 - 函數
CONVERT
必須根據這篇文章遷移到PostgreSQL中 - 函數
DATEADD($interval, $n_units, $date)
可以轉換為使用運算符+
的PostgreSQL表達式,具體取決於$interval
的值如下:天 / DD / D / 年中的天數 / DY
($date + $n_units * interval ‘1 day’)::date
小時 / HH ($date + $n_units * interval ‘1 hour’)::date 分鐘 / MI / N ($date + $n_units * interval ‘1 minute’)::date 月份 / MM / M ($date + $n_units * interval ‘1 month’)::date 季度 / QQ / Q ($date + $n_units * 3 * interval ‘1 month’)::date 秒 / SS / S ($date + $n_units * interval ‘1 second’)::date 週 / WW / WK ($date + $n_units * interval ‘1 week’)::date 星期 / DW / W ($date + $n_units * interval ‘1 day’)::date 年 / YY ($date + $n_units * interval ‘1 year’)::date
- 函數
DATEDIFF($interval, $date1, $date2)
在 PostgreSQL 中可以透過DATE_PART
來模擬如下:天 / DD / D / 年中的第幾天 / DY date_part(‘day’, $date2 – $date1)::int
小時 / HH 24 * date_part(‘day’, $date2 – $date1)::int + date_part(‘hour’, $date2 – $date1) 分鐘 / MI / N 1440 * date_part(‘day’, $date2 – $date1)::int + 60 * date_part(‘hour’, $date2 – $date1) + date_part(‘minute’, $date2 – $date1) 月份 / MM / M (12 * (date_part(‘year’, $date2) – date_part(‘year’, $date1))::int + date_part(‘month’, $date2) – date_part(‘month’, $date1))::int 秒 / SS / S 86400 * date_part(‘day’, $date2 – $date1)::int + 3600 * date_part(‘hour’, $date2 – $date1) + 60 * date_part(‘minute’, $date2 – $date1) + date_part(‘second’, $date2 – $date1) 週 / WW / WK TRUNC(date_part(‘day’, $date2 – $date1) / 7) 星期幾 / DW / W date_part(‘day’, $date2 – $date1)::int 年 / YY (date_part(‘year’, $date2) – date_part(‘year’, $date1))::int - 每次出現的
DATEPART
必須替換為DATE_PART
- SQL Server 函數
GETDATE
必須轉換為 PostgreSQLNOW()
- 條件運算子
IIF($condition, $first, $second)
必須轉換為CASE WHEN $condition THEN $first ELSE $second END
- 每個出現的
ISNULL
必須替換為COALESCE
- SQL Server 函數
REPLICATE
必須轉換為 PostgreSQL 對應的REPEAT
- 每個出現的
SPACE($n)
必須替換為REPEAT(' ', $n)
結論
在兩個 DBMS之間遷移存儲過程和函數是一個相當複雜的過程,需要大量時間和精力。雖然無法完全自動化,但一些在線可用的工具可以幫助部分自動化該過程。
Source:
https://dzone.com/articles/convert-stored-procedures-sql-server-to-postgresql