SQL Server 容易忽略的錯誤
一、概述
因為每天需要稽核程式設計師釋出的SQL語句,所以收集了一些程式設計師的一些常見問題,還有一些平時收集的其它一些問題,這也是很多人容易忽視的問題,在以後收集到的問題會補充在文章末尾,歡迎關注,由於收集的問題很多是針對於生產資料,測試且資料量比較大,這裡就不把資料共享出來了,大家理解意思就行。
二、概念
1.大小寫
大寫T-SQL 語言的所有關鍵字都使用大寫,規範要求。
2.使用“;”
使用“;”作為 Transact-SQL 語句終止符。雖然分號不是必需的,但使用它是一種好的習慣,對於合併操作MERGE語句的末尾就必須要加上“;”
(cte表表達式除外)
3.資料型別
避免使用ntext、text 和 image 資料型別,用 nvarchar(max)、varchar(max) 和 varbinary(max)替代
後續版本會取消ntext、text 和 image 該三種類型
4.查詢條件不要使用計算列
例如year(createdate)=2014,使用createdate>=’ 20140101’ and createdate<=’ 20141231’來取代。
IF OBJECT_ID('News','U') IS NOT NULL DROP TABLE News GO CREATE TABLE News (ID INT NOTNULL PRIMARY KEY IDENTITY(1,1), NAME NVARCHAR(100) NOT NULL, Createdate DATETIME NOT NULL ) GO CREATE NONCLUSTERED INDEX [IX1_News] ON [dbo].[News] ( [Createdate] ASC ) INCLUDE ( [NAME]) WITH (STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO GO INSERT INTO News(NAME,Createdate) VALUES( '新聞','2014-08-20 00:00:00'),( '新聞','2014-08-20 00:00:00'),( '新聞','2014-08-20 00:00:00'),( '新聞','2014-08-20 00:00:00')
---使用計算列查詢(走的是索引掃描)
SELECT ID,NAME,Createdate FROM News WHERE YEAR(Createdate)=2014
---不使用計算列(走的是索引查詢)
SELECT ID,NAME,Createdate FROM News WHERE CreateDate>='2014-01-01 00:00:00' and CreateDate<'2015-01-01 00:00:00'
對比兩個查詢顯然絕大部分情況下走索引查詢的查詢效能要高於走索引掃描,特別是查詢的資料庫不是非常大的情況下,索引查詢的消耗時間要遠遠少於索引掃描的時間,如果想詳細瞭解索引的體系結構可以查看了我前面寫的幾篇關於聚集、非聚集、堆的索引體系機構的文章。
5.建表時欄位不允許為null
發現很多人在建表的時候不會注意這一點,在接下來的工作中當你需要查詢資料的時候你往往需要在WHERE條件中多加一個判斷條件IS NOT NULL,這樣的一個條件不僅僅增加了額外的開銷,而且對查詢的效能產生很大的影響,有可能就因為多了這個查詢條件導致你的查詢變的非常的慢;還有一個比較重要的問題就是允許為空的資料可能會導致你的查詢結果出現不準確的問題,接下來我們就舉個例子討論一下。
T-SQL是三值邏輯(true,flase,unknown) IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL); ----查詢沒有訂單的顧客 SELECT Customerid FROM DBO.Customer WHERE Customerid NOT IN(SELECT custid FROM OrderS); ---分析為什麼查詢結果沒有資料 /* 因為true,flase,unknown都是真值 因為not in 是需要結果中返回flase值,not true=flase,not flase=flase,not unknown=unknown 因為null值是unknown所以not unknownn無法判斷結果是什麼值所以不能返回資料 */ --可以將查詢語句修改為 SELECT Customerid FROM DBO.Customer WHERE Customerid NOT IN(SELECT custid FROM OrderS WHERE custid is not null); --或者使用EXISTS,因為EXISTS是二值邏輯只有(true,flase)所以不存在未知。 SELECT Customerid FROM DBO.Customer A WHERE NOT EXISTS(SELECT custid FROM OrderS WHERE OrderS.custid=A.Customerid ); ---in查詢可以返回值,因為in是true,子查詢true,flase,unknown都是真值所以可以返回子查詢的true SELECT Customerid FROM DBO.Customer WHERE Customerid IN(SELECT custid FROM OrderS);
----如果整形欄位可以賦0,字元型可以賦值空(這裡只是給建議)這裡的空和NULL是不一樣的意思
--增加整形欄位可以這樣寫 ALTER TABLE TABLE_NAME ADD COLUMN_NAME INT NOT NULL DEFAULT(0) --增加字元型欄位可以這樣寫 ALTER TABLE TABLE_NAME ADD COLUMN_NAME NVARCHAR(50) NOT NULL DEFAULT('')
6.分組統計時避免使用count(*)
IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL); 例如:需要統計每一個顧客的訂單數量 ---如果使用count(*) SELECT Customerid,COUNT(*) FROM Customer TA LEFT JOIN OrderS TB ON TA.Customerid=TB.custid GROUP BY Customerid ;
實際情況customerid=3是沒有訂單的,數量應該是0,但是結果是1,count()裡面的欄位是左連線右邊的表字段,如果你用的是主表字段結果頁是錯誤的。
----正確的方法是使用count(custid) SELECT Customerid,COUNT(custid) FROM Customer TA LEFT JOIN OrderS TB ON TA.Customerid=TB.custid GROUP BY Customerid;
7.子查詢的表加上表別名
IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL);
大家發現下面語句有沒有什麼問題,查詢結果是怎樣呢?
SELECT Customerid FROM Customer WHERE Customerid IN(SELECT Customerid FROM OrderS WHERE Orderid=2 );
正確查詢結果下查詢出的結果是沒有customerid為3的值
為什麼結果會這樣呢?
大家仔細看應該會發現子查詢的orders表中沒有Customerid欄位,所以SQL取的是Customer表的Customerid值作為相關子查詢的匹配欄位。
所以我們應該給子查詢加上表別名,如果加上表別名,如果欄位錯誤的話會有錯誤標示
正確的寫法:
SELECT Customerid FROM Customer WHERE Customerid IN(SELECT tb.custid FROM OrderS tb WHERE Orderid=2 );
8.建立自增列時單獨再給自增列新增唯一約束
USE tempdb CREATE TABLE TEST (ID INT NOT NULL IDENTITY(1,1), orderdate date NOT NULL DEFAULT(CURRENT_TIMESTAMP), NAME NVARCHAR(30) NOT NULL, CONSTRAINT CK_TEST_NAME CHECK(NAME LIKE '[A-Za-z]%' ) ); GO INSERT INTO tempdb.DBO.TEST(NAME) VALUES('A中'),('a名'),('Aa'),('ab'),('AA'),('az'); ----4.插入報錯後,自增值依舊增加 INSERT INTO tempdb.DBO.TEST(NAME) VALUES('中'); GO SELECT IDENT_CURRENT('tempdb.DBO.TEST'); SELECT * FROM tempdb.DBO.TEST; ---插入正常的資料 INSERT INTO tempdb.DBO.TEST(NAME) VALUES('cc'); SELECT IDENT_CURRENT('tempdb.DBO.TEST') SELECT * FROM tempdb.DBO.TEST; ----5.顯示插入自增值 SET IDENTITY_INSERT tempdb.DBO.TEST ON INSERT INTO tempdb.DBO.TEST(ID,NAME) VALUES(8,'A中'); SET IDENTITY_INSERT tempdb.DBO.TEST OFF ----會發現ID並不是根據自增值排列的,而且根據插入的順序排列的 SELECT IDENT_CURRENT('tempdb.DBO.TEST'); SELECT * FROM tempdb.DBO.TEST; ----6.插入重複的自增值 SET IDENTITY_INSERT tempdb.DBO.TEST ON INSERT INTO tempdb.DBO.TEST(ID,NAME) VALUES(8,'A中'); SET IDENTITY_INSERT tempdb.DBO.TEST OFF SELECT IDENT_CURRENT('tempdb.DBO.TEST') SELECT * FROM tempdb.DBO.TEST; ---所以如果要保證ID是唯一的,單單隻設定自增值不行,需要給欄位設定主鍵或者唯一約束 DROP TABLE tempdb.DBO.TEST;
9.查詢時一定要制定欄位查詢
l 查詢時一定不能使用”*”來代替欄位來進行查詢,無論你查詢的欄位有多少個,就算欄位太多無法走索引也避免瞭解析”*”帶來的額外消耗。
l 查詢欄位值列出想要的欄位,避免出現多餘的欄位,欄位越多查詢開銷越大而且可能會因為多列出了某個欄位而引起查詢不走索引。
建立測試資料庫
CREATE TABLE [Sales].[Customer]( [CustomerID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, [PersonID] [int] NULL, [StoreID] [int] NULL, [TerritoryID] [int] NULL, [AccountNumber] AS (isnull('AW'+[dbo].[ufnLeadingZeros]([CustomerID]),'')), [rowguid] [uniqueidentifier] ROWGUIDCOL NOT NULL, [ModifiedDate] [datetime] NOT NULL, CONSTRAINT [PK_Customer_CustomerID] PRIMARY KEY CLUSTERED ( [CustomerID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
建立索引
CREATE NONCLUSTERED INDEX [IX1_Customer] ON [Sales].[Customer] ( [PersonID] ASC ) INCLUDE ( [StoreID], [TerritoryID], [AccountNumber], [rowguid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO
查詢測試
---使用SELECT * 查詢 SET STATISTICS IO ON SET STATISTICS TIME ON SELECT * FROM [Sales].[Customer] WHERE PersonID=1; SET STATISTICS TIME OFF SET STATISTICS IO OFF
由於建的索引‘IX1_Customer’沒有包含ModifiedDate欄位,所以需要通過鍵查詢去聚集索引中獲取該欄位的值
---列出需要的欄位查詢,因為欄位不包含不需要的列,所以走索引 SET STATISTICS IO ON SET STATISTICS TIME ON SELECT CustomerID, [PersonID] ,[StoreID] ,[TerritoryID] ,[AccountNumber] ,[rowguid] FROM [Sales].[Customer] WHERE PersonID=1; SET STATISTICS TIME OFF SET STATISTICS IO OFF
由於查詢語句中沒有對ModifiedDate欄位進行查詢,所以只走索引查詢就可以查詢到需要的資料,所以建議在查詢語句中列出你需要的欄位而不是為了方便用*來查詢所有的欄位,如果真的
需要查詢所有的欄位也同樣建議把所有的欄位列出來取代‘*’。
10.使用儲存過程的好處
減少網路通訊量。呼叫一個行數不多的儲存過程與直接呼叫SQL語句的網路通訊量可能不會有很大的差別,可是如果儲存過程包含上百行SQL語句,那麼其效能絕對比一條一條的呼叫SQL語句要高得多。
執行速度更快。有兩個原因:首先,在儲存過程建立的時候,資料庫已經對其進行了一次解析和優化。其次,儲存過程一旦執行,在記憶體中就會保留一份這個儲存過程快取計劃,這樣下次再執行同樣的儲存過程時,可以從記憶體中直接呼叫。
更強的適應性:由於儲存過程對資料庫的訪問是通過儲存過程來進行的,因此資料庫開發人員可以在不改動儲存過程介面的情況下對資料庫進行任何改動,而這些改動不會對應用程式造成影響。
布式工作:應用程式和資料庫的編碼工作可以分別獨立進行,而不會相互壓制。
更好的封裝移植性。
安全性,它們可以防止某些型別的 SQL 插入攻擊。
PROCEDURE [dbo].[SPSalesPerson] (@option varchar(50)) AS BEGIN SET NOCOUNT ON IF @option='select' BEGIN SELECT [DatabaseLogID] ,[PostTime] ,[DatabaseUser] ,[Event] ,[Schema] ,[Object] ,[TSQL] ,[XmlEvent] FROM [dbo].[DatabaseLog] END IF @option='SalesPerson' BEGIN SELECT [BusinessEntityID] ,[TerritoryID] ,[SalesQuota] ,[Bonus] ,[CommissionPct] ,[SalesYTD] ,[SalesLastYear] ,[rowguid] ,[ModifiedDate] FROM [Sales].[SalesPerson] WHERE BusinessEntityID<300 END SET NOCOUNT OFF END
EXEC SPSalesPerson @option='select' EXEC SPSalesPerson @option='SalesPerson' DBCC FREEPROCCACHE----清空快取 ---測試兩個查詢是否都走了快取計劃 SELECT usecounts,size_in_bytes,cacheobjtype,objtype,TEXT FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st; --執行計劃在第一次執行SQL語句時產生,快取在記憶體中,這個快取的計劃一直可用,直到 SQL Server 重新啟動,或直到它由於使用率較低而溢位記憶體。 預設情況下,儲存過程將返回過程中每個語句影響的行數。如果不需要在應用程式中使用該資訊(大多數應用程式並不需要),請在儲存過程中使用 SET NOCOUNT ON 語句以終止該行為。根據儲存過程中包含的影響行的語句的數量,這將刪除客戶端和伺服器之間的一個或多個往返過程。儘管這不是大問題,但它可以為高流量應用程式的效能產生負面影響。
11.判斷一條查詢是否有值
--以下四個查詢都是判斷連線查詢無記錄時所做的操作 ---效能最差消耗0.8秒 SET STATISTICS IO ON SET STATISTICS TIME ON DECLARE @UserType INT ,@Status INT SELECT @UserType=COUNT(c.Id) FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000' IF(@UserType=0) BEGIN SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF go ----效能較好消耗0.08秒 SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT c.Id FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000') BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF go ----效能較好消耗0.08秒 SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT top 1 c.id FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000' ORDER BY NEWID() ) BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF GO ---效能和上面的一樣0.08秒 SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT 1 FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13410700660' ) BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF
這裡說一下SELECT 1,之前因為有程式設計師誤認為查詢SELECT 1無論查詢的資料有多少隻返回一個1,其實不是這樣的,和查詢欄位是一樣的意思只是有多少記錄就返回多少個1,1也不是查詢的第一個欄位。
12.理解TRUNCATE和DELETE的區別
---建立表Table1 IF OBJECT_ID('Table1','U') IS NOT NULL DROP TABLE Table1 GO CREATE TABLE Table1 (ID INT NOT NULL, FOID INT NOT NULL) GO
<