使用PL/SQL创建一个Excel文件(.xlsx)

13
关于我之前的问题Return the SQL Statement of an Explicit Cursor, 我使用DBMS_SQLUTL_FILE生成了一个Excel(.xls)文件,并传递了一个SYS_REFCURSOR(代码在上一个问题中)。 然而,我遇到的一个挑战是生成的文件超过25 MB。 我将通过电子邮件将其发送给商业用户,它肯定会占满他们的邮箱大小。 当我使用SQL Developer进行手动提取到xlsx时,它只会生成约4 MB的数据。
为了解决这个问题,通过PL/SQL是否可以做到以下几点?
  1. 使用较新版本的Excel (.xlsx) 生成文件以压缩文件大小
  2. 在传输之前压缩.xls文件
我还查看了此处的类似帖子Writing in ExcelSheet using UTL_FILE package in Oracle, 但如答案所述,它需要使用Java。 因此对我不适用。 另一篇文章Create an Excel Spreadsheet from a Oracle Database也使用了xls。 因此也不适用。
有什么想法吗?
Oracle版本:
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
PL/SQL Release 11.2.0.4.0 - Production
"CORE   11.2.0.4.0  Production"
TNS for Solaris: Version 11.2.0.4.0 - Production
NLSRTL Version 11.2.0.4.0 - Production

如果你关心文件大小的话,最好使用 CSV 格式? - Leo
嗨@Leo,是的,我现在正在运行一个.csv测试。但主要还是希望使用.xlsx,因为业务用户更喜欢这种格式。 - Migs Isip
@Leo,没有运气,.csv文件几乎相同大小,25 MB。 - Migs Isip
数据的实际大小是多少?因为在我的提到中,csv = txt with delimiters。 - Leo
@Leo,.xls文件大小为25.9 MB,.csv文件大小为25.4 MB。我还没有研究过使用分隔符的文本。我会看看是否能够创建一个程序来处理它。 - Migs Isip
显示剩余2条评论
3个回答

13

我看到了 Anton Scheffer 开发的一个叫做 as_xlsx 的包,使用 PL/SQL 创建 Excel 文件,它解决了我的问题。我还修改了一下,加入了工作表名称并允许将 SYS_REFCURSOR 作为参数,而不是我之前发布的帖子 (返回显式游标的 SQL 语句) 中所要求的 VARCHAR2

我在包规范中添加了该过程重载:

procedure query2sheet
( p_cur             IN OUT      SYS_REFCURSOR
, p_column_headers  boolean     := true
, p_directory       varchar2    := null
, p_filename        varchar2    := null
, p_sheet           pls_integer := null
, p_sheetname       varchar2    := null
);

我在过程重载的包体中添加了这个(注意:行注释是我修改的行):

procedure query2sheet
( p_cur IN OUT SYS_REFCURSOR
, p_column_headers boolean := true
, p_directory varchar2 := null
, p_filename  varchar2 := null
, p_sheet     pls_integer := null
, p_sheetname varchar2 := null
)
is
    t_sheet     pls_integer;
    t_c         integer;
    t_col_cnt   integer;
    t_desc_tab2 dbms_sql.desc_tab2;
    t_desc_tab  dbms_sql.desc_tab;
    d_tab       dbms_sql.date_table;
    n_tab       dbms_sql.number_table;
    v_tab       dbms_sql.varchar2_table;
    t_bulk_size pls_integer := 200;
    t_r         integer;
    t_cur_row   pls_integer;
    t_d         number;
begin
    -- Changed
    if p_sheetname is not null then
        new_sheet(p_sheetname);      
    else
        new_sheet;
    end if;
    -- End of Change
    --t_c := dbms_sql.open_cursor;                       
    --dbms_sql.parse( t_c, p_sql, dbms_sql.native );

    t_d := DBMS_SQL.TO_CURSOR_NUMBER(p_cur);

    --dbms_sql.describe_columns2( t_c, t_col_cnt, t_desc_tab );
    dbms_sql.describe_columns( t_d, t_col_cnt, t_desc_tab );

    for c in 1 .. t_col_cnt
    loop
        if p_column_headers
        then
        cell( c, 1, t_desc_tab( c ).col_name, p_sheet => t_sheet );
        end if;
        --dbms_output.put_line( t_desc_tab( c ).col_name || ' ' || t_desc_tab( c ).col_type );
        case
        when t_desc_tab( c ).col_type in ( 2, 100, 101 )
        then
            --dbms_sql.define_array( t_c, c, n_tab, t_bulk_size, 1 );
            dbms_sql.define_array( t_d, c, n_tab, t_bulk_size, 1 );
        when t_desc_tab( c ).col_type in ( 12, 178, 179, 180, 181 , 231 )
        then
            --dbms_sql.define_array( t_c, c, d_tab, t_bulk_size, 1 );
            dbms_sql.define_array( t_d, c, d_tab, t_bulk_size, 1 );
        when t_desc_tab( c ).col_type in ( 1, 8, 9, 96, 112 )
        then
            --dbms_sql.define_array( t_c, c, v_tab, t_bulk_size, 1 );
            dbms_sql.define_array( t_d, c, v_tab, t_bulk_size, 1 );
        else
            null;
        end case;
    end loop;
    --
    t_cur_row := case when p_column_headers then 2 else 1 end;
    t_sheet := nvl( p_sheet, workbook.sheets.count() );
    --
    --t_r := dbms_sql.execute( t_c );
    loop
        --t_r := dbms_sql.fetch_rows( t_c );
        t_r := dbms_sql.fetch_rows( t_d );
        if t_r > 0
        then
        for c in 1 .. t_col_cnt
        loop
            case
            when t_desc_tab( c ).col_type in ( 2, 100, 101 )
            then
                --dbms_sql.column_value( t_c, c, n_tab );
                dbms_sql.column_value( t_d, c, n_tab );
                for i in 0 .. t_r - 1
                loop
                if n_tab( i + n_tab.first() ) is not null
                then
                    cell( c, t_cur_row + i, n_tab( i + n_tab.first() ), p_sheet => t_sheet );
                end if;
                end loop;
                n_tab.delete;
            when t_desc_tab( c ).col_type in ( 12, 178, 179, 180, 181 , 231 )
            then
                --dbms_sql.column_value( t_c, c, d_tab );
                dbms_sql.column_value( t_d, c, d_tab );
                for i in 0 .. t_r - 1
                loop
                if d_tab( i + d_tab.first() ) is not null
                then
                    cell( c, t_cur_row + i, d_tab( i + d_tab.first() ), p_sheet => t_sheet );
                end if;
                end loop;
                d_tab.delete;
            when t_desc_tab( c ).col_type in ( 1, 8, 9, 96, 112 )
            then
                --dbms_sql.column_value( t_c, c, v_tab );
                dbms_sql.column_value( t_d, c, v_tab );
                for i in 0 .. t_r - 1
                loop
                if v_tab( i + v_tab.first() ) is not null
                then
                    cell( c, t_cur_row + i, v_tab( i + v_tab.first() ), p_sheet => t_sheet );
                end if;
                end loop;
                v_tab.delete;
            else
                null;
            end case;
        end loop;
        end if;
        exit when t_r != t_bulk_size;
        t_cur_row := t_cur_row + t_r;
    end loop;
    --dbms_sql.close_cursor( t_c );
    dbms_sql.close_cursor( t_d );
    if ( p_directory is not null and  p_filename is not null )
    then
        save( p_directory, p_filename );
    end if;
exception
when others
then
    --if dbms_sql.is_open( t_c )
    if dbms_sql.is_open( t_d )
    then
    --dbms_sql.close_cursor( t_c );
    dbms_sql.close_cursor( t_d );
    end if;
end query2sheet;

这是我的并发请求中创建文件的示例块:

Procedure EMP_ROSTER_REPORT (p_empno        per_all_people_f.employee_number%type                              
                           , p_bg_id        per_business_groups.business_group_id%type
                           , p_email_add    per_all_people_f.email_address%type)
is

    l_fh            UTL_FILE.FILE_TYPE;
    l_directory     VARCHAR2(30) := 'EXT_TAB_DATA';
    l_filename      VARCHAR2(100);
    emp_cur         SYS_REFCURSOR;
    l_message       varchar2(100);
    g_stage         varchar2(100);
    g_zipped_blob   blob;

    cursor  p_payroll_cur is
    select  payroll_id
        ,   payroll_name
        ,   business_group_id
    from    pay_all_payrolls_f
    where   business_group_id = p_bg_id;

BEGIN

    -----------------------------------
    g_stage := 'setting the filename';
    -----------------------------------

    l_filename := 'EMPLOYEE_ROSTER_REPORT_'||TO_CHAR(SYSDATE, 'DD-MON-YYYY-HHMISS');

    ------------------------------------------
    g_stage := 'Assigning Emp SysRefCursor';
    ------------------------------------------

    for i in p_payroll_cur loop

        OPEN emp_cur FOR
        SELECT  'extra long query here with parameters'
        from    table_a
        where   payroll_id = i.payroll_id;

        ----------------------------------------------------------
        g_stage := 'open Employee Cursor and write into the File';
        ----------------------------------------------------------

        as_xlsx.query2sheet( p_cur          => emp_cur            -- Uses Sys_RefCursor Instead of Dynamic SQL (Varchar2)
                           , p_sheetname    => i.payroll_name);   -- This is where we assign the Sheet Names         
        as_xlsx.freeze_pane( 1,1 );                               -- Freeze the topmost and rightmost pane in the Excel Sheet

    end loop;

    ------------------------------
    g_stage := 'Create the File';
    ------------------------------

    as_xlsx.save( l_directory , l_filename||'.xlsx');

END EMP_ROSTER_REPORT;

希望这能帮助到某些人!:)


当您创建XLSX文件时,它存储在哪里?存储在DB所在的远程机器上吗?如何使用UTL_MAIL发送此创建的文件? - sbrbot
@sbrbot 是的,它在远程机器上,那里有很多UTL_MAIL包可以发送文件作为附件。 - Migs Isip
我的最大问题是UTL_MAIL在附件大小方面非常有限(32K左右)。因此,我无法发送更大的文件,如果将附件保存在外部,我能否使用UTL_MAIL发送它? - sbrbot
这个软件包有哪些依赖关系?还是独立的? - ManuelJE
@ManuelJE 这是一个独立的软件包。 - Migs Isip

1

在Oracle数据库中,xlsxlsx文件没有支持。

你可以做的是创建一个CSV(逗号分隔)文件,然后使用自定义的Java存储过程将其压缩成zip文件,该过程使用java.util.zip或使用UTL_COMPRESS的PL/SQL过程。


嗨@gurwinder-singh,我找到了一个本地的PL/SQL包来解决这个需求。请看下面我的回答。谢谢! - Migs Isip

0

如果您需要生成XLSX文件并将其发送给业务用户,那么这似乎更像是一些DB外部服务的工作。您应该准备一个返回报告内容的ref cursor的过程,然后设置一些消耗数据生成PDF或XLSX并发送的服务。

如果您无法在DB外部执行此操作,则仍然可以在Oracle内部使用Java。您可以创建一个Java过程来创建XLSX。这里是Java创建Excel文件的示例。但是创建复杂的Java过程不是最佳解决方案,可能需要在DB服务器上安装一些JAR文件,因此我会创建一个返回带有数据的ref cursor的过程和创建文件并将其发送到DB外部的小程序。


嗨@kacper,是的,那是我在上一篇文章中采取的方法http://stackoverflow.com/questions/41281665/return-the-sql-statement-of-an-explicit-cursor,但是,我遇到了文件大小和行限制的挑战。我正在通过Oracle EBS中的Concurrent Program发送这些报告。 - Migs Isip
嗨@kacper,我能够找到一个解决方案,请看一下我在这个问题中的答案。我还对指定的程序包进行了一些修改,并成功地将SYS_REFCURSOR作为参数传递。 - Migs Isip

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