Как сделать Distinct по DataTable с помощью DataView.ToTable

РазделДанные и привязкаУровеньНовичок Чтение6 минОпубликовано ТегиASP.NETDataTableVB.NET

Нужно получить уникальные значения из DataTable, а в базу за ними идти уже поздно? С .NET 2.0 для этого есть встроенный метод, и самописная сортировка с удалением дублей больше не нужна.

Главное. Для уникальных значений из DataTable не нужен самописный цикл: DataView.ToTable(True, колонки) учитывает фильтр и сортировку и возвращает новую таблицу. Если данных много, Distinct выгоднее сделать в SQL.

Откуда берётся задача

Обычный сценарий: страница уже получила из базы DataTable с заказами, и теперь из тех же данных нужно заполнить выпадающий список городов или стран. Второй запрос SELECT DISTINCT к серверу выглядит расточительно, особенно если таблица пришла из кеша или из веб-сервиса, а не из SQL.

Во времена .NET 1.0 и 1.1 встроенного решения не было. На форумах советовали писать свою функцию: отсортировать строки по нужным колонкам, пройти циклом, сравнивать каждую строку с предыдущей и удалять совпадения. Такой код работает, но он длинный, его легко сломать при смене набора колонок, а сравнение с DBNull в нём обычно забывают.

Если вы сопровождаете такой код, не спешите переписывать всё сразу: сначала найдите все места, где вызывается самописная функция, и проверьте, по скольким колонкам она на самом деле должна считать уникальность.

Старый способ: сортировка и сравнение

Для понимания полезно увидеть, от чего мы избавляемся. Вот компактная версия ручного алгоритма для одной колонки. Её ещё можно встретить в старых проектах, и при сопровождении такую функцию стоит заменить.

LegacyDistinct.vbVB.NET
' Ручной Distinct по одной колонке (.NET 1.x)
Public Function DistinctOld(src As DataTable, col As String) As DataTable
    Dim result As DataTable = src.Clone()
    Dim rows() As DataRow = src.Select("", col) ' сортировка
    Dim prev As Object = Nothing

    For Each r As DataRow In rows
        ' сравниваем с предыдущим значением
        If prev Is Nothing OrElse Not prev.Equals(r(col)) Then
            result.ImportRow(r)
            prev = r(col)
        End If
    Next
    Return result
End Function
Обратите внимание: такая функция копирует строку целиком, но уникальность проверяет только по одной колонке. В выходной таблице остаются «случайные» значения остальных полей. Это частый источник тихих ошибок.

Встроенное решение: ToTable(True, ...)

Начиная с .NET Framework 2.0 у класса DataView есть перегрузка ToTable(distinct As Boolean, ParamArray columnNames() As String). Первый параметр включает удаление дублей, остальные перечисляют колонки, которые попадут в новую таблицу. Уникальность считается по комбинации именно этих колонок.

DataView можно не создавать вручную: у каждой таблицы есть готовое представление DefaultView. Результат всегда новая независимая таблица, исходные данные не меняются.

Orders.aspx.vbVB.NET
' уникальные города из уже загруженных заказов
Dim cities As DataTable =
    ordersTable.DefaultView.ToTable(True, "ShipCity")

ddlCity.DataSource = cities
ddlCity.DataTextField = "ShipCity"
ddlCity.DataValueField = "ShipCity"
ddlCity.DataBind()

' уникальные пары страна + город
Dim pairs As DataTable =
    ordersTable.DefaultView.ToTable(True, "ShipCountry", "ShipCity")

Фильтр и сортировка до Distinct

ToTable учитывает текущее состояние DataView: RowFilter, Sort и RowStateFilter. Поэтому порядок действий простой. Сначала создаёте представление, задаёте фильтр и сортировку, затем вызываете ToTable. Результат будет и отфильтрован, и упорядочен.

Лучше заводить отдельный New DataView, а не менять DefaultView. Представление по умолчанию может использоваться другими контролами на странице, и неожиданный фильтр в нём превращается в трудноуловимую ошибку.

Orders.aspx.vbVB.NET
Dim view As New DataView(ordersTable)
view.RowFilter = "ShipCountry = 'Germany'" ' только Германия
view.Sort = "ShipCity ASC"                 ' по алфавиту

Dim germanCities As DataTable = view.ToTable(True, "ShipCity")

' пример с переменной: экранируем апострофы
Dim country As String = txtCountry.Text.Replace("'", "''")
view.RowFilter = String.Format("ShipCountry = '{0}'", country)

Подводные камни

Метод простой, но у него есть особенности, о которых полезно знать заранее:

  • строки сравниваются с учётом свойства CaseSensitive таблицы: по умолчанию «Berlin» и «berlin» считаются одинаковыми, при CaseSensitive = True это разные значения;
  • DBNull считается обычным значением, и одна строка с пустым городом в результате останется;
  • пробелы в конце строки делают значения разными, так что «Berlin » и «Berlin» дадут две записи;
  • в результат не попадают колонки, не перечисленные в параметрах, поэтому взять «первый попавшийся» OrderID вместе с уникальным городом этим методом нельзя;
  • для таблиц на сотни тысяч строк ToTable заметно нагружает память: в таком случае лучше выполнить DISTINCT в SQL.

Альтернатива через LINQ

В проектах на .NET 3.5 и новее тот же результат получают через LINQ to DataSet. Это удобно, когда нужен не DataTable, а просто список значений, например для привязки к контролу или для дальнейшей обработки в коде.

Для сравнения целых строк есть DataRowComparer.Default, он сравнивает все поля. Если же нужна таблица на выходе, CopyToDataTable превращает последовательность строк обратно в DataTable.

Orders.aspx.vbVB.NET
' Imports System.Data.DataSetExtensions
Dim cityList As List(Of String) =
    ordersTable.AsEnumerable().
        Select(Function(r) r.Field(Of String)("ShipCity")).
        Where(Function(c) Not String.IsNullOrWhiteSpace(c)).
        Select(Function(c) c.Trim()).
        Distinct(StringComparer.OrdinalIgnoreCase).
        OrderBy(Function(c) c).
        ToList()

Как это делают сегодня

В новом коде DataTable встречается всё реже: данные приходят как список объектов из EF Core или Dapper. Уникальные значения получают в запросе (Select(...).Distinct() транслируется в SQL DISTINCT), а в памяти с .NET 6 есть удобный DistinctBy, который оставляет первый объект для каждого ключа. Это закрывает и случай «уникальный город плюс любой заказ», для которого ToTable не годится.

Вопросы и ответы

Сохраняется ли порядок строк после ToTable(True)?

Да, строки идут в порядке DataView, то есть с учётом Sort. Если сортировка не задана, порядок соответствует исходной таблице.

Можно ли сделать Distinct по одной колонке, но вернуть все поля?

ToTable так не умеет. Используйте LINQ: группировку GroupBy по ключу с выбором первой строки группы или DistinctBy в .NET 6 и новее.

Меняет ли ToTable исходную таблицу?

Нет. Метод всегда создаёт новую независимую DataTable, исходные строки и их состояние остаются прежними.

Читайте также