Использование LINQ .Select () для приведения в новый тип слишком медленное? - PullRequest
1 голос
/ 13 августа 2011

Текущий проект, сломал голову над этой проблемой:

Клиентское хранилище:

public class ClientRepository
{
    // Members
    private masterDataContext _db;

    // Constructor
    public ClientRepository()
    {
        _db = new masterDataContext();
    }

    public IEnumerable<ClientName> GetCorporateClientNames()
    {
        return _db.corporate_client_tbs.Select(o => new ClientName { id = o.id, name = o.company_name }).AsEnumerable();
    }

    public IEnumerable<ClientName> GetRetailClientNames()
    {
        return _db.retail_client_tbs.Select(o => new ClientName { id = o.id, name = o.name }).AsEnumerable();
    }

    // Define return type
    public class ClientName
    {
        public int id { get; set; }
        public string name { get; set; }
    }
}

Теперь в контроллере у меня есть следующее:

public ActionResult Index()
{
    var _visits = _db.GetAllServiceVisits();
    return View(_visits);
}

Требуется приблизительно 4 секунды для загрузки представления с 200 нечетными строками, присутствующими в настоящее время.

Я хочу добавить свойство "клиент" в модель посещения, которое содержит имя клиента. Имя клиента будет получено из одной из двух разных таблиц, которая извлекается из одного из двух массивов типа «ClientName».

Это первый подход, который использовал LINQ:

public ActionResult Index()
{
    private ClientRepository _cr = new ClientRepository();
    var _retailclients = _cr.GetRetailClientNames().ToArray();
    var _corporateclients = _cr.GetCorporateClientNames().ToArray();
    var _visits = _db.GetAllServiceVisits();

    var _temp = _visits.Select(o => new ServiceVisitViewModel
        {
            service_visit = o,
            client = (o.client_type ? _corporateclients.Where(p => p.id == o.client_id).First().name : _retailclients.Where(p => p.id == o.client_id).First().name)
        }).ToArray();

    return View(_temp);
}

Это второй подход, использующий обычный 'ol C #:

public ActionResult Index()
{
    private ClientRepository _cr = new ClientRepository();
    var _retailclients = _cr.GetRetailClientNames().ToArray();
    var _corporateclients = _cr.GetCorporateClientNames().ToArray();
    var _visits = _db.GetAllServiceVisits();

    List<ServiceVisitViewModel> _temp = new List<ServiceVisitViewModel>();
    foreach (service_visit_tb v in _visits)
    {
        _temp.Add(new ServiceVisitViewModel { service_visit = v, client = (v.client_type ? _corporateclients.Where(p => p.id == v.client_id).First().name : _retailclients.Where(p => p.id == v.client_id).First().name) });
    }

    return View(_temp);
}

Второй подход примерно в 8-10 раз быстрее по моим тестам.

Единственное отличие, которое я вижу, это оператор .Select.

Может кто-нибудь сказать мне, если я сделал что-то не так в первом подходе или в альтернативе, почему первый подход такой медленный!

Edit: Определение _db.GetAllServiceVisits () выглядит следующим образом:

public IEnumerable<service_visit_tb> GetAllServiceVisits()
{
    var _visits = _db.service_visit_tbs;
    return _visits.AsEnumerable();
}

Конец редактирования

Второе редактирование: Я удалил эту строку из каждой записи в журнале:

-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1

Журнал контекста выглядит следующим образом:

// This query is to fetch all the clients of Type One (corresponding to _cr.GetRetailClientNames() )
SELECT [t0].[id], [t0].[name]
FROM [genii].[retail_client_tb] AS [t0]

// This query is to fetch all the clients of Type Two (corresponding to _cr.GetCorporateClientNames() )
SELECT [t0].[id], [t0].[company_name] AS [name]
FROM [genii].[corporate_client_tb] AS [t0]

// This is the main query (loading roughly 250 records) which fetchs all Visits
SELECT [t0].[id], [t0].[client_type], [t0].[client_id], [t0].[machine_type], [t0].[machineID], [t0].[visit_type], [t0].[scheduledon], [t0].[arrivedon], [t0].[completedon], [t0].[reported_problem], [t0].[diagnosed_problem], [t0].[action_taken], [t0].[visit_status], [t0].[engineer_id], [t0].[reference_id], [t0].[addedby], [t0].[addedon], [t0].[modifiedby], [t0].[modifiedon]
FROM [genii].[service_visit_tb] AS [t0]

// These next queries are not being manually called by me, I assume they are being
// called when the Razor view is compiled since I am calling the name value of a linked table as such:
// @item.service_visit.engineer_tb.name
SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [8]

SELECT [t0].[id], [t0].[status]
FROM [genii].[visit_status_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [1]

SELECT [t0].[id], [t0].[name]
FROM [genii].[engineer_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [3]

SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [11]

SELECT [t0].[id], [t0].[name]
FROM [genii].[engineer_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [2]

SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [7]

SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [2]

SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [6]

SELECT [t0].[id], [t0].[type]
FROM [genii].[visit_type_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [3]

SELECT [t0].[id], [t0].[name]
FROM [genii].[engineer_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [5]

SELECT [t0].[id], [t0].[name]
FROM [genii].[engineer_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [4]

SELECT [t0].[id], [t0].[status]
FROM [genii].[visit_status_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [8]

SELECT [t0].[id], [t0].[status]
FROM [genii].[visit_status_tb] AS [t0]
WHERE [t0].[id] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [2]

Добавление к вопросу: есть ли лучший способ получить эти данные? Я всегда предполагал (никогда не проверял), что доступ к данным на основе внешнего ключа, который дал мне контекст LINQ, был настолько хорош, насколько это возможно, но, увидев эти дополнительные запросы, я уже не уверен!

Опубликуем скорости для второй части исполнения позже сегодня (это долгие выходные здесь, в Мумбаи, но мы работаем до конца)

Конец редактирования

Третье редактирование (Я рассматриваю ответ от веб-сервера клиенту, так как все вычисления / выборки / привязки / и т. Д. Должны учитываться.)

Метод первый: 6,85 секунд (вызов из 3 таблиц, а затем приведение в View-Model с использованием C #)

public IEnumerable<service_visit_tb> GetAllServiceVisits()
{
    var _visits = _db.service_visit_tbs;
    _db.Log = new DebuggerWriter();
    return _visits.AsEnumerable();
}

public ActionResult Index()
{
    var _retailclients = _cr.GetRetailClientNames().ToArray();
    var _corporateclients = _cr.GetCorporateClientNames().ToArray();
    var _visits = _db.GetAllServiceVisits();
    List<ServiceVisitViewModel> _temp = new List<ServiceVisitViewModel>();
    foreach (service_visit_tb v in _visits)
    {
        _temp.Add(new ServiceVisitViewModel { service_visit = v, client = (v.client_type ? _corporateclients.Where(p => p.id == v.client_id).First().name : _retailclients.Where(p => p.id == v.client_id).First().name) });
    //}
    return View(_temp);
}

Метод второй: 8,59 секунды (вызов из 3 таблиц, а затем приведение в View-Model с использованием LINQ)

public IEnumerable<service_visit_tb> GetAllServiceVisits()
{
    var _visits = _db.service_visit_tbs;
    _db.Log = new DebuggerWriter();
    return _visits.AsEnumerable();
}

public ActionResult Index()
{
    var _retailclients = _cr.GetRetailClientNames().ToArray();
    var _corporateclients = _cr.GetCorporateClientNames().ToArray();
    var _visits = _db.GetAllServiceVisits();
    var _temp = _visits.Select(o => new ServiceVisitViewModel
    {
        service_visit = o,
        client = (o.client_type ? _corporateclients.Where(p => p.id == o.client_id).First().name : _retailclients.Where(p => p.id == o.client_id).First().name)
    });
    return View(_temp);
}

Метод третий: 5,76 секунды (Все в одном запросе LINQ - выполняется в базе данных)

public IEnumerable<ServiceVisitViewModel> GetAllServiceVisitsNew()
{
    var _visits = _db.service_visit_tbs.Select(o => new ServiceVisitViewModel
    {
        service_visit = o,
        client = (o.client_type ? _db.corporate_client_tbs.Where(c=> c.id == o.client_id).First().company_name : _db.retail_client_tbs.Where(c=> c.id == o.client_id).First().name)
    });
    _db.Log = new DebuggerWriter();
    return _visits;
}

public ActionResult Index()
{
    var _visits = _db.GetAllServiceVisitsNew();
    return View(_visits());
}

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

Конец редактирования

Ответы [ 2 ]

8 голосов
/ 13 августа 2011

Здесь выражение Select имеет огромное значение - потому что оно меняет то, что делается в базе данных.(Я предполагаю, что _db.GetAllServiceVisits() возвращает IQueryable<T>.)

Вы извлекаете всех розничных и корпоративных клиентов в память уже потому, что используете ToArray.Ваш Select вызов (я подозреваю) затем отправит все эти данные обратно в базу данных, чтобы там могли выполняться Select и Where.Я подозреваю, что если вы заглянете в профилировщик SQL, это будет довольно странный запрос.

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

var _visits = _db.GetAllServiceVisits().ToList();

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

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

То, что может быть таким же простым, как изменение методов хранилища для возврата IQueryable<T> вместо IEnumerable<T> и удаление вызовов на AsEnumerable.

2 голосов
/ 13 августа 2011

Ничего себе. Вы делаете много циклов там. Каждый Where внутри цикла завершает цикл массивом, чтобы найти элемент, каждую итерацию в цикле.

Составьте словари клиентов, чтобы вы могли быстро их найти. Это должно дать вам резкое увеличение скорости:

public ActionResult Index()
{
    private ClientRepository _cr = new ClientRepository();
    var _retailclients = _cr.GetRetailClientNames().ToDictionary(c => c.id);
    var _corporateclients = _cr.GetCorporateClientNames().ToDictionary(c => c.id);
    var _visits = _db.GetAllServiceVisits();

    var _temp = _visits.Select(o => new ServiceVisitViewModel
        {
            service_visit = o,
            client = (o.client_type ? _corporateclients[o.client_id].name : _retailclients[o.client_id].name)
        }).ToArray();

    return View(_temp);
}
...