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>=20140101and createdate<=20141231’來取代。
IF OBJECT_ID('News','U') IS NOT NULL DROP TABLE News
GO
CREATE TABLE News
(ID INT NOT NULL 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'

對比兩個查詢顯然絕大部分情況下走索引查找的查詢性能要高於走索引掃描,特別是查詢的數據庫不是非常大的情況下,索引查找的消耗時間要遠遠少於索引掃描的時間,如果想詳細了解索引的體系結構可以查看了我前面寫的幾篇關於聚集、非聚集、堆的索引體系機構的文章。

 

請參看:http://www.cnblogs.com/chenmh/p/3780221.html

 請參看:http://www.cnblogs.com/chenmh/p/3782397.html

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

--插入測試數據 INSERT INTO Table1 VALUES(1,101),(2,102),(3,103),(4,104) GO ---創建表Table2 IF OBJECT_ID('Table2','U') IS NOT NULL DROP TABLE Table2 GO CREATE TABLE Table2 ( FOID INT NOT NULL) GO
--插入測試數據
INSERT INTO Table2 VALUES(101),(102),(103),(104)
GO
SELECT * FROM Table1
GO
SELECT * FROM Table2
GO

在Table1表中創建觸發器,當表中的數據被刪除時同時刪除Table2表中對應的FOID
CREATE TRIGGER TG_Table1 ON Table1
AFTER DELETE
AS
BEGIN
  DELETE FROM TA FROM Table2 TA INNER JOIN deleted TB ON TA.FOID=TB.FOID 
END
GO
 
         
 
         
---測試DELETE刪除操作
DELETE FROM Table1 WHERE ID=1

GO
---執行觸發器成功,Table2表中的FOID=101的數據也被刪除
SELECT * FROM Table1
GO
SELECT * FROM Table2
---測試TRUNCATE刪除操作
TRUNCATE TABLE Table1

GO
---Table2中的數據沒有被刪除
SELECT * FROM Table1
GO
SELECT * FROM Table2
---查看TRUNCATE和DELETE的日志記錄情況
CHECKPOINT
GO
SELECT * FROM fn_dblog(NULL,NULL)
GO
DELETE FROM Table2
WHERE FOID=102
GO
SELECT * FROM fn_dblog(NULL,NULL)


在第四行記錄有一個lop_delete_rows,lcx_heap的刪除操作日志記錄
----TRUNCATE日志記錄
CHECKPOINT
GO
SELECT * FROM fn_dblog(NULL,NULL)
GO
TRUNCATE TABLE Table2
GO
SELECT * FROM fn_dblog(NULL,NULL)
GO

 TRUNCATE操作沒有記錄刪除日志操作

主要的原因是因為TRUNCATE操作不會激活觸發器,因為TRUNCATE操作不會記錄各行的日志刪除操作,所以當你需要刪除一張表的數據時你需要考慮是否應該如有記錄日志刪除操作,而不是根據個人的習慣來操作。 

13.事務的理解

---創建表Table1
IF OBJECT_ID('Table1','U') IS NOT NULL
DROP TABLE Table1
GO
CREATE TABLE Table1
(ID INT NOT NULL PRIMARY KEY,
Age INT NOT NULL CHECK(Age>10 AND Age<50));
GO

---創建表Table2
IF OBJECT_ID('Table2','U') IS NOT NULL
DROP TABLE Table2
GO
CREATE TABLE Table2
(
ID INT NOT NULL)
GO

1.簡單的事務提交

BEGIN TRANSACTION
INSERT INTO Table1(ID,Age)
VALUES(1,20)
INSERT INTO Table1(ID,Age)
VALUES(2,5)
INSERT INTO Table1(ID,Age)
VALUES(2,20)
INSERT INTO Table1(ID,Age)
VALUES(3,20)
COMMIT TRANSACTION
GO
---第二條記錄沒有執行成功,其他的都執行成功
SELECT * FROM Table1
所以並不是事務中的任意一條語句報錯整個事務都會回滾,其它的可執行成功的語句依然會執行成功並提交。

2.TRY...CATCH

DELETE FROM Table1

BEGIN TRY
BEGIN TRANSACTION
INSERT INTO Table1(ID,Age)
VALUES(1,20)
INSERT INTO Table1(ID,Age)
VALUES(2,20)
INSERT INTO Table1(ID,Age)
VALUES(3,20)
INSERT INTO Table3
VALUES(1) 
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH

----重新打開一個回話執行查詢,發現由於存在對象出錯BEGIN CATCH並沒有收到執行報錯,且事務一直處於打開狀態,沒有被提交,也沒有執行回滾。
SELECT * FROM Table1

---如果事務已經提交查詢XACT_STATE()的狀態值是0,或者執行DBCC OPENTRAN
SELECT XACT_STATE()

DBCC OPENTRAN

---手動執行提交或者回滾操作
ROLLBACK TRANSACTION

TRY...CATCH不會返回對象錯誤或者字段錯誤等類型的錯誤

想詳細了解TRY...CATCH請參考http://www.cnblogs.com/chenmh/articles/4012506.html

 

3.打開XACT_ABORT

SET XACT_ABORT ON
BEGIN TRANSACTION
INSERT INTO Table1(ID,Age)
VALUES(1,20)
INSERT INTO Table1(ID,Age)
VALUES(2,20)
INSERT INTO Table1(ID,Age)
VALUES(3,20)
INSERT INTO Table3
VALUES(1) 
COMMIT TRANSACTION
SET XACT_ABORT OFF

---事務全部執行回滾操作(對象table3是不存在報錯,但是也回滾所有的提交,跟上面的TRY...CATCH的區別)
SELECT * FROM Table1

---查詢是否有打開事務
SELECT XACT_STATE()

DBCC OPENTRAN
未查詢到有打開事務

當 SET XACT_ABORT 為 ON 時,如果執行 Transact-SQL 語句產生運行時錯誤,則整個事務將終止並回滾。

當 SET XACT_ABORT 為 OFF 時,有時只回滾產生錯誤的 Transact-SQL 語句,而事務將繼續進行處理。如果錯誤很嚴重,那么即使 SET XACT_ABORT 為 OFF,也可能回滾整個事務。OFF 是默認設置。

編譯錯誤(如語法錯誤)不受 SET XACT_ABORT 的影響。

      所以我們應該根據自己的需求選擇正確的事務。

14.修改字段NOT NULL的過程

在Address表中的有一個Address字段,該字段允許為NULL,現在需要將其修改為NOT NULL.
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
CREATE TABLE dbo.Tmp_Address
    (
    ID int NOT NULL,
    Address nvarchar(MAX) NOT NULL
    )  ON [PRIMARY]
     TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE dbo.Tmp_Address SET (LOCK_ESCALATION = TABLE)
GO
IF EXISTS(SELECT * FROM dbo.Address)
     EXEC('INSERT INTO dbo.Tmp_Address (ID, Address)
        SELECT ID, Address FROM dbo.Address WITH (HOLDLOCK TABLOCKX)')
GO
DROP TABLE dbo.Address
GO
EXECUTE sp_rename N'dbo.Tmp_Address', N'Address', 'OBJECT' 
GO
COMMIT

---從上面就是一個重置字段為非空的過程,從上面的語句我們可以看到首先要創建一張臨時表在臨時表中Address字段建成了NOT NULL,然后將原表中的數據插入到臨時表當中,最后修改表名,大家可以想一下如果我要修改的表有幾千萬數據,那這個過程該多么長而且內存一下子就會增加很多,所以大家建表的時候就要養成設字段為NOT NULL

--當你要向現有的表中增加一個字段的時候你也要不允許為NULL,可以用默認值替代空
Alter Table Address Add Type smallint Not Null Default (1)

 15.條件字段的先后順序

你平時在寫T_SQL語句的時候WHERE條件后面的字段的先后順序你有注意嗎?

---創建測試表 
IF OBJECT_ID('TAINFO','U')IS NOT NULL DROP TABLE TAINFO
GO

CREATE TABLE [dbo].[TAINFO](
ID INT NOT NULL PRIMARY KEY IDENTITY(1,1),
OID INT NOT NULL,
Stats SMALLINT CHECK (Stats IN(1,2)),
MAC uniqueidentifier NOT NULL

) ON [PRIMARY]

GO
---插入測試數據
INSERT INTO TAINFO(OID,Stats,MAC)
 VALUES(101,1,'46B550F9-6E24-436D-9BC7-F0650F562E54'),(101,2,'46B550F9-6E24-436D-9BC7-F0650F562E54'),(102,1,'46B550F9-6E24-436D-9BC7-F0650F562E54'),
 (102,2,'46B550F9-6E24-436D-9BC7-F0650F562E54'),(103,2,'46B550F9-6E24-436D-9BC7-F0650F562E54'),(103,2,'46B550F9-6E24-436D-9BC7-F0650F562E54'),
 (103,1,'46B550F9-6E24-436D-9BC7-F0650F562E54'),(103,1,'46B550F9-6E24-436D-9BC7-F0650F562E54')
 GO

 

如果這是你的寫的查詢語句

 SELECT ID,OID,Stats MAC FROM TAINFO WHERE MAC='46B550F9-6E24-436D-9BC7-F0650F562E54' AND STATS=1  AND OID=102  

我現在根據你的查詢語句創建一條索引

CREATE INDEX IX2_TAINFO ON TAINFO(MAC,STATS,OID)

分別執行三條查詢語句

 ---1.WHERE條件是索引字段且查詢字段也是索引字段
 SELECT ID,OID,Stats MAC FROM TAINFO WHERE MAC='46B550F9-6E24-436D-9BC7-F0650F562E54' AND STATS=1  AND OID=102  
 --2.WHERE 條件是索引的部分字段(這條語句或許是平時查詢該表用到的最多的一條語句)
 SELECT ID,OID,Stats MAC FROM TAINFO WHERE OID=102  AND STATS=1
 --3.WHERE 條件是索引的部分字段
  SELECT ID,OID,Stats MAC FROM TAINFO WHERE STATS=1

執行計划分別為

 從上面三天查詢語句可以看出,只有第一條語句走的是索引查找,另外兩條語句走的是索引掃描,而我們從字段的名稱應該可以看的出OID字段應該是該表的一個外鍵字段也是經常會被用作查詢的字段。

接下來我們重新換一下索引順序

 --創建索引
DROP INDEX IX2_TAINFO ON TAINFO
GO
CREATE INDEX IX1_TAINFO ON TAINFO(OID)
INCLUDE(STATS,MAC)
GO

依然執行前面的三條查詢語句分析執行計划

 分析執行計划前面兩條查詢語句都走的是索引查找,第三條查詢的是索引掃描,而根據一般單獨用第三條查詢的業務應該不會常見,所以現在一條索引解決了兩個常用查詢的索引需求,避免了建兩條索引的必要(所以當你建索引的時候索引的順序很重要,一般把查詢最頻繁的字段設第一個字段,可以避免建多余的索引)。

為什么要把這個問題提出來呢,因為平時有遇到程序員在寫查詢語句的時候對於同一個查詢條件每次的寫法都不一樣,往往是根據自己想到哪個字段就寫哪個字段先,這樣的習慣往往是不好的,就好比上面的例子如果別人看到你的查詢條件建一個索引也是這樣寫的話往往一個表會出現很多多余的索引(或許有人會說DBA建好索引的順序就好了,這里把這個因素排除吧),像后面的那個索引就解決了兩個查詢的需求。

所以這里我一般是這樣規定where條件的,對於經常用作查詢的字段放在第一個位置(比如上面例子的OID),其它的字段根據表的實際字段順序排列,這樣往往你的查詢語句走索引的概率會更大。

 16.理解外連接

---創建測試表
IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS
GO
CREATE TABLE DBO.OrderS
(Orderid INT NOT NULL,
custid INT NOT NULL,
stats INT NOT NULL);
GO
IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer
GO
CREATE TABLE DBO.Customer
(Customerid INT NOT NULL );
GO

---插入測試數據
INSERT INTO OrderS VALUES(1,101,0),(2,102,0),(3,103,1),(4,104,0);
GO
INSERT INTO Customer VALUES(101),(102),(103);


----查詢OrderS 表中stats不等於1且不在Customer 表中的數據
SELECT TA.Orderid,TA.custid,TA.stats,TB.Customerid  FROM OrderS TA LEFT JOIN Customer TB ON TA.stats<>'1' AND TA.custid=TB.Customerid 
WHERE TB.Customerid IS NULL

看到這結果是不是有點疑惑,我在連接條件里面寫了TA.stats<>'1',為什么結果還會查詢出。

接下來我們換一種寫法吧!

----查詢OrderS 表中stats不等於1且不在Customer 表中的數據
SELECT TA.Orderid,TA.custid,TA.stats,TB.Customerid  FROM OrderS TA LEFT JOIN Customer TB ON  TA.custid=TB.Customerid 
WHERE TA.stats<>'1' AND TB.Customerid IS NULL

 接下來我就解釋一下原因:對於外連接,連接條件不會改變主表的數據,即不會刪減主表的數據

對於上面的查詢主表是orders,所以無論你在連接條件on里面怎樣設置主表的條件都不影響主表數據的輸出,影響主表數據的輸出只在where條件里,where條件影響最后數據的輸出。而對於附表Customer 的條件就應該寫在連接條件(on)里而不是where條件里,這里說的是外連接(包括左連接和右連接)。

對於inner join就不存在這種情況,無論你的條件是寫在where后面還是on后面都是一樣的,但是還是建議寫在where后面。

17.謂詞類型要與字段類型對齊

IF OBJECT_ID('Person','u')IS NOT NULL DROP TABLE Person
GO
CREATE TABLE Person
(ID INT NOT NULL PRIMARY KEY IDENTITY(1,1),
Phone NVARCHAR(20) NOT NULL,
CreateDate DATETIME NOT NULL
)
---插入測試數據
INSERT INTO Person(Phone,CreateDate)
VALUES('13700000000',GETDATE()),('13700000000',GETDATE()),('13800000000',GETDATE())

---創建索引
CREATE INDEX IX_Person ON Person(Phone,CreateDate) 

1.謂詞類型與字段類型不一致

SELECT ID FROM Person WHERE Phone=13700000000 AND DATEDIFF(DAY,CreateDate,GETDATE())=0

由於定義表的phone字段類型是字符型,而上面的查詢條件phone寫成了整形,導致執行計划走了索引掃描,且執行計划select也有提示。

 2.謂詞類型與字段類型一致

SELECT ID FROM Person WHERE Phone='13700000000' AND DATEDIFF(DAY,CreateDate,GETDATE())=0

      第二種查詢phone謂詞類型與字段類型一致,所以查詢走了索引查找

在日常的語句編寫過程中需要注意這類問題,這將直接影響性能。

18.避免使用長字節字段排序

 SELECT O.name,O.create_date,C.name  FROM SYS.columns C INNER JOIN SYS.objects  O ON C.object_id=O.object_id 
 ORDER BY O.create_date DESC
 GO
 SELECT O.name,O.create_date,C.name  FROM SYS.columns C INNER JOIN SYS.objects  O ON C.object_id=O.object_id 
 ORDER BY O.object_id  DESC

 

上面的語句查詢結果是一樣的,只是寫法不一樣,O.create_date是表的創建時間而object_id 是一個自增值根據兩者的倒序排序得到的結果是一樣的,但是二者的執行效率卻不一樣。無論是從執行時間還是執行計划明顯是后者的效率要好,從執行計划可以看出后者的不需要進行排序操作因為object_id 本身就是排序好的,而且object_id 是整形而create_date是時間類型,如果是兩個大表進行連接操作再進行排序效率更明顯甚至前面用時間排序還可能查詢很久不出來。

 

三、總結

后面收集到類似的問題會補充在文章的末尾,文章持續更新中....,歡迎關注討論。

  

 

備注:

    作者:pursuer.chen

    博客:http://www.cnblogs.com/chenmh

本站點所有隨筆都是原創,歡迎大家轉載;但轉載時必須注明文章來源,且在文章開頭明顯處給明鏈接,否則保留追究責任的權利。

《歡迎交流討論》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM