2015-09-20 5 views
4

У меня длинный рабочий процесс на C#, который попадает в таблицу Sql в любом месте от 10 до 200 раз. Когда процесс превышает примерно 50 обращений и запросов, превышающих примерно 100 000 строк из одной таблицы, он выдает в этой строке исключение из системного извлечения, а именно внизу, где он преобразует объект IQuery в список:Linq System.OutofMemoryException

var cht = from p in _db.TickerData 
      where p.Time >= Convert.ToDateTime(start) && 
      p.Time <= Convert.ToDateTime(end) 
      orderby p.Time 
      select p; 

_prices = cht.ToList(); < this is where the System.OutofMemoryException occurs > 

Что можно сделать, чтобы предотвратить эту ошибку?

+0

Я попытался создать индекс в столбце [Время] в Sqlexpress. Текущий индекс - это первичный ключ [Id]. – CraigJSte

+0

У вас заканчивается память при попытке загрузить 100 000 строк в память? Как вы думаете, как это исправить? – DavidG

+0

Вопрос в том, что вы делаете с '_prices'? После этого мы расскажем вам, как исправить эту проблему. – Magnus

ответ

3

Во-первых:

специально на дне, где он преобразует объект IQuery к списку

Да, где можно было ожидать, чтобы произойти при нехватке памяти.

Назначение cht, приведенное выше, фактически не попадает в базу данных; все, что он делает, объявляет форму запроса. Это называется отложенным исполнением, а LINQ использует его повсюду. Это означает, что «мы фактически ничего не обрабатываем, пока ваш код не нуждается в этом».

Вызов ToList, однако, по сути говорит, что «код нуждается в этом, все это прямо сейчас». Таким образом, он отправляет запрос в базу данных, отталкивает все результаты сразу, использует магию LINQ, чтобы превратить их в объекты CLR, и наполнить их все в List<T> для вас.

Сказав это, это только догадка, но возможно, что ваш поставщик LINQ не знает, что такое Convert.ToDateTime. Если он не знает, как с этим справиться, он не будет помещать его в предложение WHERE в запросе, который он выполняет, и вместо этого он будет загружать всю таблицу и фильтровать ее на стороне клиента, что может быть причиной того, что вы сбой, когда таблица становится слишком большой, а не когда результат становится слишком большим.

Чтобы проверить это, используйте профайлер базы данных для перехвата запроса и посмотрите, выглядит ли предложение WHERE, как вы ожидали. Если это не перевод верно, попробуйте вместо этого:

var startTime = Convert.ToDateTime(start); 
var endTime = Convert.ToDateTime(end); 
var cht = from p in _db.TickerData 
      where p.Time >= startTime && p.Time <= endTime 
      orderby p.Time 
      select p; 
_prices = cht.ToList(); 

Если это не поможет, вы, вероятно, просто отходили слишком много данных, и вы должны будете работать над этим те же способы, которыми Вы бы работа с обработкой слишком большого количества данных в любом другом контексте.

+1

Проблема возникла из-за Convert.ToDateTime(). Спасибо @Mason – CraigJSte

+0

Эта проблема, однако, была только подмножеством реальной проблемы, которая использовала ToList вместо того, чтобы использовать IEnumerable для создания объекта класса, который, казалось, решал большую проблему. Надеюсь, это точное описание того, как использовать IEnumerable. Этот ответ был упомянут @IVan ниже в комментариях. – CraigJSte

5

Данные, которые вы пытаетесь получить, слишком велики для вашего списка. Исключение составляет ToList(), потому что это именно то место, где выполняется запрос. Чего вы хотите достичь с таким большим списком? Возможные решения:

1) Ограничить поиск по нескольким критериям. Загружайте не все данные, а часть их, а другую часть, если она вам действительно нужна.

2) Используйте другую структуру данных, чем список, если вы хотите загрузить всю информацию в памяти, посмотри на ConcurrentDictionary

2

Вашей проблемы заключается в том, что запрос возвращает очень большой набор данных, который должен быть сохранен в памяти нашего процесса. Очень много данных => OutOfMemoryException. Это нормально. То, что ненормально, пытается это сделать.Вместо этого, вы можете ограничить набор результатов с некоторой дополнительной фильтрацией или разбить большой набор результатов на более мелкие, может быть, например, так:

 DateTime startDateTime = Convert.ToDateTime(start); 
     DateTime endDateTime = Convert.ToDateTime(end); 
     int fetched = 0; 
     int totalFetched = 0; 

     do 
     { 
      //fetch a batch of 1000 records 
      var _prices = _db.TickerData.Where(p => p.Time >= startDateTime && p.Time <= endDateTime) 
            .OrderBy(p => p.Time) 
            .Skip(totalFetched) 
            .Take(1000) 
            .ToList();     

      //do here whatever you want with your batch of 1000 records 
      fetched = _prices.Count; 
      totalFetched += fetched; 
     } 
     while (fetched > 0); 

Таким образом, вы можете обрабатывать любое количество данных в пакетах.

EDIT: исправлены некоторые проблемы, о которых сообщает @ Code.me в разделе комментариев.

EDIT: Я предлагаю вам настроить индекс на уровне базы данных в столбце «Время», если вы еще этого не сделали, чтобы ускорить эти запросы.

+0

Я вижу здесь 3 вопроса. Во-первых, 'Take (1000)' всегда будет извлекать первые 1000 записей. Вам нужно «Пропустить (1000)» после первой итерации и продолжить выборку. Во-вторых, '_prices' выходит за рамки в выражении while. В-третьих, 'IEnumerable ' не имеет свойства 'Count'. Вам нужно вызвать метод расширения .Count() '. –

+0

@ Code.me Благодарим вас за выявление этих проблем. Исправлены первые две проблемы (сообщение было сделано в спешке ...). Что касается третьего, ToList() возвращает список , который имеет свойство Count. –

+0

Вы правы. Я всегда работаю с IEnumerable . Я забыл, что он возвращает IList . –

0

Из-за отложенного выполнения запрос будет выполняться при вызове ToList() на нем. Поскольку загрузка всех данных будет потреблять слишком много памяти, это хорошая практика для пакетного процесса.

Приведенный ниже код позволит вам получить 1000 (или то, что лучше всего подходит для вас) за раз, и вы можете их обработать.

var startTime = Convert.ToDateTime(start); 
var endTime = Convert.ToDateTime(end); 

IEnumerable<T> prices= new List<T>(); // whatever T is 

var currentFetched = 0; 
var totalFetched = 0; 

do 
{ 
    var cht = _db.TickerData.Where(p => p.Time >= startTime && p.Time << endTime) 
         .OrderBy(p => p.Time) 
         .Skip(totalFetched) 
         .Take(1000) 
         .ToList(); 

    currentFetched = cht.Count(); 
    totalFetched += currentFetched; 

    // prices = prices.Concat(cht).ToList(); 
    //^This might throw an exception later when the list becomes too big 
    // So you can probably process currently fetched data 
} 
while (currentFetched > 0); 
+0

Эта реализация извлекает записи 1-1000, затем 1001-2000 и несколько раз 1001-2000, потому что после первой выборки вы всегда пропускаете только первые 1000 записей. –

+0

А, спасибо, что заметили. Я случайно удалил часть для вычисления общего количества при фиксации более ранней опечатки. Исправлено. –

+0

Я не понимаю, как этот ответ помогает. Если OP хотел обработать результат запроса, он мог бы сделать это сам, превратив его в 'IEnumerable' вместо использования' ToList'. Кроме того, загрузка 100 тыс. Записей в память сейчас не такая уж большая. –

0

Что я сделал с моим, я просто позволю ему вернуть объект IQueryable. Это все еще список, но он работает намного лучше.

Смежные вопросы