[SQL]中文字排序和過濾的問題處理

[SQL]中文字排序和過濾的問題處理

遇到一個好友傳來一個問題,想要請我幫忙解釋為什麼資料在 SQL Server 內排序怎麼怪怪的,於是花了一點時間測試一下,他的案例是這樣的:

 

原本的狀況是這樣,資料也會按照所預定的排序去排序資料

-- 測試資料
DECLARE @TABLE TABLE ( NAME VARCHAR(10) )
INSERT @TABLE VALUES ( '舊'  ), ( '勞'  ),  ( '9舊' ), ( '9勞' )
-- 測試指令
SELECT NAME 
  FROM @TABLE ORDER BY NAME

image

 

但後來為了考量 UNICODE 的處理,因此把欄位的型態給換成的 NVARCHAR,但怎麼前面兩筆排列的方式和預期的不同呢 ?

-- 測試資料
DECLARE @TABLE TABLE ( NAME NVARCHAR(10) )
INSERT @TABLE VALUES ( N'舊'  ), ( N'勞' ), ( N'9舊' ), ( N'9勞' )
-- 測試指令
SELECT NAME
  FROM @TABLE ORDER BY NAME

image

 

於是詢問一下他的測試環境中的定序,是採用在台灣很常被使用的 「Chinese_Taiwan_Stroke_BIN 」,以往會使用這樣的定序是因為把資料直接用 ASCII 排列,不用做一些轉換,因此會是比較快的一種,但資料為什麼改成 NVARCHAR 型態之後,會排列成這樣呢 ? 根據 MSDN 上的文件說明之中,有兩段很重要的敘述:

「如果是非 Unicode 資料類型,資料比較是依據 ANSI 字碼頁中所定義的字碼元素。 如果是 Unicode 資料類型,資料比較則是依據 Unicode 字碼指標。」

「舊版 SQL Server 中的二進位定序會以 WCHAR 比較第一個字元,然後使用逐一比較位元組的方式。 為了與舊版相容,現有的二進位定序語意不變。」

 

這樣看起來似乎還是很文言文, 我把範例改一下大家就比較容易看懂了

-- 範例資料
DECLARE @TABLE TABLE ( NAME NVARCHAR(10) )
INSERT @TABLE VALUES 
 ( N'舊' ), ( N'勞' ), ( N'9舊' ), ( '9勞' ), ( N'張舊' ), ( '張勞' )
-- 範例指令                      
SELECT 
  NAME,
  CAST(NAME as VARBINARY ) BINARY,
  UNICODE(SUBSTRING(NAME,1,1)) UNICODE1,UNICODE(SUBSTRING(NAME,2,1)) UNICODE2
  FROM @TABLE ORDER BY NAME

image

 

從上面的表中看出,如果定序是 Chinese_Taiwan_Stroke_BIN  的模式下,第一個字會先用 UNICODE 比對,而「舊」和「勞」這兩個字剛好 ASCII CODE 和 UNICODE 的順序相反,因此如果用 ANSI 字碼表比較的時候「舊」會排在「勞」的前面;但如果用 UNICODE 來看的話,「勞」反而排在「舊」的前面;而在第一個字後面的,則會一個一個字都用 ANSI 字碼表來比對,因此會有這樣的狀況。

 

既然知道了原因,那要怎麼要來避免排序和過濾資料呢 ? 我個人是建議可以參考 MSDN 的作法,把資料庫的定序換成新版的二進位定序,也就是「Chinese_Taiwan_Stroke_BIN2

SELECT 
  NAME,
  CAST(NAME as VARBINARY ) BINARY,
  UNICODE(SUBSTRING(NAME,1,1)) UNICODE1,UNICODE(SUBSTRING(NAME,2,1)) UNICODE2
  FROM @TABLE ORDER BY NAME COLLATE Chinese_Taiwan_Stroke_BIN2 

image

 

這樣雖然可以解決排序的問題,但如果我們要做判斷,要取出某個範圍的資料的時候,利用 Between 指令的時候,到底要怎麼判斷哪個值比較小要放前面,哪個要放後面呢 ?

-- 這兩個哪個才是正確的寫法呢 ?
SELECT NAME FROM @TABLE WHERE NAME BETWEEN N'勞' and N'舊'
SELECT NAME FROM @TABLE WHERE NAME BETWEEN N'舊' and N'勞'

 

如果您有這樣的困擾,或許您可以用個小偏方來試試看,把條件寫成以下的方式

-- 管他哪個大哪個小,反正就是這個區間內就對了
SELECT NAME FROM @TABLE 
  WHERE (( NAME BETWEEN N'勞' and N'舊' ) OR ( NAME BETWEEN N'舊' and N'勞' ))

image