一、使用場景
(1)可擴展架構設計,比如一個ERP用5年不卡,到了10就卡了因爲數據太多了,這個時候很多人都是備份然後清空數據
(2) 數據量太多 ,例如每天都有 幾十上百萬的數據進入庫,如果不分表後面查詢將會非常緩慢
(3) 性能瓶頸 ,數據庫現有數據超過1個億,很多情況下索引會莫名失效,性能大大下降
二、 內置分表使用
自帶分表支持按年、按季、按月、按周、按日進行分表(支持混合使用)
2.1 定義實體
我們定義一個實體,主鍵不能用自增或者int ,設爲long或者guid都可以,我例子就用自帶的雪花ID實現分表
[SplitTable(SplitType.Year)] //按年分表 (自帶分表支持 年、季、月、周、日) [SugarTable( "SplitTestTable_{year}{month}{day}" )] //3個變量必須要有,這麼設計爲了兼容開始按年,後面改成按月、按日 public class SplitTestTable { [SugarColumn(IsPrimaryKey = true )] public long Id { get ; set ; } public string Name { get ; set ; } [SugarColumn(IsNullable = true )] //設置爲可空字段 (更多用法看文檔 遷移) public DateTime UpdateTime{ get ; set ;} [SplitField] //分表字段 在插入的時候會根據這個字段插入哪個表,在更新刪除的時候用這個字段找出相關表 public DateTime CreateTime { get ; set ; } } //按年分表格式如下 SplitTestTable_20220101 //比如現在5月按月分表格式如下 SplitTestTable_20220501 //比如現在5月11日按日分表格式如下 SplitTestTable_20220511 //因爲插入會根據實體自動建表 //比如何設置可空類型等設置需要知道如何配置:https://www.donet5.com/Home/Doc?typeId=1206 |
2.2 同步表和結構
假如分了20張表,實體類發生變更,那麼 20張表可以自動同步結構,與實體一致
注意:插入會自動建表不需要這行代碼,主要用於實體改動後同步多個表結構,或者一張表沒有初始一張
禁止寫到業務裏面多次執行
//不寫這行代碼 你也可以用插入建表,插入用法看文檔下面 db.CodeFirst .SplitTables() //標識分表 .InitTables<SplitTestTable>(); //程序啓動時加這一行,如果一張表沒有會初始化一張 |
2.3.1 查詢: 時間過濾
通過開始時間和結束時間自動生成CreateTime的過濾並且找到對應時間的表
//簡單示例 var list=db.Queryable<OrderSpliteTest>().SplitTable(beginDate,endDate).ToPageList(1,2); //結合Where var list=db.Queryable<OrderSpliteTest>().Where(it=>it.Id>0).SplitTable(beginDate,endDate).ToPageList(1,2); //注意: //1、 分頁有 OrderBy寫 SplitTable 後面 ,uinon all後在排序 //2、 Where儘量寫到 SplitTable 前面,先過濾在union all //原理:(sql union sql2) 寫SplitTable 後面生成的括號外面,寫前生成的在括號裏面 |
2.3.2 查詢: 選擇最近的表
如果下面是按年分表 Take(3) 表示 只查 最近3年的 分表數據
var list=db.Queryable<OrderSpliteTest>() .Where(it=>it.Pk==Guid.NewGuid()) .SplitTable(tabs => tabs.Take(3)) //近3張,也可以表達式選擇 .ToList(); |
2.3.3 查詢: 精準定位一張表
根據分表字段的值可以精準的定位到具體是哪一個分表,比Take(N)性能要高出很多
var name=Db.SplitHelper<SplitTestTable>().GetTableName(data.CreateTime); //根據時間獲取表名 //推薦: 表不存在不會報錯 var list=db.Queryable<OrderSpliteTest>().SplitTable(tabs => tabs.InTableNames(name)).ToList() //不推薦:查詢不推薦用,刪除和更新可以用 var list=Db.Queryable<SplitTestTable>().AS(name).Where(it => it.Id==data.Id).ToList(); //修改、刪除、更新都可以用As(Name) |
2.3.4 查詢: 表達式定位哪幾張表
Db.Queryable<SplitTestTable>() .Where(it => it.Id==data.Id) .SplitTable(tas => tas.Where(y=>y.TableName.Contains( "2019" ))) //表名包含2019的表 .ToList(); |
2.3.5 查詢: 分表Join正常表
(推薦插入存全,儘量不要聯表影響性能)
//分表使用聯表查詢 var list=db.Queryable<Order>() // Order是分表 .SplitTable(tabs=>tabs.Take(3)) //可以換成1-8的所有分表寫法,不是隻能take .LeftJoin<Custom>((o,c)=>o.CustomId==c.Id) //Custom正常表 .Select((o,c)=> new { name=o.Name,cname=c.Name }).ToPageList(1,2); |
2.3.6 查詢: 分表JOIN分表
(推薦插入存全,儘量不要聯表影響性能)
var rightQuery=db.Queryable<Custom>().SplitTable(tabs=>tabs.Take(3)) ; var list=db.Queryable<Order>().SplitTable(tabs=>tabs.Take(3)) .LeftJoin<Custom>(rightQuery,(o,c)=>o.CustomId==c.Id) // Join rightQuery .Select((o,c)=> new { name=o.Name,cname=c.Name }).ToPageList(1,2); //技巧:如果是單表分表沒有表返回第一個表可以防止報錯 升級到:5.1.4.127 + SplitTable(it=>it.ContainsTableNamesIfNullDefaultFirst( "table" )) |
2.3.7 查詢: 性能優化
條件儘可能寫在SplitTable前面,因爲這樣會先過濾在合併
.Where(it => it.Pk == Guid.NewGuid()) //先過濾 .SplitTable(tabs => tabs.Take(3)) //在分表 |
2.3.8 查詢: 所有分表檢索
沒辦法精確過濾表時用,Where一定要寫SplitTable前面
//如果是主鍵查詢哪怕100個分表都很快 var list = db.Queryable<OrderSpliteTest>() .Where(it => it.Pk == Guid.NewGuid()) //適合有索引列,單條或者少量數據查詢 .SplitTable().ToList(); //沒有條件就是全部表 //老版本 var list = db.Queryable<OrderSpliteTest>() .Where(it => it.Pk == Guid.NewGuid()) //適合有索引列,單條或者少量數據查詢 .SplitTable(tab=>tab).ToList(); |
2.4 插入
因爲我們用的是Long所以採用雪花ID插入(guid也可以禁止使用自增列), 實體結構看上面 3.1
注意:.SplitTable不要漏掉了
var data = new SplitTestTable() { CreateTime=Convert.ToDateTime( "2019-12-1" ), //要配置分表字段通過分表字段建表 Name= "jack" }; //雪花ID+表不存在會建表 db.Insertable(data).SplitTable().ExecuteReturnSnowflakeIdList(); //插入並返回雪花ID並且自動賦值ID //服務器時間修改、不同端口用同一個代碼、多個程序插入一個表都需要用到WorkId //保證WorkId唯一 ,程序啓動時配置 SnowFlakeSingle.WorkId=從配置文件讀取; //GUID+表不存在會建表 db.Insertable(data).SplitTable().ExecuteCommand(); //插入GUID 自動賦值 ID //大數據寫入+表不存在會建表 db.Fastest<OrderSpliteTest>().SplitTable().BulkCopy(List<OrderSpliteTest>); //自動找表大數據寫入 //不會自動建表 如果表100%存在用這個性能好些 db.Fastest<OrderSpliteTest>().AS(表名).BulkCopy(List<OrderSpliteTest>); //固定表大數據寫入 //大數據寫入方式如果用到雪花ID需要手動賦值:SnowFlakeSingle.Instance.NextId() //部分數據庫需配置 具體用法看文檔: https://www.donet5.com/Home/Doc?typeId=2404 |
批量插入 因爲我們是根據CreateTime進行的分表,生成的SQL語句如下:
var datas = new List<SplitTestTable>(){ new SplitTestTable(){CreateTime=Convert.ToDateTime( "2019-12-1" ),Name= "jack" } , new SplitTestTable(){CreateTime=Convert.ToDateTime( "2022-02-1" ),Name= "jack" }, new SplitTestTable(){CreateTime=Convert.ToDateTime( "2020-02-1" ),Name= "jack" }, new SplitTestTable(){CreateTime=Convert.ToDateTime( "2021-12-1" ),Name= "jack" } }; db.Insertable(datas).SplitTable().ExecuteReturnSnowflakeIdList(); //插入返回雪花ID集合 |
執行完生成的表
生成的Sql:
自動識別4條記錄,分別插入4個不同的表中
2.5 刪除數據
(1)推薦用法:新功能 5.0.7.7 preview及以上版本
//直接根據實體集合刪除 (全自動 找表插入) db.Deleteable(deleteList).SplitTable().ExecuteCommand(); //,SplitTable不能少 |
(2)最近3張表都執行一遍刪除
db.Deleteable<SplitTestTable>().In(id).SplitTable(tas=>tas.Take(3)).ExecuteCommand(); |
(3)精準刪除
相對於上面的操作性能更高,可以精準找到具體表
var tableName=Db.SplitHelper<SplitTestTable>().GetTableName(data.CreateTime); //根據時間獲取表名 db.Deleteable<SplitTestTable>().AS(tableName).Where(deldata).ExecuteCommand(); //DELETE FROM [SplitTestTable_20210101] WHERE [Id] IN (1454676863531089920) |
(4)範圍刪除
var tables = db.SplitHelper<OrderSpliteTest>().GetTables().Take(3); //近3張分表 foreach ( var item in tables) { //刪除1點到6點時間內數據 db.Deleteable<OrderSpliteTest>() .AS(item.TableName) //使用當前分表名 .Where(it => it.Time.Hour < 1&&it.Time.Hour<6) .ExecuteCommand(); } |
2.6 更新數據
推薦用法: 新功能 5.0.7.7 preview及以上版本
//直接根據實體集合更新 (全自動 找表更新) db.Updateable(updateList).SplitTable().ExecuteCommand(); //.SplitTable()不能少 //BulkCopy分表更新 db.Fastest<OrderSpliteTest>().SplitTable().BulkUpdate(List<OrderSpliteTest>); //部分數據庫需配置 具體用法看文檔: //範圍更新 var tables = db.SplitHelper<OrderSpliteTest>().GetTables().Take(3); //近3張分表 foreach ( var item in tables) { //更新1點到6點時間內數據 db.Updateable<OrderSpliteTest>() .AS(item.TableName) //使用分表名 .SetColumns(it=> new OrderSpliteTest(){ Static=1 }) .Where(it => it.Time.Hour < 1&&it.Time.Hour<6) .ExecuteCommand(); } |
更多用法:
//更新近3張表 db.Updateable(deldata).SplitTable(tas=>tas.Take(3)).ExecuteCommand(); //精準找單個表 var tableName=Db.SplitHelper<SplitTestTable>().GetTableName(data.CreateTime); //根據時間獲取表名 db.Updateable(deldata).AS(tableName).ExecuteCommand(); //實體 db.Updateable<TestEnity>().AS(tableName).SetColumns(..).Where(...).ExecuteCommand(); //表達式 //通過表達式過濾出要更新的表 db.Updateable(deldata).SplitTable(tas => tas.Where(y=>y.TableName.Contains( "_2019" ))).ExecuteCommand(); |
2.7 輔助方法
ORM不是所有功能都支持分表,我們可以分表輔助方法實現
//用例1:獲取所有表表名 ,可以用於循環處理每個表 var tables=db.SplitHelper<Order>().GetTables(); //例2:根據分表字段的值獲取表名 var tableName = db.SplitHelper<Order>().GetTableName(DateTime.Now); //有重載 db.Updateable(data).AS(tableName).ExecuteCommand() //例3:根據當前對象獲取表名,有這個功能就可以把List進行分類 var tableNames =db.SplitHelper(List<T> dataList)).GetTableNames(); //根據實體集合獲取表名集合 var tableName =db.SplitHelper(T data).GetTableName(); //根據實體獲取表名 Order_20220101- [記錄1,記錄2 ....] Order_20210101- [記錄1,記錄2 ...] Db.Storageable(Order_20220101所有記錄集合).As( "Order_20220101" ).ExecuteCommand() |
2.8 唯一判段
唯一列要加上索引,這樣性能才能保證上百張表裏面也不會慢
if (!db.Queryable<OrderSpliteTest>().SplitTable().Any(it => it.Name==p)) { //唯一不存在 } |
三、 自定義分表:按單詞
疑問: 自定義分表可以支持多個字段
答:可以的,只要你重寫的方法兼容多個字段就可以
上面的分表功能是我們自帶集成的,比如我想實現自定義的分表我該如何實現呢?
3.1 按首字母拼音分表
我們就寫個按24個字母進行分表的小例子,來學習一下如何自定義分表
3.2 創建分表類
我們新建一個類繼承成ISplitTableService 接口
public interface ISplitTableService { //獲取表名用於 SplitTable tas 篩選 List<SplitTableInfo> GetAllTables(ISqlSugarClient db,EntityInfo EntityInfo,List<DbTableInfo> tableInfos); //獲取默認表名 string GetTableName(ISqlSugarClient db, EntityInfo EntityInfo); string GetTableName(ISqlSugarClient db, EntityInfo EntityInfo, SplitType type); //根據分表字段會值(下面函數獲取分表字段值)獲取表名 string GetTableName(ISqlSugarClient db, EntityInfo entityInfo, SplitType splitType, object fieldValue); //獲取分表字段的值 (可以多字段返回一個組合值) object GetFieldValue(ISqlSugarClient db, EntityInfo entityInfo, SplitType splitType, object entityValue); } |
3.3 使用自定義分表
創建一個WordSplitService.cs繼承ISplitTableService
用例下載:
//配置方式1:配置自定義分表 db.CurrentConnectionConfig.ConfigureExternalServices.SplitTableService = new WordSplitService(); //配置方式2:高版本支持了特性使用自定義分表 5.1.4.78 [SplitTable(SplitType._Custom01, typeof (WordSplitService))] //插入數據 db.Insertable( new WordTestTable(){CreateTime=DateTime.Now,Name= "BC" }).SplitTable().ExecuteReturnSnowflakeId(); db.Insertable( new WordTestTable(){CreateTime=DateTime.Now,Name= "AC" }).SplitTable().ExecuteReturnSnowflakeId(); db.Insertable( new WordTestTable(){CreateTime=DateTime.Now,Name= "ZBZ" }).SplitTable().ExecuteReturnSnowflakeId(); |
執行完數據庫就多了3張表,因爲是按首字母分的表 ,插入了3條記錄自動創建了3張表,插入生成的SQL:
INSERT INTO [WordTestTable_FirstB] ([Id],[Name],[CreateTime]) VALUES (@Id,@Name,@CreateTime) ; INSERT INTO [WordTestTable_FirstA] ([Id],[Name],[CreateTime]) VALUES (@Id,@Name,@CreateTime) ; INSERT INTO [WordTestTable_FirstZ] ([Id],[Name],[CreateTime]) VALUES (@Id,@Name,@CreateTime) ; |
查詢分表
//查詢字母A開頭的分 var listall = db.Queryable<WordTestTable>().Where(it => it.Name == "all" ).SplitTable(tas => tas.ContainsTableNames( "_FirstA" )).ToList(); //生成的SQL: //SELECT * FROM (SELECT [Id],[Name],[CreateTime] FROM [WordTestTable_FirstA] WHERE ( [Name] = @Name0UnionAll1 )) unionTable |
四、自定義分表2:按年月
支持2種方式配置自定義分表
//配置方式1:配置自定義分表 db.CurrentConnectionConfig.ConfigureExternalServices.SplitTableService = new yyyyMMService(); //配置方式2:高版本支持了特性使用自定義分表 5.1.4.78 [SplitTable(SplitType.Month,SplitType._Custom01, typeof (yyyyMMService))] |
我們自帶的時間分表格式爲 xx_20220101這種,如果我想格式爲 xx_202201 那麼我們可以用自定義分表
查看:https://www.donet5.com/Ask/9/16110
注意:自定義的時間分表是沒辦法後期改變分表類型,比如你設置了按月那麼以後就不能改按周,如果想擴展性強用自帶的時間分表
五、性能優化
(1) 分表字段要建索引
(2) where寫到SplitTable前面
var list=db.Queryable<OrderSpliteTest>() Where(it=>it.id>1) //where寫SplitTable前面 .SplitTable(beginDate,endDate).ToPageList(1,2) |
(3) 如果分頁可以不查詢count,給個固定前10頁 (特別mysql查詢count是比較慢的)
六、視頻教程
https://www.bilibili.com/video/BV13B4y1h7Wu?p=4
七、時序數據庫
上面介紹的都是關係型數據庫對業務進行分表,如果使用時序數據庫 那麼數據庫會自動分表
並且使用和正常表一樣, 分表的事情都交給數據庫,當成正常表去用就行了
https://www.donet5.com/Home/Doc?typeId=2434
八、老表數據遷移分表
步驟1:
老表改名爲 xxxxxx ( 隨便什麼名字), 防止和分表的表名衝突引起錯誤
如果比較慢:你也可以改新建的名字 比如以前叫 order現在改 OrderNew
步驟2:
var pageList = new List<新類>(); //主鍵不能是自增 var pageSize=200000; //每次讀取20萬 (數據庫是1-20萬 20萬-40萬這樣讀取) db.Queryable< object >().As( "xxxxxx" ).Select<新類>().ForEach(it =>{ pageList.Add(it); if (pageList.Count==pageSize) //每個分頁的最一次執行,防止循操作庫 { db2.Fastest<新類>().SplitTable().BulkCopy(pageList); //插入分批數據 pageList = new List<新類>(); //清空 } },pageSize); //設置分頁 db2.Fastest<新類>().SplitTable().BulkCopy(pageList); //插入剩餘的 (最後一頁可能有不足20萬的) |