SQL Server 数据库比较工具






4.95/5 (159投票s)
在本文中,我将向您展示如何为 SQL Server 2005 和 SQL Server 2008 创建一个基本的数据库架构比较工具。
引言
我五年前开始在我现在的公司工作。起初,我们每次创建新的数据库对象时,都会生成一个脚本,然后通过邮件发送给负责实现的人。两年前,我们开始使用 Subversion 服务器对源代码和脚本进行版本控制。这是存储和管理项目和数据库(对象脚本)的一种更好的方式。有时会发生有人忘记将所有对象存储在 Subversion 上,特别是当项目很大时,因此我决定创建一个工具,让我的同事(以及我自己)能够比较数据库架构。现在,我只需要比较本地开发数据库和其他参考数据库,就可以看到哪些对象不同或缺失,然后可以为它们创建脚本。
背景
当我决定创建数据库架构比较工具时,我开始研究这个工具应该提供什么以及如何生成和比较脚本。我发现了两种生成脚本的方法。第一种是使用 SQL Server 系统视图。这种方法最快,但需要更多的编程工作。第二种方法是使用服务器管理对象 (SMO) 的脚本编写功能。如何在我的上一篇文章中描述了如何使用 SMO 编写对象脚本。我决定使用这种方法(但将来,我想创建一个使用 SQL Server 系统视图的自定义脚本编写库)。如前所述,这不如使用系统视图高效,但您仍然可以使用一些性能优化技术来提高它。
在这个项目中,第一步是获取您想要编写脚本的对象的 URN。URN 可以从 DataBase
对象的属性中获得。此工具将编写以下数据库对象的脚本:
- 应用程序角色
- 数据库角色
- 默认值
- 全文目录
- 全文停止列表 - 仅适用于 SQL Server 2008
- 用户定义表类型 - 仅适用于 SQL Server 2008
- 消息类型
- 分区函数
- 分区方案
- 计划指南
- 远程服务绑定
- 规则
- 模式
- 服务契约
- 队列
- 路由
- Assemblies
- 存储过程
- DDL 触发器
- 视图
- 同义词
- 表格
- 用户
- 用户定义聚合
- 用户定义类型
- 用户定义函数
- 用户定义数据类型
- XML 架构集合
- 索引
我创建了一个名为 ScriptedObject
的类,该类使用 hsObject
的 Hashtable 来存储 URN 和其他属性(名称、类型、架构、对象定义)。hsObject
中的所有对象都有一个唯一的键,该键是类型和名称属性的组合。我将在脚本生成中使用 hsObjects
,特别是用于存储每个对象生成的脚本。获取所有 URN 后,我们就可以开始编写脚本了。
对于脚本生成,我使用了 Scripter
对象的 Script()
方法,该方法接受您要编写脚本的对象的 URN 数组。Scripter
对象有一个 Options
属性,您可以在其中设置脚本编写选项(是否要编写排序规则、文件组…)。Script()
方法返回一个 StringCollection
,其中包含所有对象的脚本。问题是,您实际上不知道此集合中的哪个项属于我之前提到的 hsObject
Hastable 中的哪个项。我发现 ScriptingOptions
对象的一个属性允许您添加脚本头。我使用这些脚本头来区分脚本并拆分这些脚本。
程序循环遍历 StringCollection
并测试此 StringCollection
中的项是否包含脚本头。如果是,则可以从脚本头中提取对象的名称和类型。这两个属性唯一标识 hsObject
中的对象。StringCollection
中的每个字符串,如果跟在脚本头之后,则属于由类型和名称先前确定的对象,直到出现另一个头或字符串以 ALTER、GRAND、REVOKE、DENY 开头(在这种情况下,您必须从脚本中提取对象的名称并将其附加到相应对象的脚本)。“ALTER”情况并不难处理,因为您可以从脚本中提取对象的类型和名称,然后将脚本附加到相应对象的脚本。问题出现在“REVOKE”、“DENY”和“GRAND”情况下。在这些情况下,您无法获得对象的类型(我稍后将修复这些情况)。
使用代码
此解决方案包含两个项目。第一个项目称为 DBCompare,是此解决方案的主项目,用于脚本生成和比较。第二个项目称为 DefferenceEngine,是用于检索两个字符串(在此例中为两个脚本)之间差异的基类。有关该类的更多信息,请参见 此处。DBCompare 项目包含六个窗体:
- Login - 用于创建数据库服务器连接。
- MDIMain - 作为所有其他窗体的容器。
- ObjectCompre - 用于比较选定的数据库对象。
- ObjectFetch - 在此屏幕中,将生成所有脚本,然后将它们传递给 ObjectCompare 屏幕进行比较。
- ScriptView - 在此屏幕中,将显示同步脚本。
以及一个类
public class ScriptedObject
{
public string Name="";
public string Schema="";
public string Type="";
public DateTime DateLastModified;
public string ObjectDefinition="";
public Urn Urn;
}
ObjectCompare 窗体
ObjectCompare
窗体是此项目的主要部分。它显示数据库对象并显示它们之间的差异。当此窗体显示时,它会自动显示登录屏幕,您可以在其中输入数据库服务器的登录参数并设置脚本编写选项。当所有这些参数都设置好后,它们将被传递到 ObjectCompare
屏幕。当检索到此信息后,将显示 ObjectFetch
屏幕并开始脚本编写。
此屏幕由三个部分组成。第一部分是一个左侧面板,您可以在其中选择要比较的数据库对象(例如,当您仅选择 Tables 复选框时,列表中将仅显示表;当您不想比较所有类型的数据库对象时,这是一个很好的功能)。第二部分是所有数据库对象的列表。此列表分为四个部分:
- 仅存在于 DB1 中的对象
- 仅存在于 DB2 中的对象
- 同时存在于两个数据库中且不同的对象
- 同时存在于两个数据库中且相同的对象
当您单击最后一个第三部分中的某个项目时,将比较并显示所选对象的脚本。
所有对象都存储在 DataTable dbObjects
中,该表有六列:
- ResultSet - 它指定了对象所属的组(仅存在于 DB1 中的对象 [1],仅存在于 DB2 中的对象 [2],…)。
- Name - 数据库对象的名称。
- Type - 数据库对象的类型。
- Schema - 数据库对象所属的架构(并非所有对象都属于数据库架构)。
- ObjectDefinition1 - 如果两个数据库具有相同名称和类型的对象但定义不同,ObjectDefinition1 将存储源数据库数据库对象的定义。
- ObjectDefinition2 - 如果两个数据库具有相同名称和类型的对象但定义不同,ObjectDefinition2 将存储目标数据库数据库对象的定义。如果对象仅存在于其中一个数据库中,则此属性为空,并且对象的定义存储在 ObjectDefinition1 中。
显示的对象的列表通过 RefreshObjectsList
函数刷新。
private void RefreshObjectsList()
{
bool isFirstConditionAdded = false;
StringBuilder condition =new StringBuilder();
for (int i = 0; i < tableLayoutPanel1.Controls.Count;i++ )
{
Control ctrl = tableLayoutPanel1.Controls[i];
if (ctrl.GetType() == typeof(CheckBox))
{
if (((CheckBox)ctrl).Checked)
{
if (!isFirstConditionAdded)
{
condition.Append("Type='" + ctrl.Tag.ToString() + "'");
isFirstConditionAdded = true;
}
else
{
condition.Append(" OR Type='" + ctrl.Tag.ToString() + "'");
}
}
}
}
DataView dwObjects = new DataView(dbOjects, condition.ToString(),
"Type,Name", DataViewRowState.CurrentRows);
lwDatabaseObjects.Items.Clear();
lwDatabaseObjects.Groups.Clear();
lwDatabaseObjects.Groups.Add("1", "Objects that exist only in " + Database1 + " ");
lwDatabaseObjects.Groups.Add("2", "Objects that exist only in " + Database2 + " ");
lwDatabaseObjects.Groups.Add("3", "Objects that exist in both databases and are identical");
lwDatabaseObjects.Groups.Add("4", "Objects that exist in both databases and are different");
foreach (DataRowView dr in dwObjects)
{
int imgIndex = 0;
switch (dr["Type"].ToString())
{
case "STOREDPROCEDURE":
imgIndex = 0;
break;
case "TABLE":
imgIndex = 1;
break;
case "USERDEFINEDFUNCTION":
imgIndex = 2;
break;
case "VIEW":
imgIndex = 3;
break;
case "APPLICATIONROLE":
imgIndex = 4;
break;
case "DATABASEROLE":
imgIndex = 5;
break;
case "DEFAULT":
imgIndex = 6;
break;
case "FULLTEXTCATALOG":
imgIndex = 7;
break;
case "FULLTEXTSTOPLIST":
imgIndex = 7;
break;
case "USERDEFINEDTABLETYPE":
imgIndex = 1;
break;
case "MESSAGETYPE":
imgIndex = 8;
break;
case "PARTITIONFUNCTION":
imgIndex = 9;
break;
case "PARTITIONSCHEME":
imgIndex = 10;
break;
case "PLANGUIDE":
imgIndex = 12;
break;
case "REMOTESERVICEBINDING":
imgIndex = 15;
break;
case "RULE":
imgIndex = 13;
break;
case "SCHEMA":
imgIndex = 14;
break;
case "SERVICECONTRACT":
imgIndex = 16;
break;
case "SERVICEQUEUE":
imgIndex = 17;
break;
case "SERVICEROUTE":
imgIndex = 18;
break;
case "SQLASSEMBLY":
imgIndex = 19;
break;
case "DDLTRIGGER":
imgIndex = 20;
break;
case "SYNONYM":
imgIndex = 21;
break;
case "USER":
imgIndex = 22;
break;
case "USERDEFINEDAGGREGATE":
imgIndex = 23;
break;
case "USERDEFINEDTYPE":
imgIndex = 24;
break;
case "USERDEFINEDDATATYPE":
imgIndex = 25;
break;
case "XMLSCHEMACOLLECTION":
imgIndex = 26;
break;
case "TRIGGER":
imgIndex = 27;
break;
case "INDEX":
imgIndex = 28;
break;
}
switch (dr["ResultSet"].ToString())
{
case "1":
this.lwDatabaseObjects.Items.Add(new ListViewItem(new string[] {
dr["Type"].ToString(),dr["Schema"].ToString(),
dr["Name"].ToString() }, imgIndex, lwDatabaseObjects.Groups[0]));
break;
case "2":
this.lwDatabaseObjects.Items.Add(new ListViewItem(new string[] {
dr["Type"].ToString(),dr["Schema"].ToString(),
dr["Name"].ToString() }, imgIndex, lwDatabaseObjects.Groups[1]));
break;
case "3":
this.lwDatabaseObjects.Items.Add(new ListViewItem(new string[] {
dr["Type"].ToString(),dr["Schema"].ToString(),
dr["Name"].ToString() }, imgIndex, lwDatabaseObjects.Groups[2]));
break;
case "4":
this.lwDatabaseObjects.Items.Add(new ListViewItem(new string[] {
dr["Type"].ToString(),dr["Schema"].ToString(),
dr["Name"].ToString() }, imgIndex, lwDatabaseObjects.Groups[3]));
break;
}
}
}
单击“刷新”按钮时会调用此函数。
登录窗体
登录窗体用于创建要比较的数据库的连接。它包含两个选项卡。在第一个名为“登录选项”的选项卡中,您可以输入 SQL Server 数据库名称和凭据。前两个 ComboBox
对象(cboServer1
和 cboServer2
)加载 SQL Server 的本地和远程实例。
DataTable dt = SmoApplication.EnumAvailableSqlServers(false);
if (dt.Rows.Count > 0)
{
foreach (DataRow dr in dt.Rows)
{
cboServer1.Items.Add(dr["Name"].ToString());
cboServer2.Items.Add(dr["Name"].ToString());
}
}
然后,您可以从两种身份验证模式中选择(Windows 身份验证和 SQL Server 身份验证)。选择第二种时,必须输入用户名和密码。在此步骤之后,您可以从 Database ComboBox
对象(cboDatabase1
和 cboDatabse2
)中选择数据库。当您单击其中一个数据库组合框时,当前服务器的数据库列表将使用 RefreshDatabaseList
自动填充。此函数被重载。第一个版本接受两个参数:cbo
,它指定要填充哪个组合框,以及 server
,它指定服务器。第二个版本接受四个参数。前两个与前一个版本相同,后两个参数是登录名和密码。第一个版本用于 Windows 身份验证,第二个版本用于 SQL Server 身份验证。
private void RefreshDatabaseList(ComboBox cbo, string server, string login, string password)
{
try
{
cbo.Items.Clear();
ServerConnection conn = new ServerConnection();
conn.ServerInstance = server;
conn.LoginSecure = false;
conn.Login = login;
conn.Password = password;
Server srv = new Server(conn);
foreach (Database db in srv.Databases)
{
cbo.Items.Add(db.Name);
}
}
catch (Exception err)
{
MessageBox.Show(err.Message, "Authentication Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
在脚本编写选项类中,您可以为生成脚本的 Scripter
对象设置脚本编写选项。
以下是支持的选项列表:
聚集索引 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含定义聚集索引的语句。 |
DRI 检查 | 获取或设置布尔属性值,该值指定是否在脚本中包含具有强制声明性引用完整性的检查约束中定义的列特定依赖关系。 |
DRI 聚集 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的聚集索引中定义的依赖关系包含在脚本中。 |
DRI 默认值 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的默认值中定义的依赖关系包含在脚本中。 |
DRI 外键 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的外键中定义的依赖关系包含在脚本中。 |
DRI 索引 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的唯一索引(用于实现声明性引用完整性)的 PRIMARY KEY 约束包含在脚本中。 |
DRI 非聚集 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的非聚集索引中定义的依赖关系包含在脚本中。 |
DRI 主键 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的主键中定义的依赖关系包含在脚本中。 |
Dri 唯一键 | 获取或设置布尔属性值,该值指定是否在具有强制声明性引用完整性的唯一键中定义的依赖关系包含在脚本中。 |
DRI 禁用检查 | 获取或设置一个布尔属性值,该值指定是否在脚本中包含 NOCHECK 语句。 |
扩展属性 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含扩展对象属性。 |
全文目录 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含全文目录。 |
全文索引 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含全文索引。 |
全文停止列表 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含全文停止列表。 |
索引 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含索引。 |
无程序集 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含程序集。 |
无标识 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含标识属性种子和增量的定义。 |
无 FILESTREAM | 获取或设置一个对象,该对象指定在生成的脚本中创建 VarBinaryMax 列时是否包含 FILESTREAM_ON 子句。 |
无文件组 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含“ON <filegroup> ”子句。 |
无排序规则 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含 Collation 子句。 |
无 FILESTREAM 列 | 获取或设置一个对象,该对象指定在生成的脚本中创建 VarBinaryMax 列时是否包含 FILESTREAM_ON 子句。 |
无索引分区方案 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含索引的分区方案。 |
无表分区方案 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含表的分区方案。 |
权限 | 即将推出。 |
触发器 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含触发器的定义。 |
非聚集索引 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含非聚集索引。 |
XML 索引 | 获取或设置一个布尔属性值,该值指定是否在生成的脚本中包含 XML 索引。 |
设置完所有参数(服务器、数据库、脚本编写选项)后,您可以单击“登录”按钮。当所有输入参数都正确时,它们将被发送到 ObjectCompare
屏幕。所有脚本编写选项都由登录屏幕的 GetScriptiongOptions
函数检索。
public ScriptingOptions GetScriptiongOptions()
{
ScriptingOptions so = new ScriptingOptions();
so.ClusteredIndexes = chkClusteredIndexes.Checked;
so.DriChecks = chkDriChecks.Checked;
so.DriClustered = chkDriClustered.Checked;
so.DriDefaults = chkDriDefaults.Checked;
so.DriForeignKeys = chkDriForeignKeys.Checked;
so.Indexes = chkIndexes.Checked;
so.DriIndexes = chkDriIndexes.Checked;
so.DriNonClustered = chkDriNonClustered.Checked;
so.DriPrimaryKey = chkDriPrimaryKeys.Checked;
so.DriUniqueKeys = chkDriUniqueKeys.Checked;
so.DriWithNoCheck = chkDriWithNoCheck.Checked;
so.ExtendedProperties = chkExtendedProperties.Checked;
so.FullTextCatalogs = chkFullTextCatalogs.Checked;
so.FullTextIndexes = chkFullTextIndexes.Checked;
so.FullTextStopLists = chkFullTextStopLists.Checked;
so.NoAssemblies = chkNoAssemblies.Checked;
so.NoCollation = chkNoCollation.Checked;
so.NoIdentities = chkNoIdentities.Checked;
so.NoFileStream = chkNoFileStream.Checked;
so.NoFileGroup = chkNoFileGroup.Checked;
so.NoFileStreamColumn = chkNoFileStreamColumn.Checked;
so.NoIndexPartitioningSchemes = chkNoIndexPartitionSchemes.Checked;
so.NoTablePartitioningSchemes = chkNoTablePartitionSchemes.Checked;
so.Permissions = false;
so.Triggers = chkTriggers.Checked;
so.NonClusteredIndexes = chkNonClusteredIndexes.Checked;
so.XmlIndexes = chkXMLIndexes.Checked;
return so;
}
ObjectFetch 窗体
ObjectFetch
是脚本生成的核心。在此窗体中,所有对象都将被编写脚本,并且脚本将存储在 DataTable
对象中,然后传递给 ObjectCompare
屏幕进行显示。脚本生成从确定数据库对象开始。在此阶段,将收集对象的 URN。
foreach (ApplicationRole obj in db.ApplicationRoles)
{
ScriptedObject scriptedObj = new ScriptedObject();
scriptedObj.Name = obj.Name;
scriptedObj.Type = "ApplicationRole".ToUpper();
scriptedObj.Urn = obj.Urn;
scriptedObj.Schema = obj.DefaultSchema;
hsObject.Add(scriptedObj.Type.ToUpper() + "." +
scriptedObj.Name.ToLower(), scriptedObj);
}
...
foreach (XmlSchemaCollection obj in db.XmlSchemaCollections)
{
ScriptedObject scriptedObj = new ScriptedObject();
scriptedObj.Name = obj.Name;
scriptedObj.Type = "XmlSchemaCollection".ToUpper();
scriptedObj.Schema = obj.Schema;
scriptedObj.Urn = obj.Urn;
hsObject.Add(scriptedObj.Type.ToUpper() + "." +
scriptedObj.Name.ToLower(), scriptedObj);
}
所有对象都存储在 hsObject
哈希表中,其中键是通过连接对象的类型和名称属性创建的。接下来,检索 URN 数组
var urns = new Microsoft.SqlServer.Management.Sdk.Sfc.Urn[hsObject.Count];
int i = 0;
foreach (string key in hsObject.Keys)
{
urns[i] = new Urn(((ScriptedObject)hsObject[key]).Urn.ToString());
i++;
}
并将 URN 数组作为参数传递给 scripter
对象的 Script()
方法。
var scripts = scripter.Script(urns);
Script()
方法返回包含生成的脚本的 StringCollection
对象。
string prevObjName = "";
string prevObjType = "";
foreach (var script in scripts)
{
string scriptParp = script;
if (scriptParp.IndexOf("/****** Object:") > -1)
{
int objectPos = scriptParp.IndexOf("/****** Object:");
int scriptDatePos = scriptParp.IndexOf("Script Date:");
objectPos = objectPos + "/****** Object:".Length;
string objectName =
scriptParp.Substring(objectPos, scriptDatePos - objectPos).Trim();
string type = "";
if (objectName.ToUpper() == "FullTextIndex".ToUpper())
{
type = prevObjType;
objectName = prevObjName;
}
else
{
type = objectName.Substring(0, objectName.IndexOf(" ")).Trim();
objectName = objectName.Substring(type.Length + 1);
}
string schema = "";
if (objectName.IndexOf("].[") > -1)
{
schema = objectName.Substring(0, objectName.IndexOf("].["));
schema = schema.Replace("[", "").Replace("]", "");
objectName = objectName.Substring(objectName.IndexOf("].[") + 3);
objectName = objectName.Replace("[", "").Replace("]", "");
}
else if (objectName.IndexOf("[") > -1 && objectName.IndexOf("]") > -1)
{
objectName = objectName.Replace("[", "").Replace("]", "");
}
else
{
objectName = prevObjName;
}
if (!lstDBObjecTypes.Contains(type.ToUpper()))
{
if (objectName.IndexOf(" ") > -1)
{
objectName = objectName.Substring(objectName.IndexOf(" ") + 1);
}
type = prevObjType;
objectName = prevObjName;
}
else
{
if (scriptParp.IndexOf("/****** Object:") > -1)
{
if (scriptParp.IndexOf("******/") > -1)
{
int beginHeaderPos =
scriptParp.IndexOf("/****** Object:") + "/****** Object:".Length;
int endHeaderPos = scriptParp.IndexOf("******/");
scriptParp = scriptParp.Substring(endHeaderPos + "******/".Length);
}
}
prevObjType = type;
prevObjName = objectName;
}
objectName = objectName.ToLower();
//alter object, grant ... dorobit
if (!hsObject.Contains(type.ToUpper() + "." + objectName.ToLower()))
{
ScriptedObject scriptedObj = new ScriptedObject();
scriptedObj.Name = objectName;
scriptedObj.Type = type.ToUpper();
scriptedObj.Schema = schema;
scriptedObj.Urn = "";
hsObject.Add(scriptedObj.Type + "." + scriptedObj.Name.ToLower(), scriptedObj);
}
((ScriptedObject)hsObject[type.ToUpper() + "." + objectName.ToLower()]).ObjectDefinition =
((ScriptedObject)hsObject[type.ToUpper() + "." + objectName.ToLower()]).ObjectDefinition +
scriptParp.Trim() + System.Environment.NewLine + "GO" + System.Environment.NewLine;
}
else if (scriptParp.Trim().Substring(0, 5).ToUpper() == "GRANT"
|| scriptParp.Trim().Substring(0, 6).ToUpper() == "REVOKE"
|| scriptParp.Trim().Substring(0, 5).ToUpper() == "DENY")
{
string permission = scriptParp; /// permissions COMMING SOON
}
else if (scriptParp.Trim().Substring(0, 5).ToUpper() == "ALTER")
{
string t = scriptParp.Substring("ALTER".Length).Trim();
t = t.Substring(0, t.IndexOf(" ")).Trim();
string n = scriptParp.Substring(scriptParp.IndexOf(t) + t.Length).Trim();
n = n.Substring(0, n.IndexOf(" ")).Trim();
if (n.IndexOf("].[") > -1)
{
n = n.Substring(n.IndexOf("].[") + 3);
}
n=n.Replace("]", "").Replace("[","");
string key = t.ToUpper() + "." + n.ToLower();
((ScriptedObject)hsObject[key]).ObjectDefinition =
((ScriptedObject)hsObject[key]).ObjectDefinition + scriptParp.Trim() +
System.Environment.NewLine + "GO" + System.Environment.NewLine;
}
else
{
((ScriptedObject)hsObject[prevObjType.ToUpper() + "." +
prevObjName.ToLower()]).ObjectDefinition = ((ScriptedObject)hsObject[prevObjType.ToUpper() +
"." + prevObjName.ToLower()]).ObjectDefinition + scriptParp.Trim() +
System.Environment.NewLine + "GO" + System.Environment.NewLine;
}
}
未来发展
此程序可用作 SMO 脚本生成工具。此方法速度不快,但在进行一些性能调整后,您可以加快脚本生成速度。预取对象可以极大地提高性能。我计划创建一个自定义类来编写数据库对象的脚本,该类将使用 SQL Server 动态管理视图。这将比通过 SMO 编写脚本快得多,但需要更多的编程工作。
历史
- 2011 年 6 月 1 日 - 发布初始版本。