這絕對是 ORM 的使用者,開發人員與 DBAs 共同想要問的議題,到底我使用了 ORM 和使用傳統的 ADO.NET 下 SQL 指令的方式會差多少? 這個問題不但會發生在 Entity Framework 上,也會發生在 NHibernate 等 ORM Framework 內,連同我自己在這個系列文中開發的 ORM 機制也會受到影響...
這絕對是 ORM 的使用者,開發人員與 DBAs 共同想要問的議題,到底我使用了 ORM 和使用傳統的 ADO.NET 下 SQL 指令的方式會差多少? 這個問題不但會發生在 Entity Framework 上,也會發生在 NHibernate 等 ORM Framework 內,連同我自己在這個系列文中開發的 ORM 機制也會受到影響。
我們在前面的 ORM 1-9 系列文中看到了整個開發 ORM 所需要的技術和方法,然後也實作出了一個完整的 ORM Framework (再簡單不過的版本),我們在這個 ORM Framework 中用了下列技術:
- Reflection,這是最重要的技術,沒有它,就無法動態的對應 Property 和 Field。
- Attribute,少了它就沒辦法宣告自訂對應 (在 ORM (9) 有程式化的對應)。
- Type Converter,沒有它的話就無法轉換資料,尤其是資料中有列舉值時。
- SQL Generation,自動產生 SQL 指令,讓開發人員不再需要撰寫 SQL。
- ADO.NET,最核心的物件,而且只使用 Connection, Command 與 DataReader。
- Provider Pattern,利用它來切換不同的資料提供者。
- LINQ Expression,在 ORM (9) 時實作 Coded Map 之用。
而在使用這些技術時,有哪些情況會影響到效能?
1. 動態產生的 SQL
當 ORM Framework 接到物件存取資料庫的要求時,通常會有一個機制來產生 SQL 指令,再交由 ADO.NET 存取資料,但是你知道 ORM Framework 產生的 SQL 指令的樣貌嗎?因為 SQL 指令的下法會影響 DBMS 存取資料的效能,而 SQL 指令若是由 ORM Framework 產生的話,表示開發人員沒辦法自己去修改 SQL 來做這件事。最常見的解決方法,就是自己寫 SQL,而多數的 ORM Framework 都還是會提供一個讓開發人員直接執行 SQL 的方法,而回傳的可以是 DataReader 或是由開發人員指定的 DTO (POCO) 物件。
ORM 被 DBA 攻擊的其中一個點,就是 DBA 無法介入寫 SQL 這件事,對於一些具有規模的系統來說,SQL 都是 DBA 在寫的,但卻無法相容於 ORM Framework 或是開發人員所寫的 POCO 物件,這時就會有問題,而且多數的大系統,通常不會允許由開發人員自己寫 SQL 存取表格,而是用檢視表或是預存程序或函數等資料庫物件來存取,所以主流的 ORM Framework 都有提供可存取資料庫物件的功能,這樣 DBA 就能將 SQL 指令的效能影響降低 (或是把效能責任推給開發人員…)。但對於小型專案而言,SQL 指令要由開發人員來處理,因此若要用 ORM 來替代寫 SQL 的工作,就等於要承受 ORM 產生的 SQL 的效能問題,但還是可以透過自己寫 SQL 來處理,所以最後的問題還是會落在 SQL 的寫法好不好。
Entity Framework 和 NHibernate 等主流 ORM 都會花不少的心思來調校自動產生的 SQL,也會提供輸出它所產生的 SQL 的方式,例如 EF 有 ObjectQuery.ToTraceString(), NH 也有提供方法捕捉。雖然我自己寫的 ORM Framework 沒有提供這功能,但仍可以改寫它來提供這樣的能力。而 DBA 或開發人員就可以利用得到的 SQL 來做調校工作,例如調整索引或是更新統計資料等工作,來調整 SQL 執行的效能,或是自己寫 SQL 來做。
2. 撈取資料時的方式
使用 ADO.NET 撈取資料的話,用 DataReader 一定是最快的,搭配上 Sequential Access 那幾乎等於無敵,除非開發人員要自己處理 SQL protocols (ex: SQL Server 的 TDS),否則以 ADO.NET 內建的資料存取方式來說,DataReader 己經是最快的作法了,若是轉成 DataTable 再讀入,會有兩段的 Type Casting 負擔,不符合 ORM Framework 的基本要求。
Sequential Access 的好處是會逐項依目前游標的位置來讀取,不必推算資料流的位置,所以速度很快,但因為它不會推算資料流,所以它也不能捲回前面的資料流位置,這點在使用上要特別注意。
3. 對應欄位與屬性時的處理
這點就是 ORM 最大的罩門了,要做到程式化的 Property <-> Field 對應,一定得依賴 Reflection,而 Reflection 最花時間的地方莫過於取得 Property Metadata 這一塊,所以如果可以事先快取 Property Metadata 的話,在讀取資料時就可以快近三四倍 (這是我自己實證的經驗)。
再來是 Type Casting 的問題,這個其實很難避免,數值型別的話還好有 Convert 物件可用,但若是自訂型別的話,就只能自己寫一個 Type Converter 來做,這時用泛型會比用 object 來得優,因為可以省下 box/unbox 的效能損耗。
4. 延遲載入
延遲載入 (Lazy Loading) 在前面的 ORM 文章有提到,在有關聯性的資料模型內,有時使用 ORM 產生物件時,不一定會用到關聯的資料,所以我們可以選擇要用時再載入,如此可以省下一次 SQL 的負載,而且就算是物件本身,也可以只載入像 ID/Name 這種必要資料,若需要更多資料時再進一步載入即可,但負面的問題就是它需要好幾個 SQL 才能完全載入必要的資料,但如果一次載入大資料和分多次載入小資料相比,我想後者的效能會好些。
5. 其他的考量
懂得整個資料存取與資料庫處理流程的開發人員,而且是在特定的 DBMS 之下時,可以針對該 DBMS 做更進一步的最佳化,例如像 SQL 查詢計畫快取 (Query Plan Caching),SQL 快取 (SQL Caching),物件快取 (Object Caching) 等,簡化 SQL 和 DBMS 的查詢負擔,並且加快整個 ORM Framework 的速度,另外像是語法的下法 (ex: LINQ to Entities 的查詢下法) 和關聯的使用技巧等都會間接影響到 ORM 的效能。
說了這麼多,我所要強調的是,ORM 是好用的資料存取方式,但它一定會有某種程度不可避免的效能損耗,我們可以做的事,就是將這些損耗降到最低,讓整體的資料存取速度加快,所以當看到了 ORM 的好處時,也不能忘了它也有缺點,而事先了解它的缺點並予以注意或改善,再來好好的享受它的優點,也不錯。
Reference: