none
OralceのデータでNumber列(10,2)を取得時の問題 RRS feed

  • 質問

  • お世話になります。

    DataAdapterでORACLEからデータを取得した時にできるDataTableの列の型と値ですが、
    Number型の列が、double型となっており、

    以下の変遷をおこなうと、値に誤差がうまれます。

    ORACLE(1.4)→DataTable(1.4)→double d(1.4000000000000001) = (double)tbl.Row[0]["XXXX"];

    この誤差が生まれる原因を教えてください。

    2013年7月25日 5:51

回答

  • DataAdapterでORACLEから

    どの DataAdapter をお使いですか?

    Number型の列が、double型となっており、

    スレッドの表題では NUMBER(10,2) になっていましたが、実際には桁数指定無しの NUMBER 型なのですか?
    でも、後続の回答では NUMBER(11,2) と書かれているのですよね…。本当の型は何でしょうか?

    なお、手元の環境で、「CREATE TABLE A (C1 NUMBER(15,3), C2 NUMBER )」なテーブルを用意して ODBC 接続した場合、C1 は Decimal になりましたが、C2 は Double になりました。

    ORACLE(1.4)→DataTable(1.4)→double

    DataTable に格納した時点で System.Double になっているのではなく、 DataTable の値を System.Double に変換すると言う話なのでしょうか?

    以下の変遷をおこなうと、値に誤差がうまれます。

    System.Data.OracleClient や Oracle.DataAccess.Client の場合、通常は System.Decimal にマッピングされるはずです。

    System.Data.OleDb の場合は、OLE DB Provider 依存、 System.Data.Odbc の場合は、ODBC Driver 依存となりますが、やはり、System.Decimal にマッピングされるのが一般的です。

    ただし、それは NUMBER の有効桁数などによっても異なります。そもそも System.Decimal 型は、内部的には96bit整数の精度までなので、最大精度は 28 が限界となりますが、Oracle の NUMBER 型の精度限界は、それよりも広いからです。

    たとえば System.Data.Odbc の場合、ドライバ依存という注意書きの元、 NUMBER(p,s) において、1≦p≦15 かつ s≦p であると記されています。
    http://msdn.microsoft.com/ja-jp/library/system.data.odbc.odbctype.aspx

    また、OleDb の場合には、
    http://msdn.microsoft.com/ja-jp/library/system.data.oledb.oledbtype.aspx
    http://docs.oracle.com/cd/E16338_01/win.112/b58886/appxtype.htm#i624168
    をみると、Numeric・VarNumeric 共に Decimal であるとされていました。ただし、
    http://msdn.microsoft.com/ja-jp/library/vstudio/cc668759.aspx
    については、VarNumeric が非サポートであるとの表記もあります。

    この誤差が生まれる原因を教えてください。

    まずは、何故 Decimal ではなく Double にマッピングされているのかを調べないといけませんね。

    →double d(1.4000000000000001)

    Double バイナリとしては、0x3FF6666666666667 になっているようです。この値は 1.4 よりも 0.0000000000000001332267629550187848508358001708984375 だけ大きな値です。

    ちなみに、隣値となる 0x3FF6666666666666 な Double バイナリの場合は、1.4 よりも 0.0000000000000000888178419700125232338905334472656250 だけ小さな値です。

    より 1.4 に近いのは後者の Double 値ですね。(実際の有効桁数はもっと荒いですが)


    2013年7月25日 8:32
  • CREATE TABLE A
    ( C1 NUMBER(15,3)
    , C2 NUMBER(10,2)
    , C3 NUMBER(11,2)
    , C4 NUMBER)

    に対しての実験です。

    VS2008 + Oracle.DataAccess 2.112.1.2 においては、

    NUMBER(15,3) → double
    NUMBER(10,2) → double
    NUMBER(11,2) → double
    NUMBER → decimal

    という結果になりました。先の ODBC とは逆の結果になってしまいました。

    これを下記のようにすると、すべて string になります。

    var adp = new OracleDataAdapter("SELECT * FROM A", c);
    adp.SafeMapping.Add("C1", typeof(string));
    adp.SafeMapping.Add("C2", typeof(string));
    adp.SafeMapping.Add("C3", typeof(string));
    adp.SafeMapping.Add("C4", typeof(string));
    DataSet ds = new DataSet();
    adp.Fill(ds, "A");

    string の代わりに byte[] にマッピングさせれば、22 バイト長のバイナリとして取り扱うこともできます。バイナリと数値の変換には、OracleDecimal 構造体が使えます。

    あるいは、DataTable を先に用意しておいて、

    var adp = new OracleDataAdapter("SELECT * FROM A", c);
    DataSet ds = new DataSet();
    
    var cols = ds.Tables.Add("A").Columns;
    cols.Add("C1", typeof(decimal));
    cols.Add("C2", typeof(decimal));
    cols.Add("C3", typeof(decimal));
    cols.Add("C4", typeof(decimal));
    
    adp.Fill(ds.Tables["A"]);

    とすることもできますが、これで Double 化の誤差が常に回避できるのかは未調査です。後者を採用する場合は、型付DataSet を用いたほうが良いかもしれませんね。

    (もしかしたら、もっと他に良い方法があるのかも知れませんが…)

    • 回答としてマーク TAKAKUN 2013年7月26日 6:53
    2013年7月25日 10:18

すべての返信

  • doubleにキャストする前のデータ型は把握されていますか?

    例えばOracle データ型のマップによるとOracleのNUMBER型は.NET FrameworkにおいてはDecimal構造体C#におけるdecimal型)ですが。doubleに変換したことにより誤差が生じているものと思われます。

    2013年7月25日 6:13
  • ご返事ありがとうございます。

    ORACLEでは、
    NUMBER(11,2)

    取得後のDataTableのカラム情報を調べてみると
    DataTypeプロパティは、System.Double
    ExtendedProperties(key/value)
    OraDbType/108

    となっていました。

    VS2008を使用です。


    2013年7月25日 7:19
  • ODBCあたりなどを使ってると、勝手に変換されそうですが。odbcad32.exeとかでその辺設定できましたっけ。

    何にせよ、doubleを使った時点で(二進浮動小数点数由来の)誤差は避け得ません。

    OracleならODP.NETを使って接続することをお勧めしますが……。

    2013年7月25日 7:44
  • DataAdapterでORACLEから

    どの DataAdapter をお使いですか?

    Number型の列が、double型となっており、

    スレッドの表題では NUMBER(10,2) になっていましたが、実際には桁数指定無しの NUMBER 型なのですか?
    でも、後続の回答では NUMBER(11,2) と書かれているのですよね…。本当の型は何でしょうか?

    なお、手元の環境で、「CREATE TABLE A (C1 NUMBER(15,3), C2 NUMBER )」なテーブルを用意して ODBC 接続した場合、C1 は Decimal になりましたが、C2 は Double になりました。

    ORACLE(1.4)→DataTable(1.4)→double

    DataTable に格納した時点で System.Double になっているのではなく、 DataTable の値を System.Double に変換すると言う話なのでしょうか?

    以下の変遷をおこなうと、値に誤差がうまれます。

    System.Data.OracleClient や Oracle.DataAccess.Client の場合、通常は System.Decimal にマッピングされるはずです。

    System.Data.OleDb の場合は、OLE DB Provider 依存、 System.Data.Odbc の場合は、ODBC Driver 依存となりますが、やはり、System.Decimal にマッピングされるのが一般的です。

    ただし、それは NUMBER の有効桁数などによっても異なります。そもそも System.Decimal 型は、内部的には96bit整数の精度までなので、最大精度は 28 が限界となりますが、Oracle の NUMBER 型の精度限界は、それよりも広いからです。

    たとえば System.Data.Odbc の場合、ドライバ依存という注意書きの元、 NUMBER(p,s) において、1≦p≦15 かつ s≦p であると記されています。
    http://msdn.microsoft.com/ja-jp/library/system.data.odbc.odbctype.aspx

    また、OleDb の場合には、
    http://msdn.microsoft.com/ja-jp/library/system.data.oledb.oledbtype.aspx
    http://docs.oracle.com/cd/E16338_01/win.112/b58886/appxtype.htm#i624168
    をみると、Numeric・VarNumeric 共に Decimal であるとされていました。ただし、
    http://msdn.microsoft.com/ja-jp/library/vstudio/cc668759.aspx
    については、VarNumeric が非サポートであるとの表記もあります。

    この誤差が生まれる原因を教えてください。

    まずは、何故 Decimal ではなく Double にマッピングされているのかを調べないといけませんね。

    →double d(1.4000000000000001)

    Double バイナリとしては、0x3FF6666666666667 になっているようです。この値は 1.4 よりも 0.0000000000000001332267629550187848508358001708984375 だけ大きな値です。

    ちなみに、隣値となる 0x3FF6666666666666 な Double バイナリの場合は、1.4 よりも 0.0000000000000000888178419700125232338905334472656250 だけ小さな値です。

    より 1.4 に近いのは後者の Double 値ですね。(実際の有効桁数はもっと荒いですが)


    2013年7月25日 8:32
  • ご返事ありがとうございます。

    ODP.NETを使用しています。

    2013年7月25日 8:39
  • ご返事ありがとうございます。

    問題が起こっている別の環境にて「CREATE TABLE A (C1 NUMBER(15,3), C2 NUMBER )」を作成して、
    OracleDataAdapter.Fill()にてデータを取得しました。

    カラムのDataTypeプロパティを見ると、System.Doubleでした。

    p.s.表題と異なっていました。NUMBER(11,2)の列を参照しております。

    別の環境
    Oracle Database 11g Express EditionODTwithODAC1120320_32bitをインストールVS2008Sp1


    2013年7月25日 9:13
  • CREATE TABLE A
    ( C1 NUMBER(15,3)
    , C2 NUMBER(10,2)
    , C3 NUMBER(11,2)
    , C4 NUMBER)

    に対しての実験です。

    VS2008 + Oracle.DataAccess 2.112.1.2 においては、

    NUMBER(15,3) → double
    NUMBER(10,2) → double
    NUMBER(11,2) → double
    NUMBER → decimal

    という結果になりました。先の ODBC とは逆の結果になってしまいました。

    これを下記のようにすると、すべて string になります。

    var adp = new OracleDataAdapter("SELECT * FROM A", c);
    adp.SafeMapping.Add("C1", typeof(string));
    adp.SafeMapping.Add("C2", typeof(string));
    adp.SafeMapping.Add("C3", typeof(string));
    adp.SafeMapping.Add("C4", typeof(string));
    DataSet ds = new DataSet();
    adp.Fill(ds, "A");

    string の代わりに byte[] にマッピングさせれば、22 バイト長のバイナリとして取り扱うこともできます。バイナリと数値の変換には、OracleDecimal 構造体が使えます。

    あるいは、DataTable を先に用意しておいて、

    var adp = new OracleDataAdapter("SELECT * FROM A", c);
    DataSet ds = new DataSet();
    
    var cols = ds.Tables.Add("A").Columns;
    cols.Add("C1", typeof(decimal));
    cols.Add("C2", typeof(decimal));
    cols.Add("C3", typeof(decimal));
    cols.Add("C4", typeof(decimal));
    
    adp.Fill(ds.Tables["A"]);

    とすることもできますが、これで Double 化の誤差が常に回避できるのかは未調査です。後者を採用する場合は、型付DataSet を用いたほうが良いかもしれませんね。

    (もしかしたら、もっと他に良い方法があるのかも知れませんが…)

    • 回答としてマーク TAKAKUN 2013年7月26日 6:53
    2013年7月25日 10:18
  • 念のため、ODP.NET Types OverviewでもNUMBERはDecimalに対応するとされています。
    # 既に魔界の仮面弁士さんが実際に確認されているので、仕様的な意味で。
    2013年7月25日 10:34
  • ご返事ありがとうございます。

    double d = double.Parse(dt[0][0].ToString());

    であれば、誤差がなくなったので、この対応で実装しようと思っていますが、
    もし問題あれば教えてください。

    ありがとうございました。

    2013年7月26日 6:53
  • double d = double.Parse(dt[0][0].ToString());

    上記を見る限りでは、double型にパースされたようですね。

    先の回答でも多少触れていますが、そもそも double 型は2進小数で管理される型ですので、1.4 という値を正確には表現できず、実際には1.4 よりも僅かにズレた値で格納される仕様です(1.5 ならば誤差なく格納されますが)。

    そのわずかな誤差が、切り捨て・切り上げなどの処理に影響を与える場合もあります。10進小数として誤差なく扱いたいのであれば、まずは decimal で扱うことができないかを検討されることをお奨めします。decimal では扱えないような精度の型の場合は、OracleDecimal型を採用することもできます。

    double でないと演算できない処理があるのならば仕方ありませんが…。

    2013年7月26日 9:19