结构化存储过程的最佳实践

Posted

技术标签:

【中文标题】结构化存储过程的最佳实践【英文标题】:Best practices of structuring stored procedures 【发布时间】:2011-08-24 19:21:51 【问题描述】:

作为主要编写 c# 的开发人员,我在编写 c# 代码时采用了一些好的做法。当我有时编写存储过程时,我很难将这些做法应用到存储过程代码中。

在某些情况下,我继承了噩梦般的存储过程代码,前三四层存储过程设置了一些临时表并主要相互调用。没有完成真正的工作,只有几行代码。然后最后调用了“最终的”存储过程,一个 3000-5000 行 SQL 代码的大怪物。该代码通常有很多代码气味,例如代码重复、复杂的控制流(又名意大利面条)和一种方法,该方法将太多的事情堆叠在一起,没有明确区分一块工作的开始和结束位置(甚至不是一个注释为除数)。

我还注意到从中间临时表中选择的注释选择语句的使用。可以出于调试目的重新打开选择,但需要在任何期望返回结果集的特定顺序的调用代码之前删除。

显然我的队友也分享了我缺乏良好的 SQL 编写习惯。

所以...(真正的问题来了)...编写模块化可维护存储过程的良好做法是什么?

欢迎自制实践和参考书籍/博客。有助于完成某些任务的方法和工具。

让我们总结一些我没有找到好的做法的领域

模块化和封装(通过临时表进行存储过程通信真的可行吗?) 在 c# 中,我使用带有访问修饰符的程序集、类和方法来实现这一点。 调试/测试(比修改调试目标好?) 调试工具? 调试跟踪? 测试夹具? 使用代码强调代码/逻辑/数据/控制流代码的结构 在 c# 中,我重构并分解了较小的方法,每个方法只执行一项逻辑任务。 代码重复

大多数情况下,我将 SQL Server 作为 DBMS 遇到,但 DBMS 不可知论者的答案或指出其他 DBMS 功能的答案:在上述情况下提供帮助的答案也是受欢迎的。

提供一些背景知识:我遇到的大多数大型存储过程都在报告场景中,其中基础只是从大表中创建一些汇总值。但是一路上你需要排除一些恰好在某个异常表中的值,在一些尚未完成的东西表中添加一些值,与去年相比(你能想象处理产品更换部门的丑陋代码吗?年之间?)等

【问题讨论】:

由于这些原因以及无法在 SCM 中使用存储过程,除了最必要和最明显的用途(触发器等)之外,我已经避免使用它们。我们将业务逻辑保留在业务层 (.NET) 中,并为此使用数据库——一个数据库。 理想情况下,存储过程不应调用其他存储过程。这就是 BL 层的用途。不幸的是,如果您已经拥有一个充满嵌套 SP 调用的数据库,那么这种情绪并没有真正的帮助。您是否正在寻找有关如何使当前模型更易于维护的建议,或有关如何重构您的系统以更好地遵循最佳实践的建议? @gahooa 我也尽可能多地在 .NET 中使用(部分是因为那是我的主场),但在某些情况下,例如在继承大型代码库或拥有大量数据时或多或少被存储过程所困扰。 我非常强烈地认为您应该将数据库视为具有边界的服务,就像您在 .NET 中对待您的类一样。即它有一个定义良好的接口,并保证它所提供的完整性。版本控制下的存储过程以及对基表的有限访问或无访问权限是其中非常重要的一部分。在数据库层应用尽可能多的约束,以确保数据库能够满足其保证。如果您将数据库用作一组愚蠢的表,那么您将得到的就是这些。 @cade Roux,如果可以的话,我会支持你评论一百万次。将数据库用作哑存储是业余数据库设计者的标志。 【参考方案1】:

我写了很多复杂的存储过程。我会考虑的一些最佳做法:

不要在存储过程中使用动态 SQl,除非您正在执行包含许多可能需要或可能不需要的参数的搜索过程(那么它是目前最好的解决方案之一)。如果必须在 proc 中使用动态 SQl,则始终具有调试输入参数,并且如果设置了调试参数,则打印创建的 SQL 语句而不是执行它。这将节省数小时的调试时间!

如果您在 proc 中执行多个操作查询(插入/更新/删除),请使用 Try Cacth 块和事务处理。在输入参数中添加一个测试参数,当它设置为 1 时,总是回滚整个事务。在以测试模式回滚之前,我通常有一个部分返回我正在影响的表中的值,以确保我认为我对数据库所做的实际上是我所做的。或者您可以进行如下所示的检查。这就像在您拥有 @test 参数后将以下代码放在您当前注释掉的选择周围(并取消注释它们)一样简单。

If @test =1
Begin
Select * from table1 where field1 = @myfirstparameter
End

现在您不必在每次测试时都进行评论和取消评论。

@test 或@debuig 应始终设置为默认值 0 并放在列表的最后。这样添加它们不会破坏 proc 的现有调用。

考虑为执行插入/更新/删除的 proc 提供日志和/或错误日志表。如果您在执行过程中将步骤和/或错误记录在表变量中,则它们在回滚后仍然可用以插入到日志记录表中。了解复杂过程的哪一部分失败以及错误是什么在以后可能非常宝贵。

尽可能不要嵌套存储过程。如果您需要在循环中运行多条记录,请将存储的过程替换为具有表值参数的过程,并将过程设置为以基于集合而不是单个记录的方式运行。如果表值参数有一条或多条记录,这将起作用。

如果您有一个包含大量子查询或派生表的复杂选择,请考虑改用 CTE。重构任何相关的子查询或游标以更好地执行基于集合的代码。始终考虑数据集而不是一条记录。

在任何可能的情况下都不要嵌套视图。性能损失比任何少量节省的开发时间都要糟糕得多。相信我,当需要对视图链中最远的视图进行更改时,嵌套视图并不能节省维护时间。

所有存储的过程(和其他数据库代码)都应该在源代码控制中。

表变量适用于较小的数据集,但临时表(以 # 或 ## 开头的真实表,而不是暂存表)对于大型数据集的性能可能更好。如果使用临时表在您不再需要它们时将它们删除。尽量避免使用全局临时表。

学习编写高性能 SQL。通常,编写性能良好的 SQL 与一旦您了解技术就不会执行的 SQL 一样容易。如果您编写复杂的存储过程,没有理由不知道哪些技术比其他技术更有效。了解如何确保您的查询是可搜索的。避免使用游标、相关子查询、标量函数和其他逐行运行的东西。

【讨论】:

RE 嵌套视图:我继承了一个带有嵌套视图的系统,并且在对 SQL Server 进行性能调查时,它没有引起任何问题。也许它曾经是一个问题,但在 MSSQL 2008 及更高版本中,查询计划器非常聪明,并且有效地只是内联视图。 我们遇到此问题的服务器是 SQl Server 2008 服务器。 T 可能取决于如何编写视图。我们遇到的另一个问题是视图变得非常难以维护,最终我们实际上达到了系统的硬限制,视图根本无法运行。我坚持我所说的,使用视图调用其他视图是一种不好的做法。【参考方案2】:

通过临时表进行通信有时会产生巨大的代码气味。这样的过程通常不能由用户运行而不相互干扰(如果您将临时表名称重新用于不同过程的输入和输出并且它们没有重新创建,或者如果您对两个不同的表使用相同的名称模式)。它们可能很难排除故障 - 就像任何功能一样,在必要时使用它们并且不存在更好的替代方案。临时使用真实表也可能会出现问题。

在 SQL Server 中相互传递数据的存储过程(不仅仅是参数)可能会出现问题。现在有表值参数,以前可以使用 procs 完成的许多事情现在可以使用内联表值函数或(通常优先于)多语句表值函数来完成。

在 SQL Server 中,避免在大型行集上大量使用标量函数和多语句表值函数 - 它们的性能不是很好,因此在 C# 中看似显而易见的模块化技术在这里并不真正适用。

我建议你看看Ken Henderson's Guru's Guide to SQL Server Stored Procedures - 发表于 2002 年,它仍然有很多关于数据库应用程序设计的有用信息。

【讨论】:

也许我理解错了,但是临时表可以与多个用户一起运行而不会出现问题。但也许你在谈论一个特殊情况? @Mark SQLDev 你是对的——本地临时表是本地连接的——我发现临时表(和表变量)很难调试。而且,当然,您正在决定实现什么以及何时实现,而不是将其留给优化器。而且您需要跟踪命名,以免发生冲突等。【参考方案3】:

这是一个很好的问题。作为一个不得不涉足 SQL 的 C# 开发人员,似乎 SQL 就其本质而言阻碍了我习惯于使用 C# 的最佳实践。

公用表表达式非常适合隔离存储过程中的查询,但您只能使用一次!这会导致您定义视图,但随后您就失去了封装。

一个存储过程的结果集很难在另一个存储过程中使用,因此您可能很想编写表值函数。这增加了您的权限维护负担,并迫使您“两次”编写函数——一次作为函数,另一次作为调用函数的过程。否则,你的 DAL 有不同的接口,这取决于它是否是一个过程。

随着时间的推移,所有这些都导致我在数据库中坚持使用简单的 CRUD 存储过程(不相互调用),并且在关系复杂时使用很少的孤立查询。更多 BI 的东西。其他一切都在 BLL 中。

从物理上讲,SQL 按函数或它们所围绕的表隔离在单独的文件中,并在源代码控制中进行管理。

避免使用 SELECT * 并倾向于指定列。当您更改表并且不触及所有 procs 时,这可以使您免于运行时问题。是的,对 procs 进行了重新编译,但它会遗漏一些,尤其是在涉及视图的情况下。另外,SELECT * 几乎总是返回比您真正需要的更多的列,这是对带宽的浪费。

【讨论】:

SQL 最佳实践肯定与您使用 C# 所习惯的不同。尝试在关系世界中使用 c# 最佳实践是导致数据库性能不佳的原因之一。数据库旨在最大限度地提高性能,而不是使可维护性更容易。 当您需要在 proc 中多次使用临时表而不是 CTE 时,您可以使用它们。【参考方案4】:

在编写 SQL 代码时,上面的 cmets 是对 Do's 和 Dont's 的很好建议。如果我正确理解您的问题,您是在问 SQL Developer 在单个存储过程中编写成百上千的代码是否正常。在 C# 中,这是一个很大的禁忌。您将使用方法、程序集和类将逻辑封装到小卡盘中。 SQL Developer倾向于将整个逻辑写在一个存储过程中来完成一项相关的任务;正如上面提到的HLGEM,“如​​果可能,不要嵌套存储过程”。不要嵌套视图。

例如:C# 中的简单 Get 和 Insert 设计如下所示:

调用GetData方法 调用获取数据方法 调用转换数据方法 调用 CheckAlphaNumeric 方法 调用数据丰富方法 调用负载转换ed数据方法

SQL 开发人员会这样设计它: 在单个存储过程中:

使用临时表或表变量获取数据和转换,然后将其加载到最终表中。

如果您要更改 SQL 的编写方式以匹配 C# Developer 的编写结构,您可以这样做:

执行主存储过程(调用以下存储过程。) 执行 GetData 存储过程并加载到 Stage Table 执行读取阶段表并转换数据的转换存储过程 执行加载数据存储过程以将 Staged 或转换后的数据加载到最终表中。

【讨论】:

以上是关于结构化存储过程的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

智能日志系统建设最佳实践

云存储架构框架设计 | 最佳实践

将类数据存储在随机访问文件中的最佳实践

实战Flyway迁移指南最佳实践

数据库设计的 10 个最佳实践

金融行业基于服务器通过Ceph实现分布式存储,如何选型设计及运维?| 最佳实践