No matter how user-friendly the expensive BI tool ( or the dirt-cheap Application Express for that matter), sometimes, only Excel will do for your users.
No, you can’t get away with giving them a CSV and telling them to open it in Excel.
Nor can you avail yourself of SQLDeveloper’s ability to export result sets in XLSX format.
They must have their report in Excel, ready and waiting first thing in the morning.
This leads you to ask the question, “How do you generate Excel Spreadsheets from PL/SQL ?”
The answer is, of course, that you fire up your favourite search engine and hope that it’ll land on this work of genius by Anton Scheffer.
Incidentally, if you use Morten Braten’s Alexandria PL/SQL Library, you will already have a version of Anton’s package in the form of XLSX_BUILDER_PKG.
In the example that follows, the report we need to produce is based on the HR schema.
As we’re producing a report in Excel, we may as well utilise the main advantage of an xlsx file over a delimited text file and create multiple worksheets…
The Report
The report will run on an Oracle 18c XE database (mainly because I’ve got one handy with the HR schema already installed).
Regular readers will know that use Ubuntu at home. This means that LibreOffice Calc – rather than Excel – is my spreadsheet of choice. That said, I’ve checked that the spreadsheet renders correctly in both Calc, and Excel itself, thanks to the freely available online version provided by Microsoft.
It’s worth noting that, although Calc can read xlsx formatted files, there are some minor differences in how they are rendered. This may also be the case with a desktop version of Excel as compared to the Web version that I’m using here.
Our report will consist of three worksheets :
- a title – including the parameter value used to generate the report
- a summary – a listing of total salary by job within the Department
- a detail tab – listing of all of the employees in the Department
Before we start, we need a directory to write the xlsx file out to.
In this instance, we have a directory object called APP_DIR on which we have write privileges.
create or replace directory app_dir as '/opt/oracle/appdir'; grant read, write on directory app_dir to hr;
We also need to deploy the package, which we’ve downloaded from the link above.
Note that Anton has included a number of coding examples in the header comments, which provide some guidance as to how the package works.
The code I’ve written to generate the report is in a package called salary_by_dept_report. The header looks like this :
create or replace package salary_by_dept_rpt as GC_DIR constant all_directories.directory_name%type := 'APP_DIR'; -- Package members to generate the individual worksheets in the report workbook. -- These would normally be private. procedure title_sheet( i_dept in departments.department_id%type); procedure job_summary_sheet(i_dept in departments.department_id%type); procedure detail_sheet(i_dept in departments.department_id%type); -- Main entry point - this is the procedure to create the report itself. procedure run_report( i_dept in departments.department_id%type); end salary_by_dept_rpt; /
Let’s take a look at each of the package members in turn.
The Main Procedure ( run_report)
The code is :
procedure run_report( i_dept in departments.department_id%type) is v_fname varchar2(250); begin v_fname := 'salary_report_dept_'||i_dept||'.xlsx'; -- Ensure we have nothing hanging around from a previous run in this session as_xlsx.clear_workbook; -- For the query2sheet call to work where it's not the first sheet or -- any other sheet preceeding it is not being created using this call, -- you need to define all of the sheets upfront. -- Otherwise, it will only write a single integer to that sheet. as_xlsx.new_sheet('Title'); as_xlsx.new_sheet('Summary by Job'); as_xlsx.new_sheet('Department Employees'); -- Call each procedure in turn to populate the worksheets title_sheet( i_dept); job_summary_sheet(i_dept); detail_sheet(i_dept); as_xlsx.save( GC_DIR, v_fname); end run_report;
Once we’ve dealt with the admin of determining the name of the file we want to create, we come to our first interaction with AS_XLSX :
as_xlsx.clear_workbook
This ensures that we don’t have any settings hanging around from a previous execution of this package in our session.
The next step is to create all of the worksheets we’re going to populate for this report.
This is because we’ll be populating the last of these ( Department Employees ) using as_xlsx.query2sheet. We’ll look at that call in more detail a little later.
I’m mentioning it now because, if you use this call after populating any workbooks not using it, it does not seem to work as expected, as you can see from the comments in the procedure.
To create a new worksheet :
as_xlsx.new_sheet("My New Worksheet");
Naming the worksheet, as I’ve done here, is optional.
In calls to AS_XLSX, worksheets are referred to by their number – in this case, Title is 1, “Summary by Job” is 2 and “Department Employees” is 3.
The final part of this procedure is fairly self-explainatory – i.e. call the procedures to populate their respective worksheets before saving the entire file to disc by calling :
as_xlsx.save(directory object, filename);
Generating the Title sheet
The code for this procedure is :
procedure title_sheet( i_dept in departments.department_id%type) is v_dept_name varchar2(250); begin -- the value of width appears to be 1/10th of the default width of a column. -- Default is 2.47 cm (24.7 mm), which makes 1 2.47 mm -- For reference, the title of the report is 27 characters as_xlsx.set_column_width( p_col => 1, p_width => 25, p_sheet => 1); -- Print the Report Title in bold in cell A1 as_xlsx.cell( p_col => 1, p_row => 1, p_sheet => 1, p_value => 'Salary by Department Report', p_fontid => as_xlsx.get_font('Arial', p_bold => true), p_numFmtId => 0); -- Detail the parameters used to generate the report -- Section label in A2 as_xlsx.cell( 1,2, 'Parameters :', p_sheet => 1, p_numFmtId => 0); -- Parameter Name in A3 as_xlsx.cell(1,3, 'Department', p_sheet => 1, p_numFmtId => 0); -- Parameter value and Department name in B3 select department_id||' ( '||department_name||')' into v_dept_name from departments where department_id = i_dept; as_xlsx.cell(2,3, v_dept_name, p_sheet => 1, p_numFmtId => 0); -- Run Date label in A4 as_xlsx.cell( 1, 4, 'Report Run Date', p_sheet => 1, p_numFmtId => 0); -- Run Date in B4 as_xlsx.cell( 2, 4, sysdate, p_sheet => 1, p_numfmtid => as_xlsx.get_numfmt('dd/mm/yyyy h:mm')); end title_sheet;
We begin by setting the column width.
Note that columns are referred by by their numeral position in the worksheet ( A is 1, B is 2 etc).
Next, we finally get to put some data directly into cells in the worksheet.
as_xlsx.cell(
p_col => 1,
p_row => 1,
p_sheet => 1,
p_value => 'Salary by Department Report',
p_fontid => as_xlsx.get_font('Arial', p_bold => true),
p_numFmtId => 0);
The p_numFmtId is specified here for the purposes of consistency.
This is because, if your workbook contains multiple worksheets, then calls to this procedure in second and subsequent worksheets will cause the following error to be raised if this parameter is not specified :
ORA-1403 no data found at line 724 of AS_XLSX.
Once the workbook is created, the Title worksheet looks like this :
Summary Sheet
Here we’re going to fetch some data in a for loop and apply some conditional formatting. There are even a couple of formulas thrown in for good measure :
procedure job_summary_sheet(i_dept in departments.department_id%type) is v_row pls_integer := 1; -- need to account for the header row v_tot_row pls_integer; v_formula varchar2(4000); v_ave_sal number; v_sal_mid_range number; v_rgb varchar2(25); begin -- Set the column widths to allow for the header lengths as_xlsx.set_column_width( p_col => 1, p_width => 15, p_sheet => 2); as_xlsx.set_column_width( p_col => 2, p_width => 15, p_sheet => 2); as_xlsx.set_column_width( p_col => 3, p_width => 15, p_sheet => 2); as_xlsx.set_column_width( p_col => 4, p_width => 15, p_sheet => 2); -- Set the column headings -- NOTE - for second and subsequent sheets, we need to specify the number format or else we'll get -- ORA-1403 no data found at line 724 of AS_XLSX. -- Specifying the sheet number as it seems sensible to do so at this point ! as_xlsx.cell(1,1, 'Department Name', p_sheet => 2, p_numFmtId => 0); as_xlsx.cell(2,1, 'Job Title', p_sheet => 2, p_numFmtId => 0); as_xlsx.cell(3,1, 'No of Employees', p_sheet => 2, p_numFmtId => 0); as_xlsx.cell(4,1, 'Total Salary', p_sheet => 2, p_numFmtId => 0); -- Retrive the data rows and populate the relevant cells, applying any -- conditional formatting. for r_jobs in ( select dept.department_name, job.job_title, count(emp.employee_id) as employees, sum(emp.salary) as total_salary, job.min_salary, job.max_salary from employees emp inner join departments dept on dept.department_id = emp.department_id inner join jobs job on job.job_id = emp.job_id where dept.department_id = i_dept group by dept.department_name, job.job_title, job.min_salary, job.max_salary order by job.max_salary desc, job.job_title) loop v_row := v_row + 1; as_xlsx.cell(1, v_row, r_jobs.department_name, p_numFmtId => 0, p_sheet => 2); as_xlsx.cell(2, v_row, r_jobs.job_title, p_numFmtId => 0, p_sheet => 2); as_xlsx.cell(3, v_row, r_jobs.employees, p_numFmtId => 0, p_sheet => 2); -- If the average salary is lower than the mid-range for this job then -- display in red because someone needs a pay rise ! -- Otherwise, display in green v_ave_sal := r_jobs.total_salary / r_jobs.employees; v_sal_mid_range := r_jobs.min_salary + ((r_jobs.max_salary - r_jobs.min_salary) / 2); if v_ave_sal > v_sal_mid_range then v_rgb := '009200'; -- green else v_rgb := 'ff0000'; -- red end if; as_xlsx.cell(4, v_row, r_jobs.total_salary, p_fontId => as_xlsx.get_font( 'Arial', p_rgb => v_rgb ), p_numFmtId => 0, p_sheet => 2); end loop; if v_row = 1 then raise_application_error(-20001, 'No data returned from query'); end if; v_tot_row := v_row + 1; as_xlsx.cell(1, v_tot_row, 'Total', p_numFmtID => 0, p_sheet => 2); -- Number of employees v_formula := '=SUM(C2:C'||v_row||')'; -- Number format requried to avoid ORA-1403 as_xlsx.num_formula( 3, v_tot_row, v_formula, p_numFmtID => 0, p_sheet => 2); -- Total Salary v_formula := '=SUM( D2:D'||v_row||')'; as_xlsx.num_formula( 4, v_tot_row, v_formula, p_numFmtID => 0, p_sheet => 2); end job_summary_sheet;
Whilst you may prefer to to do most of your data wrangling in SQL, it’s worth knowing that you can throw in an Excel formula should you need to.
In this example I’ve used the call :
as_xlsx.num_formula(column, row, formula, numberFormat, sheet no);
Detail Sheet – Data from a Query
Here, we get to make use of another recent addition to the package. Whilst it’s long had the ability to generate data in a worksheet directly from a SQL query, the package can now accept a Ref Cursor for this purpose :
procedure detail_sheet(i_dept in departments.department_id%type) is v_rc sys_refcursor; begin -- Format the column headings in the ref cursor query open v_rc for select emp.employee_id as "Employee ID", emp.first_name as "First Name", emp.last_name as "Last Name", job.job_title as "Job Title", emp.salary as "Salary" from employees emp inner join jobs job on job.job_id = emp.job_id where emp.department_id = i_dept; as_xlsx.query2sheet( v_rc, p_sheet => 3); -- Any formatting gets overwritten by the query2sheet so -- we need to do it afterwards as_xlsx.set_column_width( p_col => 1, p_width => 15, p_sheet => 3); as_xlsx.set_column_width( p_col => 2, p_width => 15, p_sheet => 3); as_xlsx.set_column_width( p_col => 3, p_width => 15, p_sheet => 3); as_xlsx.set_column_width( p_col => 4, p_width => 15, p_sheet => 3); -- Leave salary as is end detail_sheet;
Here, I’ve taken the opportunity to format the column headings in the query itself.
The sheet looks like this :
Some Observations on Performance
In the current Office365 version, the maximum number of rows an Excel Worksheet can hold is just over a million ( 1,048,576 to be precise). Therefore, this approach is not really suitable for large datasets.
It’s probably not that surprising therefore, that I haven’t experienced too many issues with performance when using this package.
I have occasionally found significant runtime differences between environments for the same report with the same data.
I suspect this may have been due to the resources allocated to the PGA on the respective environments but I have no specific evidence to support this.
I thought it was worth mentioning however, because a report that ran in about 5 minutes on one environment took several hours on another. If you come across something similar, that may be one area that would bear investigating.
Source Code
I’ve uploaded a copy of the version of AS_XLSX I used in this post, together with the source for the salary_by_dept_rpt package to this github repo.
And finally…
I’d just like to say a big “thank you” to Anton, who, through the medium of this package, has made a number of my users extremely happy over the years !
3 / 1 / 5 Регистрация: 13.12.2013 Сообщений: 68 |
|
1 |
|
22.03.2017, 17:41. Показов 10211. Ответов 4
Добрый день! код | название | цена
0 |
Programming Эксперт 94731 / 64177 / 26122 Регистрация: 12.04.2006 Сообщений: 116,782 |
22.03.2017, 17:41 |
4 |
761 / 662 / 195 Регистрация: 24.11.2015 Сообщений: 2,158 |
|
22.03.2017, 18:21 |
2 |
Можно поискать в интернете пакеты AS_XLSX и AS_READ_XLSX. Первый пишет, а второй читает файл *.xlsx . Сделать можно не все, но достаточно много. Есть некоторые баги (если очень большой набор данных) поэтому я переделывал (один из пакетов точно) с varchar2 на clob. Но для небольшого количества столбцов работают. Добавлено через 1 минуту
0 |
93 / 71 / 33 Регистрация: 02.08.2015 Сообщений: 202 |
|
23.03.2017, 05:56 |
3 |
Здравствуйте! Можете посмотреть объекты:
0 |
761 / 662 / 195 Регистрация: 24.11.2015 Сообщений: 2,158 |
|
23.03.2017, 10:47 |
4 |
Подключение к Oracle — ADODB.Connection Я так понимаю, что Вы на клиентском компьютере запускаете Вашу утилиту и качаете данные из одной бочки в другую (из Oracle в Excel). (Возможно, понимаю неверно.)
0 |
Бакалым 3 / 1 / 5 Регистрация: 13.12.2013 Сообщений: 68 |
||||
23.03.2017, 17:43 [ТС] |
5 |
|||
Решено. нашел в интернете пакет as_xlsx на базе него написал процедуру:
0 |
2004 г. Oracle Forms. Экспорт данных в Excel
|