技術memo

関数型ゴースト

LINQでバッファリングしてみる

結局何がやりたいのかはよくわからなかったのですが、LINQ楽しいよLINQ、というお話です。たぶん。

前置き

プログラムから動的に生成したSQLをDBに問い合わせすることって、よくあると思うんです。 例えばこういうSQLについて。

select * from product where product_id in (@0, @1, @2 ... )

ここで、パラメータ@0, @1, @2 ...はプログラム中で生成されるため、個数は実行時までわからない、とします。

ところで、SQLのIN条件に指定できるパラメータの個数には、DBMSによっては上限があります。確かOracleだと1000個だとか、そんな情報を読んだ気がします。しかし、エラーになってしまっては困ります。

プログラム

ここでプログラムの方を見てみます。

public class Product
{
    public string Id { set; get; }
    public string Name { set; get; }
    public DateTime OnSale { set; get; }
    public decimal Price { set; get; }

    public static IEnumerable<Product> Fetch(IDbConnection connection, IEnumerable<string> productIds)
    {
        return connection.Query<Product>(
            "select * from product where product_id in (@id)",
            new { id = productIds });
    }
}

ORマッパーにはDapperあたりをイメージしておきましょう。IN条件も自動で展開してくれます。

Queryメソッドシグネチャはこんな感じです

    public static IEnumerable<T> Query<T>(this IDbConnection c, string sql, object parameters)
    {
        throw new NotImplementedException();
    }

しかし、このままだと前述のようにパラメータが増えすぎるとエラーになります。死にます。

バッファリングしてみる

Buffer関数はこちらを参考に

    public static IEnumerable<IEnumerable<T>> Buffer<T>(this IEnumerable<T> source, int count)
    {
        if (source == null) throw new ArgumentNullException("source");
        return BufferImplements(source, count);
    }
    private static IEnumerable<IEnumerable<T>> BufferImplements<T>(IEnumerable<T> source, int count)
    {
        var result = new List<T>(count);
        foreach (var item in source)
        {
            result.Add(item);
            if (result.Count == count)
            {
                yield return result;
                result = new List<T>(count);
            }
        }
        if (result.Count != 0)
            yield return result;
    }

このような拡張メソッドをあらかじめ定義しておいて、

    public static IEnumerable<Product> Fetch(IDbConnection connection, IEnumerable<string> productIds)
    {
        return productIds.Buffer(1000)
            .Select(ids =>
                connection.Query<Product>(
                    "select * from product where product_id in (@id)",
                    new { id = ids }))
            .SelectMany(x => x);
    }

と、このようにすれば、パラメータの一定個数ごとにSQLを実行して、結果をまとめて取得できる、というわけです。めでたし、めでたし。

現実

実際のところSQLのパラメータに1000個も2000個も渡したらパフォーマンスがひどいことになりそうです。よくよく思い出すと身に覚えがあります。

なので、パフォーマンスを考えるなら以下のようなSQLを書きましょうか。

select * from product where product_id exists(select product_id from sale where ...(何らかの条件))

でも、これって設計上はあまり良くなさそうです。productテーブルの取得条件がsaleテーブルの条件に大きく依存してしまいます。

そもそもの話、1000個以上もデータを取得して表示するのはUI設計が悪い、ページングするべき、という議論がありそうな気もします。 集計処理のためにJOINしたデータが必要というのなら、「集計処理」としてコードをまとめるのが筋でしょうから、モデルごとのコードの依存性云々などと悩む必要はなさそうですし。

何か良い参考書籍をご存知の方がいらっしゃいましたらお報せください。