2011/09/27

エクセルファイルのバッチ出力 その5 (Excel file processing, Part 5)

検証環境の記載を忘れていました。

検証環境はVM上に構築したDBに対して、実機上のクライアントアプリから問い合わせを行う構成です。
Figure 10: Machine environment

【DBサーバ】
VM: Virtual Box 4.0.12
CPU: 2コア
メモリ: 3GB
OS: Windows 2003 Server
DB: Oracle 11g R2 (11.2.0.1)

【クライアント】
CPU: Intel Core i7 920 (2.6GHz)
メモリ: 12GB
HDD: SSDを使用
OS: Windows 7 Ult.

【サンプルアプリ】
.NET: .NET Framework 4
言語・ツール: Visual Studio 2010 / C#
DB接続: OleDbConnection

計測はVisual Studio 2010のデバッグモードで行いました。
※デバッグモードと、Visual Studioを介さない素のアプリ実行との実測差異が無かった為、簡易ログ出力も兼ねて、デバッグモードで実行しています。実際のプロジェクトでは素のアプリで都度、計測を行ってください。本ブログでの検証はあくまでもサンプルです。



[Summary]
I forgot to put the machine environment description.  Please see Figure 1.


[DB Server (Virtual Machine)]
VM: Virtual Box 4.0.12
CPU: 2 cores
Memory: 3GB
OS: Windows 2003 Server
DB: Oracle 11g R2 (11.2.0.1)


[Client Machine (Physical Machine)]
CPU: Intel Core i7 920 (2.6GHz)
Memory: 12GB
HDD: SSD
OS: Windows 7 Ult.


[Sample Application]
.NET: .NET Framework 4
Language / Tool: Visual Studio 2010 / C#
DB Connection: OleDbConnection


Performance test is held on debug mode of Visual Studio.
(*) There are few performance difference between debug mode and native application running




2011/09/26

エクセルファイルのバッチ出力 その4 (Excel file processing, Part 4)

エクセルへのデータ転送が高速化された結果、ファイルのオープンおよび保存にかかる時間が無視できなくなりました。
参考まで、前回までの検証で使用したテンプレート(Fat file)に設定されていたスパークラインやコメントを取り除き、軽量化したテンプレート(Simple file)での計測結果は以下の通りです。
Figure 9: Light template file is processed faster.

1ファイルあたりの所要時間は0.30秒から0.22秒に短縮されています。テンプレートの軽量化には一定の効果(今回は約30%)があることが確認できます。


[summary]
Now the major events are save and open file.  To reduce these events, I removed the Spark Line and cell comments on the template sheet.
The results are shown in Figure 9.  Fat file is the ordinary template file (sample template of previous post).  Simple file is the template file without Spark Line and cell comments.
Total time is reduced from 0.30 sec to 0.22 sec (per file).

Complicated templates are not better than simple ones always.

2011/09/21

Windows 8でBI Publisherを使用する (BI Publisher on Windows 8)


Windows8のDeveloper Previewがダウンロード可能になりました。
現時点で検証を行う意味はほとんどありませんが、せっかくですのでWindows8のInternet Explorer 10でOracle BI Publisherを使用してみました。

【結論】
  • Metroスタイルでの表示はできない。MetroスタイルではFlashも非対応であるため、そもそも想定される動作環境の対象外です。
  • デスクトップ表示のIEで互換モードであれば、Flash Playerプラグインのインストールも含め、一通り動作します。


ログイン画面まではMetroスタイルでも表示されます。
Figure 1: BI Publisher Sign-in form on Metro style

ログイン後、カタログ画面は表示できません。この時点でMetroスタイルでの検証は終了してしまいました。
Figure 2: Catalog doesn't appear on Metro style

デスクトップモードでのログイン画面、データモデルの編集、レポートの表示などは従来通りの動作です。
Figure 3: Sign-in form on desktop mode

Figure 4: Data model design

Figure 5: Report view


インタラクティブレポートも動作しました。
Figure 6: Designing Interactive Report

Figure 7: Interactive report view


実用性の薄い検証でしたが、以上、参考まで掲載します。



[Summary]
Is BI Publisher available on Windows 8 (Internet Explorer 10)?  It seems BI Publisher works well on desktop mode IE, but you need to set compatibility view.
There was no obstacles on Flash Player plug-in installation.


Unfortunately, Metro style IE does not support Flash Player, so that BI Publisher cannot run on it.
Please see the figures above.  You can design and view reports.  Interactive reports also works.


2011/09/20

エクセルファイルのバッチ出力 その3 (Excel file processing, Part 3)

エクセルへのデータ展開を高速化する方法についてはマイクロソフト社のナレッジに説明されています。
Visual C# 2005 または Visual C# .NET を使用してデータを Excel ブックに転送する方法:


具体的には、データを各セルに1つ1つ設定するのではなく、データを配列に格納した上で、配列をエクセルのレンジに渡す処理を実装します。この実装によりエクセルとの通信回数が減り、処理時間が短縮されます。
サンプルのソースは以下の通りです。

List 5:

public void Extract(string sql, string SheetName)
{
    const int XL_WHOLE = 1;
    const int XL_DOWN = -4121;
    const string START_TAG = "$start";

    int colCount;
    List<object[]> lst = new List<object[]>();
    DateTime lapTime;

    lapTime = DateTime.Now;
    OleDbDataReader rst = GetRecordset(sql);    // get recordset
    colCount = rst.FieldCount;
    Debug.WriteLine("[query] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    while (rst.Read())                          // output loop
    {
        object[] ColValues = new Object[rst.FieldCount];
        for (int idxCol = 0; idxCol < rst.FieldCount; idxCol++)
            ColValues[idxCol] = rst.GetValue(idxCol);
        lst.Add(ColValues);
    }
    Debug.WriteLine("[set row val] " + (DateTime.Now - lapTime).ToString());
    rst.Close();

    // convert list => array
    lapTime = DateTime.Now;
    object[,] Values2Copy = new Object[lst.Count, colCount];
    for (int idxRow = 0; idxRow < lst.Count; idxRow++)
        for (int idxCol = 0; idxCol < colCount; idxCol++)
            Values2Copy[idxRow, idxCol] = lst[idxRow][idxCol];
    Debug.WriteLine("[list to array] " + (DateTime.Now - lapTime).ToString());

    // copy array into excel sheet.
    lapTime = DateTime.Now;
    xSheet = xBook.Worksheets[SheetName];       // activate start position
    Debug.WriteLine("[activate sheet] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    xSheet.Cells.Find(What: START_TAG, LookAt: XL_WHOLE).Activate();
    Debug.WriteLine("[find $start] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    int idxStart = xApplication.ActiveCell.Row;
    xSheet.Rows[xApplication.ActiveCell.Row].Copy();
    xSheet.Rows[(idxStart + 1).ToString() + ":" + (idxStart + lst.Count-1).ToString()].Insert(Shift: XL_DOWN);
    Debug.WriteLine("[insert row] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    Range r = xApplication.ActiveCell;
    r = r.get_Resize(lst.Count, colCount);
    r.set_Value(Missing.Value, Values2Copy);
    Debug.WriteLine("[copy into range] " + (DateTime.Now - lapTime).ToString());
}


実行時にレコードセットの列数と行数を動的に判断させるため、上記のソースでは一度データをListに関連付けたObjectに格納し、データをすべて抽出した後、Listの内容を配列に代入します。

上記サンプルの実行結果(経過時間)は以下の通りです。

Figure 7: Events

データ設定部分のイベント(extract)が大幅に低減(1.24秒→0.04秒)していることが確認できます。全体(1ファイルあたり)の処理時間も1.5秒から0.3秒に短縮されました。帳票出力のバッチとしては遅いといえますが、Excelオートメーションを利用する場合、この程度を目安にする必要があるといえます。
※なお、大量データを1つのシートに保存するようなケースでは、ファイルのオープン/クローズに要する時間の割合が低下するため、処理効率は向上します。

DLL処理部の内訳は以下の通りです。※時間の縮尺が異なります。
Figure 8: Events in DLL

エクセルオブジェクトの操作の操作に関わる部分で処理時間を要していることが確認できます。


[Summary]
To speed up Excel operation, please refer to the following Microsoft Knowledge Base.


How to transfer data to an Excel workbook by using Visual C# 2005 or Visual C# .NET
http://support.microsoft.com/kb/306023/en-us


The program transfer two dimension array to a range of multiple cells at one time.  This technique runs faster than passing data cell by cell.
List 5 uses List to store the fetched rows.  This is because the sample program determines the number of rows and columns in runtime.


Figure 7 shows elapsed time per file.  The main part (extract) is reduced from 1.24 sec to 0.04 sec.  So, the total elapsed time is 1.5 sec to 0.3 sec per each.  (Still it is slow, though)


Inside "extract", please see Figure 8.  (*) Note that time scale is different.

2011/09/13

エクセルファイルのバッチ出力 その2 (Excel file processing, Part 2)

前回のサンプルプログラムの性能を確認します。テストプログラム側を修正し、100ファイルを出力する際の処理時間を計測します。

List 3: Test GUI
private void button1_Click(object sender, EventArgs e)
{

    DefaultTraceListener dtl = (DefaultTraceListener)Debug.Listeners["Default"];
    dtl.LogFileName = @"c:\Sample.log";


    Util.Ora2Excel x = new Util.Ora2Excel();

    x.OpenDb(txtUser.Text, txtPassword.Text, txtConnString.Text);
    x.InitExcel();

    DateTime lapTime;
    DateTime startTime = DateTime.Now;

    for (int i = 0; i < 100; i++)
    {
        lapTime = DateTime.Now;
        x.OpenBook(txtTemplate.Text);
        Debug.WriteLine("[file open] " + (DateTime.Now - lapTime).ToString());

        lapTime = DateTime.Now;
        x.Extract(txtSql.Text, txtSheet.Text);
        Debug.WriteLine("[extract] " + (DateTime.Now - lapTime).ToString());

        lapTime = DateTime.Now;
        x.SaveBook(@"d:\result_emp" + i.ToString() + ".xlsx");
        Debug.WriteLine("[save] " + (DateTime.Now - lapTime).ToString());

        lapTime = DateTime.Now;
        x.CloseBook();
        Debug.WriteLine("[close] " + (DateTime.Now - lapTime).ToString());
    }

    Debug.WriteLine("[total] " + (DateTime.Now - startTime).ToString());

    x.QuitExcel();
    x.CloseDb();
}
上記の処理結果ログを集計し、1ファイルあたりの平均処理時間を算出した結果は以下の通りです。最も長い処理イベントはデータをエクセルシートに展開する部分であることがわかります。
Figure 5: Events

エクセルシートに展開する部分についてより詳細を確認するため、DLL内の処理を以下のように修正し、再度計測を行います。

List 4:
public void Extract(string sql, string SheetName)
{
    const int XL_WHOLE = 1;
    const int XL_DOWN = -4121;
    const string START_TAG = "$start";

    int idxRow;
    DateTime lapTime;

    lapTime = DateTime.Now;
    OleDbDataReader rst = GetRecordset(sql);    // get recordset
    Debug.WriteLine("[query] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    xSheet = xBook.Worksheets[SheetName];       // activate start position
    Debug.WriteLine("[activate sheet] " + (DateTime.Now - lapTime).ToString());

    lapTime = DateTime.Now;
    xSheet.Cells.Find(What: START_TAG, LookAt: XL_WHOLE).Activate();
    Debug.WriteLine("[find $start] " + (DateTime.Now - lapTime).ToString());

    idxRow = xApplication.ActiveCell.Row;
            
    while (rst.Read())                          // output loop
    {
        lapTime = DateTime.Now;
        xSheet.Rows[idxRow].Copy();
        xSheet.Rows[idxRow+1].Insert(Shift: XL_DOWN);
        Debug.WriteLine("[insert row] " + (DateTime.Now - lapTime).ToString());

        lapTime = DateTime.Now;
        for (int idxCol = 0; idxCol < rst.FieldCount; idxCol++)
            xSheet.Cells[idxRow,idxCol+1].value = rst.GetValue(idxCol);
        Debug.WriteLine("[set cell val] " + (DateTime.Now - lapTime).ToString());

        idxRow++;
    }
    rst.Close();
}

結果は以下の通りです。セルへのデータ設定と行のインサート処理の占める割合が大きいことがわかります。
Figure 6: Events in DLL

今回のケースでは、10行程度の出力で、1ファイルあたり約1.5秒を要することが確認できます。
次回はデータをエクセルに出力する部分のソースを変更し、性能の改善を図ります。


[Summary]

The sample program shown in List 3 outputs 100 excel files (The original source is in the last post). This source writes down the lap time to log file.

Figure 5 shows that the longest event is extracting the data onto Excel worksheet.  Extracting takes about 1.2 second per a file.
To see more detailed log, see List 4 and Figure 6. You see that the cell and row manipulations take long time.

In the next post, these manipulations will be improved.

2011/09/08

エクセルファイルのバッチ出力 その1 (Excel file processing, Part 1)


業種を問わず、業務系のシステムではほぼ確実にエクセル形式での帳票出力は要件に挙がります。
今回から数回に渡り、バッチ処理でエクセルファイルを出力するサンプルについて検討します。

【概要】
全体の概要は以下の通りです。
Figure 1: Overall image


エクセルのテンプレートファイルをあらかじめ用意します。実行時にパラメータとして以下の3点を指定し、SQLによって抽出されたデータをテンプレート上に展開した後、保存する仕様を想定します。
  • テンプレートファイルの名前 
  • 保存するファイル名 
  • SQLスクリプト 


【環境】
サンプルはC#で実装します。そのほかの環境は以下の通りです。
  • .Net Framework: .Net Framework4
  • Excel: Excel2010
  • データベース:Oracle Database 11gR2 (11.2.0.1)
  • DB接続: OleDbConnection


【ソース(本体部)】
ソースの例を以下に示します。データの抽出、およびエクセルへの出力をDLLとして定義します。
この例では、レコードセットをMoveNextで移動する毎に、テンプレートの行をコピーおよび挿入し、セル毎にデータを挿入します。

List 1: Main logic


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.OleDb;
using Microsoft.Office.Interop.Excel;
using System.Reflection;
using System.Collections;


namespace Util
{
    public class Ora2Excel
    {
        // db objects //////////
        private OleDbConnection cnn;
        private OleDbCommand cmd;
        
        // excel objects //////////
        private Application xApplication;
        private _Workbook xBook;
        private _Worksheet xSheet;


        // ///////////////////////////////////////////////////////////
        public void OpenDb(string UserId, string Password, string HostString)
        {
            cnn = new OleDbConnection("Provider=OraOLEDB.Oracle.1;Data Source=" + HostString + ";User ID=" + UserId + ";Password=" + Password);
            cnn.Open();
            cmd = cnn.CreateCommand();
        }


        // ///////////////////////////////////////////////////////////
        public void CloseDb()
        {
            cmd.Dispose();
            cnn.Close();
            cnn.Dispose();
        }


        // ///////////////////////////////////////////////////////////
        public OleDbDataReader GetRecordset(string sql)
        {
            cmd.CommandText = sql;
            return cmd.ExecuteReader();
        }


        // ///////////////////////////////////////////////////////////
        public void InitExcel()
        {
            xApplication = new Application();
            xBook = null;
            xSheet = null;
        }


        // ///////////////////////////////////////////////////////////
        public void QuitExcel()
        {
            if (xBook != null)
                xBook.Close(false);

            xApplication.Quit();
        }


        // ///////////////////////////////////////////////////////////
        public void OpenBook(string FileName)
        {
            if (FileName == "")
                xBook = xApplication.Workbooks.Add();
            else
                xBook = xApplication.Workbooks.Open(FileName);

            xSheet = xBook.ActiveSheet;
        }


        // ///////////////////////////////////////////////////////////
        public void SaveBook(string FileName)
        {
            xBook.SaveAs(FileName);
        }


        // ///////////////////////////////////////////////////////////
        public void CloseBook()
        {
            if (xBook != null)
            {
                xBook.Close(false);
                xBook = null;
            }
        }

        
        // ///////////////////////////////////////////////////////////
        public void Extract(string sql, string SheetName)
        {
            const int XL_WHOLE = 1;
            const int XL_DOWN = -4121;
            const string START_TAG = "$start";

            int idxRow;
            OleDbDataReader rst = GetRecordset(sql);    // get recordset

            xSheet = xBook.Worksheets[SheetName];       // activate start position
            xSheet.Cells.Find(What: START_TAG, LookAt: XL_WHOLE).Activate();
            idxRow = xApplication.ActiveCell.Row;
            
            while (rst.Read())                          // output loop
            { 
                xSheet.Rows[idxRow].Copy();
                xSheet.Rows[idxRow+1].Insert(Shift: XL_DOWN);

                for (int idxCol = 0; idxCol < rst.FieldCount; idxCol++)
                    xSheet.Cells[idxRow,idxCol+1].value = rst.GetValue(idxCol);

                idxRow++;
            }
            rst.Close();
        }
    }
}


【テスト用ソース(GUI部)】
上記のDLLを使用する側(今回の例ではGUI)の例を以下に示します。
Figure 2: Test GUI

List 2: Test GUI


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Util;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Util.Ora2Excel x = new Util.Ora2Excel();

            x.OpenDb(txtUser.Text, txtPassword.Text, txtConnString.Text);
            x.InitExcel();
            x.OpenBook(txtTemplate.Text);

            x.Extract(txtSql.Text, txtSheet.Text);
            x.SaveBook(txtSaveAs.Text);
            x.CloseExcel();
            x.CloseDb();
        }
    }
}




【テンプレートファイル】
テンプレートファイルの例を示します。ヘッダおよび書式、数式などを設定します(詳細はダウンロードしたファイルの内容を参照して下さい)。
データを出力する開始点に"$start"を入力します。
Figure 3: Template file

出力結果のイメージは以下の通りです。
Figure 4: Result


次回からは性能検証、および性能改善の方策を検討します。


エクセルのテンプレートファイル、結果ファイルはここからダウンロードできます。
※ファイルをダウンロード後、手元の環境でコンパイルを行う場合には、Excelへの参照設定、およびOracle Clientの設定を適宜調整する必要があります。
※上記のソースはサンプルです。例外処理などは除外しています。
※上記のソースおよびファイルを使用したことによる一切の結果について、作成者は責任を負いません。



[Summary]
In most of cases, business application system cannot ignore Excel.
This post explains the sample program to produce Excel format files.


[Outline]
Overall image is shown in Figure 1.  What you need in run time is a template file (ordinary Excel file) and parameters below:
  • Name of the template file
  • Name of the file to save
  • SQL script

[Environment]
Source is in C#. Environment are as follows:
  • .Net Framework: .Net Framework4
  • Excel: Excel2010
  • Database: Oracle Database 11gR2 (11.2.0.1)
  • DB connection: OleDbConnection

[Source (main DLL part)]
Please see List 1.  The DLL retrieves rows from database, and set the data onto workbook cell by cell.


[Test source (GUI part)]
Please see Figure 2 and List 2.  This example is simple Windows form. On the button click event, it calls the DLL above.


[Template file]
Please see Figure 3 (Template file) and 4 (Output).  In the template, you set the report header, number format, formula and so on.  The start position, the top-left position, of data area is defined by "$start".  Please refer to the sample file for details.


The next post will explain performance issue.


The  template files are available here.
(*) To compile the source in your own environment, you may need set reference setting for Excel components.  Also, modify the DB connection part to fit your environment.
(*) The source code and other files are written as example. Exception handlings are all removed.
(*) Please use these files at your own risk.