NewLife.XCode是一個有10多年歷史的開源數據中間件,支持nfx/netcore,由新生命團隊(2002~2019)開發完成並維護至今,以下簡稱XCode。
整個系列教程會大量結合示例代碼和運行日志來進行深入分析,蘊含多年開發經驗於其中,代表作有百億級大數據實時計算項目。
開源地址:https://github.com/NewLifeX/X (求star, 754+)
擴展查詢
前文《[NewLife.XCode]實體類詳解》中有講到擴展查詢,XCode生成實體類代碼時,在模型類有一個region叫“擴展查詢”,一般是FindByAbc/FindAllByAbc的形式。
擴展查詢以數據表索引為依據來生成:
- 唯一索引(含主鍵)生成FindByAbc方法(如FindByName),返回單個對象;
- 非唯一索引生成FindAllByAbc方法(如FindAllByClassID),返回對象列表(非null);
如上圖,可知Entity實體基類內部,查詢方法分為單對象查詢的Find和對象列表的查詢FindAll。
實際上,Find最終調用FindAll方法查一行。
Find/FindAll有多個重載,最主要的地方都是構造where查詢條件。
下划線_是每個實體類都有的內嵌類,它包含了每一個字段的Field引用,借助運算符重載,可以很方便的構造查詢條件,例如上面的_.Name == name最終會生成 where Name='Stone'
因為是內嵌類,在實體類內部使用的時候非常方便。但要是想要實體類外部使用,就麻煩很多了,需要帶上實體類類名。
原則:XCode是充血模型,不管多么簡單的查詢,建議都封裝Find/FindAll/Search等方法供外部使用。
高級表達式查詢
僅靠一兩個字段的簡單查詢,肯定無法滿足各種業務要求,我們需要更強大的查詢支持,特別是根據不同條件拼接不同語句。
上面是兩個非常典型的業務查詢。
這里請出了條件表達式WhereExpression,實際上它只有兩個功能,&表示And,|表示Or,根據表達式級別支持括號運算。
exp&=xxx 是最常用的寫法,右邊一般是各種Field表達式。
上面第一個例子,生成的查詢語句可能是 select * from Student where classid=?classid and name like '%?key%'
為什么說“可能”?因為classid為0,或者key為空時,並不會參與拼接查詢語句。
第二個例子稍微復雜一些,首先對key進行精確查詢,找到了就返回,若是沒找到,則開啟模糊查詢。
這里遇到了等於、包含、區間等判斷操作,后面會詳解所有支持的操作。
如非必要,建議保留select * 的查詢方式,而不是指定列。
碼農法則:數據庫壓力小於100qps時不要考慮指明select列來優化,大多數系統活不到需要優化的明天!
高級分頁
兩個例子都出現了一個PageParameter參數page,這是分頁參數,包含分頁查詢以及排序所需要的數據。
PageIndex和PageSize指定頁序號和每頁大小,這是內部建立分頁查詢的核心依據;
Sort 指定排序字段,Desc 指定是否降序(默認升序);
RetrieveTotalCount 指定是否或者總記錄數,若為true,則在查詢記錄集之前,先查詢滿足條件的總行數TotalCount,用於分頁PageCount。此時等於執行兩次數據庫查詢;
RetrieveState 指定是否獲取統計 State,若為true,則在查詢記錄集之后,執行聚合查詢,對數字型字段使用Sum聚合。此時最多可能執行3次數據庫查詢;
在執行FindAll查詢時,若有傳入 PageParameter 且 RetrieveTotalCount 為true,則先查詢滿足條件的記錄數,大於0時才查某一頁數據。
如果 Meta.Count 評估認為本表總行數超過100萬,且FindAll查詢沒帶有條件,則page.TotalCount直接取Meta.Count(少量偏差),以避免極大的FindCount耗時。
100萬行以上數據表,如若不帶條件或者條件沒有命中索引,select count 將會極其的慢,在1000萬以上甚至查不出來,這是XCode能對100億表進行分頁查詢的關鍵所在。
Meta.Count 的初始值來自於數據庫元數據索引表,里面有該表主鍵的總行數,取得該值后如果小於100萬再異步select count一次。
10多年前博客園ORM大戰的時候,我們常說,等你支持千萬級分頁的時候再來比,就是鑽了select count很慢的這個空子,很多人count出來總數再分頁 ^_^
上圖4億數據,查詢第10000頁,在SQLite單表上,阿里雲1C1G服務器。
FindCount 分頁
在早期版本,不支持RetrieveTotalCount ,只能通過 FindCount 取得滿足該條件的總記錄數,然后進行分頁,至今仍然支持傳統方法。
因此可以看到,FindAll 和 FindCount 都是成對出現,參數一摸一樣。
並且 FindCount 方法也會帶有分頁參數,雖然用不到,但.NET2.0時代的 ObjectDataSource 要求兩者的參數名稱和順序必須一致。
所有 FindCount 方法,將會得到 select count 查詢語句,因此千萬級大表需要慎用。
PageSplit 分頁
內置支持的各種數據庫,都有實現普通查詢語句轉為分頁語句的 PageSplit(sql, start, maxNums) 方法。
MySql/SQLite/PostgreSQL 能夠很好支持,只需要在 sql 后加上 limit start, maxNums 即可;
Oracle/SqlServer/Access/SqlCe 則要麻煩一次,其中SqlServer最復雜,不同版本的分頁方法還不同,早期版本還要求有主鍵字段;
因此,sql 必須是簡單的單表查詢語句,PageSplit 才能把任意查詢拆開並轉換為分頁查詢。
大表分頁優化
大表分頁查詢,開頭會很快,越是往后越慢!
XCode采用倒置優化法,對於超過100萬行(借助Meta.Count評估)的表,如果查詢頁超過中線,則從另一個方向查詢,然后再把結果倒置回來。
XCode要求數據查詢必須考慮分頁,沒有分頁的系統一般死在100萬行以內。
Field擴展
內嵌類_引用的字段是Field,它繼承自FieldItem。
Field/FieldItem全部功能:
- Equal 等於,操作符==
- NotEqual 不等於,操作符!=
- 大於操作符>,大於等於>=
- 小於操作符<,小於等於<=
- StartsWith 字符串開始,like '{0}%'。(支持索引)
- EndsWith 字符串結束,like '%{0}'
- Contains 字符串包含,like '%{0}%'
- In 集合包含,支持列表集合、字符串子查詢和SelectBuilder子查詢,集合只有一個元素時轉為相等操作
- NotIn 集合不包含,支持列表集合、字符串子查詢和SelectBuilder子查詢,集合只有一個元素時轉為不相等操作
- IsNull 是否空
- NotIsNull 不是空
- IsNullOrEmpty 字符串空或零長度
- NotIsNullOrEmpty 字符串非空非零長度
- IsTrue 是否True或者False/Null,參數決定兩組之一
- IsFalse 是否False或者True/Null,參數決定兩組之一
- Between 時間區間,大於等於開始,小於結束,如果開始結束都只有日期而沒有時分秒,則結束加一天,如(2019-04-17, 2019-04-17)查 time>='2019-04-17' and time<2019-04-18'
排序字句/分組聚合
- Asc,升序
- Desc,降序。order by name desc
- GroupBy,分組。group by name
- As,聚合別名
- Count,計數
- Sum,求和
- Min,最小
- Max,最大
查詢的本質
查詢的本質是五參數版FindAll(where, order, selects, start, maxnums),其它查詢方式都由它轉化而來!
Entity實體基類封裝了各種常用的查詢方法:
對於單表查詢的XCode來說,五參數版FindAll很容易得到 select [selects] from [table] where [where] order by [order] limit [start], [maxnums] 語句,根據這個理念,FindAll可以支持任意復雜查詢!
最終查詢語句,由SelectBuilder類承載。
多表子查詢
XCode不支持多表Join關聯,這在前面《擴展屬性》中提到過。
擴展屬性固然可以解決關聯多表字段的問題,並且借助緩存性能還不錯,但是需要同時在兩張表上設置條件的時候,就行不通了。
於是,需要用到高級查詢,可以用子查詢 來替代,正是前面說到的FieldItem.In擴展。
要查詢名為“992班”的所有學生,一般這樣寫:
select * from student s inner join class c on s.classid=c.id where c.name='992班'
XCode從2008年起,就放棄支持多表關聯,自然也就不支持這樣的寫法。
在一般系統里面,班級表數據不多,可以借助實體緩存或者對象緩存:
// Class.FindByName 內部用緩存 var cls = Class.FindByName("992班"); var list = Student.FindAll(Student._.ClassID==cls.ID); // select * from student where classid=1234
但如果主表從表都是百萬級大表,或者從表查詢條件比較復雜,緩存就有點難以為繼了。
於是有了子查詢:
調用方法:var list = Student.Search(SexKinds.女, "992班", p);
得到結果:
select * from student where sex=2 and classid in(select id from class where name='992班')
至此,絕大部分多表關聯復雜查詢語句,可以轉化為子查詢 !
系列教程
NewLife.XCode教程系列[2019版]
- 增刪改查入門。快速展現用法,代碼配置連接字符串
- 數據模型文件。建立表格字段和索引,名字以及數據類型規范,推薦字段(時間,用戶,IP)
- 實體類詳解。數據類業務類,泛型基類,接口
- 功能設置。連接字符串,調試開關,SQL日志,慢日志,參數化,執行超時。代碼與配置文件設置,連接字符串局部設置
- 反向工程。自動建立數據庫數據表
- 數據初始化。InitData寫入初始化數據
- 高級增刪改。重載攔截,自增字段,Valid驗證,實體模型(時間,用戶,IP)
- 臟數據。如何產生,怎么利用
- 增量累加。高並發統計
- 事務處理。單表和多表,不同連接,多種寫法
- 擴展屬性。多表關聯,Map映射
- 高級查詢。復雜條件,分頁,自定義擴展FieldItem,查總記錄數,查匯總統計
- 數據層緩存。Sql緩存,更新機制
- 實體緩存。全表整理緩存,更新機制
- 對象緩存。字典緩存,適用用戶等數據較多場景。
- 百億級性能。字段精煉,索引完備,合理查詢,充分利用緩存
- 實體工廠。元數據,通用處理程序
- 角色權限。Membership
- 導入導出。Xml,Json,二進制,網絡或文件
- 分表分庫。常見拆分邏輯
- 高級統計。聚合統計,分組統計
- 批量寫入。批量插入,批量Upsert,異步保存
- 實體隊列。寫入級緩存,提升性能。
- 備份同步。備份數據,恢復數據,同步數據
- 數據服務。提供RPC接口服務,遠程執行查詢,例如SQLite網絡版
- 大數據分析。ETL抽取,調度計算處理,結果持久化