DOBON.NET DOBON.NETプログラミング掲示板過去ログ

Excel取り込みのスピードを上げる

環境/言語:[WindowsXP,VB.NET2005]
分類:[.NET]

お世話になります
WinXP、VB.NET2005、Excel2003と言う環境です
取り込み元のデータが5000件や6000件等多くなると取り込みに時間がかかったり接続エラーになったりするので、
どうにか取り込みスピードを早くしたいと思っています
以下が現在の内容です
途中にREAD_EXCEL_ERRとありますが、ここでは商品コードの有無と実棚数の桁等を確認して、
OKならそれを取り込む為に取り込み用のテーブルに追加して、
エラーならエラーとして出力する為にそれ用のテーブルに追加する様な記述をしています
Dim oleCn As New OleDb.OleDbConnection()
Dim oleCm As New OleDb.OleDbCommand()
Dim oleDa As New OleDb.OleDbDataAdapter()
Dim dt As New DataTable()
Dim strWork(1) As String
Dim intRow As Integer = 0

Dim xlApp As New Excel.Application()
Dim xlBooks As Excel.Workbooks = xlApp.Workbooks
Dim xlBook As Excel.Workbook = xlBooks.Open(Me.txt_PATH.Text & ".xls")
Dim xlSheets As Excel.Sheets = xlBook.Worksheets
Dim xlSheet As Excel.Worksheet = CType(xlSheets.Item(1), Excel.Worksheet)
strK = xlSheet.Name

xlApp.DisplayAlerts = False

xlSheet = Nothing
xlSheets = Nothing
xlBook.Close()
xlBook = Nothing
xlBooks.Close()
xlBooks = Nothing
xlApp.Quit()
xlApp = Nothing

'エクセルの場所とエクセルファイルを指定(Me.txt_PATH.Textから)
oleCn.ConnectionString = _
      "Provider=Microsoft.Jet.OLEDB.4.0; " + _
      "Data Source=" & Me.txt_PATH.Text & ".xls" & ";" + _
      "Extended Properties=""Excel 8.0;HDR=YES;IMEX=1;"""

oleCm.CommandText = "Select * From [" & strK & "$] "

oleCm.Connection = oleCn

oleCm.CommandText = "Select * from [Sheet1$] "

oleDa.SelectCommand = oleCm

Try
    oleDa.Fill(dt)
Catch ex As Exception
    oleDa.Dispose()
    oleDa = Nothing
    oleCm.Dispose()
End Try

'列数をカウント 6列で無ければダメ
intColumns = dt.Columns.Count

If intColumns = 6 Then

    Do While intRow < dt.Rows.Count
        strWork(0) = dt.Rows(intRow).Item(0).ToString   'A 商品コード
        strWork(1) = dt.Rows(intRow).Item(1).ToString   'B 品名
        strWork(2) = dt.Rows(intRow).Item(2).ToString   'C 実棚数
        strWork(3) = dt.Rows(intRow).Item(3).ToString   'D 原価
        strWork(4) = dt.Rows(intRow).Item(4).ToString   'E 売価
        strWork(5) = dt.Rows(intRow).Item(5).ToString   'F 合計

        'エラーチェック
        Call READ_EXCEL_ERR(strWork(0), strWork(1), strWork(2), strWork(3), strWork(4), strWork(5))

        intRow += 1     '次の行へ
    Loop

Else

    oleDa.Dispose()
    oleDa = Nothing
    oleCm.Dispose()

    Exit Function

End If

oleDa.Dispose()
oleDa = Nothing
oleCm.Dispose()

よろしくお願いします
■No22298に返信(まいかさんの記事)
> WinXP、VB.NET2005、Excel2003と言う環境です
> 取り込み元のデータが5000件や6000件等多くなると取り込みに時間がかかった
> り接続エラーになったりするので、
> どうにか取り込みスピードを早くしたいと思っています

一旦、CSVのファイル等に保存して、CSVファイルをEXCELで
読み込むとかでは駄目ですか?
やじゅさんありがとうございます

> 一旦、CSVのファイル等に保存して、CSVファイルをEXCELで
> 読み込むとかでは駄目ですか?
出来ればCSVではなくXLSファイルのままでやりたいと思っています
一旦CSVファイル等に保存・・も悪くないと言うより良いと思うのですが、
仕様でXLSファイルを取り込む事になってますので、今はCSVに保存としないで
XLSファイルでなんとかしたいと思っています
その上でなんとかならないでしょうか?
■No22300に返信(まいかさんの記事)
>取り込み元のデータが5000件や6000件
[WinXP(SP2)][Excel VBA][Excel2003]の環境でテストしました。
Variant 型の2次元配列を利用し、1命令でセルへの書き込み・
読み込みを実行しました。
データは行数5000、列数20の単純な文字列です。

セルから読み込みの処理時間は1秒未満でした。
書き込みは2秒未満で終了しました。

WinXP が問題なく動く PC でローカルのファイルを操作するとして、
数万件ならまだしも、数千件のデータ読み込み・書き込み処理なら
この程度でしょう。
CPU は AMD の Dual Core 2GHz です。

実行プログラムのどこがボトルネックなのか、コマンドをコメント
アウトするとか、特定の処理を抜き出してテストプログラムを作る
などして、それぞれの処理に掛かっている時間を具体的に調査して
ください。

なんとなくできました、でも時間が掛かりますなどという
デバッグ依頼はお断りです。
■No22301に返信(るしぇさんの記事)
> >取り込み元のデータが5000件や6000件
> [WinXP(SP2)][Excel VBA][Excel2003]の環境でテストしました。
> Variant 型の2次元配列を利用し、1命令でセルへの書き込み・
> 読み込みを実行しました。
> データは行数5000、列数20の単純な文字列です。
>
> セルから読み込みの処理時間は1秒未満でした。
> 書き込みは2秒未満で終了しました。
>

あ、そんなもんですか、あまり遅くないんですね。

>途中にREAD_EXCEL_ERRとありますが、ここでは
>商品コードの有無と実棚数の桁等を確認して

商品コードの有無って、DBに対して商品マスタの
存在チェックとかしてるのかな?

そういった情報が無いと回答しにくいですね。

>取り込みに時間がかかったり
これもどのくらいかかっているのか情報が欲しいところ
2008/06/18(Wed) 21:05:47 編集(投稿者)

■No22303に返信(やじゅさんの記事)
> >取り込みに時間がかかったり
> これもどのくらいかかっているのか情報が欲しいところ
提示のコードでも実行してみましたが、
> oleDa.Fill(dt)
の前後だと、2〜5秒の間ですね。Excel ファイルを
開くという作業を考慮に入れても(Excel VBAのテストでは既に
開いているファイルを読むだけになります。純粋に読み込み
処理時間ということです)時間が掛かるというほどのものでは
ないと思います。

ついでに指摘しておきますが、
> xlSheet = Nothing
> xlSheets = Nothing
このあたりの処理に意味はほとんど無く、タスクマネージャ
で確認すると、Excel のプロセスが残ってしまってることが
分かると思います。ReleaseComObject を実行する必要が
あるでしょう。

>Dim strWork(1) As String
> ・・・
>strWork(5) = dt.Rows(intRow).Item(5).ToString 'F 合計
当然エラーとなります。
やじゅさん、るしぇさんありがとうございます

>商品コードの有無って、DBに対して商品マスタの
>存在チェックとかしてるのかな?
仰る通り、商品マスタの存在チェックをしています
それと棚卸トランにもあるかどうかのチェックもしています
それと実棚数が書き込むテーブルではNumeric(7,1)なのでそれにあわせた桁のチェック
もしています
その上で、取り込み用のテーブルとエラー用のテーブルにそれぞれ追加する様にしています
エラーチェック内容も書いておきます
取り込み時、もし同じ商品があった場合、それを呼び出して実棚数を足して
更新するとしています
Dim strMessage_ERR As String = ""
Dim str_Sql As String = ""
Dim str_Sql2 As String = ""
Dim strK As String = ""
Dim dsRow As DataRow

Dim intLen As Int32 = 0
Dim intPoint As Int32 = 0
Dim strCHECK_JITTANA As String = ""
Dim cdJITUTANA As Double = 0

READ_EXCEL_ERR = False

'商品コードチェック -->
If str_SYOHIN_CD <> "" Then 'A列が無ければ飛ばす

    '商品コードチェック -->
    If strMessage_ERR = "" Then
        Call TBL.C_SYOHIN(sdr2, CInt(str_SYOHIN_CD))
        If Not sdr2.Read Then
            strMessage_ERR = "商品が存在しません"
        End If
        Call SQL_DataReader(sdr2, ActionModeEnum.UserClose)
    End If

    If strMessage_ERR = "" Then
        str_Sql = _
                "Select " & _
                        "* " & _
                "From " & _
                        "T_TANAOROSI With ( NoLock ) " & _
                "Where " & _
                        "SYOHIN_CD = " & CInt(str_SYOHIN_CD) & Space(1) & _
                "And    INPUT_KBN = 0 "
        Call SQL_DataReader(sdr3, ActionModeEnum.UserOpen, str_Sql)
        If sdr3.Read Then
            strMessage_ERR = "マニュアル入力済みです"
        End If
        Call SQL_DataReader(sdr3, ActionModeEnum.UserClose)
    End If
    '商品コードチェック <--

    '実棚数チェック -->
    '型チェック
    If strMessage_ERR = "" Then
        If IsNumeric(strJITTANA) = False Then
            strMessage_ERR = "実棚数 型が一致しません"
        End If
    End If

    '桁数チェック
    If strMessage_ERR = "" Then
        intLen = Len(strJITTANA)
        intPoint = strJITTANA.LastIndexOf(".") + 1
        If intPoint <> 0 Then
            strCHECK_JITTANA = CStr(MC.CUT_STRING(strJITTANA, intPoint + 1, intLen, ""))
            If Len(strCHECK_JITTANA) > 1 Then
                strMessage_ERR = "実棚数 桁数オーバーです"
            End If
        End If
    End If

    If strMessage_ERR = "" Then
        strK = CStr(Math.Abs(CDbl(strJITTANA)))
        If Math.Abs(CDbl(strJITTANA)) >= 1000000 Then
            strMessage_ERR = "実棚数 桁数オーバーです"
        End If
    End If
    '実棚数チェック <--

    'エラー出力用
    If strMessage_ERR <> "" Then
        dsRow = dsRpt2.Tables(strTable2).NewRow

        '商品コード
        dsRow("SYOHIN_CD") = str_SYOHIN_CD
        '品名
        dsRow("SYOHIN_NM") = strSYOHIN_NM
        '実棚数
        dsRow("JITTANA") = strJITTANA
        '原価
        dsRow("GENKA") = strGENKA
        '売価
        dsRow("BAIKA") = strBAIKA
        '合計
        dsRow("GOKEI") = strGOKEI
        'エラー内容
        dsRow("ERR") = strMessage_ERR

        dsRpt2.Tables(strTable2).Rows.Add(dsRow)
    End If

    '取込用
    If strMessage_ERR = "" Then
        If dsRpt.Tables(strTable).Select("SYOHIN_CD = '" & str_SYOHIN_CD & "'").Length = 0 Then    '08.06.18 DEL
            dsRow = dsRpt.Tables(strTable).NewRow

            '商品コード
            dsRow("SYOHIN_CD") = str_SYOHIN_CD
            '実棚数
            dsRow("JITTANA") = CDbl(strJITTANA)

            dsRpt.Tables(strTable).Rows.Add(dsRow)
        Else
            dsRow = dsRpt.Tables(strTable).Select("SYOHIN_CD = '" & str_SYOHIN_CD & "'")(0)

            cdJITUTANA = CDbl(dsRow("JITTANA"))

            dsRow("JITTANA") = cdJITUTANA + CDbl(strJITTANA)

            dsRow.AcceptChanges()
        End If

    End If

End If
■No22310に返信(まいかさんの記事)
質問と関係あることが分かったんですか?
関係ないなら必要ありません。

まずはその判断をしてから書き込むようにしてください。
るしぇさんどうもご指摘ありがとうございます

Try
oleDa.Fill(dt)
Catch ex As Exception
oleDa.Dispose()
oleDa = Nothing
oleCm.Dispose()
End Try
の所で読込に私も5秒弱位でした
そうなるとエラーチェックの所が不味いのかなと思うのですが
凡そ1行12秒位かかっちゃっています
> ■No22310に返信(まいかさんの記事)
>

エラーチェックを一時コメントアウトして、
取込みが速ければエラーチェックがネックです。

商品マスタと棚卸トランを取込む前にメモリ上(DataTable等)
に読むとかすれば、多少は速くなりそうです。

あとは、DBに一時テーブルに格納しておいて、
ストアド上にてエラーチェックして、全明細OK
なら取込むとか
やじゅさんありがとうございます

Try
oleDa.Fill(dt)
Catch ex As Exception
oleDa.Dispose()
oleDa = Nothing
oleCm.Dispose()
End Try
で読み込んだものをストアド上で確認する場合
何かしら一時テーブルを作成しなければならないのですが、
一時テーブルを作成、そしてそのテーブルにデータを入れる際に
沢山件数があるとそこでまた時間がかかってしまうかと浅はかかもしれませんが
そう思ってしまうのですが、その様な事にはならないのでしょうか?
仮にそうならないとしても、一時テーブルを作成・そこに挿入するにはどうすれば
良いのでしょうか?
2008/06/19(Thu) 18:32:37 編集(投稿者)

■No22314に返信(まいかさんの記事)
>ストアド上で確認する場合
ストアド?ストアドプロシージャ??
http://e-words.jp/w/E382B9E38388E382A2E38389E38397E383ADE382B7E383BCE382B8E383A3.html
そんなもの使ってないのでは???

[訂正]やじゅさんの提案でストアド実行を考慮に入れ始めた
 わけですね。読み取れていませんでした。

Excel からデータを読込むのに5000件で5秒以内ですよね?
先にボクが Excel VBA で示したように、この5秒というのも
Excel ファイルを開くからであって、5000件のデータの処理
自体をメモリ上で行うことに関しては1秒以内で終わります。

ただ、まだ原因はハッキリしてませんよね?
どう考えても1行12秒は無いだろうとww
DB検索はどのくらい時間が掛かってるの?
>Call SQL_DataReader
これ1行で5秒とか掛かってるなら、DB設計とかから見直す必要が
出てくるのでは?(独自関数内部で変なことしてる可能性も 0 ではないですが…)

ろくに整理されておらず、大量のレコードが存在するのにインデックスとかも
適用されておらず、検索条件に引っ掛かるデータがほとんど存在しないと
通常検索にさえ時間が掛かるのは必須です。
検索方法も工夫しないといけないんじゃないでしょうか?

>一時テーブルを作成・そこに挿入するにはどうすれば
>良いのでしょうか?
エラーチェックロジックの最後で取込用テーブル作って操作してるじゃん?

[訂正]ストアド自体は
 OleDb.OleDbCommand.CommandType = CommandType.StoredProcedure
 にして ExecuteNonQuery とかだと思うけど DB が Oracle だと
 OracleClient とか使わないとうまくいかないかも?

# Excel関係なくなっちゃったw
るしぇさんの指摘の
--------------------------------------------------------
このあたりの処理に意味はほとんど無く、タスクマネージャ
で確認すると、Excel のプロセスが残ってしまってることが
分かると思います。ReleaseComObject を実行する必要が
あるでしょう。
--------------------------------------------------------
この部分を先に理解を深めた方がいいですよ。
動作速度以前にこのコードはメモリリークしてるみたいですから。
■No22306に返信(るしぇさんの記事)
> このあたりの処理に意味はほとんど無く、タスクマネージャ
> で確認すると、Excel のプロセスが残ってしまってることが
> 分かると思います。ReleaseComObject を実行する必要が
> あるでしょう。

それと、

> > CType(xlSheets.Item(1), Excel.Worksheet)

こいつも完全にアウトですね。
先頭の Worksheet の参照の取得漏れです。
http://jeanne.wankuma.com/tips/vb.net/programming/releasecom.html

慎重に扱えたとしても別の言語で実装した方が良いです。
■No22314に返信(まいかさんの記事)
> 何かしら一時テーブルを作成しなければならないのですが、
> 一時テーブルを作成、そしてそのテーブルにデータを入れる際に
> 沢山件数があるとそこでまた時間がかかってしまうかと浅はかかもしれませんが
> そう思ってしまうのですが、その様な事にはならないのでしょうか?

ストアド作成までしないまでも、一時表にInsert句でDBに書き込んで
SELECT句にて、フラグ項目を設けて、商品マスタと棚卸トランを
外部結合(LEFT OUTER JOIN)する、フラグ項目がNULLなら未存在と扱う

現状の方法では、1行に12秒かかっているとのことなので、
一時表にInsert句でDBに5000件を書き込んだとしても、
存在チェックしているコストの方が大きいと思います。
DBがoracleならバインド配列でInsertするといいかも
Excel のプロセスが残る問題は有名で、あちこちで質問されてるので
ボクはこのスレでは深く突っ込むつもりは無いです。

結局、データベースの話になってきちゃう匂いがぷんぷんです。
提示されたコードを見ると物流関係みたいだけど、チェックに使ってる
テーブルのデータ件数って何件ですか?

環境にもよるけど数十万件くらいまでなら、データベースの知識が初心者
レベルでもなんとかそれなりに動かせると思います。。。
桁が上がって数百万件とか数千万件になると、SQL一つ投げるにしても
テクニックが必要になるはずです。

それを何も考えずに 5000 * 2 回検索してるんだとしたら話にならないです
よね?その辺、何が原因なのかハッキリさせてから対策へ移ってください。

# 物理的な限界があるから、ストアドもそうだけど DWH とかが考えられて
# いるわけです。そういった知識が必要な話なのかも?
失礼しました。問題がある箇所については、いくみさんに続けて指摘したかっただけです。

質問者本人がどこにボトルネックがあるのか示してくれないと判断できませんね。
(なので言及しようがないというか、なんというか...)
そんなこんなで話が先に進まないわけですが、2 度ほど指摘を受けているのに気付いてらっしゃらないご様子。

一応チェックと称しているところも気になるところはありますが、あまり期待できませんね。
本筋から外れて、横槍脱線させてしまいすみません。

>凡そ1行12秒位かかっちゃっています
体感ですか?

Dim t

t= Timer
何かの処理1
MsgBox Timer - t
t= Timer
何かの処理2
MsgBox Timer - t

こんな感じで まず遅い箇所を特定しましょう
みなさん、ありがとうございます
ご指摘を受けて、一度整理しつつ、資料を漁りつつ練り直しています

現時点では、全てのご指摘に回答出来ませんが分かっている点で回答します
>それを何も考えずに 5000 * 2 回検索してるんだとしたら話にならないです
>よね?その辺、何が原因なのかハッキリさせてから対策へ移ってください。
仮に5000件あった場合、5000件*2回検索と言うことはないです。
5000件あれば、5000回検索と言う感じです

>このあたりの処理に意味はほとんど無く、タスクマネージャ
>で確認すると、Excel のプロセスが残ってしまってることが
>分かると思います。ReleaseComObject を実行する必要が
>あるでしょう。
この点に関しては、作業終了後にプロセスには残っていません
以前はプロセスに残ってしまい、数回Excelを取り込みをすると取り込み自体が出来なくなってしまっていました。
現時点では、プロセスに残る事なくエラー等も出ては居ません
皆様から見れば色々問題点はあるかもしれませんが、現時点では大丈夫です
私もプロセスに残る事に関しては、その時に色々見て周り試してみた結果、現在の形に落ち着いています。
■No22326に返信(まいかさんの記事)
>仮に5000件あった場合、5000件*2回検索と言うことはないです。
そうかな?一応、コードはチェックしてるんだけど?

>'商品コードチェック -->
DataReader だと思われる sdr2 を利用してます。独自関数 TBL.C_SYOHIN
で検索してるかどうかは不明だけど、
>Call SQL_DataReader(sdr2, ActionModeEnum.UserClose)
この独自関数は引数で接続を閉じてると予想できます。

つまり商品コードが変わったら再度検索してるのでは?

その直後の
>Call SQL_DataReader(sdr3, ActionModeEnum.UserOpen, str_Sql)
>If sdr3.Read Then
> strMessage_ERR = "マニュアル入力済みです"
>End If
>Call SQL_DataReader(sdr3, ActionModeEnum.UserClose)
は SQL を編集して渡してるから、確実にDB検索実行してますよね?

じゃあ、普通に考えれば1行チェックするのに最大2回のDB検索が発生するのでは?
…まぁ、コードが提示された部分で完結してないから検証は出来ませんが。


>皆様から見れば色々問題点はあるかもしれませんが、現時点では大丈夫です
>私もプロセスに残る事に関しては、その時に色々見て周り試してみた結果、
>現在の形に落ち着いています。
ボクの端末では提示されたコードでプロセスが残ることを報告しておきます。
また、プロセスを解放するのに必要な手順が実行されていないため、プロセス
が残る方が正常な結果であることも言っておきます。

開発スケジュール等の問題もあるでしょうから、これ以上の指摘はしませんが、
いつプロセスが残るようになっても不思議でないコードであることだけは
認識しておいてください。
# そのまま、この手法でたくさんリリースした後に、何かの切っ掛けで
# 全て作り直しになった時の覚悟をしておいてくださいということです。
■No22326に返信(まいかさんの記事)

もしかしたら、お金の都合上用意できないかもしれませんが、
うまく動いていて、テストしてみても問題ないコードを
最適化するのであれば、Profilerなどを市販されているもので、
買ってみて試してみるのも良いかもしれません。

http://www.compuware.co.jp/products/devpartner_fm/library/dps_library.html

試用版もあったので、試してみるのもいいかも知れません。

# コードの間違いどうこうは、前の方々が言っていますし、
# 動いているとの事ですので、指摘はなしで、
# 現状のものが、無事動いている前提で、最適化を行いたい
# ということで、お勧めしています。
みなさん、色々ありがとうございます
今大幅に書き直しています

そこでまずはご報告を。
エクセルデータを一時テーブルに入れる様にしました。
5000件のエクセルデータを一時テーブルに入れるのにホントにあっという間に格納出来ました。
体感なのですが1秒かからない位で格納出来ました。

それで、チェックの方は列毎にチェックをする様に変えてみました
A列に商品コードがあるのですが、見るテーブルが2つなので
まずは商品テーブルを5000件分見て、それから棚卸テーブルを5000件分見るとしました
勿論エラーがあればエラー行は飛ばす様にしています
各々のテーブルを見て無ければ無いで、エラーを出しています
商品テーブルに無ければ、「商品コードがありませんよ」
棚卸テーブルにあれば、「以前に入力してあるのがありますよ」
っと言った具合にです。
それで無い場合に新たにその商品コードで再検索はしてないです

実棚数チェックにしても、3個チェックする項目があるので
それも1つずつに分けて、それぞれ5000回繰り返すとしました

現在、5000件のエラー無しの状態で約2分弱でチェックが出来る様になりました
> エクセルデータを一時テーブルに入れる様にしました。
> それも1つずつに分けて、それぞれ5000回繰り返すとしました
> 現在、5000件のエラー無しの状態で約2分弱でチェックが出来る様になりました

一時テーブルにいれる事ができたなら
ループもなくせますね
マスターテーブルとleft joinすればエラーチェックもそれぞれ1回で済みますし
最後のINSERTもSQL文1行でできるのではないでしょうか

DOBON.NET | プログラミング道 | プログラミング掲示板