如何在不耗尽内存的情况下部署包含 130,000 多个条目的脚本

Posted

技术标签:

【中文标题】如何在不耗尽内存的情况下部署包含 130,000 多个条目的脚本【英文标题】:How can I deploy a script with 130,000+ entries without running out of memory 【发布时间】:2015-11-09 07:40:54 【问题描述】:

我在使用 SSDT 构建的 Visual Studio 2015 中有一个数据库项目。在这个项目中是一个 T4 模板,它生成对存储过程的 SQL 调用,以输入世界数据库的初始数据。

它包括国家、地区、时区、地区和城市。

当 T4 模板完成后,它会生成大约 130,000 个:

EXEC [geo].[addUpdateCountry] @countryCode = N'AD', @countryName = N'Andorra', @initData = 1
EXEC [geo].[addUpdateCountry] @countryCode = N'AE', @countryName = N'United Arab Emirates', @initData = 1
EXEC [geo].[addUpdateCountry] @countryCode = N'AF', @countryName = N'Afghanistan', @initData = 1
EXEC [geo].[addUpdateCountry] @countryCode = N'AG', @countryName = N'Antigua and Barbuda', @

然后,我将文件链接到 PostDeploy 脚本,以便在使用特定发布配置文件时运行这些脚本。

它太大了,发布过程内存不足,所以我想我必须以某种方式将它们分成几批,但我不确定在这种情况下我会如何做到这一点。

【问题讨论】:

【参考方案1】:

当我遇到部署脚本和内存问题时,我已将我的脚本放入存储过程中,并从部署脚本中调用这些存储过程,您的 t4 模板是否会生成如下内容:

create proc deploy.country_data
as
   EXEC [geo].[addUpdateCountry] @countryCode = N'AD', @countryName = N'Andorra', @initData = 1
   EXEC [geo].[addUpdateCountry] @countryCode = N'AE', @countryName = N'United Arab Emirates', @initData = 1
   EXEC [geo].[addUpdateCountry] @countryCode = N'AF', @countryName = N'Afghanistan', @initData = 1
   etc...

然后从您的部署后脚本中调用它。

我通常将这些部署存储过程分离到一个单独的 ssdt 项目中,并使用 /p:IncludeCompositeObject=true 进行部署。

我还会提出一个关于 OO 异常的连接项。

您还可以通过使用 :r import 将不同的脚本导入主部署后脚本来分离部署脚本,但您需要知道脚本的位置。

【讨论】:

是的,我想我也会让它们成为一个单独的 SSDT,因为它会使主项目花费很长时间来构建 lol。【参考方案2】:

我建议打开与您遇到的 OOM 异常相关的 Connect 问题。这可以在https://connect.microsoft.com/SQLServer/feedback/CreateFeedback.aspx 使用“开发人员工具(SSDT、BIDS 等)”类别完成。

要解决发布期间内存不足的问题,可以使用(64 位)命令行工具 SqlPackage.exe 来执行发布操作。 SqlPackage.exe 是一个命令行包装器,它围绕在 Visual Studio 和 SSMS 中执行发布操作的同一数据层应用程序框架代码。

SqlPackage.exe 可以从这里下载:http://www.microsoft.com/en-us/download/details.aspx?id=49500

可在此处获取文档:https://msdn.microsoft.com/en-us/library/hh550080(v=vs.103).aspx

示例用法:

SqlPackage.exe /a:publish /sf:C:\temp\mydb.dacpac /tcs:"Data Source=myserver;Initial Catalog=mydb;Integrated Security=true"

【讨论】:

【参考方案3】:

我在使用 T4 模板为我的数据库生成初始设置数据时遇到了一些问题。

1:没有 .sql 输出扩展名的 T4 模板在每次模板重新运行时将其构建操作重置为构建。这是一个问题,因为我想将脚本链接到 postDeploy 脚本并且不希望它构建,因为它会引发构建错误。

2:部署后脚本太大以至于部署它时内存不足,因为它还有用于调试的打印语句。

为了解决问题,我使用@Ed Alliot 的解决方案来制作我所有的初始化数据存储过程。但是,我创建了一个名为 setup 的新模式,并将所有设置存储过程放在该模式中,以便它们可以与数据库的其余部分分开保护。因为我没有生成存储过程,所以我可以让他们的构建操作保持“构建”,它将部署存储过程。

如果有人对这个设计过程感兴趣,我会发布一些屏幕和 t4 模板以供参考。

geoDataSql.ttinclude

<#@ template language="C#" hostspecific="true" #>
<#@ CleanupBehavior processor="T4VSHost" CleanupAfterProcessingtemplate="true" #>
<#@ output extension=".sql" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#+
    string countriesFile = "";
    string zonesFile = "";
    string timeZonesFile = "";
    string regionsFile = "";
    string citiesFile = ""; 

    public void GenerateCountries() 
           
        List<country> countries = ParseCSV<country>(Host.ResolvePath("..\\T4\\geoSetup\\" + countriesFile));        
        WriteLine("");
        WriteLine("-- Sql to add/update countries");
        foreach(var country in countries)
        
            if(string.IsNullOrEmpty(country.country_name) || string.IsNullOrEmpty(country.country_code))
            
                WriteLine("--Skipped: country_code:" + country.country_code + "\ncountry_code was null or empty in the csv file.");
                continue;
            
            WriteLine(string.Format("EXEC [geo].[addUpdateCountry] @countryCode = N'0', @countryName = N'1', @initData = 1", country.country_code, country.country_name.Replace("'", "''")));
        
    

    public void GenerateZones()
    
        List<zone> zones = ParseCSV<zone>(Host.ResolvePath("..\\T4\\geoSetup\\"  + zonesFile));
        WriteLine("");
        WriteLine("-- Sql to add/update zones");
        foreach(var zone in zones)
        
            WriteLine(string.Format("EXEC [geo].[addUpdateZone] @id=0, @countryCode = N'1', @zoneName = N'2', @initData = 1", zone.zone_id, zone.country_code, zone.zone_name.Replace("'", "''")));
        
    
    public void GenerateTimeZones()
    
        List<timezone> timeZones = ParseCSV<timezone>(Host.ResolvePath("..\\T4\\geoSetup\\"  + timeZonesFile));
        WriteLine("");
        WriteLine("-- Sql to add/update zones");
        foreach(var timeZone in timeZones)
        
            if (string.IsNullOrEmpty(timeZone.time_start))
                timeZone.time_start = "0";
            if (string.IsNullOrEmpty(timeZone.gmt_offset))
                timeZone.gmt_offset = "0";
            WriteLine(string.Format("EXEC [geo].[addUpdateTimeZone] @zoneId=0, @zoneShortName = N'1', @timeStart = 2, @gmtOffset = 3, @dst = 4, @initData = 1", timeZone.zone_id, timeZone.abbreviation, timeZone.time_start, timeZone.gmt_offset, timeZone.dst));
        
    

    public void GenerateRegions()
    
        List<city> cities = ParseCSV<city>(Host.ResolvePath("..\\T4\\geoSetup\\"  + citiesFile));
        List<region> regionsOther = ParseCSV<region>(Host.ResolvePath("..\\T4\\geoSetup\\" + regionsFile));
        Dictionary<string, region> regions = new Dictionary<string, region>();

        foreach(var city in cities)
        
            if (string.IsNullOrEmpty(city.city_name))
                continue;

            if (!string.IsNullOrEmpty(city.subdivision_1_iso_code))
            
                string rKey = city.country_iso_code + "_" + city.subdivision_1_iso_code;
                region r = new region()  countryCode = city.country_iso_code, regionCode = city.subdivision_1_iso_code, regionName = city.subdivision_1_name ;
                if (!regions.ContainsKey(rKey))
                    regions[rKey] = r;
                       
            if (!string.IsNullOrEmpty(city.subdivision_2_iso_code))
            
                string rKey = city.country_iso_code + "_" + city.subdivision_2_iso_code;
                region r = new region()  countryCode = city.country_iso_code, regionCode = city.subdivision_2_iso_code, regionName = city.subdivision_2_name ;
                if (!regions.ContainsKey(rKey))
                    regions[rKey] = r;              
            
        

        foreach (var region in regionsOther)
        
            string rKey = region.countryCode + "_" + region.regionCode;
            if (!regions.ContainsKey(rKey))
                regions[rKey] = region;
        

        WriteLine("");
        WriteLine("-- Regions (Pulled from cities.csv, only regions with cities/towns etc are here.)");
        foreach(var region in regions.Values)
        
            WriteLine(string.Format("EXEC [geo].[addUpdateRegion] @countryCode = N'0', @regionCode = N'1', @regionName = N'2', @initData = 1", region.countryCode, region.regionCode, region.regionName.Replace("'", "''")));
        

    

    public void GenerateCities()
    
        List<city> cities = ParseCSV<city>(Host.ResolvePath("..\\T4\\geoSetup\\"  + citiesFile));
        WriteLine("");
        WriteLine("-- Cities");
        foreach (var city in cities)
        
            if (string.IsNullOrEmpty(city.city_name))
                continue;
            if (string.IsNullOrEmpty(city.subdivision_1_iso_code) && string.IsNullOrEmpty(city.subdivision_2_iso_code))
            
                WriteLine("--Skipped City: " + city.geoname_id.ToString() + " it doesn't have any region info!");
                continue;
            
            string sql = "EXEC [geo].[addUpdateCity] @countryCode = N'0', @cityName = N'1', @region1Code = 2, @region2Code = 3,  @zoneName = N'4', @initData = 1";
            WriteLine(string.Format(sql, city.country_iso_code, city.city_name.Replace("'", "''"), city.subdivision_1_iso_code == null ? "null" : "N'" + city.subdivision_1_iso_code + "'", city.subdivision_2_iso_code == null ? "null" : "N'" + city.subdivision_2_iso_code + "'", city.time_zone));
        
    
    public List<T> ParseCSV<T>(string filePath) where T: class, new()
    
        if (!System.IO.File.Exists(filePath))
            return null;

        string[] csvContents = File.ReadAllText(filePath).Split(new string[]  "\n" , StringSplitOptions.RemoveEmptyEntries);
        if (csvContents.Length <= 1)
            return null;
        Regex csvSplit = new Regex("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)", RegexOptions.Compiled);
        string[] fieldNames = csvContents[0].Split(new string[]  "," , StringSplitOptions.None);
        if (fieldNames.Length <= 0)
            return null;
        List<T> ret = new List<T>();
        Type objType = typeof(T);

        for(int i = 1; i < csvContents.Length; ++i)
        
            List<string> values = new List<string>();
            foreach (Match match in csvSplit.Matches(csvContents[i]))                
                values.Add(match.Value.TrimStart(',').Trim('"').Trim());   
            if (values.Count != fieldNames.Length)                              
                throw new Exception("Test");                

            T obj = new T();
            for(int i2 = 0; i2 < fieldNames.Length; ++i2)
            
                var field = fieldNames[i2];
                var props = objType.GetProperties();
                var property = objType.GetProperty(field.Trim(), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.SetProperty);
                if (property == null) 
                    throw new Exception("PROPERTY NULL");
                                   
                if (property == null)
                    throw new Exception("Unable to parse field: " + field + " into Type: " + objType.FullName + "\n unable to find property on the Type matching the fields name.");
                if (property.PropertyType != typeof(string))
                    throw new Exception("Unable to parse field: " + field + " into Property becuase the Property is not of Type string. Type:" + objType.FullName);
                string v = values[i2] == null || values[i2] == string.Empty ? null : values[i2].Trim();
                property.SetValue(obj, v);
            
            ret.Add(obj);
        
        return ret;
    
    public class city
    
        public string geoname_id  get; set;
        public string locale_code  get; set;
        public string continent_code  get; set;
        public string continent_name  get; set;
        public string country_iso_code  get; set;
        public string country_name  get; set;
        public string subdivision_1_iso_code  get; set;
        public string subdivision_1_name  get; set;
        public string subdivision_2_iso_code  get; set;
        public string subdivision_2_name  get; set;
        public string city_name  get; set;
        public string metro_code  get; set;
        public string time_zone  get; set;
    
    public class region
    
        public string countryCode  get; set;
        public string regionCode  get; set;
        public string regionName  get; set;
    
    public class country
    
        public string country_code  get; set; 
        public string country_name  get; set; 
    
    public class zone
    
        public string zone_id  get; set;
        public string country_code  get; set;
        public string zone_name  get; set;
    
    public class timezone
    
        public string zone_id  get; set;
        public string abbreviation  get; set;
        public string time_start  get; set;
        public string gmt_offset  get; set;
        public string dst  get; set;
    
#>    

setupCountryData.tt

<#  
    countriesFile = "countries.csv";
    WriteLine("CREATE PROCEDURE [setup].[setupCountryData] AS");
    GenerateCountries();
    WriteLine("RETURN 0;");
#>

要创建其他 csv 驱动的存储过程,只需将名为 GenerateXYZ() 的方法添加到 geoDataSql.ttinclude。然后创建一个类似 setupCountryData.sql 的文件并设置文件名并调用您添加的相应生成方法。

缺点: 这使得构建需要永远......所以我可能会在一段时间后将此逻辑移到控制台应用程序......然后在部署/更改后运行它。

【讨论】:

感谢您的文章,看看不同的人用 ssdt 做了什么真的很有趣 是的,由于 T4 模板随机重新运行,我在尝试浏览解决方案资源管理器时遇到了 Visual Studio 内存不足的问题......所以我想我要做一个使用 peta poco 运行和部署所有设置数据并写入日志的控制台应用程序。

以上是关于如何在不耗尽内存的情况下部署包含 130,000 多个条目的脚本的主要内容,如果未能解决你的问题,请参考以下文章

如何在不耗尽内存的情况下制作大型 3D 数组?

如何在不耗尽内存的情况下运行大型 Mahout 模糊 kmeans 聚类?

如何使用 Python Ray 在不耗尽内存的情况下并行处理大量数据?

在不耗尽内存的情况下检索图像

如何在不耗尽电池的情况下监控 MPMoviePlayerController 播放进度?

如何在不使用太多内存的情况下强制下载大文件?