前段時間做一個項目,其中涉及到報表部分編寫了大量復雜的SQL,比如其中的一個存儲過程就有700多行。項目上線過后,進入維護階段,發現大量的SQL很難維護。於是總結點經驗教訓:
設計
一、數據庫命名遵循一些通用規范。
數據庫命名規范是個基本的命名標准,每個團隊都有自己的命名規范,我們做項目中以全大寫下划線分割作為標准。表名或字段名要准確表達其業務含義。以DATE結尾的數據類型都是date類型,以TIME結尾的數據類型是datetime類型。以IS開頭的都是bool類型。
二、大數據對象列應該獨立成表。
比如員工照片是一個blob對象,按照范式來說,這個字段完全可以放在Employee表中,但是出於性能的考慮,最好單獨出一張EmployeePhoto表,與Employee是一對一的關系,這樣使用ORM的時候,平時取Employee對象就不會取到照片,只有需要顯示照片時才取EmployeePhoto對象。
三、數據庫字段盡量不要為null。
一個字段允許為空,那么在SQL查詢時就需要進行一些特殊處理,比如在WHERE條件中用上 t1.COLUMN1 IS NULL或者在SELECT時用上ISNULL()函數。而在ORM時,對應的對象的數據類型如果是不允許為空的,還必須加上?表示允許為空。在編程時也要進行判斷該值是否為空。一不小心就容易漏掉空的判斷,造成計算結果不正確。所以在數據庫設計時,盡量將每個字段設計為not null。
四、帶小數的字段使用Decimal數據類型而不要使用Float數據類型。
因為Float類型是用於表示浮點數據的近似數據類型,所以存儲后可能會造成一點誤差,如果在程序中傳入2.4進行保存,可能讀取到的值卻是2.4000001或者2.399999999。
五、使用配置表來存儲可能經常變化的配置項,而不是寫死在代碼中。
在編寫查詢語句,寫存儲過程或者出報表時,經常會對某些字段進行過濾。比如ProjectAssignment表中有個RoleCode字段,表示在往項目上分配人時,該人的角色。在查詢時經常會把角色A、B、C放在一起作為管理層角色,那么在關於管理層分配的各種報表中,就充斥着where pa.ROLE_CODE in ('A','B','C')這樣的條件。但是有一天,用戶說現在角色D也算是管理層角色了,那么之前做的所有報表,都要將這段代碼進行修改。
所以對於這種可能修改的查詢條件,那么最好是建立一個配置表,然后所有查詢都是從這個配置表中讀取數據進行查詢。那么前面是SQL可以改為:
where pa.ROLE_CODE in (select CODE from CONFIG where CODE_TYPE='Management')
雖然這樣要犧牲一點點的性能,但是由於本身配置表數據量不會很大,而且可以以CODE_TYPE建立聚集索引,那么性能不是很大的問題。
六、不要使用ORM工具通過對象模型生成數據庫。
數據庫的創建和修改都應該以腳本來完成,而每個字段的數據類型、長度、表的各種約束(主鍵約束、外鍵約束、唯一約束、非空約束、CHECK約束等)、索引都應該是需要根據實際需求進行設計的,而使用ORM工具通過對象模型只能生成一個大概的表和列,無法生成准確的Schema。推薦使用專業的數據庫建模工具PowerDesigner或者ERWin進行數據庫建模,然后生成數據庫腳本。
開發
一、使用有意義的表別名。
在進行查詢時經常會JOIN很多表,那么就經常用到表別名,表別名使得SQL開發更簡單,查看起來也更簡潔。表別名一般就1個字母,或者2個字母,采用表的單詞首字母作為別名即可。
select 'ProjectAssignAuth', p.PROJECT_ID , 0 , 0 , gs.EMPLOYEE_ID,'STAFF' from GROUP_STAFF gs join GROUP_PROJECT gp on gs.GROUP_ID = gp.GROUP_ID join PROJECT p on gp.PROJECT_ID = p.PROJECT_ID
二、SQL語句中應該寫上詳細注釋。
這個算是老生常談了,SQL也是一種語言,對於復雜的邏輯,一不小心存儲過程就寫出了幾百行,如果沒有注釋,那么看一個幾百行的SQL那真是無比痛苦的事情,即使這個SQL是自己寫的,那么一個月以后,沒有注釋連自己都看不懂自己在寫什么。
三、使用print打印出過程信息。
在編寫復雜的存儲過程時,不可避免的就是要調試存儲過程的正確性,雖然SQL Server支持調試SQL語句的功能,但是在對於幾百行的SQL來說,還是很麻煩的。所以在編寫SQL時加入print過程信息的功能,這個相當於寫程序時的Debug.WriteLine(),打印的信息對外部程序並沒有影響,只是在SSMS調用存儲過程時能夠打印一些有用的信息。
四、增加調試參數幫助輸出更多的調試信息。
在編寫C#代碼的時候,我們在VS中可以設置Debug或者Release模式,同樣我們可以在存儲過程中增加一個帶有默認值的參數,比如我們有一個計算項目金額的存儲過程,計算邏輯復雜,我們可以增加一個@debug參數,默認情況下是關閉的,輸入一些調試信息。
create proc PROC_CALC_PROJECT_AMOUNT @pid int,--項目ID @debug bit=0 as …SQL if(@debug=1) begin --輸出一下調試信息 end
這樣我們平時調用時只傳入一個參數,在SSMS中想打開調試信息時,只需要增加第二個參數1即可:
exec PROC_CALC_PROJECT_AMOUNT 100,1
五、盡量避免在WHERE條件中對字段使用函數。
這個是編程人員容易犯的錯誤。因為對字段使用函數后將無法使用到字段中的索引,降低了執行效率。比如查詢所有2012年新建的項目,那么應該寫成:
select * from Project p where p.CreateDate between ‘2012-1-1’ and ‘2012-12-31’;
而不要寫成
select * from Project p where Year(p.CreateDate)=2013;
六、使用視圖來抽象公共的查詢部分。
在設計中提到使用配置表來把一些可能變化的查詢條件放在數據庫中,這樣在需求更改時只修改數據庫中的配置,而不用一個一個的改存儲過程和SQL語句。另外還有一種方法就是使用視圖來抽取公共查詢的部分,將一些邏輯和條件放在視圖中,然后其他存儲過程和SQL直接使用視圖,在需求發生變化時,我們只需要修改視圖,其他的存儲過程和SQL都不用修改。
七、小心查詢時數據類型不匹配隱式轉換導致的性能問題。
對於數據庫中每個字段的類型不一定完全和其存儲的值匹配。比如我們在設計員工表的員工號字段時,考慮到員工號不一定是個整數,所以設計成了varchar(10),但是在實際應用中所有員工號都是5位數的整數,那么我們可能在寫查詢時可能就直接把int類型的員工號傳入進行查詢。
八、公用表表達式CTE、臨時表和表變量的使用。
CTE 可用於:
-
創建遞歸查詢。這個在樹結構查詢中常用。
-
在不需要常規使用視圖時替換視圖,也就是說,不必將定義存儲在元數據中。
-
啟用按從標量嵌套 select 語句派生的列進行分組,或者按不確定性函數或有外部訪問的函數進行分組。
-
在同一語句中多次引用生成的表。
臨時表分為局部臨時表#開頭和全局臨時表##開頭。臨時表可以建立索引,對於大數據量的臨時存儲時就使用臨時表。
表變量適用於存儲數據量不大的臨時數據。表變量不可用創建索引。
運維
一、數據庫操作必須腳本化並進行版本控制。
所有數據庫的操作,包括前期的建表、初始化數據、建索引后期的增量修改和數據維護,都必須以SQL腳本來執行。這些腳本都保存到源代碼管理中。這樣方便於測試和部署。
二、數據庫腳本應該能夠重復執行。
在創建或者修改數據庫對象時,先判斷現有數據庫中是否已經有這個對象,有的話就不再創建或者改為更新對象或者將原對象刪除,重新創建。這樣腳本可以重復執行,避免了環境不一致導致腳本在這個環境可以正常運行,在另外一個數據庫卻報錯的情況。
三、在修改或刪除數據時,先把原有的數據值SELECT出來並將結果保存在Log中。
系統上線后有可能因為用戶操作的原因,也可能是系統的Bug,導致了錯誤數據的產生,那么就需要出維護腳本將這些錯誤的數據刪除或者更新回來。對於delete和update類的維護腳本,需要在刪除和修改之前先select出要修改的數據,維護人員將查詢的結果保存到維護日志中,這樣如果編寫的維護腳本有問題,那么還可以根據維護日志看到原來的數據,將數據修復回來。