Возврат массива из запроса Linq SQL для таблицы строк на 80 миллионов с использованием EFCore - PullRequest
1 голос
/ 31 января 2020

Я запрашиваю базу данных SQL, используя Linq и Entity Framework Core в проекте Razorpages для создания графика резидентности, вот тот, который я сделал ранее.

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

Данные взяты из таблицы с ~ 80 миллионами строк, и я нашел решения от SO, которые могли бы работать с меньшим количеством записей, но которые не подходят в этом случае использования (обычно поиск Linq, group, join). Я думаю, что проблема заключается в комбинации фильтров, групп и объединений, за которыми следует подсчет, происходящий на стороне сервера без предварительной загрузки необработанных данных.

Просмотр команды SQL в SSMS (извлечено из LINQPad) очень плохо оптимизирован - я могу опубликовать это, если это будет полезно, но он состоит из 236 строк, состоящих из повторяющихся секций.

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

Шаг 1 (строки между определенным временем, с определенным LocationTypeId и channelId = engSpeed):

var speedRows = context.TestData
.Where(a => a.Time >= start
&& a.Time < end
&& a.LocationTypeId == 3
&& a.channelId == 7)
.Select(s => new
{
    s.Time,
    s.ChannelValue
})
.Distinct();

Шаг 2 (строки с Идентификатор канала = крутящий момент):

var torqueRows = context.TestData
.Where(a => a.LocationTypeId == 3
&& a.channelId == 8)
.Select(s => new
{
    s.Time,
    s.ChannelValue
})
.Distinct();

Шаг 3 (объедините ряды скорости и крутящего момента из Шаг 1 и Шаг 2 по времени ):

var joinedRows = speedRows.Join(torqueRows, arg => arg.Time, arg => arg.Time,
    (speed, torque) => new
    {
        Id = speed.Time,
        Speed = Convert.ToDouble(speed.ChannelValue),
        Torque = Convert.ToInt16(torque.ChannelValue)
    });

Шаг 4 (создайте динамические c группировки, используя объединенную таблицу из Шаг 3 ):

var response = (from a in joinedRows
            group a by (a.Torque / 100) into torqueGroup
            orderby torqueGroup.Key
            select new
            {
                TorqueBracket = $"{100 * torqueGroup.Key} <> {100 + (100 * torqueGroup.Key)}",
                TorqueMin = 100 * torqueGroup.Key,
                TorqueMax = 100 + (100 * torqueGroup.Key),
                Speeds = (from d in torqueGroup
                            group d by (Math.Floor((d.Speed) / 500)) into speedGroup
                            orderby speedGroup.Key
                            select new
                            {
                                SpeedBracket = $"{500 * speedGroup.Key} <> {500 + (500 * speedGroup.Key)}",
                                SpeedMin = 500 * (int)speedGroup.Key,
                                SpeedMax = 500 + (500 * (int)speedGroup.Key),
                                Minutes = speedGroup.Count()
                            })
            }).ToList();

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

Класс TestData:

public partial class TestData {
    public int LiveDataId { get; set; }
    public DateTime? Time { get; set; }
    public int? LocationTypeId { get; set; }
    public int? TestNo { get; set; }
    public int? LogNo { get; set; }
    public int? LiveDataChannelId { get; set; }
    public decimal? ChannelValue { get; set; }
    public virtual LiveDataChannelNames LiveDataChannel { get; set; }
    public virtual LocationType LocationType { get; set; }
}

Любая помощь или указатели будут оценены.

Спасибо.

Ответы [ 2 ]

1 голос
/ 03 февраля 2020

Я сомневаюсь, что фактическая сгенерированная команда SQL настолько велика - вы, вероятно, проверяете команду SQL, сгенерированную EF6.

Сгенерированная SQL EF Core не такая большая, но проблема в том, что Speeds = ... часть GroupBy не может быть переведена в SQL, и оценивается на стороне клиента после извлечения всех данных из предыдущих частей запроса.

Что вы можете do - это промежуточный запрос create, который повторяет только необходимые данные (2 ощупывающих ключа + количество) и клиентскую часть do rest.

Сначала необходимо убедиться, что подзапросы из шагов 1, 2 и 3 выполнены переводится на SQL. Convert.ToDouble и Convert.ToInt16 не могут быть переведены, поэтому замените их приведениями:

Speed = (double)speed.ChannelValue,
Torque = (short)torque.ChannelValue

Затем разделите Step4 на две части. Серверная часть:

var groupedData = joinedRows
    .GroupBy(arg => new { TorqueGroupKey = arg.Torque / 100, SpeedGroupKey = Math.Floor((arg.Speed) / 500) })
    .Select(g => new
    {
        g.Key.TorqueGroupKey,
        g.Key.SpeedGroupKey,
        Minutes = g.Count()
    });

и клиентская часть:

var response = (from a in groupedData.AsEnumerable() // <-- swicth to client evaluation
                group a by a.TorqueGroupKey into torqueGroup
                orderby torqueGroup.Key
                select new
                {
                    TorqueBracket = $"{100 * torqueGroup.Key} <> {100 + (100 * torqueGroup.Key)}",
                    TorqueMin = 100 * torqueGroup.Key,
                    TorqueMax = 100 + (100 * torqueGroup.Key),
                    Speeds = (from d in torqueGroup
                              orderby d.SpeedGroupKey
                              select new
                              {
                                  SpeedBracket = $"{500 * d.SpeedGroupKey} <> {500 + (500 * d.SpeedGroupKey)}",
                                  SpeedMin = 500 * (int)d.SpeedGroupKey,
                                  SpeedMax = 500 + (500 * (int)d.SpeedGroupKey),
                                  Minutes = d.Minutes
                              })
                }).ToList();

Обратите внимание, что в EF Core 3.0+ вам придется делать что-то подобное, потому что неявная оценка клиента была удален.

Теперь сгенерированный запрос SQL должен выглядеть примерно так:

  SELECT [t].[ChannelValue] / 100 AS [TorqueGroupKey], FLOOR([t].[ChannelValue] / 500.0E0) AS [SpeedGroupKey], COUNT(*) AS [Minutes]
  FROM (
      SELECT DISTINCT [a].[Time], [a].[ChannelValue]
      FROM [TestData] AS [a]
      WHERE ((([a].[Time] >= @__start_0) AND ([a].[Time] < @__end_1)) AND ([a].[LocationTypeId] = 3)) AND ([a].[LiveDataChannelId] = 7)
  ) AS [t]
  INNER JOIN (
      SELECT DISTINCT [a0].[Time], [a0].[ChannelValue]
      FROM [TestData] AS [a0]
      WHERE ([a0].[LocationTypeId] = 3) AND ([a0].[LiveDataChannelId] = 8)
  ) AS [t0] ON [t].[Time] = [t0].[Time]
  GROUP BY [t].[ChannelValue] / 100, FLOOR([t].[ChannelValue] / 500.0E0) 
0 голосов
/ 10 февраля 2020

Хотя ответы, опубликованные здесь, помогли мне понять тонкости проблемы (спасибо всем, кто написал), они не работали с Entity Framework Core версии 2.2.6. Мне удалось добиться разумной и стабильной производительности с помощью приведенного ниже кода C#.

При выполнении шагов 1 и 2 .ToList () останавливает тайм-ауты при более длинных запросах (я полагаю, ) деление перечисления результатов, возможно, с небольшим временным штрафом. Также преобразования (double) и (short) выполняются непосредственно на стороне сервера, в отличие от Convert.ToDouble и Convert.ToInt16

Шаг 1 (строки между определенным временем, с определенным LocationTypeId и channelId = engSpeed):

var speedRows = context.TestData
.Where(a => a.Time >= start
&& a.Time < end
&& a.LocationTypeId == 3
&& a.channelId 7)
.Select(s => new
{
    s.Time,
    ChannelValue = (double)s.ChannelValue
})
.Distinct().ToList();

Шаг 2 (строки с channelId = крутящий момент):

var torqueRows = context.TestData
.Where(a => a.LocationTypeId == 3
&& a.channelId == 8)
.Select(s => new
{
    s.Time,
    ChannelValue = (short)s.ChannelValue
})
.Distinct().ToList();

Шаг 3 (объедините строки скорости и крутящего момента из Шаг 1 и Шаг 2 по времени):

var joinedRows = speedRows.Join(torqueRows, arg => arg.Time, arg => arg.Time,
    (speed, torque) => new
    {
        Id = speed.Time,
        Speed = speed.ChannelValue,
        Torque = torque.ChannelValue
    });

Шаг 4 (сгруппировать строки в ключевые группы)

var groupedData = joinedRows
    .GroupBy(arg => new { TorqueGroupKey = (arg.Torque / 100), SpeedGroupKey = Math.Floor((arg.Speed) / 500) })
    .Select(g => new
    {
        g.Key.TorqueGroupKey,
        g.Key.SpeedGroupKey,
        Minutes = g.Count()
    });

Шаг 5 ( создайте динамические c группировки, используя groupedData из Шаг 4 ):

var response = (from a in groupedData.AsEnumerable()
            group a by a.TorqueGroupKey into torqueGroup
            orderby torqueGroup.Key
            select new ResidencySqlResult
            {
                TorqueBracket = $"{100 * torqueGroup.Key} <> {100 + (100 * torqueGroup.Key)}",
                TorqueMin = 100 * torqueGroup.Key,
                TorqueMax = 100 + (100 * torqueGroup.Key),
                Speeds = (from d in torqueGroup
                          orderby d.SpeedGroupKey
                          select new Speeds
                          {
                              SpeedBracket = $"{500 * d.SpeedGroupKey} <> {500 + (500 * d.SpeedGroupKey)}",
                              SpeedMin = 500 * (int)d.SpeedGroupKey,
                              SpeedMax = 500 + (500 * (int)d.SpeedGroupKey),
                              Minutes = d.Minutes
                          })
            }).ToList();

Еще раз спасибо всем, кто выручил.

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