DataTableの一部の列でのDistinct
DataTableの一部の列でDistinctするコードを書いたので、記録しておく。
まずはコード
using System; using System.Collections.Generic; using System.Data; using System.Linq; namespace Sample { /// <summary> /// <c>System.Data</c> 名前空間の型に対する拡張メソッドを定義するクラス。 /// </summary> internal static class DataExtensions { #region メソッド #region public #region Distinct:データテーブル内のデータ行の重複を排除 #region 全ての列を使う /// <summary> /// 指定されたデータテーブルに含まれるデータ行の、重複を排除したデータテーブルを返す。 /// </summary> /// <param name="table">データテーブル。</param> /// <returns> /// <paramref name="table"/> が含むデータ行の、重複を排除したデータテーブル。 /// </returns> /// <exception cref="ArgumentNullException"> /// <paramref name="table"/> に <c>null</c> が指定された。 /// </exception> /// <remarks> /// <para> /// 重複の排除は、<see cref="Enumerable.Distinct<TSource>(IEnumerable<TSource>, /// IEqualityComparer<TSource>)"/> を使って行う。 /// <see cref="IEqualityComparer<TSource>"/> として、<see cref="DataRowComparer.Default"/> /// を指定する。 /// </para> /// <para> /// 指定されたデータテーブルに含まれる行数が <c>1</c> 以下の場合、 /// 指定されたデータテーブルをそのまま返す。 /// </para> /// </remarks> public static DataTable Distinct(this DataTable table) { return table.Distinct(null); } #endregion #region 使用する列名を指定 /// <summary> /// 指定されたデータテーブルに含まれるデータ行の、重複を排除したデータテーブルを返す。 /// </summary> /// <param name="table">データテーブル。</param> /// <param name="columnNames"> /// <paramref name="table"/> が含むデータ行の、 /// 重複を排除するために比較する必要がある列名の配列。 /// </param> /// <returns> /// <paramref name="table"/> が含むデータ行の、重複を排除したデータテーブル。 /// </returns> /// <exception cref="ArgumentNullException"> /// <paramref name="table"/> に <c>null</c> が指定された。 /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="columnNames"/> が <paramref name="table"/> /// に存在しない列名を含む。 /// </exception> /// <remarks> /// <para> /// 重複の排除は、<see cref="Enumerable.Distinct<TSource>(IEnumerable<TSource>, /// IEqualityComparer<TSource>)"/> を使って行う。 /// <see cref="IEqualityComparer<TSource>"/> として、 /// <paramref name="columnNames"/> で指定された全ての列の値を比較して、 /// データ行同士が等しいかどうかを判定するオブジェクトを指定する。 /// ただし、<paramref name="columnNames"/> が <c>null</c>、または要素を含まない場合、 /// <see cref="DataRowComparer.Default"/> を指定する。 /// </para> /// <para> /// 指定されたデータテーブルに含まれる行数が <c>1</c> 以下の場合、 /// 指定されたデータテーブルをそのまま返す。 /// </para> /// </remarks> public static DataTable Distinct(this DataTable table, params string[] columnNames) { ExceptionUtils.ThrowArgumentNull(table, "table"); if (table.Rows.Count <= 1) { return table; } if (columnNames.IsNullOrEmpty()) { return table.AsEnumerable().Distinct(DataRowComparer.Default).CopyToDataTable(); } var indexes = table.Columns.IndexesOf(columnNames); var equalityComparer = new DataRowEqualityComparer(indexes); return table.AsEnumerable().Distinct(equalityComparer).CopyToDataTable(); } #endregion #endregion #endregion #region private #region IndexesOf:列名の配列からインデックスの配列を取得 /// <summary> /// 指定された列名の配列から、それぞれの列の <see cref="DataColumnCollection"/> /// 内でのインデックスの配列を取得する。 /// </summary> /// <param name="columns"><see cref="DataColumnCollection"/> オブジェクト。</param> /// <param name="columnNames">列名の配列。</param> /// <returns><paramref name="columnNames"/> に対応するインデックスの配列。</returns> /// <exception cref="ArgumentException"> /// <paramref name="columnNames"/> が、存在しない列名を含む。 /// </exception> private static int[] IndexesOf(this DataColumnCollection columns, string[] columnNames) { var indexes = new int[columnNames.Length]; for (var i = 0; i < columnNames.Length; i++) { var index = columns.IndexOf(columnNames[i]); if (index == -1) { throw new ArgumentException( string.Format(Properties.Resources.DataColumnNameNotFound, columnNames[i]), "columnNames"); } indexes[i] = index; } return indexes; } #endregion #endregion #endregion #region 入れ子になった型 #region DataRowEqualityComparer:IEqualityComparer<DataRow> 実装クラス /// <summary> /// <see cref="IEqualityComparer<T>"/> のジェネリック型引数に /// <see cref="DataRow"/> を指定したインターフェイスの実装クラス。 /// </summary> private class DataRowEqualityComparer : IEqualityComparer<DataRow> { #region コンストラクタ /// <summary> /// <see cref="DataRowEqualityComparer"/> の新しいインスタンス初期化する。 /// </summary> /// <param name="columnIndexes"> /// <see cref="DataRow"/> の等値比較に使用する列インデックスの配列。 /// </param> public DataRowEqualityComparer(int[] columnIndexes) { _columnIndexes = columnIndexes; } #endregion #region フィールド /// <summary> /// 列インデックスの配列。 /// </summary> private readonly int[] _columnIndexes; #endregion #region メソッド #region Equals:指定したオブジェクトが等しいかどうかを判断 /// <summary> /// 指定された <see cref="DataRow"/> 同士が等しいかどうかを判定する。 /// </summary> /// <param name="x"><see cref="DataRow"/> オブジェクト。</param> /// <param name="y"><see cref="DataRow"/> オブジェクト。</param> /// <returns> /// 指定された <see cref="DataRow"/> 同士が等しいかどうか: /// <list type="bullet"> /// <item><description><c>true</c>:等しい</description></item> /// <item><description><c>false</c>:等しくない</description></item> /// </list> /// </returns> public bool Equals(DataRow x, DataRow y) { return _columnIndexes.All((columnIndex) => object.Equals(x[columnIndex], y[columnIndex])); } #endregion #region GetHashCode:このインスタンスのハッシュコードを返す /// <summary> /// 指定された <see cref="DataRow"/> のハッシュコードを返す。 /// </summary> /// <param name="dataRow"><see cref="DataRow"/> オブジェクト。</param> /// <returns>指定された <see cref="DataRow"/> のハッシュコード。</returns> /// <exception cref="T:System.ArgumentNullException"> /// <paramref name="dataRow"/> に <c>null</c> が指定された。 /// </exception> public int GetHashCode(DataRow dataRow) { ExceptionUtils.ThrowArgumentNull(dataRow, "dataRow"); switch (_columnIndexes.Length) { case 1: return Utils.GetHashCode(dataRow[_columnIndexes[0]]); case 2: return Utils.GetHashCode(dataRow[_columnIndexes[0]], dataRow[_columnIndexes[1]]); case 3: return Utils.GetHashCode(dataRow[_columnIndexes[0]], dataRow[_columnIndexes[1]], dataRow[_columnIndexes[2]]); } var builder = new HashCodeBuilder(); foreach (var columnIndex in _columnIndexes) { builder.Append(dataRow[columnIndex]); } return builder.ToHashCode(); } #endregion #endregion } #endregion #endregion } }
解説
このコードの目的は、DataTableのDistinctの高速化。単にDataTableのDistinctをしたいだけなら、このコードのcolumnNamesを指定しない場合のように、DataRowComparer.Defaultを使用すれば可能。しかしその場合、おそらく全ての列の値を比較してDistinctしているはずなので、一部の列値だけでDistinctできる場合には、パフォーマンス上の無駄がある。
パフォーマンス計測結果
以下のようなDataTableで、DataRowComparer.Defaultを使用したDistinctと、上のコードのcolumnNamesを指定したDistinctの処理時間を比較した。結果、後者が前者の5倍程度高速だった。
パフォーマンス計測に使用したDataTable:
- 列数:500
- Distinctに必要な列数:1
- Distinct後の行数:1,000
- Distinct前の行数:10,000
計測結果(時間の単位:ミリ秒):
n回目 | DataRowComparer.Default | columnNames指定 |
1 | 1921 | 352 |
2 | 1716 | 372 |
3 | 1732 | 362 |
4 | 1668 | 286 |
5 | 1712 | 290 |
コメント
- 特定のプロジェクト用に書いたコードなので、列の値が配列の場合に対応していない
- DataRowEqualityComparerのコンストラクタ引数がint[]なのは高速化のため。DataRow.Itemプロパティ値の取得は、string指定よりint指定の方が高速
- DataRowEqualityComparerはprivateクラスなのがポイント。外から見えるクラスにするなら、もっと防御的でなくてはならない。privateクラスなので、高速化を優先できる
- ExceptionUtils.ThrowArgumentNull、Utils.GetHashCode、HashCodeBuilderなど、載せていないコードがある