none
LINQ строка с максимальным значением по столбцу RRS feed

  • Вопрос

  • Здравствуйте,
    Нужно найти по части таблицы значение столбца С1, соответствующее максимальному значению по столбцу С2.
    Или, на крайний случай, строку, содержащую максимум по С2.
    Конструкция типа

    var qq = dt.AsEnumerable().
                    Where(бла-бла-бла).
                    Select(r => new
                    {
                        с1 = r.Field<double>("с1"),
                        с2 = r.Field<double>("с2")
                    }).
                    Max(r => r.с2);
    

    К сожалению возвращает только максимум С2. А нужна пара С1-С2...

    На примере
    Есть часть таблицы
    С1 С2
    1 11
    -1 222
    3 33
    Как найти пару С1=-1, С2=222?
    PS Массив большой, поэтому хотелось бы обойтись без сортировки...

    abb269 на форуме Обратить внимание администрации на это сообщение

Ответы

  • Давайте по порядку.

    Если написать просто:

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            с1 = r.Field<double>("c1"),
            с2 = r.Field<double>("c2")
        })
        .Max(r => r); // или .Max();

    то будет выброщено исключение ArgumentException: должен быть реализован интерфейс IComparable.

    У анонимных типов этот интерфейс не реализован, потому что неизвестно, по каким именно параметрам их сравнивать.

    -----

    Становится понятно, что вместо анонимного типа нужно использовать такой тип, в котором этот интерфейс реализован. Например, Tuple:

    var qq = dt.AsEnumerable()
        .Select(r => Tuple.Create(
            r.Field<double>("c1"),
            r.Field<double>("c2")))
        .Max();

    Вуаля! Всё работает. Однако, сравнение по умолчанию будет производиться по всем полям кортежа. А вам нужно только по второму.

    -----

    Создадим свой класс с нужными свойствами и заданным компаратаром:

    public class Pair : IComparable<Pair>
    {
        public double C1 { get; set; }
        public double C2 { get; set; }
    
        public int CompareTo(Pair other)
        {
            return this.C2.CompareTo(other.C2);
        }
    }

    Как видите, в нём сравнение идёт именно по свойству C2.

    var qq = dt.AsEnumerable()
        .Select(r => new Pair
        {
            C1 = r.Field<double>("c1"),
            C2 = r.Field<double>("c2")
        })
        .Max();

    Теперь запрос работает, как и ожидалось.

    -----

    Можно ли обойтись без написания отдельного класса? Можно.

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .First(a => a.c2 ==
            dt.AsEnumerable().Max(r => r.Field<double>("c2")));

    Пожалуйста, - одним запросом! Однако, эффективность этого варианта очень низка для коллекций в памяти (Enumerable), т. к. происходит множество проходов.

    Однако, всё кардинально меняется, если такой же запрос выполняется через linq2sql/EF к базе данных (dt - таблица БД, AsEnumerable ни в коем случае не нужен). Выполнением этого запроса в виде sql будет заниматься движок СУБД. Оптимизатор плана преобразует его в такой вид, что многократных проходов по таблице не будет. А вот запрос всего один (это важно)!

    Для коллекций в памяти намного эффективней сделать два запроса (как вы сами упомянули в одном из сообщений):

    var max = dt.AsEnumerable().Max(r => r.Field<double>("c2"));
    
    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .First(a => a.c2 == max);

    Это намного эффективнее, чем предыдущий вариант.

    Однако, в случае с БД (IQueryable) этот вариант менее эффективен! Потому что запросов уже два! А при работе с удалённым базами именно ожидание раунд-трипа зачастую составляет большую часть времени.

    Так как LINQ работает с разными источниками данных (объекты в памяти, датасеты, xml, базы данных), то нужно учитывать их особенности. Именно поэтому такая мощная технология нуждается в ручной подстройке: для разных источников разные запросы.

    -----

    Ещё один способ. Устанавливаем популярную библиотеку MoreLinq.

    Открываем пространство имён:

    using MoreLinq;


    Пишем:

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .MaxBy(a => a.c2);


    Метод расширения MaxBy производит сравнение по указанному свойству, возвращая при этом весь объект.



    4 июля 2017 г. 14:47

Все ответы

  • Добрый день.

    Шаг 1 вы уже сделали, нашли максимальное. Шаг 2 вам нужно воспользоваться First, для того, чтобы найти строку у которой в C2 находится найденное на шаге 1 значение.

    Кстати, без LINQ, обычным циклом будет быстрее, т.к. уже при первом проходе вы можете сразу запоминать строку.

    Отвечающий
  • Правильно ли понимать, что то, что мне нужно, можно получить только в два прохода LINQ (сначала ищем максимум, потом - строку с этим значением)?

    Тогда цикл технологичнее, конечно.

    ЗЫ Просто кажется странным, что в такой мощной технологии нету решения для в общем-то простой задачи

  • LINQ - это набор расширяющих методов, которые решают типовые задачи. В принципе, никто не мешает вам написать свой расширяющий метод и пользоваться им (если эта задача у вас возникает часто). Ну а внутри (если не IQuerable) вам лучше подойдет цикл.
    Отвечающий
  • Давайте по порядку.

    Если написать просто:

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            с1 = r.Field<double>("c1"),
            с2 = r.Field<double>("c2")
        })
        .Max(r => r); // или .Max();

    то будет выброщено исключение ArgumentException: должен быть реализован интерфейс IComparable.

    У анонимных типов этот интерфейс не реализован, потому что неизвестно, по каким именно параметрам их сравнивать.

    -----

    Становится понятно, что вместо анонимного типа нужно использовать такой тип, в котором этот интерфейс реализован. Например, Tuple:

    var qq = dt.AsEnumerable()
        .Select(r => Tuple.Create(
            r.Field<double>("c1"),
            r.Field<double>("c2")))
        .Max();

    Вуаля! Всё работает. Однако, сравнение по умолчанию будет производиться по всем полям кортежа. А вам нужно только по второму.

    -----

    Создадим свой класс с нужными свойствами и заданным компаратаром:

    public class Pair : IComparable<Pair>
    {
        public double C1 { get; set; }
        public double C2 { get; set; }
    
        public int CompareTo(Pair other)
        {
            return this.C2.CompareTo(other.C2);
        }
    }

    Как видите, в нём сравнение идёт именно по свойству C2.

    var qq = dt.AsEnumerable()
        .Select(r => new Pair
        {
            C1 = r.Field<double>("c1"),
            C2 = r.Field<double>("c2")
        })
        .Max();

    Теперь запрос работает, как и ожидалось.

    -----

    Можно ли обойтись без написания отдельного класса? Можно.

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .First(a => a.c2 ==
            dt.AsEnumerable().Max(r => r.Field<double>("c2")));

    Пожалуйста, - одним запросом! Однако, эффективность этого варианта очень низка для коллекций в памяти (Enumerable), т. к. происходит множество проходов.

    Однако, всё кардинально меняется, если такой же запрос выполняется через linq2sql/EF к базе данных (dt - таблица БД, AsEnumerable ни в коем случае не нужен). Выполнением этого запроса в виде sql будет заниматься движок СУБД. Оптимизатор плана преобразует его в такой вид, что многократных проходов по таблице не будет. А вот запрос всего один (это важно)!

    Для коллекций в памяти намного эффективней сделать два запроса (как вы сами упомянули в одном из сообщений):

    var max = dt.AsEnumerable().Max(r => r.Field<double>("c2"));
    
    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .First(a => a.c2 == max);

    Это намного эффективнее, чем предыдущий вариант.

    Однако, в случае с БД (IQueryable) этот вариант менее эффективен! Потому что запросов уже два! А при работе с удалённым базами именно ожидание раунд-трипа зачастую составляет большую часть времени.

    Так как LINQ работает с разными источниками данных (объекты в памяти, датасеты, xml, базы данных), то нужно учитывать их особенности. Именно поэтому такая мощная технология нуждается в ручной подстройке: для разных источников разные запросы.

    -----

    Ещё один способ. Устанавливаем популярную библиотеку MoreLinq.

    Открываем пространство имён:

    using MoreLinq;


    Пишем:

    var qq = dt.AsEnumerable()
        .Select(r => new
        {
            c1 = r.Field<double>("c1"),
            c2 = r.Field<double>("c2")
        })
        .MaxBy(a => a.c2);


    Метод расширения MaxBy производит сравнение по указанному свойству, возвращая при этом весь объект.



    4 июля 2017 г. 14:47