數據庫快速遷移10億級數據

咱們一直在追求架構的藝術!!數據庫

image

問題分析

通過幾分鐘的排查,數據庫狀況以下:服務器

  • 數據庫採用Sqlserver 2008 R2,單表數據量21億。

image

  • 無水平或者垂直切分,可是採用了分區表。分區表策略是按時間降序分的區,將近30個分區。正由於分區表的緣由,系統才保證了在性能不是太差的狀況下堅持至今。
  • 此表除彙集索引以外,無其餘索引,無主鍵(主鍵實際上是利用索引來快速查重的)。因此在頻繁插入新數據的狀況下,索引調整所耗費的性能比較低。
    image

至於彙集索引和非彙集索引等知識,請各位移步google或者百度。架構

至於業務,不是太複雜。通過相關人員諮詢,大約40%的請求爲單條Insert,大約60%的請求爲按class_id 和in_time(倒序)分頁獲取數據。Select請求所有命中彙集索引,因此性能很是高。這也是彙集索引之因此這樣設計的目的。 app

解決問題

因爲單表數據量已經超過21億,而且2017年之前的數據幾乎不影響業務,因此決定把2017年之前(不包括2017年)的數據遷移到新表,僅供之後特殊業務查詢使用。通過查詢大約有9億數據量。
數據遷移工做包括三個個步驟:ide

  1. 從源數據表查詢出要遷移的數據
  2. 把數據插入新表
  3. 把舊錶的數據刪除
傳統作法

這裏申明一點,就算是傳統的作法也須要分頁獲取源數據,由於你的內存一次性裝載不下9億條數據。性能

  1. 從源數據表分頁獲取數據,具體分頁條數,太少則查詢原表太頻繁,太多則查詢太慢。
    SQL語句相似於
    SELECT * FROM (
    SELECT *,ROW_NUMBER() OVER(ORDER BY class_id,in_time) p FROM  tablexx WHERE in_time <'2017.1.1'  
    ) t WHERE t.p BETWEEN 1 AND 100
  2. 把查詢出來的數據插入目標數據表,這裏強調一點,必定不要用單條插入策略,必須用批量插入。
  3. 把數據刪除,其實這裏刪除仍是有一個小難點,表沒有標示列。這裏不展開,由於這不是菜菜要說的重點。

若是你的數據量不大,以上方法徹底沒有問題,可是在9億這個數字前面,以上方法顯得愛莫能助。一個字:慢,太慢,很是慢。google

能夠大致算一下,假如每秒能夠遷移1000條數據,大約須要的時間爲(單位:分)設計

900000000/1000/60=15000(分鐘)code

大約須要10天^ V ^server

改進作法

以上的傳統作法弊端在哪裏呢?

  1. 在9億數據前查詢必須命中索引,就算是非彙集索引菜菜也不推薦,首推彙集索引。
  2. 若是你瞭解索引的原理,你應該明白,不停的插入新數據的時候,索引在不停的更新,調整,以保持樹的平衡等特性。尤爲是彙集索引影響甚大,由於還須要移動實際的數據。

提取以上兩點共同的要素,那就是彙集索引。相應的解決方案也就應運而生:

  1. 按照彙集索分頁引查詢數據
  2. 批量插入數據迎合彙集索引,即:按照彙集索引的順序批量插入。
  3. 按照彙集索引順序批量刪除

因爲作了表分區,若是有一種方式把2017年之前的分區直接在磁盤物理層面從當前表剝離,而後掛載到另一個表,可算是神級操做。有誰能指導一下菜菜,感激涕零

擴展閱讀

  1. 一個表的彙集索引的順序就是實際數據文件的順序,映射到磁盤上,本質上位於同一個磁道上,因此操做的時候磁盤的磁頭沒必要跳躍着去操做。

  2. 存儲在硬盤中的每一個文件均可分爲兩部分:文件頭和存儲數據的數據區。文件頭用來記錄文件名、文件屬性、佔用簇號等信息,文件頭保存在一個簇並映射在FAT表(文件分配表)中。而真實的數據則是保存在數據區當中的。日常所作的刪除,實際上是修改文件頭的前2個代碼,這種修改映射在FAT表中,就爲文件做了刪除標記,並將文件所佔簇號在FAT表中的登記項清零,表示釋放空間,這也就是日常刪除文件後,硬盤空間增大的緣由。而真正的文件內容仍保存在數據區中,並未得以刪除。要等到之後的數據寫入,把此數據區覆蓋掉,這樣纔算是完全把原來的數據刪除。若是不被後來保存的數據覆蓋,它就不會從磁盤上抹掉。

NetCore 代碼(實際運行代碼)

  1. 第一步:因爲彙集索引須要class_id ,因此寧肯花2-4秒時間把要操做的class_id查詢出來(ORM爲dapper),而且升序排列
    DateTime dtMax = DateTime.Parse("2017.1.1");
            var allClassId = DBProxy.GeSourcetLstClassId(dtMax)?.OrderBy(s=>s);
  2. 按照第一步class_id 列表順序查詢數據,每一個class_id 分頁獲取,而後插入目標表,所有完成而後刪除源表相應class_id的數據。(所有命中彙集索引)

    int pageIndex = 1; //頁碼
            int pageCount = 20000;//每頁的數據條數
            DataTable tempData =null;
            int successCount = 0;
            foreach (var classId in allClassId)
            {
                tempData = null;
                pageIndex = 1;
                while (true)
                {
                    int startIndex = (pageIndex - 1) * pageCount+1;
                    int endIndex = pageIndex * pageCount;
    
                    tempData = DBProxy.GetSourceDataByClassIdTable(dtMax, classId, startIndex, endIndex);
                    if (tempData == null || tempData.Rows.Count==0)
                    {
                        //最後一頁無數據了,刪除源數據源數據而後跳出
                         DBProxy.DeleteSourceClassData(dtMax, classId);
                        break;
                    }
                    else
                    {
                        DBProxy.AddTargetData(tempData);
                    }
                    pageIndex++;
                }
                successCount++;
                Console.WriteLine($"班級:{classId} 完成,已經完成:{successCount}個");
            }

    DBProxy 完整代碼:

    class DBProxy
    {
        //獲取要遷移的數據全部班級id
        public static IEnumerable<int> GeSourcetLstClassId(DateTime dtMax)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @"SELECT class_id FROM  tablexx WHERE in_time <@dtMax GROUP BY class_id ";
            using (connection)
            {
                return connection.Query<int>(Sql, new { dtMax = dtMax }, commandType: System.Data.CommandType.Text);
    
            }
        }
    
        public static DataTable GetSourceDataByClassIdTable(DateTime dtMax, int classId, int startIndex, int endIndex)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @" SELECT * FROM (
                        SELECT *,ROW_NUMBER() OVER(ORDER BY in_time desc) p FROM  tablexx WHERE in_time <@dtMax  AND class_id=@classId
                        ) t WHERE t.p BETWEEN @startIndex AND @endIndex ";
            using (connection)
            {
                DataTable table = new DataTable("MyTable");
                var reader = connection.ExecuteReader(Sql, new { dtMax = dtMax, classId = classId, startIndex = startIndex, endIndex = endIndex }, commandType: System.Data.CommandType.Text);
                table.Load(reader);
                reader.Dispose();
                return table;
            }
        }
         public static int DeleteSourceClassData(DateTime dtMax, int classId)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @" delete from  tablexx WHERE in_time <@dtMax  AND class_id=@classId ";
            using (connection)
            {
                return connection.Execute(Sql, new { dtMax = dtMax, classId = classId }, commandType: System.Data.CommandType.Text);
    
            }
        }
        //SqlBulkCopy 批量添加數據
        public static int AddTargetData(DataTable data)
        {
            var connection = Config.GetConnection(Config.TargetDBStr);
            using (var sbc = new SqlBulkCopy(connection))
            {
                sbc.DestinationTableName = "tablexx_2017";               
                sbc.ColumnMappings.Add("class_id", "class_id");
                sbc.ColumnMappings.Add("in_time", "in_time");
                .
                .
                .
                using (connection)
                {
                    connection.Open();
                    sbc.WriteToServer(data);
                }               
            }
            return 1;
        }
    
    }

    運行報告

    程序本機運行,開***鏈接遠程DB服務器,運行1分鐘,遷移的數據數據量爲 1915560,每秒約3萬條數據

    1915560 / 60=31926 條/秒

    cpu狀況(不高):

    image

    磁盤隊列狀況(不高):

    image

    在如下狀況下速度還將提升

    1. 源數據庫和目標數據庫硬盤爲ssd,而且分別爲不一樣的服務器
    2. 遷移程序和數據庫在同一個局域網,保障數據傳輸時候帶寬不會成爲瓶頸
    3. 合理的設置SqlBulkCopy參數
    4. 菜菜的場景大多數場景下每次批量插入的數據量達不到設置的值,由於有的class_id 對應的數據量就幾十條,甚至幾條而已,打開關閉數據庫鏈接也是須要耗時的
    5. 單純的批量添加或者批量刪除操做

領取架構師進階資料大禮包

相關文章
相關標籤/搜索