StringBuilder はどれだけ早いのだろうか?、実は大してかわりません

SqlCommand や DataTable を使うときに文字列をたくさん使うのですが、果たして世間一般(?)で言われているほど、String は遅く、StringBuilder は早いのでしょうか?というベンチマークです。

非常に長い場合は StringBuilder を使うほうが良いのですが、SqlCommand などに渡す SQL 文程度(長くても 5000 文字ぐらい)は、どうなのでしょうか?ということです。

■結論

結論から言えば、大して変わりません。以下は 20,000 件のデータを廻したときの結果です。

avg. Normal 7.54 sec
avg. StirngBuilder 7.22 sec
avg. SqlCommand 7.28 sec

違いは 2,3 % ぐらいしかないので誤差の範囲ですね。

以下は、ベンチマーク用の実験コード

■通常の String パターン

''' <summary>
''' 1.通常の string の連結で作成
''' </summary>
''' <remarks></remarks>
Public Sub TestNormal()
    Dim s As String = toMD5(DateTime.Now.ToString())

    Dim cn As New SqlConnection("")
    For j As Integer = 0 To MAX - 1
        Dim sql As String = ""
        ' よくある string.Format を使った作り方
        sql += "SELECT * FROM DUAL "
        sql += " WHERE 1 = 1 "
        sql += String.Format(" AND col0 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col1 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col2 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col3 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col4 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col5 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col6 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col7 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col8 = '{0}' ", s) : s = toMD5(s)
        sql += String.Format(" AND col9 = '{0}' ", s) : s = toMD5(s)

        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(sql, cn) 'dummy
    Next
End Sub

string.Format を使って、ぽちぽちと文字列をフォーマットしていきます。

■StringBuilder を使ったパターン

よくあるように、文字列の連結は StringBuilder を使えッ!!! ってことなので、使ってみたのですが、大して変わりません。もっと長い SQL の場合、効果があるんでしょうが…そんなに長い SQL を書くのはどうなの???ってことなのです。

''' <summary>
''' 2.StringBuilder を使う
''' </summary>
''' <remarks></remarks>
Public Sub TestStringBuilder()
    Dim s As String = toMD5(DateTime.Now.ToString())

    Dim cn As New SqlConnection("")
    For j As Integer = 0 To MAX - 1
        Dim sql As String = ""
        ' よくある StringBuilder を使った作り方
        Dim sb As New System.Text.StringBuilder("")
        sb.Append("SELECT * FROM DUAL ")
        sb.Append(" WHERE 1 = 1 ")
        sb.AppendFormat(" AND col0 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col1 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col2 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col3 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col4 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col5 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col6 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col7 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col8 = '{0}' ", s) : s = toMD5(s)
        sb.AppendFormat(" AND col9 = '{0}' ", s) : s = toMD5(s)

        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(sql, cn) 'dummy
    Next
End Sub

■SqlCommand を使う

SqlCommand を使うと、最初に SQL 文を使うので文字列編集部分が極端に減ります。
これの場合は、20000 回の編集が 1 回になるわけですが、劇的に早くなる…はずなんですけど、結果は変わりません。

''' <summary>
''' SqlCommand で Const/Dim  を使う
''' </summary>
''' <remarks></remarks>
Public Sub TestConst()
    Dim s As String = toMD5(DateTime.Now.ToString())
    Dim cn As New SqlConnection("")

    ' ここは dim でも同じ
    Const sql As String = _
        "SELECT * FROM DUAL0 " + _
        " WHERE 1 = 1 " + _
        " AND col0 = @col0 " + _
        " AND col1 = @col1 " + _
        " AND col2 = @col2 " + _
        " AND col3 = @col3 " + _
        " AND col4 = @col4 " + _
        " AND col5 = @col5 " + _
        " AND col6 = @col6 " + _
        " AND col7 = @col7 " + _
        " AND col8 = @col8 " + _
        " AND col9 = @col9 ; "

    Dim cmd As New SqlCommand(sql, cn)
    cmd.Parameters.Add(New SqlParameter("@col0", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col1", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col2", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col3", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col4", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col5", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col6", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col7", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col8", Nothing))
    cmd.Parameters.Add(New SqlParameter("@col9", Nothing))

    For j As Integer = 0 To max - 1
        cmd.Parameters("@col0").Value = s : s = toMD5(s)
        cmd.Parameters("@col1").Value = s : s = toMD5(s)
        cmd.Parameters("@col2").Value = s : s = toMD5(s)
        cmd.Parameters("@col3").Value = s : s = toMD5(s)
        cmd.Parameters("@col4").Value = s : s = toMD5(s)
        cmd.Parameters("@col5").Value = s : s = toMD5(s)
        cmd.Parameters("@col6").Value = s : s = toMD5(s)
        cmd.Parameters("@col7").Value = s : s = toMD5(s)
        cmd.Parameters("@col8").Value = s : s = toMD5(s)
        cmd.Parameters("@col9").Value = s : s = toMD5(s)
        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(cmd) 'dummy
    Next
End Sub

以上、こんな風に実験してみると普通に string を使っている限りはスピードに変化はありません、ってことです。
ただし、データベースアクセスに関しては、単純な SqlDataAdapter の呼び出しよりも、SqlCommand でプリコンパイル SQL を使ったほうが、10 倍以上早くなるので、件数が多い場合はパフォーマンスに注意が必要です。

カテゴリー: 開発, C# | 5件のコメント

意外と遅い DataTable 、なので List を使うと 5 倍早くなる

以前から気になっていたのですが、DataTable/DataSet を使うと遅いのでは?と思っていました。
実際、Visual Studio で自動生成する型付の DataTable を使うと思ったように性能がでないことが多く、結局 SQL でチューニング、ってことになります。

で、具体的に遅そうなところを実験してみました。

単純に DataTable の性能を比較したいので、データベースには使わず値の代入だけ実験します。

  1. 列が 100 のテーブルを想定する。
  2. 行数を 10000 件挿入する。

これを次のパターンで比較します。

  1. 普通に DataTable を使う
  2. With 構文を使って、高速化する?
  3. for earc を使ってカウンタを使わない方法
  4. 名前を使わずに index を使う
  5. generic list を使う
  6. generic list で構造体/クラスを使う

先に結論から書くと、1 から 4 までは大してスピードは変わりません。
期待してたのは for each を使うと早くなれば良かったんですけどね。5 % ぐらいしか早くなりません。

ですが、DataTable を使うのを止めて、List コレクションを使うと 5 倍ぐらい早くなります。

avg. Go1 13.88 sec
avg. Go2 13.09 sec
avg. Go3 12.98 sec
avg. Go4 12.93 sec
avg. Go5 2.12 sec
avg. Go6 2.08 sec

どうも、単純に DataTable.Rows へのアクセスが複雑怪奇なんでしょうねぇ。DataSet, DataTable が出来たのは generic ができる前なので、そのあたりの制限で遅くなっているのかもしれません。
.NET Framework の標準ライブラリ(あるいは自動生成したもの)も generic を考慮していないコードがあるので、そのあたり、パフォーマンス的に無駄になっている可能性があるということのなのかも。

以下は、VB のサンプルコードです。

■素直に DataTalbe を使う

''' <summary>
''' 素直に Item に名前を指定する
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go1() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		dt.Rows(i).Item("col000") = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item("col001") = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item("col002") = tm.ToString() : tm.AddSeconds(1)
		...
		dt.Rows(i).Item("col097") = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item("col098") = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item("col099") = tm.ToString() : tm.AddSeconds(1)
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go1 {0:#.00} sec", span)
	Return span
End Function

■With 構文を使う

いわゆるポインタアクセスの代わりに with 構文を使います。
dt.Rows(i) のところが無くなるので、コード的には安全になるのですが、スピードは 2,3 % 程度早くなる程度です。

''' <summary>
''' With 構文を使ってみる
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go2() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		With dt.Rows(i)
			.Item("col000") = tm.ToString() : tm.AddSeconds(1)
			.Item("col001") = tm.ToString() : tm.AddSeconds(1)
			.Item("col002") = tm.ToString() : tm.AddSeconds(1)
			...
			.Item("col097") = tm.ToString() : tm.AddSeconds(1)
			.Item("col098") = tm.ToString() : tm.AddSeconds(1)
			.Item("col099") = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go2 {0:#.00} sec", span)
	Return span
End Function

■for each を使う

for each を使うと劇的に早くなる…のが良かったのですが、あまり早くなりません。
10 % 弱ほど早くなります。

''' <summary>
''' for each を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go3() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For Each row As DataRow In dt.Rows
		With row
			.Item("col000") = tm.ToString() : tm.AddSeconds(1)
			.Item("col001") = tm.ToString() : tm.AddSeconds(1)
			.Item("col002") = tm.ToString() : tm.AddSeconds(1)
			...
			.Item("col097") = tm.ToString() : tm.AddSeconds(1)
			.Item("col098") = tm.ToString() : tm.AddSeconds(1)
			.Item("col099") = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go3 {0:#.00} sec", span)
	Return span

End Function

■名前を使わずに数値を使う

比較のために列名ではなくて、番号を使ってアクセスさせてみます…が、あまり早くなりません。
for each と同じで 10 % ぐらいですね。

''' <summary>
''' 名前を使わずに index を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go4() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		With dt.Rows(i)
			.Item(0) = tm.ToString() : tm.AddSeconds(1)
			.Item(1) = tm.ToString() : tm.AddSeconds(1)
			.Item(2) = tm.ToString() : tm.AddSeconds(1)
			...
			.Item(97) = tm.ToString() : tm.AddSeconds(1)
			.Item(98) = tm.ToString() : tm.AddSeconds(1)
			.Item(99) = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go4 {0:#.00} sec", span)
	Return span

End Function

■list を使う

これ以上は無理かなぁと思って、list に変えてみます。
最初は、list(of object()) で、object の配列にしてみます。
すると、すさまじく早くなります。5,6 倍は早くなりますね。

''' <summary>
''' generic List を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go5() As Double

	Dim dt As New List(Of Object())

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		Dim item(100) As Object
		item(0) = tm.ToString() : tm.AddSeconds(1)
		item(1) = tm.ToString() : tm.AddSeconds(1)
		item(2) = tm.ToString() : tm.AddSeconds(1)
		...
		item(97) = tm.ToString() : tm.AddSeconds(1)
		item(98) = tm.ToString() : tm.AddSeconds(1)
		item(99) = tm.ToString() : tm.AddSeconds(1)

		dt.Add(item)

	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go5 {0:#.00} sec", span)
	Return span

End Function

■list に構造体を使う

object の配列では扱いづらいので、型付 DataTable 風にフィールドでアクセスできるように POJO なクラスを作ります。
アクセスが遅くなると思いきや…全然変わりませんね。
DataRow のところにクラスを使えるのが generic の良いところです。

''' <summary>
''' generic List で構造体を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go6() As Double

	Dim dt As New List(Of TITEM)

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1

		Dim it As New TITEM
		With it
			.col000 = tm.ToString() : tm.AddSeconds(1)
			.col001 = tm.ToString() : tm.AddSeconds(1)
			.col002 = tm.ToString() : tm.AddSeconds(1)
			...
			.col097 = tm.ToString() : tm.AddSeconds(1)
			.col098 = tm.ToString() : tm.AddSeconds(1)
			.col099 = tm.ToString() : tm.AddSeconds(1)
		End With

		dt.Add(it)

	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine("Go6 {0:#.00} sec", span)
	Return span

End Function

使うクラス

Public Class TITEM
	Public col000 As String
	Public col001 As String
	Public col002 As String
	...
	Public col097 As String
	Public col098 As String
	Public col099 As String
End Class

という訳で、DataTable.Rows を使うよりも List(Of クラス) を使ったほうが 5 倍早いわけです。
このあたり、SQL Server へのアクセス(SqlCommand, SqlDataAdapter, Linq)の組み合わせで考えると、

  • データベースは SqlCommand でアクセス
  • 取得したデータのアクセスは generic List を利用

とする組み合わせ一番早いわけです。

さて、これをどのように実装するのか?あるいは、既にあるのか?ってことですね。これは後程。

~~~

追記 2015/06/02 C# で書き換えたのはこちら
DataTable よりも List を使うと 10 倍早くなる(続編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7217

カテゴリー: 開発, C# | 6件のコメント

SELECT のパフォーマンスチェック(続き)

.NET を使って SQL Server に対して SELECT するには、ってことでパフォーマンスのチェックをしました。

  1. 10 個のテーブルに 20000 件ずつデータを入れておきます。
  2. 10 個のテーブルに対して、2000 回ずつ検索します。
  3. テーブルには主キーを付けておきます。

という形でデータを作っておいて、以下のパターンで検索をします。

  1. 個別に SqlDataAdapter を使って呼び出し
  2. 10 個のテーブルの検索をひとつにまとめて、SqlDataAdapter で呼び出し
  3. 10 個のテーブルの検索をひとつにまとめて、SqlCommand で呼び出し
  4. LINQ to Entities を使って呼び出し
  5. LINQ to SQL を使って呼び出し
1.個別SELECT
56.5
59.3
57.5

2.まとめてSELECT
※ 1 より遅くなったのでパス

3.SqlCommand
7.6
7.5
7.4

4.LINQ to Entities
112.5
110.6
108.2

5.LINQ to SQL
92.4
112.3
93.9

結論から言えば、

  1. 個別SELECTよりも、LINQ を使うと 2 倍ぐらい遅くなる。
  2. LINQ to Entities と LINQ to SQL はスピードが変わらない。
  3. SqlCommand を使うと、個別SELECTよりも 5 倍以上、LINQ よりも 10 倍以上早い。

という結果になりました。

LINQ の場合、10 個のテーブルに対して、検索を実行するために 10 回呼び出してしまうので、この手の方法は不利なのですが、SqlDataAdapter よりも遅いのは意外です…スピードが要求されるところで、LINQ を使うのは禁物ですね。

パフォーマンスのチューニングとしては、SqlCommand が一番やりやすく、10 個のテーブルに対する SELECT を 1 回で済ませられるのも SQL Server で複数の検索結果(DataTable)を返せるからです。
また、prepared statements という形で、あらかじめ SQL 文を発行させて再利用できるとろも SqlCommand の有利なところですね。

実験ソースを晒しておきます。

■テーブルの作成

/// <summary>
/// テーブル作成
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
    string sql = "";
    SqlConnection cn = new SqlConnection(CNSTR);
    cn.Open();
    SqlCommand cmd;
    for (int i = 0; i < 10; i++)
    {
        sql = string.Format("drop table table{0}", i);
        cmd = new SqlCommand(sql, cn);
        try
        {
            cmd.ExecuteNonQuery();
        }
        catch { }

        sql = string.Format("create table table{0} (", i);
        for (int j = 0; j < 10; j++)
        {
			if (j == 0)
			{
				sql += string.Format("col{0} varchar(50) not null,", j);
			}
			else
			{
				sql += string.Format("col{0} varchar(50),", j);
			}
        }
		sql += string.Format(" CONSTRAINT pk_table{0} PRIMARY KEY ( col0 ) ", i );
        sql += ")";
        cmd = new SqlCommand(sql, cn);
        cmd.ExecuteNonQuery();
    }
    cn.Close();
}

■20000件のデータ insert

/// <summary>
/// データ作成
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
    int max = int.Parse(textBox1.Text);
    SqlConnection cn = new SqlConnection(CNSTR);
    cn.Open();

    string s = toMD5(DateTime.Now.ToString());
    int count = 0;
    for (int i = 0; i < 10; i++)
    {
        string sql = string.Format("insert into table{0} values (", i);
        SqlCommand cmd = new SqlCommand("", cn);
        for (int k = 0; k < 10; k++)
        {
            cmd.Parameters.Add(new SqlParameter(
                string.Format("@col{0}", k), SqlDbType.VarChar, 50));
            sql += string.Format("@col{0},", k);
        }
        sql = sql.Substring(0, sql.Length - 1);
        sql += ")";
        cmd.CommandText = sql;

        for (int j = 0; j < max; j++)
        {
            for (int k = 0; k < 10; k++)
            {
                s = toMD5(s);
                cmd.Parameters[k].Value = s;
            }
            cmd.ExecuteNonQuery();
            count++;
            if (count % 100 == 0)
            {
                toStatus(count, max * 10);
            }
        }
    }
    cn.Close();
}

■個別SELECT

まずいやり方ですが、string.Format で値込みで文字列を作っています。
よくあるパターンですね。

/// <summary>
/// 個別SELECT
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
    // 律儀にSELECT文を10回発行します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for ( int j=0; j<10; j++ ) {
            DataTable dt = new DataTable();
            SqlDataAdapter da = new SqlDataAdapter(
                string.Format("SELECT * FROM table{0} WHERE col0 = '{1}'", j, s), cn);
            s = toMD5(s);
            da.Fill( dt );
        }
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■まとめてSELECT

SqlDataAdapter に渡す SQL 文をひとまとめにしたのですが、全然駄目です。

/// <summary>
/// まとめてSELECT
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// 遅くてぜんぜんだめ
/// </remarks>
private void button4_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = "";
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            sql += string.Format("SELECT * FROM table{0} WHERE col0 = '{1}' ", j, s);
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(sql, cn);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max );
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■SqlCommand を使う

あらかじめコンパイルされるように SqlCommand を使います。
ループの中で SqlCommand を使ってはいけません。ループの外で作成して、パラメータで渡します。

/// <summary>
/// SqlCommand の利用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button5_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = "";
</p>
<p>
    SqlCommand cmd = new SqlCommand("",cn);
    for (int j = 0; j < 10; j++)
    {
        sql += string.Format("SELECT * FROM table{0} WHERE col0 = @param{1} ", j, j);
        s = toMD5(s);
        cmd.Parameters.Add(new SqlParameter(
            string.Format("@param{0}", j), SqlDbType.VarChar, 50));
    }
    cmd.CommandText = sql;
</p>
<p>
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            cmd.Parameters[j].Value = s;
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■LINQ to Entities

比較のために LINQ to Entities でも作ってみます。
LINQ の場合は、複数の SELECT 文をまとめることができないので、ループの中で 10 回呼び出します。この方法は、SqlDataAdapter と同じですよね。
スピードを比較したものがないので、初めて試してみたのですが、個別SELECTよりも2倍ほどおそくなります。

/// <summary>
/// LINQ to Entities の利用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button6_Click(object sender, EventArgs e)
{
	// 律儀にSELECT文を10回発行します。
	DateTime start = DateTime.Now;
	string s = toMD5(start.ToString());
	int count = 0;

	stressEntities ent = new stressEntities();
	int max = 2000;
	for (int i = 0; i < max; i++)
	{
		var items0 = from t in ent.table0
					where t.col0 == s
					select t;
		s = toMD5(s);
		var item1 = from t in ent.table1
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items2 = from t in ent.table2
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items3 = from t in ent.table3
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items4 = from t in ent.table4
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items5 = from t in ent.table5
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items6 = from t in ent.table6
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items7 = from t in ent.table7
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items8 = from t in ent.table8
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items9 = from t in ent.table9
						where t.col0 == s
						select t;
		s = toMD5(s);

		var it0 = items0.SingleOrDefault();
		var it1 = items0.SingleOrDefault();
		var it2 = items0.SingleOrDefault();
		var it3 = items0.SingleOrDefault();
		var it4 = items0.SingleOrDefault();
		var it5 = items0.SingleOrDefault();
		var it6 = items0.SingleOrDefault();
		var it7 = items0.SingleOrDefault();
		var it8 = items0.SingleOrDefault();
		var it9 = items0.SingleOrDefault();

		if (++count % 10 == 0)
			toStatus(count, max);
	}
	DateTime end = DateTime.Now;
	textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■LINQ to SQL を利用

比較のために LINQ to SQL でも動かしてみます。
不利なのは、LINQ to Entities と同じで、10 回呼び出さないといけないところですね。
スピードに関しては、LINQ to Entities と変わりません。

/// <summary>
/// LINQ to SQL の利用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button7_Click(object sender, EventArgs e)
{
	// 律儀にSELECT文を10回発行します。
	DateTime start = DateTime.Now;
	string s = toMD5(start.ToString());
	int count = 0;

    SqlConnection cn = new SqlConnection(CNSTR);
	DataClasses1DataContext cnt = new DataClasses1DataContext(cn);

	int max = 2000;
	for (int i = 0; i < max; i++)
	{
		var items0 = from t in cnt.Dtable0
						where t.col0 == s
						select t;
		s = toMD5(s);
		var item1 = from t in cnt.Dtable1
					where t.col0 == s
					select t;
		s = toMD5(s);
		var items2 = from t in cnt.Dtable2
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items3 = from t in cnt.Dtable3
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items4 = from t in cnt.Dtable4
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items5 = from t in cnt.Dtable5
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items6 = from t in cnt.Dtable6
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items7 = from t in cnt.Dtable7
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items8 = from t in cnt.Dtable8
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items9 = from t in cnt.Dtable9
						where t.col0 == s
						select t;
		s = toMD5(s);

		var it0 = items0.SingleOrDefault();
		var it1 = items0.SingleOrDefault();
		var it2 = items0.SingleOrDefault();
		var it3 = items0.SingleOrDefault();
		var it4 = items0.SingleOrDefault();
		var it5 = items0.SingleOrDefault();
		var it6 = items0.SingleOrDefault();
		var it7 = items0.SingleOrDefault();
		var it8 = items0.SingleOrDefault();
		var it9 = items0.SingleOrDefault();

		if (++count % 10 == 0)
			toStatus(count, max);
	}
	DateTime end = DateTime.Now;
	textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

そんな訳で、実行時のパフォーマンスが気にかかる場合は、SqlCommand にすると 10 倍ほど早くなるよ、という話です。逆に言えば、たいしてパフォーマンスが気にならないときは、書きやすい LINQ を使ったほうがよいでしょうってことで。

カテゴリー: 開発, C# | SELECT のパフォーマンスチェック(続き) はコメントを受け付けていません

SELECT のパフォーマンスチューニング(メモ)

SELECT のパフォーマンスチューニング(メモ)

ちょっと、メモ的に書き下しておきます。

1. 10 個のテーブルに 20000 件ずつデータを入れておきます。
2. 10 個のテーブルに対して、2000 回ずつ検索します。

というパターンがあったとき、どうやると早いでしょうか?という問題。

■1回ずつ SqlDataAdapter を呼び出す

だいたい 200 秒ぐらいかかります。

/// <summary>
/// 個別SELECT
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
    // 律儀にSELECT文を10回発行します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for ( int j=0; j<10; j++ ) {
            DataTable dt = new DataTable();
            SqlDataAdapter da = new SqlDataAdapter(
                string.Format("SELECT * FROM table{0} WHERE col0 = '{1}'", j, s), cn);
            s = toMD5(s);
            da.Fill( dt );
        }
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■10回の呼び出しをひとつにまとめる

SQL Server の場合、複数の検索結果を取れるので DataSet を使って 1 回にまとめます。
SQL Server 2008 だと 10 倍ぐらい早くなって 15 秒程度なんですが、
SQL Server 2000 だと、最初のパターンよりも遅くなってしまうんですよね…何故だろう?

/// <summary>
/// まとめてSELECT
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// 遅くてぜんぜんだめ
/// SQL Server 2008 だと ok なのだが、SQL Server 2000 だと駄目らしい。
/// </remarks>
private void button4_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = "";
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            sql += string.Format("SELECT * FROM table{0} WHERE col0 = '{1}' ", j, s);
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(sql, cn);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max );
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

■あらかじめ SqlCommand を作成する

prepared sql statement ということで、あらかじめ SqlCommand で作成しておきます。
これをやると、12,3 秒になります。
ループの中で SqlCommand を作ると早くならないので注意が必要ですね。

/// <summary>
/// SqlCommand の利用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button5_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = "";

    SqlCommand cmd = new SqlCommand("",cn);
    for (int j = 0; j < 10; j++)
    {
        sql += string.Format("SELECT * FROM table{0} WHERE col0 = @param{1} ", j, j);
        s = toMD5(s);
        cmd.Parameters.Add(new SqlParameter(
            string.Format("@param{0}", j), SqlDbType.VarChar, 50));
    }
    cmd.CommandText = sql;

    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            cmd.Parameters[j].Value = s;
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString("#.0");
}

更に高速化する場合はどうするんでしょうね?
.NET の文だけおそくなるから、C++ で書き直すと早くなるのかな?

カテゴリー: 開発, C# | SELECT のパフォーマンスチューニング(メモ) はコメントを受け付けていません

時には機動警察パトレーバーのように、あるいは、600 万件のデータ加工を

たまぁーに、1巻だけ取り出して読み進めて、最後の熊耳巡査が出てくるところ(手元にあるのは文庫本)で、毎度思うところがある。

泉巡査がイングラムの能力を十分引き出せないのは何故か?格闘技術に疎いから、という結論になるのだけれど、イングラムというハードウェアに、イングラムを動かすソフトウェア(学習機能付き)が乗り、更に人の動かし方が最終的にイングラムの能力を決定づける(「個性」ともいうし、「個体差」ともいう)、という流れになる。

ハードウェアというのは、パソコンだったり、CPU だったり、メモリだったりするわけで、OS というのは、Windows だったり、Mac だったりする。いや、OSの取り方をもうちょっと広げれば、SQL Server だったり、Oracle だったり、.NET Framework だたり、するわけです。で、これを使うのが人なわけで、この場合はプログラマなのですね。

最近のハードウェアは 10 年前よりもぐんと良くなっているわけで、メモリが 2GB なんてのは当たり前で、HDD も 500GB という形で桁が違います。なので、同じ大量のデータを扱うにしても、昔のデータベースの扱い方と、最近の扱い方では全く違う…と言いますか、ハードウェアが違うところにが関わって、でてくる性能に大きな違いがでます…と言いますか、作り方によって全く違うってのを実感しています。

たまたま、600 万件のアクセスログを解析しようと思ったわけですが、この 600 万件というデータ、10 年前だったら Oracle でもひいこらいう程度のものなので、ちょっと躊躇しました。テキストデータで 2GB 弱あるわけです。
で、ひとまず、手元の SQL Server 2008 Experss Edition にテキストデータを入れて、データ解析をしてみると、これが早い早い。最初のインポート(C# で作りました)はツールの作りが悪いのか、2 時間程データ挿入に掛かるのですが、その後データを加工する場合は、クエリを使うと結構のスピードで動きます。

インデックスが無い状態でも、全検索で 3 分位で結果がでるし、適当なインデックスをつければ 2,3 秒かからずに結果がでてきます。データの加工ですら、1 分位で済みます。

データ加工するところは、

25/May/2011:23:58:39 +0900

のようなアクセスログの日付データを、データベースの Datetime 型に直します。
なので、update 文で使えるように、

2011-5-25 23:58:39

な風に変えないと駄目なわけです。

最初は、C# のツールを使ってやろうと思ったのですが、そもそも SqlCommand を使って insert 文を使ってデータ挿入をすると、600 万件挿入するのに 2 時間かかるわけです。ということは、同じように SqlCommand を使って update 文を使ってデータを変更すると、2 時間掛かるのではないか?という予想が経ちますね。
データ加工のたびに、こんなに時間が掛かってしまってはろくなデータ解析ができません。

で、この遅い理由としては、

  1. SqlCommand の呼び出しで、SQL Server との通信が入っている。
  2. SqlCommand の呼び出しで、.NET Framework とネイティブデータの変換が入っている。
  3. SqlCommand の呼び出しで、insert 文の解析が Sql Server で行われている。

が考えられるわけです。

1 の場合は、データ通信がなくなるように直接 SQL Server 上で行います。今回は、同じパソコン内でデータベースを動かしているので、あまり関係がないでしょう。
2 の場合は、C# でツールを作る限り駄目です。.NET Framework(実は Java も同じ)の場合は、.NET とネイティブデータの変換が必ず入ってしまうので、C/C++ で直接データを書き込むよりも遅くなるのは当たり前なのです。これは、SQL Server 自体が C/C++ で書かれている(内部データ自体は単なるバイナリ形式)であろうことから予想ができます。
3. SQL 文を解析しないようにして、bulk データを挿入できればよいのですが、あいにく SQL Server には bulk 以外にデータを挿入する方法がありません。SQLite みたいに SQL 文ではない形でデータ加工ができればよいんですけどね。

という訳で、2 時間の作業を短縮させるためには、2 のように .NET で書かないという方法を取らないと駄目なのです。

さて、ツールを作るならば C/C++ を使ってもよいのですが、結構面倒です(ADOを扱えばいいんですが、まぁ、面倒と言えば面倒)。なので、SQL Server で直接クエリを実行するようにします。

直接クエリを実行する場合は、SQL Server Management Studio でクエリ文を作るか、最終的にファンクションあるいはストアドプロシージャにしてしまうかです。

で、ざっくりと作ったの以下です。

CREATE function [dbo].[getltime]
( @d as varchar(50) )
returns datetime
as
begin
declare @dt varchar(50)
declare @i int
declare @day   varchar(10)
declare @month varchar(10)
declare @year  varchar(10)
declare @time  varchar(20)
declare @gmt   varchar(20)
declare @ltime datetime
declare @gtime datetime

if @d is null 
  return null

set @dt = @d
set @i = CHARINDEX( '/', @dt )
if @i = 0 
 return @d

set @day = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( '/', @dt )
if @i = 0 
 return @d
set @month = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( ':', @dt )
if @i = 0 
 return @d
set @year = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( ' ', @dt )
if @i = 0 
 return @d
set @time = SUBSTRING( @dt,0,@i )
set @gmt = RIGHT( @dt, len(@dt)-@i )
set @month = REPLACE(@month,'May','5')

set @gtime = CAST( @year+'-'+@month+'-'+@day+' '+@time as datetime )
if SUBSTRING(@gmt,0,1) = '+' 
begin
	set @gmt = (SUBSTRING(@gmt,2,2)+':'+SUBSTRING(@gmt,4,2))
	set @ltime = @gtime - @gmt
end
else
begin
	set @gmt = (SUBSTRING(@gmt,2,2)+':'+SUBSTRING(@gmt,4,2))
	set @ltime = @gtime + @gmt
end 

-- select @year, @month, @day, @time, @gmt, @ltime, @gtime , @gmt 
return @ltime
end

月の表示(May)が、いい加減ですが、まぁ汎用的に作るつもりはないので、これで良しとします。

これをあらかじめ作成しておいて、

update log set ltime = getltime([date]) 

で動かせばよいわけです。

これがどのくらいのスピードで動くのかというと、600 万件のデータを加工するに 3 分程度なんですね。
その、なんといいますか、ADO.NET が如何に遅いか、というのが分かるのです。いや、こんなに違うとは思わなかったのですが…ちょっと、.NET でデータベースを扱うときは、加工の仕方を考え直したほうがいいですね。

という話でした。

カテゴリー: 開発, C# | 5件のコメント

上海で実演したテストケースの方針(メモ)その2

上海で実演したテストケースの方針(メモ) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2217

の第2弾…のメモです。
先の例では、DAO(Data Access Object)なのでテスト対象は主に SQL になる訳ですが、今度は業務ロジック(business logic)を中心に解説します。

業務ロジックの場合は、基本は以下の 4 箇所がテストの対象になります。

  • メソッドのパラメータ
  • if 文による分岐
  • for 文による繰り返し
  • 内部メソッドの呼び出し

ここで、内部メソッドの呼び出しは既にテスト済みであることが前提です。

サンプル的には次のコードを頭に入れます。

// 引数は, id と name の 2 つ
public int func( int id, string name )
{
	// パラメータで判別
	if ( id == 0 ) {
		return -1;
	}
	// 内部関数の呼び出し
	DataTable dt = this.GetPersons( name );
	// 繰り返し処理
	foreach ( DataRow row in dt.Rows )
	{
		if ( id == (int)row["id"] ) {
			// 正常値
			return (int)row["age"];
		}
	}
	// 異常値
	return -1;
}

■テストコードを書く順番

前回の DAO と同じように、簡単なものから複雑なものへと進めていきます。前のテストコードがコピー&ペーストで使えるようにします。

0) TestZero

動作確認のテストコードを書きます。

1) TestOK

正常系が通るテストコードを書きます。

2) TestNG

戻り値が異常値となるテストコードを書きます。

3) TestParam

パラメータが変化した時の戻り値をチェックします。

4) TestIf

条件分岐に対するテストです。
switch-case による複数分岐もここに入れます(複数分岐の場合は、網羅性を追求せず、サンプリングします)

5) TestFor

繰り返しが1回の場合、複数回の場合、0回の場合をチェックします。

6) TestBorder

パラメータの変更による境界値試験です。

7) TestInner

内部メソッドに渡すパラメータが変化するパターンをチェックします。
内部メソッドについてテスト済みなので、パラメータより引き渡される条件で、2,3 ぐらいチェックすれば十分です。

まぁ、ざっとこんな感じでパターンを頭に入れておけば、OK ということで。

カテゴリー: 開発, xUnit | 上海で実演したテストケースの方針(メモ)その2 はコメントを受け付けていません

上海で実演したテストケースの方針(メモ)

上海出張して、NUnit の作り方を教え/実演してきました。

日本だと google で調べればそれなりに出てくるし「テスト技法」の本を読めば、それなりに分かるんですが、上海チームではそのあたりの資料が不足しております…という訳で、テストを作るときのメモを残しておきます。

■テストコードを書く手順

TDD(テスト駆動)を正確にやるためには、テストコード作成後、実コード(=テスト対象のコード)を書く、という手順になるのですが、時間的な制約やテストコードの記述の慣れ、テストケースの作り方の慣れ≒理解度、もあるので、まぁ、「実コードを書いてからテストコードを書いても構いません」という方針で説明しました。

その中で、ひとつルールとして守ってほしいのは、以下のひとつだけ。

「コードを書いたら、その日のうちにテストコードを書いて確認することッ!!!」

テストコード初心者がやりがちなのは、大量の実コードを書いた後に大量のテストコードを書くことです。全ての実コードを書いた後では、テストコードを書くと、時間が経ってしまっているのでテストが通らないときのデバッグに時間が掛かるし、開発の工数が少ないときにはテストコード自体がばっさり省略されてしまうことがあります。
なので、これを避けるために、その日のうちにテストコードを書いて確認します。
例えば、実コードを15分で書いたらテストコードを1時間という感じ。テスト自体は動作確認を兼ねるので、長めに時間を取ると良いでしょう。慣れれば、実コード 30 分、テストコード 30 分、的な配分も可能です。

■テストコードは省略可能である

QA(品質システム)としては、網羅性テストをしたほうが良いのですが、プロジェクトの状況によっては、網羅性よりも実品質を優先させます。いわゆる、QCD の Delivery を優先させるために、テストの Quality を落とします。
ええ、勿論、製品自体の Quality も落ちるので、そのバランスは大切です。

■テストコードを書く順番

これらを踏まえて、実コードに対してテストコードを書いていきます。

以下は、私の経験を踏まえた順番です(私自身の実コードの癖もあるので、それぞれ自分にあった形で進めてください)。

今回は、SQL 文を内部に持つ DAO(Data Access Object)のテストを例にとります。

0) TestZero

動作確認用のコードを書きます。

1) TestOK

正常動作ができるコードをひとつだけ書きます。

2) TestNG

TestOK とは反対の、エラーになるコードをひとつだけ書きます。

3) TestMulti

TestOK を利用して、複数行呼び出しのテストコードを書きます。

4) TestSelect

TestMulti を利用して、複数行から1行だけ取り出す選択のコードを書きます。

5) TestOrderBy

TestMulti を利用して、insert する順序を逆にしても正常に取れかテストします。

6) TestWhere

TestSelect を利用して、選択時のパラメータを変更させます。

7) TestJoin

TestMulti を利用して LEFT OUTER JOIN が意図通りに動いているかをチェックします。

8) TestMultiCall

TestOK, TestMulti を利用して、実コードを2回以上呼び出しても、問題ないことを確認します。

大体、こんな流れで作ると、単純なパターンから複雑なパターンにテストコードを書き進めることができます。
単純なコードがうまく動いていないと、複雑なパターンは動きませんからね。

実コードの複雑度によって、適度に省略しても構いません。

ちなみにテスト技法的な名前をつけると以下になります。

0) 動作確認
1) 正常系(単数)
2) 異常系(単数)
3) 正常系(複数)
4) 選択性(境界値)
5) 順序性
6) 条件選択(パラメータ指定)
7) 連結、他データとの関連
8) 複数回呼び出し(単一性)

# 正確な名前かどうかはさておき、こんな感じで網羅ができます。

■テストデータの作り方

テストするときのデータは、できるだけわかりやすいものにします。
例えば、業務に即したデータ(ID を “MSD02AX001” のようにするとか)を使う必要はありません。
むしろ、テストの邪魔になります。

正常系のID: “001” or 1
異常系のID: “999” or 999
正常系の名前: “masuda”
異常系の名前: “XXXXXX”

のように違っているとことが分かるように付けます。

// 正常系
public void TestOK() {
	obj.SetData("...")
	string id = obj.GetUserID();
	Assert.AreEqual( "masuda", id );
}

// 異常系
public void TestNG() {
	obj.SetData("...")
	string id = obj.GetUserID();
	Assert.AreEqual( "XXXXXX", id );
}

あと、テストデータを参照させるときは、【できる限り変数を使わないようにします】

// 即値を使う
public void TestOK() {
	obj.SetData("masuda")
	string id = obj.GetUserID();
	Assert.AreEqual( "masuda", id );
}

下記のように変数をつかってはいけません。

// 変数に入れてはいけない。
public void TestOK() {
    string TESTID = "masuda";
	obj.SetData( TESTID )
	string id = obj.GetUserID();
	Assert.AreEqual( TESTID , id );
}

変数に入れると、どれが正しいデータなのかが分かりづらくなります。
また、基本的にテストデータはコピー&ペーストで作るので、コピーした後にテストデータを手軽に変更することができなくなります。

という訳で、もろもろのメモを up しておきます。
整理するのは後ほど(時間があれば)。

カテゴリー: 開発, xUnit | 上海で実演したテストケースの方針(メモ) はコメントを受け付けていません

string へのキャストと ToString の動作は異なる

いつまでも危ない(?)PDFを晒しておくわけには行かないのでw、小ネタでブログを進めます。

時々(特にVBプログラマの方?)では、文字列へのキャストを ToString で行っている箇所を見掛けるのですが、これは危ういです、という話ですね。

例えば、データベースから読み込みをした時に VB だと、こういう風に書いているのです。

Dim da as new SqlDataAdapter("SELECT * ...", cn )
Dim dt as new DataTable
da.Fill(dt)
' 文字列を取り出す
dim name as String = ""
if dt.Rows.Count > 0 then
  name = dt.Rows(0)("name").ToString()
end if

こんな風に、ToString メソッドを使って object 型を文字列に変換しています。
ただし、本来はここはキャストを使うべきです。後述しますが、キャストと ToString メソッドの【意味】が異なるので、必ずしも同じ動作をするとは限らないのためです。

dim name as string = CType( dt.Rows(0)("name"), String )

C# の場合は、

string name = (string)dt.Rows[0]["name"];

# 詳細に言えば、dynamic cast を使うんでしょうが、ここでは普通のキャストで。

さて、ToString でも用途は足りるので、これでも良いような気がしますが、何故キャストを使わないといけないかというと、以下の理由があります。

dim name As String = obj.ToString()

とした時に、name には必ず期待する【文字列】が入るかというと、実は異なるのです。これは、ListBox を扱うと分かるのですが、ToString メソッドはオーバーライド可能なので、単純なキャストとは異なる値を入れることができるのです。

動作が分かるように極端な例を示すと、

Public Class AClass
    Public Shadows Function ToString() As String
        Return "ToString AClass"
    End Function

    Public Shared Narrowing Operator CType(ByVal b As AClass) As String
        Return "Cast AClass"
    End Operator
End Class

ToString をオーバーライドしたものと、キャストを再定義したものを用意します。
すると次のコードでは実行結果が違ってきます。

        Dim a As New AClass
        ' ToString の場合
        Debug.Print(a.ToString())
        ' Cast の場合
        Debug.Print(CType(a, String))

▼実行結果

ToString AClass
Cast AClass

こんな風に、ToString が定義されている時は、思ったとおりには動かないのです。まぁ、こういう時は、String 型へのキャスト自体も危ういところなのですが、ひとまず ToString とキャストは違う動作をする、ってことを覚えてコーディングして欲しいなぁ、と。

余談を言えば、キャストの場合は string 型にキャストできない場合は例外を発生させるけど、ToString 型は例外にはならない(多分ならない)ですよね。このあたり、意図して ToString メソッド(ToIntegerメソッドとかと同列の意味で)を使う分には ok ってことなのです。

カテゴリー: 開発 | string へのキャストと ToString の動作は異なる はコメントを受け付けていません

デスマーチを脱するための5つの方法(プレゼン)

デスマーチを脱するための5つの方法(メモ) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2184

国家的なデスマーチの問題と、ITプロジェクトのデスマーチの問題をいっしょに論じることはできない…とは思っていませんがw、まぁ、貼り付けてある画像は特に悪意がある訳でもなく、特別な意味がある訳でもありません。

PDF 版は、こちらからダウンロードしてください。







な感じで、頭の隅に入れておくとよいです。

カテゴリー: プロジェクト管理, 雑談 | 1件のコメント

メソッドチェーンではテスト項目数は減らない

コードを短くすると単体テストが楽になる、の検証編 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2167

の続きで、実は適切なコードの書き方をしないとテスト項目は減りません。

# マーチン・ファウラー氏の言う「流れるようにコーディングをする」ことと
# コード自体の品質とは別なのです。
# 当然、コーディングがしやすい、というのも品質を上げる要因のひとつ
# であることは変わりありませんが。

これを検証してみます。
まずは、以下の3つのパターンを見てください。

A.通常パターン

Window win = new Window(800,600);
win.SetMessage("hello world.");
win.Show(0,0);

B.プロパティで設定するパターン

Window win = new Window();
win.Width = 800;
win.Height = 600 ;
win.Message = "hello world.";
win.X = 0;
win.Y = 0;
win.Show();

C.メソッドチェーンのパターン

Window win = new Window(800,600).Message("hello world.").Show(0,0);

非常に極端に書いていますが、典型的なコーディングの3つのパターンです。

A の通常パターンは、コンストラクタでサイズを指定して、その後でメッセージを設定しています。
表示をするときには、Show メソッドの引数で指定する、という関数呼び出し風なパターンですね。

この関数呼び出し風のパターンは、一見わかりやすそうですが、ぱっと見たときに引数が何を意味するのか分からないという欠点があります。例えば、次のように XY 座標をコンストラクタで設定するコードを書いたときは、

Window win = new Window(0,0,800,600);
win.SetMessage("hello world.");
win.Show();

なのか、

Window win = new Window(800,600,0,0);
win.SetMessage("hello world.");
win.Show();

なのかは、わかりませんよね。経験上、X,Y,Width,Height)の順と知っていても、それはマニュアルなど読まないと本当は分からないわけで、引数の意味はコード上には出て来ないのが普通です。

# よく MSDN で引数が混在しているコンストラクタやメソッド(Drawメソッド系とか)がありますが、この現象に陥っています。

さて、B のようにプロパティで指定する方法が、VB2.0(.NETにあらず)ありました。VBA でもこんな書き方をするし、tcl/tk もこんな書き方です。この書き方の欠点は、縦に長くなる(行数が多くなる)ということですね。大抵の場合、1 行にひとつの処理を書くことが多いので、A のコードよりも長くなるのが欠点です。
ただし、VB の場合は「:」を使ったり、C# の場合は「;」で複数行をまとめることができるので、

Window win = new Window();
win.Width = 800; win.Height = 600 ;
win.Message = "hello world.";
win.X = 0; win.Y = 0;
win.Show();

このように書いて、行数を短くすることができるのですが、A と B のパターンは決定的に違うところがあります。
A の場合は、Width と Height を同時に指定することができるのですが、B は、ひとつずつしか指定できません。なので、画面を表示したままのときに、Width, Height の順番に設定すると、横幅が変わった後に、縦幅が変わる、という現象が起こるのですね(実際、VB2.0 の頃には、そのような現象が発生していました)。なので、Width と Height の順番が重要で、実は変な感じなのです。

次は、最近流行(?)のメソッドチェーンの C パターンを見ていくと、あたかも 1 行で書けるために A パターンよりもすっきりしているように見えます。
コンストラクタで画面サイズを指定して、メッセージを追加して、画面の左上に表示するということがわかります。

ただし、これもメソッドの作り方が悪い、先の B のように順序が関係してきます。

Window win = new Window().Width(800).Height(600).Message("hello world.").X(0).Y(0).Show();

何をやっているのか、分かるような分からないような微妙なコードになります。C# の場合は、改行を入れることで少し読み易くできます。

Window win = new Window()
 .Width(800)
 .Height(600)
 .Message("hello world.")
 .X(0)
 .Y(0)
 .Show();

あれ?これって、B のパターンと同じですよね。
また VB.NET の場合は、With を使うと、B のプロパティのパターンを C のメソッドチェーンのように書けます。

Dim win = new Window();
With win
 .Width = 800
 .Height = 600
 .Message = "hello world."
 .X = 0
 .Y = 0
 .Show()
End With

ということはですね。実は、B のパターンで書いても、C のパターンで書いても、同じ数のテストをこなさないといけないのです。自由度が変わらないですからね。

じゃあ、A の通常パターンで書いた時には一番テストの数が少ないかというと、そうでもありません。

Window win = new Window(800,600);
win.SetMessage("hello world.");
win.Show(0,0);

1. コンストラクタの Window の引数は Width,Height なのか? Height,Width なのか?
2. Show メソッドの引数は、X,Y なのか? Y,X なのか?

という自由度が増えています。
実は、Window(0,0,800,600) としたときには、自由度が倍増します。実際コーディングをいくと実感できると思いますが、

func( int x, int y );
func( int x, string s );

ような 2 つの関数があったときには、int, int よりも、int, string のほうが間違いが減ります。
というのも、int, int の場合は、引数の順序を間違える可能性がありますが、int, string の場合はコンパイル時にはじかれてしまいますよね。これは、string, int のように逆のメソッドを作ることもできることを意味します(勿論、int,string と string,int の 2 つのメソッドが共存する場合は、どちらがどちらの意味なのかという別の自由度が発生しますが)。

というわけで、結論を言えば、A も B も C もテスト項目は同じぐらいになります。ゆえに、複雑度が同じという推測が立ちます。
推測を証明するために、先のようにメソッドと変数の数を勘定すると、

A パターン: 10
B パターン: 11
C パターン: 9

という数になります(引数が2つある場合は、それぞれの引数と交換を含めて自由度は3で勘定します)。
メソッドチェーンを使った場合は、若干小さくなりますが、思ったほど楽にならない、というのが検証できると思います。
まぁ、それでもインテリセンスが利いている最近の統合開発環境では、ピリオドを打った後にメソッドやプロパティ名が自動で出てくるのはいいですよね。メソッドの多重化が過ぎて 10 以上のメソッドから選ばないといけない、というアンチパターンよりいいと思います。

↓これとか、しんどくて…

Graphics.DrawImage メソッド (System.Drawing)
http://msdn.microsoft.com/ja-jp/library/9f3f6hy1.aspx

カテゴリー: 開発 | メソッドチェーンではテスト項目数は減らない はコメントを受け付けていません