概述 html
由於天天須要審覈程序員發佈的SQL語句,因此收集了一些程序員的一些常見問題,還有一些平時收集的其它一些問題,這也是不少人容易忽視的問題,在之後收集到的問題會補充在文章末尾,歡迎關注,因爲收集的問題不少是針對於生產數據,測試且數據量比較大,這裏就不把數據共享出來了,你們理解意思就行。 程序員
步驟 sql
大寫T-SQL 語言的全部關鍵字都使用大寫,規範要求。 數據庫
使用「;」做爲 Transact-SQL 語句終止符。雖然分號不是必需的,但使用它是一種好的習慣,對於合併操做MERGE語句的末尾就必需要加上「;」 緩存
(cte表表達式除外) 安全
避免使用ntext、text 和 image 數據類型,用 nvarchar(max)、varchar(max) 和 varbinary(max)替代 服務器
後續版本會取消ntext、text 和 image 該三種類型 網絡
例如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 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')
---使用計算列查詢(走的是索引掃描) ide
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
發現不少人在建表的時候不會注意這一點,在接下來的工做中當你須要查詢數據的時候你每每須要在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);
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(custid) SELECT Customerid,COUNT(custid) FROM Customer TA LEFT JOIN OrderS TB ON TA.Customerid=TB.custid GROUP BY Customerid;
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 );
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;
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字段進行查詢,因此只走索引查找就能夠查詢到須要的數據,因此建議在查詢語句中列出你須要的字段而不是爲了方便用*來查詢全部的字段,若是真的
須要查詢全部的字段也一樣建議把全部的字段列出來取代‘*’。
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 語句以終止該行爲。根據存儲過程當中包含的影響行的語句的數量,這將刪除客戶端和服務器之間的一個或多個往返過程。儘管這不是大問題,但它能夠爲高流量應用程序的性能產生負面影響。
--如下四個查詢都是判斷鏈接查詢無記錄時所作的操做 ---性能最差消耗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也不是查詢的第一個字段。
理解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操做不會記錄各行的日誌刪除操做,因此當你須要刪除一張表的數據時你須要考慮是否應該若有記錄日誌刪除操做,而不是根據我的的習慣來操做。
事務的理解
---建立表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 的影響。
因此咱們應該根據本身的需求選擇正確的事務。
在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 MemberType smallint Not Null Default (1)
總結
後面收集到相似的問題會補充在文章的末尾,文章持續更新中....,歡迎關注討論。
若是以爲文章對你們有所幫助,麻煩給個推薦,謝謝!!
備註: 做者:pursuer.chen 博客:http://www.cnblogs.com/chenmh 本站點全部文章都是原創,歡迎你們轉載;但轉載時必須註明文章來源,且在文章開頭明顯處給明連接,不然保留追究責任的權利。 《歡迎交流討論》 |