atsukanrockのブログ

Microsoft系技術を中心にぼちぼち更新します

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&lt;TSource&gt;(IEnumerable&lt;TSource&gt;,
        /// IEqualityComparer&lt;TSource&gt;)"/> を使って行う。
        /// <see cref="IEqualityComparer&lt;TSource&gt;"/> として、<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&lt;TSource&gt;(IEnumerable&lt;TSource&gt;,
        /// IEqualityComparer&lt;TSource&gt;)"/> を使って行う。
        /// <see cref="IEqualityComparer&lt;TSource&gt;"/> として、
        /// <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&lt;T&gt;"/> のジェネリック型引数に
        /// <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など、載せていないコードがある