【问题标题】:MySQL stored procedures or php code?MySQL存储过程还是php代码?
【发布时间】:2011-03-26 01:53:08
【问题描述】:

一个一般性的问题,没有考虑特定的情况 - 通常更喜欢使用 MySQL 存储过程而不是编写执行相同计算和查询的 PHP 脚本?

每种方法的好处是什么?

【问题讨论】:

标签: php mysql database stored-procedures


【解决方案1】:

Jeff Atwoods "Who Needs Stored Procedures, Anyways?" from 2004 的点/反点:

1) 存储过程是用大型数据库“语言”编写的,例如 PL/SQL (Oracle) 或 T-SQL (Microsoft)。这些所谓的语言是陈旧的,充满了疯狂的、不连贯的设计选择,这些选择总是源于十年向后兼容性的痛苦演变。你真的不想在这些东西上写很多代码。就上下文而言,JavaScript 是 PL/SQL 或 T-SQL 的一大进步。

响应:“SQL”中的“S”表示“结构化”,而不是“标准化” - PLSQL 和 TSQL 都是自定义扩展 SQL,这也使 ANSI SQL 发挥作用,因为很少有 SQL 与数据库无关。一般来说,如果您想要一个执行良好的查询,您不能依赖 ANSI SQL。

ORM 不是灵丹妙药 - 由于数据库抽象,大多数支持运行本机存储过程/函数以获得性能良好的查询。这很好,但完全违背了 ORM 的目的......

我永远无法理解为什么 Web 开发,无数技术(HTML、Javascript/AJAX、Flash...)的组合总是将 SQL 隔离为家庭中的害群之马。像所有其他人一样,您必须学习它才能从中获得一些东西。一定是您在使用其他技术时获得的即时满足感...

2) 存储过程通常无法在您编写 UI 的同一 IDE 中进行调试。每次我在 procs 中隔离一个异常时,我都必须停止我正在做的事情,取出我的 Toad 副本,并加载数据库包以查看发生了什么问题。频繁在两个完全不同的 IDE 之间转换,具有完全不同的界面和语言,效率并不高。

响应:在 Eclipse 或 Visual Studio 中最初是否有 Javascript 调试器?不,他们允许使用插件来将产品推出市场并为以前不存在的市场注入活力。大多数人在 Visual Studio/Eclipse 之外使用 Firebug 都没有问题,为什么 SQL 调试会有所不同?

3) 出现问题时,存储过程不会提供太多反馈。除非使用奇怪的 T-SQL 或 PL/SQL 异常处理对 proc 进行内部编码,否则我们会根据 proc 中失败的特定行返回神秘的“错误”,例如 Table 没有行。嗯,好吗?

回应:你不熟悉并不代表语言不好。就像你从来没有用谷歌搜索你选择的语言中的一个奇怪的错误......至少 Oracle 和 MySQL 会给你错误参考号。

4) 存储过程不能传递对象。所以,如果你不小心,你可能会得到无数的参数。如果您必须使用 proc 填充具有 20 多个字段的表行,请向 20 多个参数问好。最糟糕的是,如果我传递了错误的参数——要么太多、不够,要么数据类型错误——我会得到一个通用的“错误调用”错误。 Oracle 不能告诉我哪些参数有误!因此,我必须手动仔细研究 20 多个参数,以确定哪个是罪魁祸首。

响应:SQL 是基于 SET 的,完全不同于过程/OO 编程。类型接近对象,但在某些时候需要在过程/OO 对象和数据库实体之间建立映射。

5) 存储过程隐藏了业务逻辑。我不知道 proc 在做什么,也不知道它将返回给我什么样的游标(DataSet)或值。我无法查看 proc 的源代码(至少,如果我有适当的访问权限,则无需诉诸#2)来验证它实际上正在做我认为的事情 - 或者设计师打算做的事情。内联 SQL 可能并不漂亮,但至少我可以在上下文中看到它,以及其他业务逻辑。

响应:这是一件好事(tm) - 这就是您获得模型-视图-控制器 (MVC) 的方式,因此您可以在任何多种语言,而不必每次都复制逻辑,同时处理每种语言的怪癖来复制该逻辑。或者如果有人直接连接到数据库,数据库允许添加坏数据是好事吗?在应用程序和数据库之间来回切换会浪费您的应用程序永远无法弥补的时间和资源。

【讨论】:

  • 在看到您的回答之前,我几乎对这个问题失去了希望。 +1
【解决方案2】:

我认为 Jeff Atwood 在 2004 年在存储过程方面一针见血:

Who Needs Stored Procedures, Anyways?

在广泛使用存储过程和动态 SQL 之后,我绝对更喜欢后者:更易于管理、更好的封装、数据访问层中没有 BL、更大的灵活性等等。几乎每个主要的开源 PHP 项目都在存储过程上使用动态 SQL(参见:Drupal、Wordpress、Magento 等等)。

这种对话似乎有些过时了:给自己一个good ORM,停止为您的数据访问而烦恼,开始构建出色的应用程序。

【讨论】:

  • 我更容易发布我对“谁需要存储过程,无论如何?”的剖析。在 wiki 中对原始问题的回答。 “好的 ORM”是矛盾的
  • @OMG Ponies:迟到的请求,抱歉……请问你有这个链接吗?
  • @gbn:是this what you want吗?
【解决方案3】:

对我们来说,使用存储过程是绝对关键的。我们有一个相当大的 .net 应用程序。重新部署整个应用程序可能会使我们的用户在短时间内离线,这是不允许的。

但是,在应用程序运行时,我们有时必须对查询进行细微的更正。简单的事情,例如添加或删除 NOLOCK,甚至可能更改所涉及的连接。这几乎总是出于性能原因。就在今天,我们遇到了一个由无关的 NOLOCK 引起的错误。 2 分钟定位问题、确定解决方案并部署新 proc:零停机时间。使用代码中的查询这样做至少会导致轻微的中断,可能会激怒很多人。

另一个原因是安全性。使用 proc,我们将用户 ID(非顺序、不可猜测)传递给每个 proc 调用。我们验证用户有权在 Web 应用程序中运行该功能,并再次在数据库本身中运行。如果我们的 Web 应用程序遭到入侵,这会从根本上提高黑客的门槛。他们不仅不能运行他们想要的任何 sql,而且即使要运行 proc,他们也必须有一个特定的授权密钥。这很难获得。 (不,这不是我们唯一的防御)

我们的 proc 受源代码控制,所以这不是问题。另外,我不必担心如何命名事物(某些 ORM 讨厌某些命名方案),也不必担心飞行性能。要正确调整 ORM,您必须了解的不仅仅是 SQL。您必须了解 ORM 的特定行为。

【讨论】:

    【解决方案4】:

    存储过程 100 次中有 99 次。如果我要选择 1 个原因,那么如果您的 php Web 应用程序通过存储过程完成所有数据库访问,并且您的数据库用户只有执行上述存储过程的权限,那么您就是 100 % 防止 SQL 注入攻击。

    【讨论】:

      【解决方案5】:

      对我来说,在数据库中保留与数据库有关的任何内容的好处是调试。如果您在存储过程中完成了计算(至少大部分计算),并且需要进行更改,那么您只需修改、测试、保存即可。您的 PHP 代码不会有任何更改。

      如果您要在 PHP 代码中存储主要计算,则需要从代码中获取 SQL 语句,对其进行清理、修改、测试,然后将其复制回去并再次测试。

      将物品分开,便于维护。如果使用存储过程,代码看起来更干净,更容易阅读,因为我们都知道 SQL 脚本会变得非常大。将所有数据库逻辑保存在数据库中。

      如果数据库经过适当调整,您执行查询的时间可能会稍微快一些,因为不是让 PHP 解析字符串,然后将其发送到数据库,然后数据库执行它并将其发送回,您可以使用存储过程将参数推送到数据库中,它将为存储过程提供一个缓存的执行计划,并且事情会稍微快一些。一些精心放置的索引可以帮助加快任何数据检索,因为实际上 - Web 服务器只是一个管道,PHP 脚本不会加载那么多。

      【讨论】:

        【解决方案6】:

        我想说“不要对数据库做太多的魔法”。最坏的情况是项目的新开发人员注意到**一个操作**已经完成,但他看不到它在代码中的位置。所以他一直在寻找。但它是在数据库中完成的。

        所以如果你做一些“不可见”的数据库操作(我在考虑触发器),只需将它写在一些代码文档中。

        // add a new user
        $user = new User("john", "doe");
        $user->save();
        // The id is computed by the database see MYPROC_ID_COMPUTATION
        print $user->getId();
        

        另一方面,为 DB 编写函数是一个好主意,并且会为开发人员提供一个很好的抽象层。

        // Computes an ID for the given user
        DB->execute("SELECT COMPUTE_ID(" . $user->getLogin() . ") FROM DUAL");
        

        当然这都是伪代码,但我希望你能理解我晦涩的想法。

        【讨论】:

        • 这很可能是我的假设并且对人们提出了太多要求,但是开发人员无法确定某些东西是否正在数据库中运行存储过程,或者运行某些动态生成的 SQL 的函数声明?顺便说一句,如果有适当的文档,任何应用程序或数据库操作都是不可见的。我怀疑指出生成 ID 的评论就足够了。
        【解决方案7】:

        嗯,这个论点有一个我很少听到的方面,所以我会在这里写下来......

        代码受版本控制。数据库不是。因此,如果您有多个代码实例,您将需要某种方式在更新时自动执行迁移,否则您将面临破坏事情的风险。即使这样,您仍然面临“忘记”将更新的 SP 添加到迁移脚本,然后破坏构建的问题(如果您没有真正测试 idepth,可能甚至没有意识到这一点)。

        从调试和维护来看,我发现 SP 的 100 倍与原始 SQL 一样难以剖析。原因是它至少需要三个步骤。首先,查看 PHP 代码以查看调用了哪些代码。然后进入数据库并找到该程序。然后最后看一下程序的代码。

        另一个论点(沿着版本控制的方向)是 SP 没有 svn st 命令。因此,如果您找到一个手动修改 SP 的开发人员,您将花费大量时间来解决这个问题(假设它们并非全部由单个 DBA 管理)。

        当您有多个应用程序与同一个数据库架构通信时,SP真正的亮点在于。然后,您只有一个地方存储 DDL 和 DML,两个应用程序可以共享它,而无需在一个或多个库中添加交叉依赖。

        所以,简而言之,我的观点如下:

        使用存储过程:

        1. 当您有多个应用程序处理同一个数据集时
        2. 当您需要循环查询并执行其他查询时(避免 TCP 层丢失可以大大提高效率)
        3. 如果您有一个真正优秀的 DBA,因为它将强制执行由他/她处理的所有 SQL。

        几乎在任何其他情况下都使用原始 SQL/ORM/生成的 SQL(差不多,因为肯定会有我没有考虑的边缘情况)...

        再说一次,这只是我的 0.02 美元...

        【讨论】:

        • 您没有将数据库脚本存储在版本控制中?啧啧啧。
        • 关于找出哪个 proc 在哪里修改:在 sql 2005+ 下,您可以使用类似:SELECT name FROM sys.objects WHERE type = 'P' AND DATEDIFF(D,modify_date, GETDATE())
        【解决方案8】:

        出于多种原因,我尽可能多地使用存储过程。

        减少到数据库的往返次数

        如果您需要一次更改多个相关表,那么您可以使用单个存储过程,以便只调用一次数据库。

        明确定义业务逻辑

        如果查询的某些事情必须是真实的,那么存储过程可以让了解 SQL(一种相当简单的语言)的人确保事情正确完成。

        为其他程序员创建简单的界面

        您的不具备 sql 能力的队友可以使用更简单的数据库接口,您可以确定他们不会在意外情况下将关系置于不良状态。

        考虑:

        SELECT a.first_name, IFNULL( b.plan_id, 0 ) AS plan_id
        FROM account AS a
           LEFT JOIN subscription AS s ON s.account_id = a.id
        WHERE a.id = 23
        

        相比:

        CALL account_get_current_plan_id( 23 );
        

        为它们编写一个漂亮的小包装器来处理存储过程调用,它们就开始工作了。

        一次更新系统中的所有使用情况

        如果每个人都使用存储过程来查询数据库,而您需要更改某些工作方式,则可以更新存储过程,只要不更改接口,它就会随处更新。

        强制安全

        如果您可以仅使用存储过程来完成系统内的所有操作,那么您可以为访问数据的用户帐户授予严格受限的权限。无需授予他们 UPDATE、DELETE 甚至 SELECT 权限。

        简单的错误处理

        很多人没有意识到这一点,但是您可以创建存储过程,从而使追踪大多数问题变得非常容易。

        如果您使用良好的结构,您甚至可以集成您的代码库以正确处理返回的错误。

        这是一个执行以下操作的示例:

        • 对严重问题使用退出处理程序
        • 对不太严重的问题使用 continue 处理程序
        • 是否预先进行非表扫描验证
        • 如果验证没有失败,接下来是否进行表扫描验证
        • 如果事情验证,是否在事务中进行处理
        • 出现问题时回滚所有内容
        • 报告发现的任何问题
        • 避免不必要的更新

        这是一个组成的存储过程的内部结构,它接受一个帐户 ID、关闭帐户 ID 和一个 IP 地址,然后使用它们进行适当的更新。分隔符已经设置为 $$:

        BEGIN
        
                # Helper variables
        
                DECLARE r_code INT UNSIGNED;
                DECLARE r_message VARCHAR(128);
                DECLARE it_exists INT UNSIGNED;
                DECLARE n_affected INT UNSIGNED;
        
                # Exception handler - for when you have written bad code
                # - or something really bad happens to the server
                DECLARE EXIT HANDLER FOR SQLEXCEPTION
                BEGIN
                    ROLLBACK;
                    SELECT 0 as `id`, 10001 as `code`, CONCAT(r_message, ' Failed with exception') as `message`;
                END;
        
                # Warning handler - to tell you exactly where problems are
                DECLARE CONTINUE HANDLER FOR SQLWARNING
                BEGIN
                    SET r_code = 20001, r_message = CONCAT( r_message, 'WARNING' );
                END;
        
                SET r_code = 0, r_message = '', it_exists = 0, n_affected = 0;
        
                # STEP 1 - Obvious basic sanity checking (no table scans needed)
                IF ( 0 = i_account_id ) THEN
                    SET r_code = 40001, r_message = 'You must specify an account to close';
                ELSEIF ( 0 = i_updated_by_id ) THEN
                    SET r_code = 40002, r_message = 'You must specify the account doing the closing';
                END IF;
        
                # STEP 2 - Any checks requiring table scans
        
                # Given account must exist in system
                IF ( 0 = r_code ) THEN
                    SELECT COUNT(id) INTO it_exists
                    FROM account 
                    WHERE id = i_account_id;
        
                    IF ( 0 = it_exists ) THEN
                        SET r_code = 40001, r_message = 'Account to close does not exist in the system';
                    END IF;
                END IF;
        
                # Given account must not already be closed
                # - if already closed, we simply treat the call as a success
                # - and don't bother with further processing
                IF ( 0 = r_code ) THEN
                    SELECT COUNT(id) INTO it_exists 
                    FROM account 
                    WHERE id = i_account_id AND status_id = 2;
        
                    IF ( 0 < it_exists ) THEN
                        SET r_code = 1, r_message = 'already closed';
                    END IF;
                END IF;
        
                # Given closer account must be valid
                IF ( 0 = r_code ) THEN
                    SELECT COUNT(id) INTO it_exists 
                    FROM account
                    WHERE id = i_updated_by_id;
                END IF;
        
                # STEP 3 - The actual update and related updates
                # r-message stages are used in case of warnings to tell exactly where a problem occurred
                IF ( 0 = r_code ) THEN
                    SET r_message = CONCAT(r_message, 'a');
        
                    START TRANSACTION;
        
                    # Add the unmodified account record to our log
                    INSERT INTO account_log ( field_list )
                    SELECT field_list 
                    FROM account 
                    WHERE id = i_account_id;
        
                    IF ( 0 = r_code ) THEN
                        SET n_affected = ROW_COUNT();
                        IF ( 0 = n_affected ) THEN
                            SET r_code = 20002, r_message = 'Failed to create account log record';
                        END IF;
                    END IF;
        
                    # Update the account now that we have backed it up
                    IF ( 0 = r_code ) THEN
                        SET r_message = CONCAT( r_message, 'b' );
        
                        UPDATE account 
                        SET 
                            status_id = 2,
                            updated_by_id = i_updated_by_id,
                            updated_by_ip = i_updated_by_ip
                        WHERE id = i_account_id;
        
                        IF ( 0 = r_code ) THEN
                            SET n_affected = ROW_COUNT();
                            IF ( 0 = n_affected ) THEN
                                SET r_code = 20003, r_message = 'Failed to update account status';
                            END IF;
                        END IF;
                    END IF;
        
                    # Delete some related data
                    IF ( 0 = r_code ) THEN
                        SET r_message = CONCAT( r_message, 'c' );
        
                        DELETE FROM something 
                        WHERE account_id = i_account_id;
                    END IF;
        
                    # Commit or roll back our transaction based on our current code
                    IF ( 0 = r_code ) THEN
                        SET r_code = 1, r_message = 'success';
                        COMMIT;
                    ELSE 
                        ROLLBACK;
                    END IF;
                END IF;
        
                SELECT 
                    r_code as `code`,
                    r_message as `message`,
                    n_affected as `affected`;
        
            END$$
        

        状态码含义:

        • 0:永远不应该发生 - 坏结果
        • 1:成功 - 帐户已关闭或正确关闭
        • 2XXXX - 逻辑或语法问题
        • 3XXXX - 系统中出现意外数据值的问题
        • 4XXXX - 缺少必填字段

        与其信任不熟悉数据库(或根本不熟悉方案)的程序员,不如为他们提供接口要简单得多。

        无需进行上述所有检查,他们可以简单地使用:

        CALL account_close_by_id( 23 );
        

        然后检查结果代码并采取适当的措施。

        就个人而言,我相信如果您可以访问存储过程并且您没有使用它们,那么您真的应该考虑使用它们。

        【讨论】:

          【解决方案9】:

          只要有可能,最终用户将从 UI 中的数据抽象中受益。因此,您应该尽可能多地尝试和利用存储过程。

          【讨论】:

          • 我知道的旧响应。但是,我认为您需要证明您的立场是正确的,即最终用户,一个永远不应该看到您的代码,也不应该关心它是如何实现的人,将受益于任何抽象,无论它位于何处。
          • 抽象允许优化数据查询,以便最终用户受益于更快的加载时间。不确定在这么晚的日期是否需要更彻底的解释。
          【解决方案10】:

          如果在数据库上执行计算,则不一定需要基础值,然后让数据库执行它们。这有助于将数据库和 PHP 脚本之间的数据传输量降至最低;但通常使用数据库数据的计算最好由数据库本身执行。

          【讨论】:

            【解决方案11】:

            我听到有人说“让数据库尽其所能”,也有人哭着说“wtf,你对我的数据库性能做了什么”。

            所以我想这主要是使用率的决定(存储过程会强调MySQL进程,PHP代码会强调Web服务器进程)。

            【讨论】:

            • 嗯,存储过程并不一定意味着更多的负载,如果它们在 DBA 的控制之下,那么在 DBA 发现问题之前,流氓编码器对数据库施加压力的可能性就会更小,责备代码,并修复它。更多的是大型项目/部门的事情。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-04-27
            • 2018-06-21
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多