Доброго всем.
В данный момент переписываю метод , в котором используются DataSet'ы и DataTable'ы.
Суть простая. Из БД получаем некий начальный курсор данных (DataSet). Далее в методе происходит добавление строк к DataSet.Tables[0] при выполнении определенных условий. При этом для исключения дублирования строк применяется DataTable.Select(). Код выглядит примерно так:
string filter = string.Format("TYPE_CODE={0} AND DETAIL_ID={1}", addingRowInfo.TypeCode, addingRowInfo.DetailID);
if (dataSet.Tables[0].Select(filter).Length == 0)
{
// добавление новой строки в dataSet.Tables[0]
}
И так пару десятков раз для каждого типа добавляемой строки (фильтр может быть различным).
Я пришел к выводу, что мне, по сути, нужен всего лишь List<T> вместо DataTable'а (я уж молчу про DataSet). Я задался вопросом: насколько быстро будет работа с List<T>.Any по сравнению с DataTable.Select.
Код
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
namespace DataTableSelect
{
static class DataTableExtensions
{
public static DataTable ToDataTable<T>(this IList<T> data)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
foreach ( PropertyDescriptor prop in properties )
{
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
}
foreach ( T item in data )
{
DataRow row = table.NewRow();
foreach ( PropertyDescriptor prop in properties )
{
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
}
table.Rows.Add(row);
}
return table;
}
}
public class MyData
{
public int Code { get; set; }
public string Period { get; set; }
public int TypeCode { get; set; }
public int DetailID { get; set; }
public override string ToString()
{
return string.Format("Code: {0}; Period: {1}; TypeCode: {2}; DetailID: {3};", Code, Period, TypeCode, DetailID);
}
}
class Program
{
static void Main()
{
Console.WindowWidth = 120;
int count = 1 * 1000;
List<MyData> myList = CreateTestList();
DataTable table = myList.ToDataTable();
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
count = 2 * 1000;
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
count = 4 * 1000;
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
count = 8 * 1000;
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
count = 16 * 1000;
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
count = 100 * 1000;
GetResultsForList(count, myList);
GetResultsForTableWithLINQ(count, table);
GetResultsForTableWithSelect(count, table);
Console.WriteLine("Done...");
Console.ReadLine();
}
private static void GetResultsForList(int count, List<MyData> myList)
{
Measure("List",
() =>
{
for ( int i = 0; i < count; i++ )
{
foreach ( var myData in myList )
{
if ( myData.Period == "01.01.2016" && myData.DetailID == 10 )
{
break;
}
}
}
return count;
});
}
private static void GetResultsForTableWithLINQ(int count, DataTable table)
{
Measure("Table LINQ",
() =>
{
for ( int i = 0; i < count; i++ )
{
var q = table.AsEnumerable().Where(row => row.Field<string>("Period") == "01.01.2016" && row.Field<int>("DetailID") == 10);
var found = q.ToList().Count != 0;
}
return count;
});
}
private static void GetResultsForTableWithSelect(int count, DataTable table)
{
string filter2 = "Period='01.01.2016' AND DetailID=10";
Measure("Table Select",
() =>
{
for ( int i = 0; i < count; i++ )
{
var found = table.Select(filter2).Length != 0;
}
return count;
});
Console.WriteLine("=========================================================================");
}
private static List<MyData> CreateTestList()
{
List<MyData> foos = new List<MyData>();
for ( int i = 0; i < 20; i++ )
{
foos.Add(new MyData
{
Code = i,
TypeCode = i,
Period = i == 10 ? "01.01.2016" : string.Empty,
DetailID = i == 10 ? 10 : 0
});
}
return foos;
}
static void Measure(string name, Func<long> callback)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var mem = GC.GetTotalMemory(true);
var gc00 = GC.CollectionCount(0);
var gc01 = GC.CollectionCount(1);
var gc02 = GC.CollectionCount(2);
var sw = Stopwatch.StartNew();
var result = callback();
sw.Stop();
var mem2 = GC.GetTotalMemory(false);
var gc10 = GC.CollectionCount(0);
var gc11 = GC.CollectionCount(1);
var gc12 = GC.CollectionCount(2);
var memDelta = (mem2 - mem) / 1024.0;
var gcDelta0 = gc10 - gc00;
var gcDelta1 = gc11 - gc01;
var gcDelta2 = gc12 - gc02;
Console.WriteLine(
"{0,25}: {1,5}ms, ips: {2,22:N} | Mem: {3,6:N2} kb, GC 0/1/2: {4}/{5}/{6} => {7,6}",
name, sw.ElapsedMilliseconds, result / sw.Elapsed.TotalSeconds, memDelta, gcDelta0, gcDelta1, gcDelta2, result);
}
}
}
B>Не ожидал таких результатов. Думал, что работать со списком будет быстрее. Может я что-то не так делаю?
List не сортированный, он каждый раз обходится заново. DataTable же, скорее всего, использует индексы в памяти — это хорошо видно по потребляемой памяти, там разница на несколько порядков между List, LINQ и DataTable.
Чтобы получить хорошую производительность без DataTable, нужно брать индексированные структуры данных. Dictionary, например.
С уважением, Artem Korneev.
Re[2]: DataTable.Select vs List<T>.Any vs LINQ To DataTable
Здравствуйте, Artem Korneev, Вы писали:
AK>Здравствуйте, Berill, Вы писали:
B>>Не ожидал таких результатов. Думал, что работать со списком будет быстрее. Может я что-то не так делаю?
AK>List не сортированный, он каждый раз обходится заново. DataTable же, скорее всего, использует индексы в памяти — это хорошо видно по потребляемой памяти, там разница на несколько порядков между List, LINQ и DataTable. AK>Чтобы получить хорошую производительность без DataTable, нужно брать индексированные структуры данных. Dictionary, например.
Спасибо.
Насчет сортировки надо будет подумать, посмотреть какие типы фильтров чаще всего применяются. У меня там 15 полей в таблице, в основном фильтрация идет по 3, но в разной комбинации:
1. По одному из полей ;
2. По паре полей;
3. По всей тройке.
В связи с этим, не вижу как сюда приспособить вариант с Dictionary.
Re[3]: DataTable.Select vs List<T>.Any vs LINQ To DataTable
Здравствуйте, Berill, Вы писали:
B>Насчет сортировки надо будет подумать, посмотреть какие типы фильтров чаще всего применяются. У меня там 15 полей в таблице, в основном фильтрация идет по 3, но в разной комбинации: B>1. По одному из полей ; B>2. По паре полей; B>3. По всей тройке.
B>В связи с этим, не вижу как сюда приспособить вариант с Dictionary.
В этом сценарии вам нужна коллекция для хранения данных и индексы к ней. Индексы нужно обновлять после операций удаления/вставки. Если удаления данных не происходит, то в качестве коллекции можно взять обычный List, а индексы реализовать через либо через Dictionary, либо через SortedDictionary (если нужно не только строгое соответствие, но и диапазоны, сортировка по полю и т.д.). Если значение ключа в этом индексе не уникально, тогда значением в Dictionary должна быть не ссылка на элемент, а List со ссылками на элементы с одинаковым значением индекса. Индексировать нужно каждое поле, по которому нужна быстрая выборка.
А вот насколько оно в этом варианте будет быстрее готовой имплементации в виде DataTable и стоит ли овчинка выделки — это уже нужно по обстоятельствам смотреть.
С уважением, Artem Korneev.
Re[4]: DataTable.Select vs List<T>.Any vs LINQ To DataTable
Здравствуйте, Artem Korneev, Вы писали:
AK>В этом сценарии вам нужна коллекция для хранения данных и индексы к ней. Индексы нужно обновлять после операций удаления/вставки. Если удаления данных не происходит, то в качестве коллекции можно взять обычный List, а индексы реализовать через либо через Dictionary, либо через SortedDictionary (если нужно не только строгое соответствие, но и диапазоны, сортировка по полю и т.д.). Если значение ключа в этом индексе не уникально, тогда значением в Dictionary должна быть не ссылка на элемент, а List со ссылками на элементы с одинаковым значением индекса. Индексировать нужно каждое поле, по которому нужна быстрая выборка.
AK>А вот насколько оно в этом варианте будет быстрее готовой имплементации в виде DataTable и стоит ли овчинка выделки — это уже нужно по обстоятельствам смотреть.
Спасибо за советы.
Re: DataTable.Select vs List<T>.Any vs LINQ To DataTable
Вместо тормознутого DataTable.Select() мне нравится такой способ выборки нужных строк:
var allRows = table.Rows.Cast<DataRow>();
var filteredRows = allRows.Where(r => r["TYPE_CODE"] == addingRowInfo.TypeCode).Where(r => r["DETAIL_ID"] == addingRowInfo.DetailID).ToList();
Если избавиться от замыканий, то работает вполне себе шустро.
Пополнять же таблицу новыми строками можно методом Merge(). Он умеет отслеживать дубликаты по первичному ключу.
PS. А еще есть чудесный метод DataTable.Rows.Find(). Он работает только с первичным ключом, но очень быстро. В нашем случае было настолько быстро, что было выгоднее переназначить первичный ключ на другую колонку, чем использовать DataTable.Select().
Здравствуйте, Artem Korneev, Вы писали:
AK>В этом сценарии вам нужна коллекция для хранения данных и индексы к ней. Индексы нужно обновлять после операций удаления/вставки. Если удаления данных не происходит, то в качестве коллекции можно взять обычный List, а индексы реализовать через либо через Dictionary, либо через SortedDictionary (если нужно не только строгое соответствие, но и диапазоны, сортировка по полю и т.д.). Если значение ключа в этом индексе не уникально, тогда значением в Dictionary должна быть не ссылка на элемент, а List со ссылками на элементы с одинаковым значением индекса. Индексировать нужно каждое поле, по которому нужна быстрая выборка.
Можно еще и сам List отсортировать по наиболее часто используемой комбинации полей, а потом использовать бинарный поиск (в том числе и по частичному ключу).
"Будь достоин победы" (c) 8th Wizard's rule.
Re[3]: DataTable.Select vs List<T>.Any vs LINQ To DataTable
Здравствуйте, Berill, Вы писали:
B>В связи с этим, не вижу как сюда приспособить вариант с Dictionary.
Dictionary не очень годится для индексов, так как помогает только для поиска точного соответствия и джойна. Хотя в твоем варианте хватит и его — просто сделай композитный ключ по TYPE_CODE и DETAIL_ID. Т.е. словарик типа Dictionary<ValueTuple<int, int>, int>. Где ключ ValueTuple<int, int> содержит комбинацию TypeCode и DetailId, а значение — индекс записи в списке (ValueTuple можно взять в CodeJam или написать самому).
В более общем случае лучше для индексов использовать не хеш-таблицу, а бинарное дерево. В фреймворке есть одна реализация — SortedDictionary (внутри там, емнип, R-B Tree).
P.S. Если в реальном коде записей в списке много, и операций поиска по нему тоже много, то имеет смысл распараллелить код через банальный Parallel.ForEach, заменив Dictionary на ConcurrentDictionary.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
Здравствуйте, Mihas, Вы писали: M>Здравствуйте, Berill, Вы писали: M>Советы еще принимаются? M>Вместо тормознутого DataTable.Select() мне нравится такой способ выборки нужных строк: M>
M>Если избавиться от замыканий, то работает вполне себе шустро. M>Пополнять же таблицу новыми строками можно методом Merge(). Он умеет отслеживать дубликаты по первичному ключу. M>PS. А еще есть чудесный метод DataTable.Rows.Find(). Он работает только с первичным ключом, но очень быстро. В нашем случае было настолько быстро, что было выгоднее переназначить первичный ключ на другую колонку, чем использовать DataTable.Select(). M>Извините, если посоветовал банальность.
У меня нет никаких ключей и индексы для таблицы я сам не создаю (пока что).
В основном идет поиск по трем полям (мне не нужны сами эти записи, мне нужен факт их наличия или отсутствия), а также еще один часто используемый цикл по этой таблице — нахождение максимального значения поля Code для TYPE_CODE == addingRowInfo.TypeCode.
Мои замеры показали, что код
private static void GetFindResultsForTableWithCast(int count, DataTable table)
{
Measure("Table Cast",
() =>
{
for ( int i = 0; i < count; i++ )
{
var allRows = table.Rows.Cast<DataRow>();
var found = allRows.Where(r => (int)r["TypeCode"] == 10
&& (string)r["Period"] == "01.01.2016"
&& (int)r["DetailID"] == 5)
.ToList()
.Count != 0;
}
return count;
});
}
ничуть не шустрее Select'a =(
А вот если сделать так:
код
private static void GetFindResultsForList(int count, List<MyData> myList)
{
//myList.Sort(new MyDataComparer());
Measure("List",
() =>
{
//myList.Sort(new MyDataComparer());for ( int i = 0; i < count; i++ )
{
bool found = false;
foreach ( var myData in myList )
{
if ( myData.TypeCode == 10 && myData.Period == "01.01.2016" && myData.DetailID == 5 )
{
found = true;
break;
}
}
}
return count;
});
}
private static void GetFindResultsForTableWithLINQ(int count, DataTable table)
{
Measure("Table LINQ",
() =>
{
for ( int i = 0; i < count; i++ )
{
var found = table.AsEnumerable().Any(row => row.Field<int>("TypeCode") == 10
&& row.Field<string>("Period") == "01.01.2016"
&& row.Field<int>("DetailID") == 5
);
/*
var q = from r in table.AsEnumerable()
where r.Field<string>("Period") == "01.01.2016"
&& r.Field<int>("DetailID") == 5
&& r.Field<int>("TypeCode") == 10
select r;
var found = q.ToList().Count != 0;
*/
}
return count;
});
}
private static void GetFindResultsForTableWithSelect(int count, DataTable table)
{
string filter2 = "TypeCode=10 AND Period='01.01.2016' AND DetailID=5";
Measure("Table Select",
() =>
{
for ( int i = 0; i < count; i++ )
{
var found = table.Select(filter2).Length != 0;
}
return count;
});
}
private static void GetFindResultsForTableWithCast(int count, DataTable table)
{
Measure("Table Cast",
() =>
{
for ( int i = 0; i < count; i++ )
{
var allRows = table.Rows.Cast<DataRow>();
var found = allRows.Any(r => (int)r["TypeCode"] == 10
&& (string)r["Period"] == "01.01.2016"
&& (int)r["DetailID"] == 5);
}
return count;
});
}
Здравствуйте, Berill, Вы писали:
B>У меня нет никаких ключей и индексы для таблицы я сам не создаю (пока что). B>В основном идет поиск по трем полям (мне не нужны сами эти записи, мне нужен факт их наличия или отсутствия)
HashSet<ValueTuple<fieldT1,fieldT2,fieldT3>> в помощь. Один раз заполняешь данными из таблицы, потом делаешь проверки за околоконстантное время.
B>, а B>также еще один часто используемый цикл по этой таблице — нахождение максимального значения поля Code для TYPE_CODE B>== addingRowInfo.TypeCode.
Если это разовое действие (а оно разовое, если набор строк не меняется), то проще все одним сканом по таблице типа:
table.Rows.Cast<DataRow>().Where(_ => (int)_["TypeCode"] == addingRowInfo.TypeCode).Select(_ => (int)_["Code]).Max();
Можно еще ординалы для колонок заранее получить и использовать вместо имен, чтобы еще чуток ускорить.
B>Мои замеры показали, что код B>
private static void GetFindResultsForTableWithCast(int count, DataTable table)
B> {
B> Measure("Table Cast",
B> () =>
B> {
B> for ( int i = 0; i < count; i++ )
B> {
B> var allRows = table.Rows.Cast<DataRow>();
B> var found = allRows.Where(r => (int)r["TypeCode"] == 10
B> && (string)r["Period"] == "01.01.2016"
B> && (int)r["DetailID"] == 5)
B> .ToList()
B> .Count != 0;
B> }
B> return count;
B> });
B> }
B>ничуть не шустрее Select'a =(
Зачем делать полную выборку и потом преобразовывать ее в List только ради того, чтобы сравнить Count с нулем?
Тем более, что дальше идет правильный вариант с Any.