如何在匿名 PL/SQL 块中自动显示所有 SQL 语句的输出

Posted

技术标签:

【中文标题】如何在匿名 PL/SQL 块中自动显示所有 SQL 语句的输出【英文标题】:How to automatically display output of all SQL statements inside anonymous PL/SQL block 【发布时间】:2018-09-07 07:01:29 【问题描述】:

我们的数据迁移脚本使用匿名 PL/SQL 块来帮助整理代码,主要是这样我们就可以将创建和更新的用户 ID 列设置为“系统”用户。

我们的迁移脚本如下所示:

DECLARE
    v_user_id users.id%TYPE;
BEGIN
    SELECT id INTO v_user_id FROM users WHERE username = 'system';

    UPDATE table1
    SET col1 = value,
        updated_at = SYSDATE,
        updated_by = v_user_id
    WHERE some condition;

    INSERT INTO table2 (val1, SYSDATE, v_user_id);
END;
/

更新记录的用户是用户表中的数字 ID,而不是字符串用户名。这是我们数据建模团队的要求,否则我只会硬编码我们“系统”帐户的用户名。

作为补充说明,我们的 DBA 运行脚本,他不应该是更新或插入记录的人/用户。大型企业环境的另一个要求。

我想从 sqlplus 命令行看到的输出是这样的:

Updated X rows
Inserted Y rows

就像您在 PL/SQL 块之外运行 INSERT 和 UPDATE 语句一样。

真的希望有一个不需要显式调用DBMS_OUTPUT.PUT_LINE 的解决方案。

如何在匿名 PL/SQL 块中自动显示每个 DML 语句的输出,而无需为每个语句显式调用 DBMS_OUTPUT.PUT_LINE

【问题讨论】:

您可以使用 sql%rowcount 查看插入/更新了多少行(看起来在您的示例中只会插入一行)。如果是我,我会创建一个使用自治事务功能来记录进度的函数(可以提交单独的事务,以便您可以在其他程序运行时在日志表中看到进度) @PeterM:根据我所做的研究和您的评论,基本上您是说 Oracle 没有 SET FOO ON 设置自动转储 PL/SQL 中每个语句的输出阻塞到标准输出...? 如果您想自动获得“插入的 X 行”之类的反馈,这是一个 SQL*Plus 功能,仅适用于直接运行的 SQL 语句。如果它们嵌入到 PL/SQL 程序中,则 SQL*Plus 不受控制,也不会得到那种反馈。这就是dbms_output 的用途。 不,任何 dbms_output.put_line 只会在整个事情完成后打印。如果您需要审计之类的东西,如果是我,我会使用 sql plus 或其他程序生成更新语句并将它们全部作为脚本运行,那么您肯定会在每个语句之后获得输出。类似于 select 'update tab set col=''' || col_name ||''' 其中 id = '''||id||''';'来自我的标签; 您只能在另一个会话中审核 pl/sql 块的执行。真的没有办法按照你的要求去做。 【参考方案1】:

复合触发器可以在不改变原始代码的情况下统计并显示修改的行数。

这里仍然存在一些问题和挑战。此解决方案可能不适用于并行 DML - 它要么无法正确计数,要么触发器将阻止直接路径写入。它可能会在多用户环境中工作,但需要进行测试。您还需要为 DELETE 和 MERGE 构建代码。这可能会减慢 DML。

示例架构

create table users(id number, username varchar2(100));
insert into users values(1, 'system');

create table table1(col1 number, updated_at date, updated_by number);
insert into table1 values(1, null, null);
insert into table1 values(2, null, null);

create table table2(col1 number, updated_at date, updated_by number);

防止DBMS_OUTPUT过多的包

不断打印输出可能会导致问题。所以我们要默认禁用输出。而且您可能不只是想使用DBMS_OUTPUT.DISABLE,这可能会关闭其他东西,而且很难始终记住运行它。

使用全局变量创建一个简单的包。

create or replace package print_feedback is
    --Outputing large amounts of data can sometimes break things.
    --Only enable DBMS_OUTPUT when explicitly requested.
    g_print_output boolean := false;
end;
/

在运行导入之前将其设置为TRUE

--Run this block first to enable printing.
begin
    print_feedback.g_print_output := true;
end;
/

PL/SQL 块创建 INSERT 和 UPDATE 触发器

此代码动态生成触发器以捕获 INSERT 和 UPDATE。

动态 PL/SQL 有点棘手。请注意,我使用模板和替代引用机制来避免串联地狱。一旦你理解了这些技巧,代码就会变得相对可读。 (希望您的 IDE 理解 q'[ 如何比 *** 语法高亮显示更好。)

--Create automatic UPDATE and INSERT feedback triggers.
declare
    c_sql_template constant varchar2(32767) :=
    q'[
create or replace trigger #TABLE_NAME#_#UPD_or_INS#_trg for #UPDATE_OR_INSERT# on #TABLE_NAME# compound trigger

--Purpose: Print a feedback message after updates and inserts.
g_count number := 0;

after each row is
begin
    g_count := g_count + 1;
end after each row;

after statement is
begin
    if print_feedback.g_print_output then
        if g_count = 1 then
            dbms_output.put_line('#Inserted_or_Updated# '||g_count||' row in #TABLE_NAME#');
        else
            dbms_output.put_line('#Inserted_or_Updated# '||g_count||' rows in #TABLE_NAME#');
        end if;
    end if;
end after statement;

end;
    ]';
    v_sql varchar2(32767);
begin
    --Loop through the relevant tables
    for tables in
    (
        select table_name
        from user_tables
        where table_name in ('TABLE1', 'TABLE2')
        order by table_name
    ) loop
        --Create and execute update trigger.
        v_sql := replace(replace(replace(replace(c_sql_template
            , '#TABLE_NAME#', tables.table_name)
            , '#UPD_or_INS#', 'upd')
            , '#UPDATE_OR_INSERT#', 'update')
            , '#Inserted_or_Updated#', 'Updated');
        execute immediate v_sql;
        --Create and execute insert trigger.
        v_sql := replace(replace(replace(replace(c_sql_template
            , '#TABLE_NAME#', tables.table_name)
            , '#UPD_or_INS#', 'ins')
            , '#UPDATE_OR_INSERT#', 'insert')
            , '#Inserted_or_Updated#', 'Inserted');
        execute immediate v_sql;
    end loop;
end;
/

样品运行

现在您未更改的脚本将显示一些输出。 (我确实对脚本做了一些微不足道的更改,但只是为了使其可运行。)

SQL>    --Run this block first to enable printing.
SQL>    set serveroutput on;
SQL>    begin
  2             print_feedback.g_print_output := true;
  3     end;
  4     /

PL/SQL procedure successfully completed.

SQL> DECLARE
  2      v_user_id users.id%TYPE;
  3  BEGIN
  4      SELECT id INTO v_user_id FROM users WHERE username = 'system';
  5
  6      UPDATE table1
  7      SET col1 = 1,--value,
  8          updated_at = SYSDATE,
  9          updated_by = v_user_id
 10      WHERE 1=1;--some condition;
 11
 12      INSERT INTO table2 values(2/*val1*/, SYSDATE, v_user_id);
 13  END;
 14  /
Updated 2 rows in TABLE1
Inserted 1 row in TABLE2

PL/SQL procedure successfully completed.

SQL>

【讨论】:

我刚试过这个。为了让 sqlplus 将 dbms_output 转储到屏幕(set serveroutput on),需要进行一些修改,但仅此而已。很有趣。 虽然触发器可能会减慢 DML 操作,但我们可以在 SQL 脚本的开头创建它们,然后再删除它们。【参考方案2】:

我不确定是否有一些 oracle 参数或配置要更改,以便您的 PL/SQL 以您想要的方式工作,但您可以创建一个接受 DML 语句并运行该 DML 语句的过程。请参阅下面的示例,

DECLARE
    v_var VARCHAR2(10);
    PROCEDURE run_dml (p_dml VARCHAR2)
    IS
    BEGIN
        EXECUTE IMMEDIATE p_dml;
        DBMS_OUTPUT.PUT_LINE(p_dml);
        DBMS_OUTPUT.PUT_LINE(sql%rowcount||' rows '||REPLACE(LOWER(TRIM(SUBSTR(p_dml, 1, 6)))||'ed.', 'eed', 'ed'));
    END;
BEGIN
   v_var := 'hello too';
   run_dml(q'[INSERT INTO test1_log VALUES ('hello')]');
   run_dml(q'[DELETE FROM test1_log WHERE log1 = 'hello']');
   run_dml(q'[UPDATE test1_log SET log1 = 'hello1']');
   run_dml('INSERT INTO test1_log VALUES('''||v_var||''')');
END;
/

INSERT INTO test1_log VALUES ('hello')
1 rows inserted.
DELETE FROM test1_log WHERE log1 = 'hello'
1 rows deleted.
UPDATE test1_log SET log1 = 'hello1'
1 rows updated.
INSERT INTO test1_log VALUES('hello too')
1 rows inserted.

【讨论】:

如果你提出动态 SQL,你应该使用转义来避免重复 ' 所以它可能是:run_dml(q[' INSERT INTO test1 VALUES ('hello') ]') 是的,这是一个很好的建议,谢谢。我修改了上面的答案。 由于某种原因,INSERT 或 UPDATE 语句中的单引号导致解析错误:ORA-06550: line 8, column 6: PLS-00103: Encountered the symbol "[" when expecting one of the following: ... 啊,没关系。我在多行字符串的开头转置了单引号(q[' 而不是q'[)。 射击。实际上,这个解决方案不适用于我的情况,因为我需要在我的 SQL 语句中使用一个变量。【参考方案3】:

SQL*Plus 通过检查 OCI 返回状态来获取有关受影响的行数等的状态信息。 OCI 并不是我真正的领域,但我很确定在 PL/SQL 块的情况下,它拥有的唯一信息将是一个块已提交以及它是成功还是失败的事实,因为该块已提交给服务器作为一个单独的单元,并且调用接口中没有记录其中的每个步骤以及状态和受影响的行数的结构。根本没有任何机制可以捕获该信息。 (Oracle 实现这样的接口也是不明智的,因为在 PL/SQL 块中执行的语句数量可以任意大,例如,如果它对一百万行表执行循环。)

我想您也许可以在适当的粒度级别启用审计,然后在每次通话后查询DBA_AUDIT_TRAIL,并按时间戳、用户和终端过滤,以将报告限制为当前会话中最近的通话,但听起来好像与您正在寻找的东西有些距离。

【讨论】:

【参考方案4】:
spool "D:\test\test.txt"

-- YOUR ANONYMOUS PL/SQL Block here

spool off  

这将为您提供所需的输出,并且在不使用 DBMS_OUTPUT.PUT_LINE 的情况下,输出将位于指定的路径中

【讨论】:

这只是将输出捕获到本地文件中。在 OP 的示例中,它不会报告 table1 中更新或 table2 中插入的行数。

以上是关于如何在匿名 PL/SQL 块中自动显示所有 SQL 语句的输出的主要内容,如果未能解决你的问题,请参考以下文章

匿名 pl/sql 块中的声明顺序

我可以在调用同一过程后将 PL/SQL 过程放在匿名块中吗?

Oracle 匿名 PL/SQL 块中缺少关键字错误

在 DB2 PL/SQL 匿名块中声明局部变量和声明继续处理程序会导致错误?

匿名 PL/SQL 块检查异常

无法在 Oracle 匿名块中调用和执行 .sql 脚本文件