Как выполнить операцию SQL Query to DataTable, которая может быть отменена

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

 SqlConnection conn = new SqlConnection(connstring);
                    SqlCommand cmd = new SqlCommand(query, conn);
                    conn.Open();
                    SqlDataAdapter sda = new SqlDataAdapter(cmd);
                    sda.Fill(Results);
                    conn.Close();
                    sda.Dispose();

Где запрос - это строка, представляющая большой, требующий много времени запрос, а conn - объект соединения.

Теперь моя проблема: мне нужна кнопка остановки. Я понял, что убить фонового рабочего бесполезно, потому что я все еще хочу сохранить, какие результаты остались после завершения запроса. Плюс он не сможет проверить отмененное состояние до запроса.

Что я до сих пор придумал:

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

Моя идея состояла в том, чтобы использовать SqlDataReader для чтения данных из части запроса за раз, чтобы у меня был "цикл", чтобы проверить флаг, который я мог бы установить из графического интерфейса с помощью кнопки. Проблема в том, насколько я знаю, я не могу использовать метод Load() для datatable и все еще могу отменить sqlcommand. Если я ошибаюсь, сообщите мне, потому что это упростит отмену.

В свете того, что я обнаружил, я пришел к пониманию, я могу только отменить sqlcommand mid-query, если бы сделал что-то вроде ниже (псевдокод):

while(reader.Read())
{
 //check flag status
 //if it is set to 'kill' fire off the kill thread

 //otherwise populate the datatable with what was read
}

Однако мне показалось бы, что это будет крайне неэффективно и, возможно, дорогостоящим. Это единственный способ убить sqlcommand в прогресс, который абсолютно должен быть в datatable? Любая помощь будет оценена!

Ответ 1

Есть действительно два этапа, отменяющих вопросы:

  • Отмена первоначального выполнения запроса до возвращения первых строк
  • Отмена процесса чтения строк по мере их обслуживания

В зависимости от характера фактического оператора sql любой из этих шагов может составлять 99% времени, поэтому их следует учитывать. Например, вызов SELECT * на какой-то столбе с миллиардом строк займет время от времени, но не займет много времени. И наоборот, запрос на супер-сложное соединение на плохо настроенных таблицах и последующее его обертывание в некоторых агрегирующих предложениях может занять несколько минут, но ничтожное время для чтения нескольких строк после их фактического возврата.

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

Отмена выполнения запроса

Чтобы иметь возможность отменить запрос во время его выполнения, вы можете использовать одну из перегрузок SqlCommand.BeginExecuteReader, чтобы начать запроса и вызовите SqlCommand.Cancel, чтобы прервать его. В качестве альтернативы вы можете вызвать ExecuteReader() синхронно в одном потоке и по-прежнему вызывать Cancel() из другого. Я не включаю примеры кода, потому что их много в документации.

Отмена операции чтения

Здесь, используя простой булевский флаг, возможно, самый простой способ. И помните, что очень просто заполнить строку таблицы данных, используя перегрузку Rows.Add(), которая принимает массив объекта, то есть:

object[] buffer = new object[reader.FieldCount]
while(reader.Read()) {
    if(cancelFlag) break;
    reader.GetValues(buffer);
    dataTable.Rows.Add(buffer);
}

Отмена блокировки вызовов для чтения()

Вид смешанного случая возникает, когда, как упоминалось ранее, обращение к reader.Read() заставляет механизм базы данных выполнять другую партию интенсивной обработки. Как отмечено в документации MSDN, вызовы Read() могут блокироваться в этом случае, даже если исходный запрос был выполнен с помощью BeginExecuteReader. Вы можете обойти это, вызвав Read() в одном потоке, который обрабатывает все чтение, но вызывает Cancel() в другом потоке. То, как вы знаете, если ваш читатель находится в блокирующем вызове Read, должен иметь другой флаг, который обновляет поток читателя во время чтения потока мониторинга:

...
inRead = true
while(reader.Read()) {
    inRead = false
    ...
    inRead = true
}

// Somewhere else:
private void foo_onUITimerTick(...) {
   status.Text = inRead ? "Waiting for server" : "Reading";
}

Что касается производительности Reader vs Adapter

DataReader обычно быстрее, чем при использовании DataAdapter.Fill(). Весь смысл DataReader - быть действительно, очень быстрым и отзывчивым для чтения. Проверка некоторого логического флага один раз за строку не добавит измеримой разницы во времени даже над миллионами строк.

Лимитирующим фактором для большого запроса базы данных является не время обработки локального процессора, а размер канала ввода-вывода (ваше сетевое соединение для удаленной базы данных или скорость вашего диска для локального) или комбинация db серверную скорость диска и время обработки процессора для сложного запроса. Оба DataAdapter и DataReader будут тратить время (возможно, большую часть времени), просто ожидая нескольких наносекунд за один раз для следующей строки, которая будет обслуживаться.

Одно удобство DataAdapter.Fill() заключается в том, что он делает магию динамического создания столбцов DataTable для соответствия результатам запроса, но это не сложно сделать самому (см. SqlDataReader.GetSchemaTable()).

Ответ 2

Просто попробуйте

Я бы посоветовал вам поместить требующий много времени запрос в BackgroundWorker и передать ему команду . так что вы можете удерживать объект команды под контролем. Когда команда cancel приходит, просто скажите, что прошло (к исполняемому файлу BackgroundWorker) команду Cancel на command.Cancel()