SQLServer常見查詢問題

有些常見的問題在論壇中不斷出現,不妨整理一下。

以下語句是在SQLServer2005上實現的,一些語句無法在SS2000上執行。

有用指數是我根據這個問題的常見程度打的分,僅供參考。實際上,當你遇到了這個問題,這個問題哪怕再少見,解決方案也是非常有用的。


1. 生成若干行記錄
有用指數:★★★★★

常見的問題類型:根據起止日期生成若干個日期、生成一天中的各個時間段

《SQL Server 2005技術內幕:T-SQL查詢》作者建議在數據庫中創建一個數據表:
--自然數表1-1M
CREATE TABLE Nums(n int NOT NULL PRIMARY KEY CLUSTERED)
--書上介紹了很多種填充方法,以下是最高效的一種,需要SS2005的ROW_NUMBER()函數。
WITH B1 AS(SELECT n=1 UNION ALL SELECT n=1), --2
B2 AS(SELECT n=1 FROM B1 a CROSS JOIN B1 b), --4
B3 AS(SELECT n=1 FROM B2 a CROSS JOIN B2 b), --16
B4 AS(SELECT n=1 FROM B3 a CROSS JOIN B3 b), --256
B5 AS(SELECT n=1 FROM B4 a CROSS JOIN B4 b), --65536
CTE AS(SELECT r=ROW_NUMBER() OVER(ORDER BY (SELECT 1)) FROM B5 a CROSS JOIN B3 b) --65536 * 16
INSERT INTO Nums(n)
SELECT TOP(1000000) r FROM CTE ORDER BY r

有了這個數字表,可以做很多事情,除上面提到的兩個外,還有:生成一批測試數據、生成所有ASCII字符或UNICODE中文字符、等等。

經常有高手使用SELECT number FROM master..spt_values WHERE type = 'P',這是很妙的方法;但這樣只有2048個數字,而且語句太長,不夠方便。

總之,一個數字輔助表(10萬還是100萬根據個人需要而定),你值得擁有。


2. 日曆表
有用指數:★★★☆☆

《SQL編程風格》一書建議一個企業的數據庫應該創建一個日曆表:

CREATE TABLE Calendar(
    date
datetime NOT NULL PRIMARY KEY CLUSTERED,
    weeknum
int NOT NULL,
    weekday
int NOT NULL,
    weekday_desc
nchar(3) NOT NULL,
    is_workday
bit NOT NULL,
    is_weekend
bit NOT NULL
)
GO

這個表可以很容易根據第1條的數字輔助表生成出來。如果經常需要進行日期處理的話,或許會需要這個表。

還可以在這個表中包含一些企業關心的特殊日期,比如開盤日休市日(股票行業)、特殊紀念日和節日、重要員工的生日,等等。這些日期往往是很難計算的,比如中國的法定節假日(農曆問題)。


3. 字符串的拼接(Join)與切分(Split)
有用指數:★★★★★

這個問題非常常見!開發中經常需要把一組值以逗號分隔拼接在一個字符串,或是反過來把一個逗號分隔的字符串切分成一組值
用SS2005對XML的支持可以非常方便地實現這個功能。

單變量的拼接與切分:

--將一組查詢結果按指定分隔符拼接到一個變量中
DECLARE @Datebases varchar(max)
SET @Datebases = STUFF((
       
SELECT ','+name
       
FROM sys.databases
       
ORDER BY name
       
FOR XML PATH('')),1,1,'')
SELECT @Datebases
--將傳入的一個參數按指定分隔符切分到一個表中
DECLARE @SourceIDs varchar(max)
SET @SourceIDs = 'a,bcd,123,+-*/=,x&y,<key>'
SELECT v = x.n.value('.','varchar(10)')
FROM (
   
SELECT ValuesXML = CAST('<root>' +
       
REPLACE((SELECT v = @SourceIDs FOR XML PATH('')),',','</v><v>') +
       
'</root>' AS XML)
) t
CROSS APPLY t.ValuesXML.nodes('/root/v') x(n)

批量的拼接與切分:
--測試數據:
CREATE TABLE #ToJoin(
     TableName varchar(20) NOT NULL,
ColumnName varchar(20) NOT NULL,
     PRIMARY KEY CLUSTERED(TableName,ColumnName))
GO
CREATE TABLE #ToSplit(
     TableName varchar(20) NOT NULL PRIMARY KEY CLUSTERED,
     ColumnNames varchar(max) NOT NULL)
GO
INSERT INTO #ToJoin VALUES('tblEmployee','EmployeeCode')
INSERT INTO #ToJoin VALUES('tblEmployee','EmployeeName')
INSERT INTO #ToJoin VALUES('tblEmployee','HireDate')
INSERT INTO #ToJoin VALUES('tblEmployee','JobCode')
INSERT INTO #ToJoin VALUES('tblEmployee','ReportToCode')
INSERT INTO #ToJoin VALUES('tblJob','JobCode')
INSERT INTO #ToJoin VALUES('tblJob','JobTitle')
INSERT INTO #ToJoin VALUES('tblJob','JobLevel')
INSERT INTO #ToJoin VALUES('tblJob','DepartmentCode')
INSERT INTO #ToJoin VALUES('tblDepartment','DepartmentCode')
INSERT INTO #ToJoin VALUES('tblDepartment','DepartmentName')
GO
INSERT INTO #ToSplit VALUES('tblDepartment','DepartmentCode,DepartmentName')
INSERT INTO #ToSplit VALUES('tblEmployee','EmployeeCode,EmployeeName,HireDate,JobCode,ReportToCode')
INSERT INTO #ToSplit VALUES('tblJob','DepartmentCode,JobCode,JobLevel,JobTitle')
GO
 
--拼接(Join),SQL Server 2005的FOR XML擴展可以將一個列表轉成一個字串:
SELECT
     t.TableName,
     ColumnNames = STUFF(
         (SELECT ',' + c.ColumnName
         FROM #ToJoin c
         WHERE c.TableName = t.TableName
         FOR XML PATH('')),
         1,1,'')
FROM #ToJoin t
GROUP BY t.TableName
 
--切分(Split),使用SQL Server 2005對XQuery的支持:
SELECT
     t.TableName,
     ColumnName = c.ColumnName.value('.','varchar(20)')
FROM (
     SELECT
         TableName,
         ColumnNamesXML = CAST(' <Root>' + REPLACE((SELECT ColumnName = ColumnNames FOR XML PATH('')),',',' </ColumnName> <ColumnName>') + ' </Root>' AS xml)
     FROM #ToSplit
) t
CROSS APPLY t.ColumnNamesXML.nodes('/Root/ColumnName') c(ColumnName)

需要注意的是,倘若分隔符爲“;”或者字符串值中包含XML特殊字符(比如&、<、>等等),以上方法可能會無法處理。


4. 樹形結構的存儲與查詢
有用指數:★★★☆☆

數據庫設計中常常會遇到需要存儲樹形結構,比如員工關係表、組織結構表,等等。
--測試數據
CREATE TABLE #Employees(
    EmployeeCode
varchar(20) NOT NULL PRIMARY KEY CLUSTERED,
    ReportToCode
varchar(20) NULL)
GO
INSERT INTO #Employees VALUES('A',NULL)
INSERT INTO #Employees VALUES('B','A')
INSERT INTO #Employees VALUES('C','A')
INSERT INTO #Employees VALUES('D','A')
INSERT INTO #Employees VALUES('E','B')
INSERT INTO #Employees VALUES('F','B')
INSERT INTO #Employees VALUES('G','C')
INSERT INTO #Employees VALUES('H','D')
INSERT INTO #Employees VALUES('I','D')
INSERT INTO #Employees VALUES('J','D')
INSERT INTO #Employees VALUES('K','J')
INSERT INTO #Employees VALUES('L','J')
INSERT INTO #Employees VALUES('M','J')
INSERT INTO #Employees VALUES('N','K')
GO
/*
可能遇到的查詢問題:
1. 員工'D'的所有直接下屬
2. 員工'D'的所有2級以內的下屬(包括直接下屬和直接下屬的下屬)
3. 員工'N'的所有上級(按報告線順序列出)
4. 員工@EmployeeCode的所有@LevelDown級以內的下屬(@EmployeeCode和@LevelDown以變量傳入)
DECLARE @EmployeeCode varchar(20), @LevelDown int;
SET @EmployeeCode = 'D';
SET @LevelDown = 2;
5. 員工@EmployeeCode的所有@LevelUp級以內的上級(@EmployeeCode和@LevelUp以變量傳入)
DECLARE @EmployeeCode varchar(20), @LevelUp int;
SET @EmployeeCode = 'N';
SET @LevelUp = 2;
*/
--用遞歸CTE實現員工樹形關係表
WITH CTE AS(
   
SELECT
        EmployeeCode,
        ReportToCode,
        ReportToDepth
= 0,
        ReportToPath
= CAST('/' + EmployeeCode + '/' AS varchar(200))
   
FROM #Employees
   
WHERE ReportToCode IS NULL
   
UNION ALL
   
SELECT
        e.EmployeeCode,
        e.ReportToCode,
        ReportToDepth
= mgr.ReportToDepth + 1,
        ReportToPath
= CAST(mgr.ReportToPath + e.EmployeeCode + '/' AS varchar(200))
   
FROM #Employees e
   
INNER JOIN CTE mgr
   
ON e.ReportToCode = mgr.EmployeeCode
)
SELECT * FROM CTE ORDER BY ReportToPath

5. IPv4地址的存儲與查詢
有用指數:★★☆☆☆

IPv4的地址實際上是一個4字節的數據。點分十進制的字符串表示是爲了人工讀寫方便,但範圍比較則是原始二進制形式方便。因此需要實現二者的相互轉換。
--測試數據
CREATE TABLE #IPs(
    strIP
varchar(15) NULL,
    binIP
binary(4) NULL)
GO
INSERT INTO #IPs VALUES('0.0.0.0',NULL)
INSERT INTO #IPs VALUES('255.255.255.255',NULL)
INSERT INTO #IPs VALUES('127.0.0.1',NULL)
INSERT INTO #IPs VALUES('192.168.43.192',NULL)
INSERT INTO #IPs VALUES('192.168.1.101',NULL)
INSERT INTO #IPs VALUES('65.54.239.80',NULL)
INSERT INTO #IPs VALUES(NULL,0xB92AEAD3)
INSERT INTO #IPs VALUES(NULL,0x2D4B2E53)
INSERT INTO #IPs VALUES(NULL,0x31031B0B)
INSERT INTO #IPs VALUES(NULL,0x7C2D5F2F)
INSERT INTO #IPs VALUES(NULL,0x473E5D31)
INSERT INTO #IPs VALUES(NULL,0x90D7D66B)
GO
SELECT
    strIP,binIP,
    strIP_new
= CAST(CAST(SUBSTRING(binIP,1,1) AS int) AS varchar(3)) + '.' +
               
CAST(CAST(SUBSTRING(binIP,2,1) AS int) AS varchar(3)) + '.' +
               
CAST(CAST(SUBSTRING(binIP,3,1) AS int) AS varchar(3)) + '.' +
               
CAST(CAST(SUBSTRING(binIP,4,1) AS int) AS varchar(3)),
    binIP_new
= CAST(CAST(PARSENAME(strIP,4) AS int) AS binary(1)) +
               
CAST(CAST(PARSENAME(strIP,3) AS int) AS binary(1)) +
               
CAST(CAST(PARSENAME(strIP,2) AS int) AS binary(1)) +
               
CAST(CAST(PARSENAME(strIP,1) AS int) AS binary(1)),
    intIP_new
= CAST(PARSENAME(strIP,1) AS bigint) +
               
CAST(PARSENAME(strIP,2) AS bigint) * 256 +
               
CAST(PARSENAME(strIP,3) AS bigint) * 65536 +
               
CAST(PARSENAME(strIP,4) AS bigint) * 16777216  --int類型也可以,但浪費空間且不直觀
FROM #IPs
6. 中文字符處理
有用指數:★★★★☆

SQLServer中文處理涉及到字符集編碼和排序規則,是個非常糾結的問題。參看這篇博客

--ASCII字符
SELECT n,x=CAST(n AS binary(2)),u=NCHAR(n) FROM Nums WHERE n BETWEEN 32 AND 126
--UNICODE中文字符
SELECT n,x=CAST(n AS binary(2)),u=NCHAR(n) FROM Nums WHERE n BETWEEN 19968 AND 40869
19968    0x4E00    一
40869    0x9FA5    龥
--以下兩個條件用來判斷字符串是否包含漢字
LIKE N'%[吖-咗]%' COLLATE Chinese_PRC_CI_AS
LIKE N'%[一-龥]%' COLLATE Chinese_PRC_BIN
--這是因爲在以上兩種不同的排序規則下,漢字的排列順序是不同的。
--
中文全角標點符號
SELECT n,x=CAST(n AS binary(2)),uq=NCHAR(n),ub=NCHAR(n-65248) FROMas 規格3

全角半角標點的轉換:

--full2half
CREATE FUNCTION [dbo].[full2half](
@String nvarchar(max)
)
RETURNS nvarchar(max)
AS
/*
全角(Fullwidth)轉換爲半角(Halfwidth)
*/
BEGIN
   
DECLARE @chr nchar(1)
   
DECLARE @i int
   
SET @String = REPLACE(@String,N' ',N' ')
   
SET @i = PATINDEX(N'%[!-~]%' COLLATE Latin1_General_BIN,@String)
   
WHILE @i > 0
   
BEGIN
       
SET @chr = SUBSTRING(@String,@i,1)
       
SET @String = REPLACE(@String,@chr,NCHAR(UNICODE(@chr)-65248))
       
SET @i = PATINDEX(N'%[!-~]%' COLLATE Latin1_General_BIN,@String)
   
END
   
RETURN @String
END
GO
CREATE FUNCTION [dbo].[half2full](
@String nvarchar(max)
)
RETURNS nvarchar(max)
AS
/*
半角(Halfwidth)轉換爲全角(Fullwidth)
*/
BEGIN
   
DECLARE @chr nchar(1)
   
DECLARE @i int
   
SET @String = REPLACE(@String,N' ',N' ')
   
SET @i = PATINDEX(N'%[!-~]%' COLLATE Latin1_General_BIN,@String)
   
WHILE @i > 0
   
BEGIN
       
SET @chr = SUBSTRING(@String,@i,1)
       
SET @String = REPLACE(@String,@chr,NCHAR(UNICODE(@chr)+65248))
       
SET @i = PATINDEX(N'%[!-~]%' COLLATE Latin1_General_BIN,@String)
   
END
   
RETURN @String
END
GO

7. binary字符串
有用指數:★☆☆☆☆

0x1234與'0x1234'的相互轉換。很明顯,CAST/CONVERT是不行的。

--string到binary可以用這個系統函數sys.fn_varbintohexstr()(實際上是master.dbo.fn_varbintohexstr)
SELECT sys.fn_varbintohexstr(0x1234),'0x1234'
--binary到string需要自定義函數
CREATE FUNCTION dbo.hexstr2varbin(
@hexstr varchar(max)
)
RETURNS varbinary(max)
AS
/*
將表示16進制的字符串轉換爲2進制類型
--TESTCASES
SELECT dbo.hexstr2varbin(NULL),NULL
SELECT dbo.hexstr2varbin(''),0x
SELECT dbo.hexstr2varbin('0x'),0x
SELECT dbo.hexstr2varbin('30394161'),0x30394161
SELECT dbo.hexstr2varbin('0x30394161'),0x30394161
SELECT dbo.hexstr2varbin('0x1A2B3C4D5E6F'),0x1A2B3C4D5E6F
SELECT dbo.hexstr2varbin('0x1a2b3c4d5e6f'),0x1a2b3c4d5e6f
--UNIMPLEMENTED
SELECT dbo.hexstr2varbin('0x3039416'),0x3039416
*/
BEGIN
   
DECLARE @value int
   
DECLARE @ascii int
   
DECLARE @varbin varbinary(max)
   
IF @hexstr LIKE '0x%'
       
SET @hexstr = STUFF(@hexstr,1,2,'')
   
SET @hexstr = UPPER(@hexstr)
   
IF @hexstr NOT LIKE '%[^0-9A-F]%' COLLATE Chinese_PRC_BIN
   
BEGIN
       
SET @varbin = 0x
       
WHILE @hexstr <> ''
       
BEGIN
           
SET @value = ASCII(SUBSTRING(@hexstr,1,1))
           
IF @value <= 57
               
SET @value = @value - 48
           
ELSE
               
SET @value = @value - 55
           
SET @ascii = @value * 16
           
SET @value = ASCII(SUBSTRING(@hexstr,2,1))
           
IF @value <= 57
               
SET @value = @value - 48
           
ELSE
               
SET @value = @value - 55
           
SET @ascii = @ascii + @value
           
SET @varbin = @varbin + CAST(@ascii AS binary(1))
           
SET @hexstr = STUFF(@hexstr,1,2,'')
       
END
   
END
   
RETURN @varbin
END
GO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章