Как увеличить производительность Microsoft Dynamics NAV за счет уменьшения чтений из базы

Самый простой (для программиста), но самый медленный (с точки зрения производительности) способ

Так пишет большинство.

При прогоне цикла по таблице и использовании GET (а также FINDFIRST/FINDLAST) по другой таблице, в которой вам надо получить данные, большиство выполняют эти GET-операторы в каждой итерации цикла, даже если на предыдущей итерации уже получено правильное значение:

recItemLedgerEntry.RESET;
recItemLedgerEntry.SETCURRENTKEY("…");
recItemLedgerEntry.SETRANGE("…");
recItemLedgerEntry.SETRANGE("…");
recItemLedgerEntry.SETFILTER("…");
IF recItemLedgerEntry.FINDSET THEN
REPEAT
recInventorySetup.GET(); // т.н. «программисты» размещают GET прямо на этом месте!!!
recItem.GET("Item No.");
CLEAR(recLocation);
IF recLocation.GET("Location Code") THEN ;
UNTIL recItemLedgerEntry.NEXT = 0;

Более сложный (для программиста), но более быстрый (с точки зрения производительности) способ

Штука в том, чтобы избежать чтения из БД, если вы уже однажды считали информацию. Чтобы это провернуть, данные необходимо хранить во временных таблицах:

Глобальные переменные:
tmpItem Record Item (property Temporary=Yes)

Маленькая функция:

GetItem(VAR OrecItem : Record Item;IblnErrorIfNotFound : Boolean;IcodItemNo : Code[20])
OblnRecordFound : Boolean
// GetItem
// Возвращает запись из таблицы Item, аналог GET
// PARAMETERS:
//   OrecItem : record in which to put the General Posting Setup
//   IblnErrorIfNotFound :
//     TRUE : выдача errormessage, если запись не найдена
//     FALSE : отсутствие errormessage и возврат пустой записи
//   IcodItemNo : Item No.
//   RETURN-VALUE : TRUE : запись найдена ; FALSE : запись НЕ НАЙДЕНА

OblnRecordFound := (tmpItem."No." = IcodItemNo);

IF NOT OblnRecordFound THEN
  OblnRecordFound := tmpItem.GET(IcodItemNo);

IF NOT OblnRecordFound THEN BEGIN
  CLEAR(tmpItem);
  IF IblnErrorIfNotFound THEN BEGIN
    LrecItem.GET(IcodItemNo);
    OblnRecordFound := TRUE;
  END ELSE
    OblnRecordFound := LrecItem.GET(IcodItemNo);

  IF OblnRecordFound THEN BEGIN
    tmpItem := LrecItem;
    tmpItem.INSERT(FALSE);
  END;
END;

OrecItem := tmpItem;

Таким образом, первоначальный код превращается вот в такой (функции “GetSETUPInventorySetup”, ”GetLocation” можно посмотреть в кодеюните 90031):

Global Variables:
Name        DataType        SubType
cduGetRecord    Codeunit        Get Record
cduGetRecordSI  Codeunit        Get Record SI

recItemLedgerEntry.RESET;
recItemLedgerEntry.SETCURRENTKEY("…");
recItemLedgerEntry.SETRANGE("…");
recItemLedgerEntry.SETRANGE("…");
recItemLedgerEntry.SETFILTER("…");
IF recItemLedgerEntry.FINDSET THEN
  REPEAT
    cduGetRecordSI.GetSETUPInventorySetup(recInventorySetup);
    // лучше даже было бы разместить этот код в триггере "OnInitReport"
    cduGetRecord.GetItem(recItem,TRUE,"Item No.");
    cduGetRecordSI.GetLocation(recLocation,FALSE,"Location Code");
  UNTIL recItemLedgerEntry.NEXT = 0;

Зачем размещать функцию в двух кодеюнитах?

Один из них – singleinstance-кодеюнит, в котором можно разместить данные которые никогда (практически никогда) не менются. Т.н. НСИ – нормативно-справочная информация, вспоминаем институт. В их числе – таблицы-Setup, коды складов, коды причин, фин.счета и т.д. Singleinstance-кодеюнит вместе со всеми данными остается в памяти до тех, пока открыта фирма (если открыть список фирм и выбрать текущую – это тоже считается за «закрыть фирму»).

Таблицы наподобие Товар, Клиент, Поставщик и т.п. лучше разместить в другом кодеюните (не singleinstance), потому что они менются чаще. Обычно кодеюнит остается в памяти до тех пор, пока активен объект, который его использует. Во целом, в процессе, который задействует много объектов и записей, который выполняется достаточно долго, эти данные – могут считаться постоянными (static).

Singleinstance-кодеюнит и Navision Application Server

При работе таких кодеюнитов c Navision Application Server – будьте начеку. Рекомендуется рестартовать NAS один раз в день (и ВСЕГДА в случае изменений настроечных таблиц). Второй варинат – не использовать в таких случаях singleinstance-кодеюнит, а помещать все его функции и глобальные переменные в другом кодеюните.

Важно – объявляйте кодеюниты глобально, а не локальными переменными

Я, кстати, так и делаю. Надеюсь, что и вы, читатель – тоже.
Обычный кодеюнит используется в одной функции, в отличие от singleinstance. А зачем нам определять его несколько раз?

Как НЕ НАДО использовать систему GetRecord (условное конспиративное название)

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

Сравнение по количеству обращений к базе
Допустим, у нас есть отчет, который проходит по всем записям из товарной книги и при этом использует данные из карточки склада и карточки товара. Допустим, в товарной книге у нас 100 000 записей, 1000 товаров и 10 складов. Сравним стандартный подход с нашим прогрессивным методом:

Стандартный подход Система “Get record”

Чтение из БД:Item Ledger Entry 100.000 100.000
Чтение из БД:Location 100.000 10
Чтение из БД:Item 100.000 1.000

ИТОГО 300.000 101.010

И что же мы видим – уменьшение количества обращений к базе в три раза!
Таблицы Склад/Товар читаются для каждой записи только один раз, затем данные хранятся во временной таблице. Все последующие запросы к БД идут на самом деле из временной таблицы, кроме случаев, когда записей еще там нет.
Чтение из временной таблицы (читай – ИЗ ОЗУ!) намного быстрее запроса к серверу (на самом-то деле, и склады, и товары висят у сервера в КЭШе, фактически тоже – в ОЗУ :), но мы также имеют место быть потери на сеть и т.д.).

Фактически, мы работаем с толстым Navision-клиентом, но не используем его потенцил на 100%. С описанным подходом сервер можно немного разгрузить за счет увеличения нагрузки на клиента.

В приложенном fob-файле 2 отчета и 2 кодеюнита (совместимые в версией Microsoft Navision Attain 3.01 и выше):
— отчет 90030: пример работы без использования системы «GetRecord»;
— отчет 90031: пример работы того же отчета с использованием «GetRecord»;
— кодеюнит 90030 – в нем находятся функции для получения записей, которые кэшируются во время работы одного определенного объекта (например, отчета 90031). Как только отчет заканчивает работу, кодеюнит вместе с кэшированными записями очищается из памяти;
— кодеюнит 90031 – singleinstance-версия, кэш записей работает до закрытия фирмы или до момента закрытия Navision-клиента.

Небольшой тест на производительность

У меня на рабочей станции установлен MS SQL 2000, на котором провед REBUILD индексов. А также Navision-сервер с КЭШем в 20 Мб. Клиенты Navision стоят на этой же станции. То есть – момент торможения сети отсутствует для чистоты эксперимента (в сети разница подходов будем еще более ощутимой).
До начала теста я немного разогрел сервера прогоном отчетов. В базе у меня хранится 50000 различных наименований товаров, а вValue Entry – порядка одного миллиона записей. Чтение Value Entry проходит по порядку первичного ключа – полю Entry No.
Итак, я прогнал обе системы несколько раз, меняя принцип чтения записей – то GET, то GetRecord, то GET, то GetRecord. И что же :получилось

Время на выполнение
при использовании GET по таблице Item Время на выполнение
с использованием GetRecord

SQL 12 => 15 минут 10 => 12 минут
Navision 8=>10 минут 4=>6 минут

На конкретные цифры просьба не ориентироваться, на разных машинах они могут гулять. Однако, общий принцип виден.
Если вы заметили, Navision-сервер выигрывает по производительности. Причина – в том, что он использует т.н. DBCache и хранит часть записей прямо в памяти.
При реальной работе системы SQL и Navision вполне могут поменяться местами.

Материал для скачивания доступен здесь.

Автор: Ален Крикилайон, оригинал статьи находится здесь.

Автор:

В области Navision - с 2003 года. Профессиональные интересы: NAV, MS SQL, .NET, BPMN, IT-менеджмент. Предметная область: логистика, финансы, склады, 3PL.

Количество статей, опубликованных автором: 86.

Добавить комментарий