簡介
在當今這個互聯網和經濟全球化時代,需要使用越來越多的應用程序處理不同國家語言表示的數據。對開發人員而言,這就意味著在應用程序開發的各個階段 — 數據庫設計、應用程序設計和應用程序編程 — 都要考慮到不同國家的語言需求。DB2 9 支持具有不同屬性的各種語言,比如重音符號(法語)、雙向(阿拉伯語)和大字符集(中文)。這些語言在存儲、處理、訪問和表示數據方面提出了各自不同的挑戰。受國家語言影響的數據不僅限於字符串數據。還包括有數值型、日期型和貨幣型數據。
字符與字符串數據的字節語義之間的對比
DB2 9 以前版本中的一些字符串函數從字節和雙字節單元的混合角度處理字符和圖形數據。正如之前解釋的那樣,越來越多的用戶根據不同國家語言的字符來考慮數據。DB2 9 的新功能解決了字符組成及其長度計算方面的問題,本文將討論這些新功能。
對於單字節字符編碼模式,一個字節組成一個字符,單字節字符串的長度與字符串的字節長度相同。對於圖形字符串,兩個字節組成一個字符,使用雙字節數來表示字符串的長度。但是對於多字節編碼,字符的字節長度隨使用編碼模式的不同而不同,每個字符的長度可能是一個字節或多個字節。本文中將使用字節計算字符串長度的方法稱作字節語義,而使用字符數計算字符串長度的方法稱作字符語義。
考慮以下的中文字符串:
圖 1. 中文字符串
如果使用字符語義計算字符串的長度,則該字符串的長度為 2。但是如果使用字節語義並使用 UTF-8 對字符進行編碼,則該字符串的長度為 6 字節。
對基於字符的函數的需求
SQL 中基於字符的數據在很多上下文中都與數值有關,如下所述:
字符串變量的長度:SUBSTR 函數的輸入參數,決定了結果字符串的期望長度或 LENGTH 函數的輸出。
字符串中的偏移量:LOCATE 函數的第二個參數,指定了字符串中開始搜索的起始位置。
這些數值表示單字節數據的字節數和圖形或雙字節數據的雙字節數。但是對於多字節字符編碼(如 UTF-8),這些數值並不符合字符語義。下面的條件可以幫助我們理解為何需要基於字符的函數。
字符的組成
將字符看作一個單元而不是一個字節序列,這是進行多字節字符的字符串操作的必要條件。應用程序開發人員需要知道,分配緩沖區時應該給每個字符分配多大內存。因此,理解字符組成對編寫應用程序處理多字節字符數據非常重要。 可以將字符定義為一個信息單元,對應於書面語言的一個原子單元。每個字符由一個使用字符編碼的位序列表示。單個字符通常使用一個字節或多個字節進行編碼,具體情況取決於使用的編碼方式。 考慮字符 “A” 和 “上面帶圈的大寫拉丁字母 A”。字符 “A” 的十六進制表示是 x‘41’ 而 “上面帶圈的大寫拉丁字母 A” 的十六進制表示是 x‘C385’。通過 SQL 函數 hex() 可以獲取此表示。
圖 2. 字符的十六進制表示
從上面的表示可以看到,顯示期間只存在一個字符。但是,“A” 的長度是一個字節而 “上面帶圈的大寫拉丁字母 A” 的長度則是兩個字節。
根據代碼單元計算的字符串長度
字符字符串的長度取決於用於編碼字符的字符編碼方式(ASCII、EBCDIC 和 Unicode)。可以使用一個或多個各自編碼的代碼單元來表示字符。因此,如果字符串中有相同的字符集,則其長度可能隨使用編碼方式的不同而有所不同。 考慮一個字符例子 “音符 G 音譜號”。考慮表 1 中對此字符的不同編碼,您會發現不同代碼單元中的不同編碼的十六進制表示及其長度都有所不同。
表 1. 相同字符不同編碼的十六進制表示
編碼
UTF-8
UTF-16(Big-Endian)
UTF-32(Big-Endian)
十六進制格式
X'F09D849E'
X'D834DD1E'
X'0001D11E'
各自代碼單元的長度
4
2
1
從圖 3 可以看出如何獲取 “音符 G 音譜號” 字符按 UTF-8 編碼時的字節長度。
圖 3. “音符 G 音譜號” 的字節長度
搜索字符
在字符串中搜索特定的子字符串時,首先執行搜索,然後返回的結果(字符串中的位置)為字節位置數,而不是正確的字符或代碼單元的位置。圖 4 展示了對 “a” 的搜索,“a” 的實際字符位置是 2,而輸出的位置是 3,原因在於字符串中有多字節字符。
圖 4. 字符串中的搜索結果
字符分解
將多字節字符數據看作字節序列可能導致字符串函數執行意外的字符分解。在圖 5 中,已經指定字符串第一個字節的長度為 1 的子字符串。由於第一個字符是多字節的,因此會導致字符分解和錯誤輸出。
圖 5. SUBSTR 函數分解字符
指定起始位置
可能需要為 LOCATE 之類的函數提供輸入以指定搜索的起始位置。對於多字節數據可能會存在一些問題,可能得不到預期的結果。在圖 6 中,搜索第三個字節後的字符,如果所有的字符都是單字節字符的話應該搜索到第二次出現的 “a” 字符。但是由於第一個字符是多字節字符,因此得到結果 3,它是搜索字符串的第一次出現的位置。
圖 6. 使用 LOCATE 指定起始位置
基於字符的函數
除了 DB2 早期版本中使用字節語義處理字符數據的字符串函數之外,DB2 9 還引入了一組理解字符語義的基於字符的字符串函數。如果采用特殊編碼的某個字符的長度跨越了多個字節,則基於字符的字符串函數可以將每個字符處理為一個單元而不是一個字節序列。
引入字符串長度單元
DB2 的基於字符的字符串函數引入了字符串長度單元的概念來理解字符編碼,根據該概念,考慮使用輸入字符串來進行字符串操作。DB2 9 for Linux, UNIX, and Windows 的字符串單元分別為 OCTETS、CODEUNITS16 和 CODEUNITS32。
字符串函數擁有數值規范,或者說結果是輸入數據相關的數值。字符串長度單元屬於數值。待執行的字符串操作可能導致不同的輸出,取決於計算字符所使用的字符串長度單元。一些函數的輸入是數值,比如字符串函數的起始、長度和偏移量參數。而其他一些函數的返回結果是數值,比如搜索字符串中指定的子字符串的出現位置,首先執行搜索然後結果返回為字符串長度單元中隱式或顯式指定的數字。
使用 OCTETS 作為字符串長度單元時,通過簡單地計算字符串的字節數即可確定字符串的長度。CODEUNITS16 指定將 Unicode UTF-16 用於字符語義。同樣,CODEUNITS32 指定使用 Unicode UTF-32 來理解多字節字符的字符邊界。
使用 CODEUNITS16 或 CODEUNITS32 計算代碼單元得到的結果是相同的,除非使用了增補字符和代理對。使用增補字符時,對於一個增補字符,使用 CODEUNITS16 計算是兩個 UTF-16 代碼單元,而使用 CODEUNITS32 計算則是一個 UTF-32 代碼單元。
如果使用 CODEUNITS 來獲取字符的長度,則用作字符串函數輸入的 CODEUNITS 的不同會導致輸出的不同。
清單 1. 使用不同的 CODEUNITS 所得到的字符串長度
VALUES CHARACTER_LENGTH(X'F09D849E', OCTETS)
1
-----------
4
1 record(s) selected.
VALUES CHARACTER_LENGTH(X'F09D849E', CODEUNITS16)
1
-----------
2
1 record(s) selected.
VALUES CHARACTER_LENGTH(X'F09D849E', CODEUNITS32)
1
-----------
1
1 record(s) selected.
DB2 9 中基於字符的字符串函數
CHARACTER_LENGTH
如 SQL 標准中所述,此函數使用字符語義查找字符字符串的長度。此函數與 DB2 中的 LENGTH 函數類似,並擁有一個可選的字符串長度單元,可以用來表示結果。與 LENGTH 函數不同,CHARACTER_LENGTH 只接受基於字符串的輸入數據。該函數包含兩個參數,第一個參數是字符串,而第二個參數是代碼單元。在很多情形下,您需要根據代碼單元計算的字符串長度,可以使用基於字符的函數來獲取根據字符串單元計算的字符串長度。
考慮之前討論的 “音符 G 音譜號” 字符例子。
清單 2. 使用 CHARACTER_LENGTH 獲取基於 CODEUNITS 的字符串長度
VALUES CHAR_LENGTH(X'F09D849E',CODEUNITS16)
1
-----------
2
1 record(s) selected.
VALUES CHAR_LENGTH(X'F09D849E',CODEUNITS32)
1
-----------
1
1 record(s) selected.
使用基於字符的字符串函數可以解決獲取基於 CODEUNITS 的字符串長度時的問題。
OCTET_LENGTH
如 SQL 標准中所述,此函數返回輸入字符串的八位字節長度或字節長度。它與對單字節數據類型使用 LENGTH 函數類似。如果使用雙字節數據類型作為輸入,它就會給出雙倍的 LENGTH 函數值。使用 CHARACTER_LENGTH 並使用 OCTETS 作為字符串長度單元時也會產生同樣的功能。
清單 3. 使用 OCTECT_LENGTH 獲取字符串的字節長度
VALUES OCTET_LENGTH(X'F09D849E')
1
-----------
4
1 record(s) selected.
LOCATE
LOCATE 函數返回一個字符串在另一個字符串中第一次出現的起始位置。如果沒有找到搜索字符串,並且參數都不為空,則所得的結果是零。如果找到搜索字符串,則所得結果是一個從 1 到源字符串實際長度之間的一個數字。如果指定了可選的起始位置,則表明它是源字符串中開始進行搜索的字符位置。可以指定一個可選的字符串長度單元來指明在哪些單元中表示函數的起始位置和結果。
可以使用基於字符的函數來解決在 LOCATE 函數中指定起始位置的問題,如圖 7 所示:
圖 7. 通過 CODEUNITS 使用 LOCATE
POSITION
POSITION 函數返回一個字符串在另一個字符串中第一次出現的起始位置。如果沒有找到搜索字符串,並且參數都不為空,則所得的結果是 0。如果找到搜索字符串,則所得結果是一個從 1 到輸入字符串實際長度之間的一個數字,使用顯式指定的代碼單元來表示。POSITION 函數是在 SQL 標准中進行定義的。它與 DB2 家族之間實現的 POSSTR 函數相似但不相同。
使用基於字符的函數可以解決將字節位置返回為字符位置的問題。圖 8 展示了如何使用 LOCATE 函數來實現此目的。
圖 8. 通過 CODEUNITS 使用 POSITION
SUBSTRING
SUBSTRING 函數返回字符串的子字符串。子字符串是輸入字符串的零個或多個相鄰字符串長度單元。除了輸入字符串之外,SUBSTRING 函數還有其他三個參數,它們分別是:起始位置、長度和代碼單元指定。起始位置指定了輸入字符串中結果的第一個字符串長度單元所在的位置。長度參數指定了所需子字符串的長度。使用基於字符的函數時不會發生用於構建字符的 CODEUNITS 分解。圖 9 展示了如何防止多字節字符的分解。
圖 9. 通過 CODEUNITS 使用 SUBSTRING
圖片看不清楚?請點擊這裡查看原圖(大圖)。
處理不正確的數據或不完整的數據
涉及多字節字符的字符串操作可能遇到字符不正確(編碼中沒有定義字節合並)或字符不完整(擁有多字節字符的部分字節)的情形。考慮在使用新的基於字符的字符串函數執行字符串操作時可能導致這一狀況的常見情形。字符 “音符 G 音譜號”(UTF-8 十六進制格式為 X‘F09D849E’)就是這樣的例子,使用 CODEUNITS16 時其長度為 2。
輸入字符串的問題
不完整的字符串數據
可以將擁有部分字符的字符串數據稱為不完整的字符串數據。假設您擁有一個 UTF-8 編碼的字符,其長度為 3 字節,而字符串只擁有編碼的前兩個字節。如果您使用 CODEUNITS16 來計算前兩個字節的長度,則函數將給出一個警告。
清單 4. 使用不完整的輸入字符串數據
VALUES CHARACTER_LENGTH(X'849E',CODEUNITS16)
1
-----------
2
SQL1289W During conversion of an argument to "SYSIBM.CHARACTER_LENGTH"
from code page "1208" to code page "1200", one or more invalid characters were
replaced with a substitute character, or a trailing partial multi-byte character was
omitted from the result. SQLSTATE=01517
1 record(s) selected with 1 warning messages printed.
不正確的字符串數據
每種字符編碼都具有針對特殊字符的字節集或字節組合集。字符串函數的輸入字符串數據可能擁有源字符串中的一些錯誤字符或無效字符。如果 DB2 在執行 CODEUNITS16 或 CODEUNITS32 計算時遇到無效字符,則它在字節序列形成部分函數結果時用替代字符替換任何此類的字節序列。十六進制格式的 X‘80’ 用 UTF-8 編碼是無效的,遇到它時會拋出警告。
清單 5. 使用不完整的字符數據
VALUES CHARACTER_LENGTH(X'80',CODEUNITS16)
1
-----------
1
SQL1289W During conversion of an argument to "SYSIBM.CHARACTER_LENGTH"
from code page "1208" to code page "1200", one or more invalid characters were
replaced with a substitute character, or a trailing partial multi-byte character
was omitted from the result. SQLSTATE=01517
1 record(s) selected with 1 warning messages printed.
OCTETS 和 圖形字符串輸入
在 SUBSTRING FUNCTION 中,指定了 OCTETS 並且函數的輸入是圖形數據,而 <start> 參數不是奇數或 <length> 參數不是偶數,則會導致類似將圖形字符分解為兩個字節那樣的錯誤。
清單 6. 字符分解 VALUES SUBSTRING(GRAPHIC('K'),2,1,OCTETS)
1
--
SQL20289N Invalid string length unit "OCTETS" in effect for function
"SYSIBM.SUBSTRING". SQLSTATE=428GC
輸出字符串的問題
獨立代理或不完整的字符串數據
當使用兩個 16 位代碼單元序列表示字符時,將該字符稱為代理對。代理對可以區分為高代理和低代理。在字符串函數中使用 CODEUNITS16 時,DB2 進行單一代碼單元或獨立代碼單元的區分。即,如果您擁有一個代理對,則使用 CODEUNITS16 的字符長度為 2,而使用 CODEUNITS32 的字符長度為 1。因此如 SUBSTRING 之類的函數可以根據您提供的參數分解代理對。
替換字符插入導致緩沖區溢出
插入替換字符時,字符串的字節長度可能增加。如果長度增加超出了輸出所能使用的緩沖區空間,則字符串的尾部將被截斷,而且將收到一個警告:將字符串指派給另一個長度較短的字符串數據類型時字符串的值將被截斷。
向後兼容
注意:新的字符串函數位於 SYSIBM 函數路徑下,而舊一些的函數位於 SYSFUN 路徑下。希望您使用新的 SYSIBM 函數路徑,既便您沒有使用字符串單元參數也是如此。默認情況下,在默認的 CURRENT PATH 中 SYSIBM 函數路徑位於 SYSFUN 之前。所有的舊函數仍然受到支持。
性能考慮
基於字符的函數可能需要將輸入數據字符串轉換為一個中間的 UNICODE 代碼頁,比如 UTF-16 或 UTF-32,然後才能對它進行處理。如果結果數據是一個字符串,那麼中間結果也要轉換回輸入代碼頁。OCTETS 作為一種字符串單元指定不需要任何轉換,而且使用起來更加有效。CODEUNITS16 和 CODEUNIST32 作為字符串單元可能導致代碼頁轉換。雖然 DB2 執行自我優化,但是是否需要代碼頁轉換還不清楚。轉換成本對 LOB 輸入更加重要,因為輸入字符串的大小可能很大。
結束語
本文向您簡要地介紹了 DB2 數據服務器中新增的基於字符的字符串函數。首先介紹了一些關鍵概念,如針對字符串數據的字符語義和字節語義。接著討論了需要使用這些函數的原因,並舉例說明了一些常見的場景。還了解了代碼單元和基於字符的函數的概念。然後解釋了這些函數如何幫助您解決之前討論的問題,並對每個場景進行舉例說明。最後,討論了使用這些函數時的常見問題和性能考慮。理想情況下,應該使用這些函數更好地執行字符串操作,將更多的應用程序邏輯植入 SQL 層而不是在應用程序中實現這些邏輯。