2015-06-05 3 views
0

ФОН:SQL Sever 2008 и Entity Framework - Повышение производительность

У меня есть службы Windows, которая тяговая запись офф SQL таблицы (созданная с EF Code First Method). Записи добавляются очень часто (~ 10-20 в секунду) двумя клиентами, которые затем очищаются от базы данных и обрабатываются моей службой. Для резервирования есть два клиента, которые контролируют одни и те же системы и могут создавать дубликаты записей. Я ищу способ улучшить производительность программы, которая обрабатывает новые записи.

ПРОБЛЕМА:

Шаг 1: удалить дубликаты записей:

// get duplicate entries 
var duplicateEntities = context.OpcTagValueLogs.GroupBy(x => new { x.OpcTagId, x.SourceTimeStamp }).SelectMany(x => x.OrderBy(y => y.Id).Skip(1)); 
foreach (var duplicateEntry in duplicateEntries) 
{ 
    // remove duplicate entries 
    context.OpcTagValueLogs.Remove(duplicateEntry); 
} 

Шаг 2: Получить остальные записи журнала

var logs = context.OpcTagValueLogs.Include(x => x.OpcTag).Include(x => x.OpcTag.RuleSets).ToList(); 

Шаг 3: Проверить соответствующие правила и выполнять события, чтобы обработать новые значения

Я пытаюсь оптимизировать свою программу столько, сколько po потому что прямо сейчас служба Windows, которая обрабатывает данные, работает не быстрее, чем создаются записи. Если скорость создания записей увеличивается, я беспокоюсь, что услуга не сможет идти в ногу со временем.

Это единственные запросы, которые я запускаю (помимо создания записей) в этой таблице. Структура таблицы:

  • [INT] Идентификатор (Первичный ключ, Кластерный: Id)
  • [INT] OpcTagId (IX_OpcTagId)
  • [DATETIME] TimeStamp
  • [NVARCHAR (MAX)] Значение
  • [INT] SourceTimeStamp (IX_SourceTimeStamp)
  • [NVARCHAR (MAX)] ClientCode
  • [NVARCHAR (MAX)] PriorValue

Есть ли способ изменить мои индексы, чтобы улучшить производительность моих запросов?

EDIT: Это то, как бревна обрабатываются после того, как дубликаты удаляются:

foreach (var log in logs.ToList()) // because items will be removed from the list during the loop, it is important to update the list on 
       {         // each iteration, hence the .ToList() 

        if (log.PriorValue == log.Value) // check to see if the prior value equals to new value 
        {        // I am only interested in changing values, so delete the log entry 
         // remove the entity 
         _context.OpcTagValueLogs.Remove(log); 
         logs.Remove(log); 
         _context.SaveChanges(); 
        } 
        else 
        { 
         // check rules and perform actions 
         var ruleSets = log.OpcTag.RuleSets.ToList(); 
         foreach (var ruleSet in ruleSets) 
         { 
          if (ruleSet.CheckRule(log.PriorValue, log.Value)) 
          { 
           // perform action 
           // convert source timestamp to datetime 
           DateTime srcTS = new DateTime(1970, 1, 1).AddSeconds(log.SourceTimeStamp); 
           var action = ActionFactory.CreateAction(ruleSet.Action, log.PriorValue, log.Value, log.OpcTag, srcTS); 
           action.Execute(); 
          } 
         } 

         // remove entity from database 
         _context.OpcTagValueLogs.Remove(log); 
         _context.SaveChanges(); 
         logs.Remove(log); // remove the entity from the local list as well        
        } 
       } 

EDIT 2: Текущий метод

var ruleSets = _context.RuleSets.ToList(); // Get entire rulesets once into memory 
       var logsLocal = logs.ToList(); // bring all the logs into local memory 
       var maxIndex = logsLocal.Max(x => x.Id); // the last index of the local logs 
       foreach (var log in logsLocal) 
       { 
        if (log.PriorValue != log.Value) 
        { 
         foreach (var ruleSet in ruleSets.Where(x => x.OpcTagId == log.OpcTagId)) 
         { 
          if (ruleSet.CheckRule(log.PriorValue, log.Value)) 
          { 
           // perform action 
           var action = ActionFactory.CreateAction(ruleSet.Action, log.PriorValue, log.Value, log.OpcTag, srcTS); 
           action.Execute(); 
          } 
         } 
        } 
       } 

       _context.OpcTagValueLogs.Where(x=>x.Id <= maxIndex).Delete(); // batch delete only the logs that were processed on this program loop 

РЕДАКТИРОВАТЬ 3: Объект действия производится с помощью статического ActionFactory класса на основе значения ruleSet.Action.

public static Action CreateAction(ActionId pActionId, string pPrior, string pNew, OpcTag pOpcTag, DateTime pSourceTimestamp) 
    { 
     Action evt = null; 
     switch (pActionId) 
     { 
      case ActionId.A1000: evt = new A1000(pActionId, pPrior, pNew, pOpcTag, pSourceTimestamp); 
       break; 
      case ActionId.A1001: evt = new A1001(pActionId, pPrior, pNew, pOpcTag, pSourceTimestamp); 
       break; 
      case ActionId.A1002: evt = new A1002(pActionId, pPrior, pNew, pOpcTag, pSourceTimestamp); 
       break; 
      case ActionId.A1003: evt = new A1003(pActionId, pPrior, pNew, pOpcTag, pSourceTimestamp); 
       break; 
      case ActionId.A1004: evt = new A1004(pActionId, pPrior, pNew, pOpcTag, pSourceTimestamp); 
       break; 
     } 
     return evt; 
    } 

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

+0

При вызове 'ToList()' вы эффективно получить все дети (отношение), поэтому это не лень. Почему вы делаете 'ToList()'? Кроме того, для первой проблемы вы должны просто использовать библиотеку EF Extend и делать «массовое удаление» на всех дубликатах, а не делать это один за другим. – sed

+0

Спасибо, я проверю эту библиотеку. Я понял, что, вызвав ToList, я приведу записи в память и уменьшу количество последующих запросов к базе данных. Итак, я думал, что это поможет производительности ... – jaredcnance

+0

Да, но это действительно отстой, потому что он вытаскивает все, даже 9/10 вещей, которые вам на самом деле не нужны. Не беспокойтесь и не обходите ленивую загрузку, без 'ToList();' она будет фактически запрашивать базу данных ** только **, когда/если используются данные. – sed

ответ

1

Во-первых, ваша петля, вероятно, вызывает проблему N + 1. Вы выполняете цикл и элементы запроса из БД в цикле. Вы хотите, меньше ввода-вывода (звонки в БД), поскольку это дорогостоящая операция. Если сервер достаточно велик, лучше сохранить объекты в памяти вверху.Вы даже можете начать использовать методы кеширования, но сейчас это может быть немного продвинуто.

Вот код (не проверял), что я придумал, чтобы помочь попытаться решить N + 1 вопрос:

// check rules and perform actions 
// Get entire rulesets once into memory 
var ruleSets = OpcTagValueLogs.OpcTag.RuleSets.ToList(); 

foreach (var log in logs.ToList()) // because items will be removed from the list during the loop, it is important to update the list on 
{         // each iteration, hence the .ToList() 

    if (log.PriorValue != log.Value) // check to see if the prior value equals to new value 
    {        // I am only interested in changing values, so delete the log entry 
     foreach (var ruleSet in ruleSets.Where(x => x.OpcTags.Logs.OpcTagValueLogs.OpcTagId == log.OpcTagId)) 
     { 
      if (ruleSet.CheckRule(log.PriorValue, log.Value)) 
      { 
       // perform action 
       // convert source timestamp to datetime 
       DateTime srcTS = new DateTime(1970, 1, 1).AddSeconds(log.SourceTimeStamp); 
       var action = ActionFactory.CreateAction(ruleSet.Action, log.PriorValue, log.Value, log.OpcTag, srcTS); 
       action.Execute(); 
      } 
     }      
    } 

    // The below was common to both the if and else condition, hence it is moved at the end of the conditional 
    // remove the entity 
    _context.OpcTagValueLogs.Remove(log); 
    logs.Remove(log); 
} 

// Call save changes once (less I/O) 
_context.SaveChanges(); 

Я не знаю определений классов, так что вы должны изменить код соответственно специально для foreach (var ruleSet in ruleSets.Where(x => x.OpcTags.Logs.OpcTagValueLogs.OpcTagId == log.OpcTagId)) линия.

Я также отредактировал некоторые из распространенного кода, так как это не имело смысла, хотя вы можете исправить это, если чувствуете, что это неправильно.

Это framework, о котором упоминал Стэн. Это отличная схема, помогающая оптимизировать использование EF.

Кроме того, лучше всего определить, что происходит, это запустить SQL Server Profiler, когда ваша служба работает, чтобы найти плохие запросы.

+0

Пока что лучший ответ для меня - это смесь между этим и ответом Стэна. К сожалению, я не могу полностью полагаться на LazyLoading, потому что множество вызовов в базу данных (в основном в методах 'action.Execute();'), похоже, заставляют программу работать намного медленнее. Я отредактировал свой вопрос с текущим состоянием моей программы. – jaredcnance

+0

Можете ли вы опубликовать код внутри 'action.Выполнить() '? –

+0

Я отредактировал мой вопрос, чтобы немного объяснить метод Execute. Я пытаюсь избежать публикации тысяч строк кода. Первоначально этот вопрос касался оптимизации базы данных, но, как я узнал, проблема может быть намного больше. – jaredcnance

1

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

Пара вариантов. Я знаю, что вы используете EF, но существует ли возможность использования Stored Procedure в вашей среде? Если это так, вы можете использовать оператор MERGE, чтобы вы могли сделать только одну поездку в базу данных.

Другой вариант - создать метод расширения на вашем DbContext, который будет действовать как UPSERT (читайте: MERGE). Я только что наткнулся на этот класс, который настраивает его для вас, когда вы посмотрите, можете ли вы сделать EF-upserts.

https://gist.github.com/ondravondra/4001192

+1

Пропустите ORM в целом, когда он становится слишком сильным. +1 – Alejandro

+0

Мне может потребоваться пример того, о чем вы говорите. У меня нет большого опыта работы с хранимыми процедурами, но, насколько мне известно, невозможно было бы выполнить необходимые действия с помощью инструкции SQL. Существует несколько разных «типов» информации с несколькими различными источниками, которые регистрируются в базе данных. Мне нужно получить эти записи, проверить источник/тип, проверить, что установленное правило истинно (т. Е. Новое значение> предыдущее значение) и выполнить какое-либо действие, если оно есть. Это действие может быть что угодно: от создания новой записи в отдельной базе данных до нажатия предупреждений для веб-клиентов. – jaredcnance

+0

. Upsert (то есть: 'MERGE') будет в основном выполнять' if/then', а затем выполнять соответствующие действия в рамках одной транзакции, вместо этого выполнить проверку, а затем выполнить логическую сторону сервера. В основном это будет поддерживать эту логику в инструкции SQL. Если я чего-то не упускаю, я не понимаю, почему это не могло выполнить вашу цель. Посмотрите на ссылку и/или посмотрите на инструкции MERGE и посмотрите, не помогает ли это, но это хорошее решение для устранения нескольких вызовов в базе данных. – ragerory

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