SSDT项目在使用MSBuild构建时比在VS 2013中构建速度慢得多。

11
我注意到我们的SSDT项目在TFS构建定义下的构建速度比在我的本地开发机上通过VS 2013构建要慢得多。在构建服务器本身上,我可以通过VS 2013构建我们最大的SSDT项目,大约需要2分钟。完全相同的项目使用MSBuild在同一服务器上的命令行中构建需要大约16分钟(即以与TFS构建定义构建SSDT项目相同的方式构建)。我已经收集了/verbosity:diagnostic输出,但是无论如何都看不到MSBuild提供的参数有任何实质性的差异。在VS 2013中,配置是Debug:AnyCPU。我使用的MSBuild命令是:msbuild / t:rebuild /p:Configuration=Debug/p:Platform=AnyCPU .sqlproj 我已经在更小的SSDT项目上尝试了相同的过程,并且我在构建时间上得到类似的相对差异(例如,我拥有的另一个项目在VS 2013中需要10秒钟,在MSBuild中需要70-80秒钟)。我拥有最新版本的VS 2013(12.0.40629.00 Update 5)和SSDT(12.0.60629.0)。是否还有其他人遇到过这个问题,甚至解决了这个问题?转移到VS 2015是否能够解决这个问题?
编辑以响应评论并包括诊断信息-15-08-2016 谢谢大家回复我的建议。 Ed,在测试VS与MSBuild时,我使用了构建目标Rebuild,这我相信是Clean和Build的组合。在TFS构建定义中,我将“清除工作区”和“清除构建”设置为true,但“获取源”步骤的时间仅为9秒。Steven,dbmdl因素听起来非常有前途。但是,我在从解决方案中删除所有dbmdl文件后再次进行测试,而且我仍然得到相同的时间(我验证了dbmdl文件在测试构建之间没有重新生成)。此外,我在测试之前已经删除了所有bin和obj文件夹,以消除VS构建的任何缓存数据的可能性。Cece,我已经有效地从这个问题中移除了TFS,因为当我测试VS和MSBuild时,我得到了构建时间上的差异。我认为,如果我能解决这个问题,TFS构建时间也会被解决。您提出了一个关于哪个构建步骤花费时间的好观点。我不想让初始帖子中充斥着太多信息,但现在是时候了:-)。我使用/ clp:PerformanceSummary选项来获得时间花费的细分。在第1节中,您可以看到几乎所有的时间都花费在“SqlBuildTask”中。了解很好,但无法帮助我们准确定位问题。
在第2和第3节中,这是我从两种构建方法中使用/verbosity:diagnostic输出得到的信息。
我看不出两个版本之间有任何实质性的差异。两者都必须使用此DLL执行实际构建(MSBuild明确声明对此DLL的引用)。
 C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\\Extensions\Microsoft\SQLDB\Dac\130\Microsoft.Data.Tools.Schema.Tasks.Sql.dll" 

------------- 第一部分 -------------

(使用/ clp:PerformanceSummary的 MSBuild 输出)

Project Performance Summary:
        82518 ms  P:\<snip>.sqlproj   1 calls
                  82518 ms  rebuild                                    1 calls

Target Performance Summary:
        0 ms  BuildOnlySettings                          2 calls
        0 ms  GetReferenceAssemblyPaths                  2 calls
        0 ms  GetSqlSymbolsPath                          1 calls
        0 ms  BeforeBuild                                2 calls
        0 ms  ResolveReferences                          2 calls
        0 ms  GetCopyToOutputDirectoryXamlAppDefs        2 calls
        0 ms  SqlStudioSourceFilesToCopy                 2 calls
        0 ms  AssignProjectConfiguration                 1 calls
        0 ms  BeforeClean                                2 calls
        0 ms  _CopySourceItemsToOutputDirectory          2 calls
        0 ms  Build                                      2 calls
        0 ms  GetCopyToOutputDirectoryItems              2 calls
        0 ms  Clean                                      2 calls
        0 ms  CleanPublishFolder                         2 calls
        0 ms  GetFrameworkPaths                          2 calls
        0 ms  GetTargetPath                              2 calls
        0 ms  _CleanGetCurrentAndPriorFileWrites         2 calls
        0 ms  _CopyFilesMarkedCopyLocal                  1 calls
        0 ms  AfterResolveReferences                     2 calls
        0 ms  PreXsdCodeGen                              2 calls
        0 ms  CopyFilesToOutputDirectory                 2 calls
        0 ms  GetNativeManifest                          1 calls
        0 ms  GetInstalledSDKLocations                   2 calls
        0 ms  IncrementalClean                           2 calls
        0 ms  PrepareForRun                              2 calls
        0 ms  _SplitProjectReferencesByFileExistence     2 calls
        0 ms  _SetupSqlBuildOutputs                      2 calls
        0 ms  BeforeResolveReferences                    2 calls
        0 ms  GetSqlTargetPath                           1 calls
        0 ms  Rebuild                                    1 calls
        0 ms  AfterClean                                 2 calls
        0 ms  AfterBuild                                 2 calls
        0 ms  ExpandSDKReferences                        2 calls
        0 ms  CleanStaticCodeAnalysis                    2 calls
        0 ms  BeforeRebuild                              1 calls
        0 ms  CheckRequiredProperties                    2 calls
        0 ms  GenerateSqlTargetFrameworkMoniker          2 calls
        0 ms  ResolveSDKReferences                       2 calls
        0 ms  AfterRebuild                               1 calls
        0 ms  ResolveArtifactReferences                  2 calls
        0 ms  _CopyOutOfDateSourceItemsToOutputDirectoryAlways   1 calls
        0 ms  _CheckForCompileOutputs                    2 calls
        0 ms  PrepareForBuild                            2 calls
        0 ms  CleanXsdCodeGen                            2 calls
       16 ms  _CheckForInvalidConfigurationAndPlatform   2 calls
       16 ms  AssignTargetPaths                          2 calls
       16 ms  CoreClean                                  2 calls
       31 ms  ResolveAssemblyReferences                  2 calls
       31 ms  SqlPrepareForRun                           2 calls
       31 ms  CleanReferencedProjects                    2 calls
       63 ms  PostBuildEvent                             2 calls
       63 ms  PreBuildEvent                              2 calls
       94 ms  CoreCompile                                2 calls
      625 ms  _SetupSqlBuildInputs                       2 calls
     6452 ms  ResolveProjectReferences                   2 calls
    81502 ms  SqlBuild                                   2 calls

Task Performance Summary:
        0 ms  AssignTargetPath                          26 calls
        0 ms  CallTarget                                 2 calls
        0 ms  FindAppConfigFile                          2 calls
        0 ms  ConvertToAbsolutePath                      2 calls
        0 ms  AssignProjectConfiguration                 1 calls
        0 ms  ReadLinesFromFile                          4 calls
        0 ms  WriteLinesToFile                           4 calls
        0 ms  Copy                                       6 calls
        0 ms  FindUnderPath                             14 calls
        0 ms  RemoveDuplicates                           6 calls
        0 ms  MakeDir                                    4 calls
       16 ms  SqlScriptDependenciesTask                  2 calls
       16 ms  Delete                                    12 calls
       31 ms  ResolveAssemblyReference                   2 calls
       31 ms  Message                                    8 calls
       94 ms  Csc                                        1 calls
      126 ms  Exec                                       4 calls
      516 ms  SqlModelResolutionTask                     2 calls
     6468 ms  MSBuild                                    7 calls
    81424 ms  SqlBuildTask                               2 calls

------------- 第二部分 -------------

(在VS构建的SqlBuildTask阶段中给出的诊断输出)

Target "SqlBuild" in file "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" from project "P:<snip>.sqlproj" (target "Build" depends on it):
    Building target "SqlBuild" completely.
    Output file "P:<snip>.dacpac" does not exist.
    Task "SqlBuildTask"
        Task Parameter:SqlTarget=P:<snip>.dacpac
        Task Parameter:ContributorArguments=ConfigurationName=Debug;
        Task Parameter:BuildContributors=;
        Task Parameter:DeploymentContributors=;
        Task Parameter:CreateScriptFileName=<snip>.sql
        Task Parameter:DacApplicationName=<snip>
        Task Parameter:DacDescription=<snip>
        Task Parameter:DacFile=P:<snip>\bin\Debug\
        Task Parameter:DacVersion=3.27.0.0
        Task Parameter:DatabaseName=<snip>
        Task Parameter:DatabaseSchemaProviderName=Microsoft.Data.Tools.Schema.Sql.Sql100DatabaseSchemaProvider
        Task Parameter:DefaultSchema=dbo
        Task Parameter:DeploymentScriptName=<snip>.sql
        Task Parameter:DeployToDatabase=True
        Task Parameter:ImplicitDllAssemblyName=<snip>
        Task Parameter:ImplicitDllFileName=P:<snip>.dll
        Task Parameter:ImplicitDllSymbolsFileName=P:<snip>.pdb
        Task Parameter:ImplicitDllGenerateSqlClrDdl=true
        Task Parameter:IntermediateDirectory=P:<snip>\obj\Debug\
        Task Parameter:ModelCollation=1033,CI
        Task Parameter:OutputDirectory=P:<snip>\bin\Debug\
        Task Parameter:
    Source= <snip - list of all the source files in the SSDT project - matches list for MSBuild builds>

        Task Parameter:
    SqlCmdVariables= <snip - same for both builds>
        Task Parameter:
    SqlReferencePath=
        C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                CopyLocal=false
                FrameworkFile=true
                FusionName=mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
                ImageRuntime=v4.0.30319
                OriginalItemSpec=C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                ReferenceSourceTarget=ResolveAssemblyReference
                ResolvedFrom=C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                Version=4.0.0.0
        Task Parameter:SuppressTSqlWarnings=71562,71502,71558
        Task Parameter:ValidateCasingOnIdentifiers=true
        Task Parameter:CmdLineInMemoryStorage=false
        Task Parameter:DefaultCollation=SQL_Latin1_General_CP1_CI_AS
        Task Parameter:AnsiNullDefault=False
        Task Parameter:AnsiNulls=False
        Task Parameter:AnsiPadding=False
        Task Parameter:AnsiWarnings=False
        Task Parameter:ArithAbort=False
        Task Parameter:ConcatNullYieldsNull=False
        Task Parameter:QuotedIdentifier=False
        Task Parameter:NumericRoundAbort=False
        Task Parameter:RecursiveTriggersEnabled=False
        Task Parameter:DatabaseChaining=False
        Task Parameter:DatabaseState=ONLINE
        Task Parameter:CloseCursorOnCommitEnabled=False
        Task Parameter:DefaultCursor=GLOBAL
        Task Parameter:AutoClose=False
        Task Parameter:AutoCreateStatistics=True
        Task Parameter:AutoShrink=False
        Task Parameter:AutoUpdateStatistics=True
        Task Parameter:TornPageDetection=False
        Task Parameter:DatabaseAccess=MULTI_USER
        Task Parameter:Recovery=FULL
        Task Parameter:EnableFullTextSearch=False
        Task Parameter:DefaultFilegroup=PRIMARY
        Task Parameter:Trustworthy=True
        Task Parameter:AutoUpdateStatisticsAsynchronously=False
        Task Parameter:PageVerify=CHECKSUM
        Task Parameter:ServiceBrokerOption=DisableBroker
        Task Parameter:DateCorrelationOptimizationOn=False
        Task Parameter:Parameterization=SIMPLE
        Task Parameter:AllowSnapshotIsolation=False
        Task Parameter:ReadCommittedSnapshot=True
        Task Parameter:VardecimalStorageFormatOn=True
        Task Parameter:SupplementalLoggingOn=False
        Task Parameter:CompatibilityMode=100
        Task Parameter:IsChangeTrackingOn=False
        Task Parameter:IsChangeTrackingAutoCleanupOn=True
        Task Parameter:ChangeTrackingRetentionPeriod=2
        Task Parameter:ChangeTrackingRetentionUnit=Days
        Task Parameter:IsEncryptionOn=False
        Task Parameter:IsBrokerPriorityHonored=False
        Task Parameter:IncludeCompositeObjects=True
        Loading project references...
        Loading project files...
        Building the project model and resolving object interdependencies...
        Validating the project model...
        Writing model to P:<snip>\obj\Debug\Model.xml...

------------- 第3节 -------------

(在 MSBuild 构建的 SqlBuildTask 阶段给出的诊断输出)

Target "SqlBuild: (TargetId:68)" in file "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" from project "P:<snip>.sqlproj" (target "Build" depends on it):
Building target "SqlBuild" completely.
Output file "P:<snip>.dacpac" does not exist.
Using "SqlBuildTask" task from assembly "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\\Extensions\Microsoft\SQLDB\Dac\130\Microsoft.Data.Tools.Schema.Tasks.Sql.dll".
Task "SqlBuildTask" (TaskId:56)
  Task Parameter:SqlTarget=P:<snip>.dacpac (TaskId:56)
  Task Parameter:ContributorArguments=ConfigurationName=Debug; (TaskId:56)
  Task Parameter:BuildContributors=; (TaskId:56)
  Task Parameter:DeploymentContributors=; (TaskId:56)
  Task Parameter:CreateScriptFileName=<snip>.sql (TaskId:56)
  Task Parameter:DacApplicationName=<snip> (TaskId:56)
  Task Parameter:DacDescription=<snip> (TaskId:56)
  Task Parameter:DacFile=P:<snip>\bin\Debug\ (TaskId:56)
  Task Parameter:DacVersion=3.27.0.0 (TaskId:56)
  Task Parameter:DatabaseName=<snip> (TaskId:56)
  Task Parameter:DatabaseSchemaProviderName=Microsoft.Data.Tools.Schema.Sql.Sql100DatabaseSchemaProvider (TaskId:56)
  Task Parameter:DefaultSchema=dbo (TaskId:56)
  Task Parameter:DeploymentScriptName=<snip>.sql (TaskId:56)
  Task Parameter:DeployToDatabase=True (TaskId:56)
  Task Parameter:ImplicitDllAssemblyName=<snip> (TaskId:56)
  Task Parameter:ImplicitDllFileName=P:<snip>.dll (TaskId:56)
  Task Parameter:ImplicitDllSymbolsFileName=P:<snip>.pdb (TaskId:56)
  Task Parameter:ImplicitDllGenerateSqlClrDdl=true (TaskId:56)
  Task Parameter:IntermediateDirectory=P:<snip>\obj\Debug\ (TaskId:56)
  Task Parameter:ModelCollation=1033,CI (TaskId:56)
  Task Parameter:OutputDirectory=P:<snip>\bin\Debug\ (TaskId:56)
  Task Parameter:
      Source= <snip - list of all the source files in the SSDT project - matches list for VS builds>
  Task Parameter:
      SqlCmdVariables= <snip - same for both builds>
  Task Parameter:
      SqlReferencePath=
          C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                  CopyLocal=false
                  FrameworkFile=true
                  FusionName=mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
                  ImageRuntime=v4.0.30319
                  OriginalItemSpec=C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                  ReferenceSourceTarget=ResolveAssemblyReference
                  ResolvedFrom=C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
                  Version=4.0.0.0 (TaskId:56)
  Task Parameter:SuppressTSqlWarnings=71562,71502,71558 (TaskId:56)
  Task Parameter:ValidateCasingOnIdentifiers=true (TaskId:56)
  Task Parameter:CmdLineInMemoryStorage=false (TaskId:56)
  Task Parameter:DefaultCollation=SQL_Latin1_General_CP1_CI_AS (TaskId:56)
  Task Parameter:AnsiNullDefault=False (TaskId:56)
  Task Parameter:AnsiNulls=False (TaskId:56)
  Task Parameter:AnsiPadding=False (TaskId:56)
  Task Parameter:AnsiWarnings=False (TaskId:56)
  Task Parameter:ArithAbort=False (TaskId:56)
  Task Parameter:ConcatNullYieldsNull=False (TaskId:56)
  Task Parameter:QuotedIdentifier=False (TaskId:56)
  Task Parameter:NumericRoundAbort=False (TaskId:56)
  Task Parameter:RecursiveTriggersEnabled=False (TaskId:56)
  Task Parameter:DatabaseChaining=False (TaskId:56)
  Task Parameter:DatabaseState=ONLINE (TaskId:56)
  Task Parameter:CloseCursorOnCommitEnabled=False (TaskId:56)
  Task Parameter:DefaultCursor=GLOBAL (TaskId:56)
  Task Parameter:AutoClose=False (TaskId:56)
  Task Parameter:AutoCreateStatistics=True (TaskId:56)
  Task Parameter:AutoShrink=False (TaskId:56)
  Task Parameter:AutoUpdateStatistics=True (TaskId:56)
  Task Parameter:TornPageDetection=False (TaskId:56)
  Task Parameter:DatabaseAccess=MULTI_USER (TaskId:56)
  Task Parameter:Recovery=FULL (TaskId:56)
  Task Parameter:EnableFullTextSearch=False (TaskId:56)
  Task Parameter:DefaultFilegroup=PRIMARY (TaskId:56)
  Task Parameter:Trustworthy=True (TaskId:56)
  Task Parameter:AutoUpdateStatisticsAsynchronously=False (TaskId:56)
  Task Parameter:PageVerify=CHECKSUM (TaskId:56)
  Task Parameter:ServiceBrokerOption=DisableBroker (TaskId:56)
  Task Parameter:DateCorrelationOptimizationOn=False (TaskId:56)
  Task Parameter:Parameterization=SIMPLE (TaskId:56)
  Task Parameter:AllowSnapshotIsolation=False (TaskId:56)
  Task Parameter:ReadCommittedSnapshot=True (TaskId:56)
  Task Parameter:VardecimalStorageFormatOn=True (TaskId:56)
  Task Parameter:SupplementalLoggingOn=False (TaskId:56)
  Task Parameter:CompatibilityMode=100 (TaskId:56)
  Task Parameter:IsChangeTrackingOn=False (TaskId:56)
  Task Parameter:IsChangeTrackingAutoCleanupOn=True (TaskId:56)
  Task Parameter:ChangeTrackingRetentionPeriod=2 (TaskId:56)
  Task Parameter:ChangeTrackingRetentionUnit=Days (TaskId:56)
  Task Parameter:IsEncryptionOn=False (TaskId:56)
  Task Parameter:IsBrokerPriorityHonored=False (TaskId:56)
  Task Parameter:IncludeCompositeObjects=True (TaskId:56)
  Creating a model to represent the project... (TaskId:56)
  Loading project references... (TaskId:56)
  Loading project files... (TaskId:56)
  Building the project model and resolving object interdependencies... (TaskId:56)
  Validating the project model... (TaskId:56)
  Writing model to P:<snip>\obj\Debug\Model.xml... (TaskId:56)
Done executing task "SqlBuildTask". (TaskId:56)

如果你在VS中进行清理然后构建,你需要多少时间? - Ed Elliott
具体来说,尝试关闭 VS,从解决方案文件夹中删除 dbmdl 文件,然后重新构建。VS 有一些性能优化,涉及在 dbmdl 文件中缓存数据,但在从命令行运行 msbuild 时不使用该文件。 - Steven Green
你使用的是哪个版本的 TFS? 你的意思是,在 TFS 构建服务器上使用 MSBuild 命令行构建项目时,与使用 TFS 构建项目时获得相同的行为吗?日志中哪个步骤耗费了最多的时间? - Cece Dong - MSFT
我发现了一个问题。在sqlproj文件中,重复引用会显著增加通过MSBUILD构建时的构建时间。我们最大的项目有大约11,000个实体。通过MSBUILD构建的构建时间在30到80分钟之间变化,而通过VS构建相同的项目只需3-4分钟。一旦我删除了这些重复引用(大约40个),构建时间就稳定在9分钟左右!我还没有时间去推断额外的时间是否与重复数量成比例,或者是特定的重复引用。无论如何,只需删除您的重复引用即可! - Mark Neves
除了删除重复项之外,我所做的一个重要更改是在MSBUILD参数中添加/p:CmdLineInMemoryStorage=TRUE。这是由来自sabin.io的Simon D'Morias建议的(感谢Simon)。我知道该标志是为了解决构建中的内存问题而引入的,但它确实有助于我们在TFS中缩短构建时间。 - Mark Neves
似乎VS使用缓存的项目信息来加速构建(可能使用dbmdl文件?)。我没有确凿的证据。然而,这可以解释为什么MSBUILD构建需要比VS构建更长的时间,并且根据我的新修订的构建时间,即使在这样一个大型项目中没有任何缓存信息,MSBUILD需要额外7分钟的时间似乎是合理的。 - Mark Neves
3个回答

19

我对我们的大型SSDT项目和TFS MSBUILD参数进行了以下更改,这些更改通过TFS构建定义将构建时间降至可解释的水平:

1) 将/p:CmdLineInMemoryStorage=TRUE添加到MSBUILD参数中

向TFS构建定义添加CmdLineInMemoryStorage MSBUILD参数

这大大缩短了我们的构建时间。通过添加此选项,我们的构建定义从40分钟缩短到16分钟。(在删除重复引用之前的时间)

2) 从.sqlproj文件中删除所有重复引用

随着时间的推移,.sqlproj文件成功地引用了一些文件2或3次。我们有大约40个重复引用。这几乎肯定是由于我们的许多分支之间的合并问题所致。Visual Studio不会对此进行投诉,并且可以完全顺利构建,但重复项对我们在TFS中构建的时间产生了显着影响,当然使用的是MSBUILD。

在sqlproj文件中查找重复引用

以下是我用于查找我们的sqlproj文件中重复项的PowerShell脚本(将$root更改为指向包含.sqlproj文件的文件夹):

[Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null

$root = "C:\TFS"

$sqlprojFiles = Get-ChildItem -Path $root -Filter "*.sqlproj" -Recurse
$numFilesChecked=0

foreach ($f in $sqlprojFiles)
{
    $numFilesChecked++
    $fullName = $f.FullName

    $xml = [System.Xml.Linq.XDocument]::Load($fullName)
    $ns = $xml.Root.Name.Namespace;

    $itemGroups = $xml.Descendants() | Where-Object { $_.Parent.Name -eq $ns + "ItemGroup" };

    $dict = @{}
    $itemGroups.Attributes() | Where-Object { $_.Name -eq "Include"} `
        | % { if ($dict.ContainsKey($_.Value)) {$dict[$_.Value]++} else {$dict.Add($_.Value,1)} }

    $duplicates = $dict.Keys | Where-Object { $dict[$_] -gt 1} 

    $fullName

    if ($duplicates.Count -gt 0)
    {
        Write-Output "--------------------------------------------------------"
        $duplicates | % {$_+", "+$dict[$_]}
    }
    else
    {
        Write-Output "No duplicates found"
    }
}

"Files checked: $numFilesChecked"

我使用这个的输出来手动文本编辑.sqlproj文件(即在VS中“卸载项目”,然后“编辑.sqlproj”)

建立时间差异的解释

现在我们的大型sqlproj在构建服务器上构建需要9分钟,我认为可以解释VS和MSBuild之间的构建时间差异。

有人告诉我,VS利用了SSDT项目的预编译信息,而这些信息在从命令行使用MSBuild时没有使用。我无法确认这一点,在VS的“/verbosity:diagnostic”输出中也没有看到任何证据(可能我错过了)。但是,在VS完成分析项目后,它需要2分钟才能构建大型项目,这表明它在构建过程中使用了缓存信息。VS需要几分钟才能完成对大型项目的分析。

现在MSBuild构建时间(无论是从命令行还是TFS构建定义)都需要9分钟。额外的7分钟几乎肯定是由MSBuild进程在每次构建时分析项目所需的 - VS似乎只做一次(或至少定期做),然后在构建时利用缓存的信息。


谢谢,这真的很有帮助。如果PS脚本实际说明了重复引用是什么就更好了。 - Justin

4
对于那些在构建具有许多“dacpac依赖项”的项目时遇到慢速SSDT构建的人来说,还有一种选择:简化这些dacpacs并使它们变小。
关键的假设是意识到引用一个dacpac与部署它不同:它不应该需要像“正常”的sqlproj那样多的元数据。
以下是可以通过dacpac引用来减少其大小和构建时间的可能步骤(除了Mark和其他人提到的选项)。
所示的代码提示是使用Powershell。
DACPAC是一个包含XML文件的ZIP归档文件。因此,您可以打开它并查看作为单个元数据和源文件表达的“model.xml”文件。该文件可能非常大(100MB+),如您所知,XML文件的大小会影响其处理性能。SSDT生成了对不同SQL元素的非常冗长的描述。
要开始处理它,请解压缩并加载源XML。
Expand-Archive -Path $dacpac -DestinationPath $expandedFolder | Out-Null;
[xml] $dacpacXml = Get-Content "$expandedFolder/model.xml";

$ns = New-Object System.Xml.XmlNamespaceManager($dacpacXml.value.NameTable);
$ns.AddNamespace("dac", $dacpacXml.value.DocumentElement.NamespaceURI);

元数据不用于引用

授权

我相信您的TSQL代码不应该需要对不同数据库中的这些对象进行硬引用,例如:

  • 用户、登录、角色
  • 权限、角色成员、授权和执行上下文(用户/登录)信息

因此,请将它们移除。

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlRoleMembership']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlPermissionStatement']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlLogin']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlUser']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlRole']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Login']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'User']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Authorizer']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Schema']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

如果您将所有内容都保留在您的 sqlproj(用户、登录)中,我建议您审查此方法,并从源代码中删除滥用级别的数据。

再加上敏感数据和签名。

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'EncryptionPassword' or @Name = 'Password']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlCertificate' or @Type = 'SqlSymmetricKey']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSignature']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

文件存储和分区信息

如果您有包含许多分区的分区表,则您的dacpacs文件将包含数百或数千个元素每个分区

elements

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'DataCompressionOptions' or @Name = 'BoundaryValues']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Filegroups']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Filegroup']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'FilegroupForTextImage']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'PartitionScheme' or @Name = 'PartitionColumn']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

DML和DDL对象

这些对象不应该从数据库外部引用

  • 触发器
  • 索引
  • 默认约束

因此,可以将它们移除

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlIndex']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlDmlTrigger']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlIndexedColumnSpecification']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlDatabaseDdlTrigger']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlDefaultConstraint']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

来源

虽然可以引用SP,但对于此操作并不需要其来源 - 我们已经拥有所有的元数据:参数以详细的方式描述为具有类型信息的单独元素等。因此,移除所有的来源。

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlView']/dac:Property[@Name = 'QueryScript']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlProcedure']/dac:Property[@Name = 'BodyScript']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlScriptFunctionImplementation']/dac:Property[@Name = 'BodyScript']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlAssemblyFile']/dac:Property[@Name = 'Source']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlComputedColumn']/dac:Property[@Name = 'ExpressionScript']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;

由这些资源生成的依赖项

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'BodyDependencies']", $ns) | % { write-verbose $_.ParentNode.Name; $_.ParentNode.removechild($_); } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'QueryDependencies' or @Name = 'ExpressionDependencies']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'DynamicObjects']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'AssemblySources']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlQueue']/dac:Relationship", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlContract']/dac:Relationship[@Name = 'Messages']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

注释

$dacpacXml.value.DocumentElement.SelectNodes("//dac:Annotation[@Type = 'SysCommentsObjectAnnotation']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'BoundTargets']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Annotation[@Type = 'PersistedResolvableAnnotation']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlExtendedProperty']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSimpleColumn']/dac:AttachedAnnotation", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

2022年11月更新
稍后我决定“深入一点”,审查了我们清理过的dacpacs,并找到了更多的清理选项。
还有一些更多的资源/表达方式。
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'CheckExpressionScript']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'FunctionBody']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSubroutineParameter']/dac:Property[@Name = 'DefaultExpressionScript']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

"私人"关系
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlContract']/dac:Relationship[@Name = 'Messages']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlService']/dac:Relationship[@Name = 'Contracts' or @Name = 'Queue']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSynonym']/dac:Property[@Name = 'ForObjectScript']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSynonym']/dac:Relationship[@Name = 'ForObject']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

不影响构建或引用解析的属性
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsActivationOn' or @Name = 'MaxQueueReaders']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsMemoryOptimized' or @Name = 'IsNativelyCompiled' or @Name = 'IsSchemaBound']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'FillFactor' or @Name = 'IsPadded' or @Name = 'IsClustered']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'LockEscalation' or @Name = 'RetentionUnit' or @Name = 'Collation']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsAnsiNullsOn' or @Name = 'IsQuotedIdentifierOn']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsNullable' or @Name = 'IsReadOnly' or @Name = 'IsHidden']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsIdentity' or @Name = 'IdentitySeed' or @Name = 'IsRowGuidColumn']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Property[@Name = 'IsCaller' or @Name = 'IsOwner']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

过程/函数参数和列类型定义非常冗长,但 SSDT 似乎从未使用过其中任何一个。这对我来说看起来很奇怪。当我删除了这些元数据后,感觉有点危险和不真实,但在删除了70多个 dacpac 后,SSDT 并没有提到任何问题。构建结果相同(只是更快)。

以下是一个示例,说明“此列的类型为 DECIMAL(18,8)”:

<Relationship Name="TypeSpecifier">
    <Entry>
        <Element Type="SqlTypeSpecifier">
            <Property Name="Scale" Value="8"/>
            <Property Name="Precision" Value="18"/>
            <Relationship Name="Type">
                <Entry>
                    <References ExternalSource="BuiltIns" Name="[decimal]"/>
                </Entry>
            </Relationship>
        </Element>
    </Entry>
</Relationship>

使我们的ref-dacpac结构更加扁平化
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlScalarFunction']/dac:Relationship[@Name = 'Type']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlPartitionFunction']/dac:Relationship[@Name = 'ParameterType']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSubroutineParameter']/dac:Relationship[@Name = 'Type']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlSimpleColumn' or @Type = 'SqlTableTypeSimpleColumn']/dac:Relationship[@Name = 'TypeSpecifier']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'ColumnSpecifications']/dac:Entry", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

移除约束和与之相关的链接。注意,这些必须一次性全部移除!或者一个也不移除。之前我尝试过移除一些约束、一些注释,但是元数据中的内部交叉引用出现了问题,导致这些 dacpac 文件无法正常工作。最后找到了如何“一致地”移除它们的方法:
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlDefaultConstraint' or @Type = 'SqlTableTypeDefaultConstraint' or @Type = 'SqlTableTypePrimaryKeyConstraint' or @Type = 'SqlPrimaryKeyConstraint']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlForeignKeyConstraint' or @Type = 'SqlCheckConstraint' or @Type = 'SqlUniqueConstraint']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Annotation[@Type = 'SqlInlineConstraintAnnotation' or @Type = 'SqlInlineIndexAnnotation']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Relationship[@Name = 'Constraints']", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlTable' or @Type = 'SqlTableType']/dac:AttachedAnnotation", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;
# here only Disambiguator attr getting removed, not the whole element
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Element[@Type = 'SqlTableType' or @Type = 'SqlSubroutineParameter']", $ns) | % { $_.RemoveAttribute("Disambiguator") } | Out-Null;

这次更新使dacpacs减少了1.5倍。构建获得了约20%的提升。
// 更新结束
清理
删除操作导致产生了一些没有子元素的空元素。正在移除它们。
$dacpacXml.value.DocumentElement.SelectNodes("//dac:Entry[not(child::*)]", $ns) | % { $_.ParentNode.RemoveChild($_) } | Out-Null;

保存更改
$dacpacXML.Save("$expandedFolder/model.xml");

重新压缩DACPAC
编辑完成后,我们需要将我们的xml文件转换回dacpac。在此之前,需要重新计算和更新Origin.xml中的校验和,该文件也是dacpac归档的一部分。校验和不匹配的DACPAC将无法正常工作。
$newModelHash = (Get-FileHash -Path "$expandedFolder/model.xml").Hash;

[xml] $originXML = Get-Content "$expandedFolder/Origin.xml";
$originXML.DacOrigin.Checksums.Checksum.InnerText = $newModelHash;
$originXML.Save("$expandedFolder/Origin.xml");

现在我们准备好了,所以将其压缩回去(注意路径末尾的/*表示压缩所有文件但不包括包含文件夹)。复制后重命名。
Compress-Archive -Path "$expandedFolder/*" -DestinationPath "$dacpac.zip" -CompressionLevel Optimal -Force;

构建

现在将重新压缩的引用(dacpacs)放入适当的文件夹中,并重新运行构建过程。经过测试,编辑后的 dacpacs 可以变小 10-50 倍,并且可以很好地用于 VS 或 MSBuild 中的引用解析。通过这个“不正当手段”,构建时间可以减少一半。

以下是使用 ~3K 个存储过程、数百个表和约 20 个 dacpac 依赖项的 SQLPROJ 构建(cli msbuild)统计数据:

  • 普通方式:约 15 分钟
  • p:CmdLineInMemoryStorage:约 5-6 分钟
  • 修改过的 dacpacs:约 2 分钟

注意!获得的精简 dacpac 可以用于引用,但不能用于部署


0

感谢其他帖子的作者提供了一些改进数据库构建时间的好方法。

我对PS脚本进行了重大修改,以自动清理sqlproj文件中的重复包含条目。我们只需在运行msbuild之前自动运行它即可轻松解决问题。

如果有人需要,以下是代码:

-- Remove-DbprojDuplicateIncludeNode.ps1
<#
.SYNOPSIS
    Will remove any duplicate references from all sqlproj files
#>
[CmdletBinding(PositionalBinding = $true, SupportsShouldProcess)]
Param(
    $Root = "$PSScriptRoot\..\..\src\"
)

$sqlprojFiles = Get-ChildItem -Path $Root -Filter "*.sqlproj" -Recurse
$numFilesChecked = 0

foreach ($f in $sqlprojFiles) {
    Write-Host "Checking file $($f.FullName)"
    $dbProjPath = $f
    $projXml = [xml](Get-Content $dbProjPath -raw)
    $nsm = [Xml.XmlNamespaceManager]($projXml.NameTable)
    $nsm.AddNamespace('ns', $projXml.Project.xmlns)

    $sqlProjNodes = $projXml.SelectNodes('//ns:*[@Include]', $nsm)

    $numFilesChecked++

    $includedFileSet = [System.Collections.Generic.HashSet[string]]::new()
    $changed = $false

    foreach ($includeNode in $sqlProjNodes) {
        if (-not $includedFileSet.Add($includeNode.Include)) {
            Write-Host "Removing duplicate include node '$($includeNode.Include)'"
            $includeNode.ParentNode.RemoveChild($includeNode)
            $changed = $true
        }
    }

    if ($changed) {
        if ($PSCmdlet.ShouldProcess($dbProjPath)) {
            $projXml.Save($dbProjPath)
        }
    }
}

"Files checked: $numFilesChecked"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接