При работе с базами данных, содержащими большие объемы данных в формате BLOB (Binary Large Object), часто возникает проблема нехватки памяти (Out of Memory), особенно при извлечении и обработке большого количества таких объектов. В контексте Delphi и Pascal, это может быть особенно актуально, учитывая ограничения 32-битной архитектуры (хотя и 64-битная архитектура не гарантирует полное отсутствие проблем при работе с очень большими объемами данных).
В данной статье мы рассмотрим типичную проблему, связанную с извлечением файлов из BLOB-полей в SQL Server, и предложим решения для оптимизации использования памяти, чтобы избежать ошибки "Out of Memory".
Проблема:
Предположим, у нас есть таблица InvoiceStorage в SQL Server, содержащая информацию о счетах, включая сами файлы счетов в поле PrintedFile (тип BLOB). Задача: извлечь все файлы счетов в определенный каталог. При попытке извлечь большое количество файлов (например, 30 000 и более), программа завершается с ошибкой "Out of Memory".
Пример кода, демонстрирующий проблему (как предоставлено в вопросе):
procedure TForm1.ExtractInvoicesFromBlobs(AMinInvNo, AMaxInvNo: Integer);
var
PrintDate, FileName, BaseFolder, TargetFolder: string;
FileStream: TFileStream;
BlobStream: TStream;
RecordCount: Integer;
begin
// init
RecordCount := 0;
Memo1.Clear;
// retrieve data from db
FDQuery1.SQL.Clear;
FDQuery1.SQL.Text := 'SELECT ID, PrintedFile, Filename, PrintDateTime ' +
'FROM InvoiceStorage ' +
'WHERE Filename BETWEEN :MinFilename AND :MaxFilename';
FDQuery1.ParamByName('MinFilename').AsString := Format('%d.pdf', [AMinInvNo]);
FDQuery1.ParamByName('MaxFilename').AsString := Format('%d.pdf', [AMaxInvNo]);
FDQuery1.Open;
// these 2 lines are to prevent "out of memory" after approximately 30,000 pdf
FDQuery1.FetchOptions.Mode := fmAll;
FDQuery1.FetchOptions.Unidirectional := True;
while not FDQuery1.Eof do
begin
try
PrintDate := Copy(FDQuery1.FieldByName('PrintDateTime').AsString, 1, 4);
BaseFolder := IncludeTrailingPathDelimiter(Edit3.Text);
TargetFolder := Format(BaseFolder + '%s', [PrintDate]);
ForceDirectories(TargetFolder);
BlobStream := FDQuery1.CreateBlobStream(FDQuery1.FieldByName('PrintedFile'), bmRead);
try
FileName := IncludeTrailingPathDelimiter(TargetFolder) + FDQuery1.FieldByName('Filename').AsString;
FileStream := TFileStream.Create(FileName, fmCreate);
try
FileStream.CopyFrom(BlobStream, BlobStream.Size);
finally
FileStream.Free;
end;
finally
BlobStream.Free;
BlobStream := nil;
end;
// Each 1,000 files: log and provide some breathing air to the system
Inc(RecordCount);
if RecordCount mod 1000 = 0 then
begin
Memo1.Lines.Add(Format('%d files processed...', [RecordCount]));
TrimMemo;
Application.ProcessMessages;
Sleep(100);
end;
except
on E: Exception do
Memo1.Lines.Add(Format('Error while processing record %d: %s', [RecordCount, E.Message]));
end;
FDQuery1.Next;
end;
FDQuery1.Close;
end;
Решение:
Основная проблема кроется в неправильном использовании FetchOptions компонента FDQuery. Установка FDQuery1.FetchOptions.Mode := fmAll; приводит к тому, что все записи (включая все BLOB-данные) загружаются в память сразу после открытия запроса. Это, очевидно, приводит к быстрому исчерпанию памяти при большом количестве записей.
Правильное решение:
Удалить строку FDQuery1.FetchOptions.Mode := fmAll;. По умолчанию используется fmOnDemand, что означает загрузку данных только при необходимости.
Убедиться, что FDQuery1.FetchOptions.Unidirectional := True;установлено до открытия запроса (FDQuery1.Open;). Однонаправленный режим позволяет драйверу базы данных оптимизировать использование памяти, не сохраняя данные для повторного доступа.
FDQuery1.SQL.Clear;
FDQuery1.SQL.Text := 'SELECT ID, PrintedFile, Filename, PrintDateTime ' +
'FROM InvoiceStorage ' +
'WHERE Filename BETWEEN :MinFilename AND :MaxFilename';
FDQuery1.ParamByName('MinFilename').AsString := Format('%d.pdf', [AMinInvNo]);
FDQuery1.ParamByName('MaxFilename').AsString := Format('%d.pdf', [AMaxInvNo]);
FDQuery1.FetchOptions.Unidirectional := True; // Важно установить ДО открытия!
FDQuery1.Open;
Альтернативные решения и дополнительные оптимизации:
Пакетная обработка: Если даже с fmOnDemand и Unidirectional возникают проблемы, можно разбить задачу на несколько этапов, обрабатывая, например, по 10 000 файлов за раз. Это позволит снизить пиковую нагрузку на память.
procedure TForm1.ExtractInvoicesFromBlobs(AMinInvNo, AMaxInvNo, APackageSize: Integer);
var i: Integer;
begin
for i := AMinInvNo to AMaxInvNo step APackageSize do
begin
ExtractInvoicePackage(i, Min(i + APackageSize - 1, AMaxInvNo));
end;
end;
procedure ExtractInvoicePackage(AMinInvNo, AMaxInvNo: Integer);
// ... (Код из ExtractInvoicesFromBlobs, но с использованием AMinInvNo и AMaxInvNo)
end;
Оптимизация SQL запроса: Убедитесь, что SQL запрос максимально эффективен. Используйте индексы для полей, используемых в WHERE условии. Избегайте SELECT *, выбирайте только необходимые поля.
Увеличение доступной памяти: Если возможно, скомпилируйте приложение как 64-битное. Это позволит использовать больше памяти. Однако, это потребует перекомпиляции всех используемых библиотек и компонентов.
Ручное управление памятью (осторожно!): В крайнем случае, можно попробовать использовать ручное управление памятью с помощью GetMem и FreeMem, но это требует очень аккуратного подхода, чтобы избежать утечек памяти. В большинстве случаев, стандартных средств Delphi достаточно.
Использование потоков (Threads): Распараллеливание задачи извлечения файлов с помощью потоков может улучшить производительность, но также требует внимательного управления ресурсами и синхронизации. Неправильное использование потоков может привести к нестабильности программы.
Профилирование памяти: Используйте инструменты профилирования памяти (например, встроенный в Delphi или сторонние инструменты) для выявления утечек памяти или неэффективного использования памяти.
Заключение:
Проблема "Out of Memory" при обработке больших BLOB-данных в Delphi решается, прежде всего, правильной настройкой FetchOptions компонента FDQuery. Использование fmOnDemand и Unidirectional позволяет загружать данные только при необходимости и оптимизировать использование памяти. Дополнительные оптимизации, такие как пакетная обработка, оптимизация SQL запроса и переход на 64-битную архитектуру, могут еще больше улучшить производительность и стабильность приложения. Важно помнить о необходимости аккуратного управления ресурсами и избегать утечек памяти.
В статье рассматривается проблема нехватки памяти при извлечении больших объемов BLOB-данных из SQL Server в Delphi и предлагаются решения, включающие правильную настройку FetchOptions, пакетную обработку и оптимизацию SQL-запросов.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.