пятница, 2 декабря 2011 г.

[prog] Слоистость, мать ее!

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

Этот афоризм вспомнился в попытках понять, почему select из временной таблички с сотней записей и двумя столбцами занимает десятки миллисекунд. Хотя более тяжелые запросы из больших таблиц, как правило, отрабатывают в три-четыре раза быстрее.

Есть MS SQL Server (не важно, 2005 или 2008), есть ODBC, над ODBC еще есть OTL. И простой select длится 50 миллисекунд (при том, что insert-ы – всего 7 миллисекунд). Какой слой здесь выступает тормозом? На кого вешать собак?… Тайна сия велика есть.

Павбывавбы, короче.

Под катом маленькая тестовая программка, на которой и получены эти удивительные времена.

#include <iostream>
#include <sstream>
#include <ctime>

#define OTL_EXCEPTION_DERIVED_FROM std::exception
#define OTL_EXCEPTION_HAS_MEMBERS \
   virtual const char * what() const throw() { \
      return reinterpret_cast< const char * >( this->msg ); \
   }

#define OTL_STREAM_POOLING_ON
#define OTL_STL
#define OTL_BIGINT long long
#define OTL_ODBC_MULTI_MODE
#include <otlv4.h>

std::string
make_a( int index )
   {
      std::ostringstream s;

      s << index;
      const std::string & v = s.str();

      return v + "-" + v + "--" + v + "---" + v + "----" + v;
   }

void
fill_table( otl_connect & db )
   {
      otl_stream stream( 100,
            "insert into #A(a, b) values(:a<char[65]>, :b<int>)",
            db );

      for( int i = 0; i != 100; ++i )
         {
            stream << make_a( i ) << i;
         }
   }

void
load_table( otl_connect & db )
   {
      otl_stream stream( 100,
            "select a, b from #A",
            db );

      while( !stream.eof() )
         {
            std::string a;
            int b;

            stream >> a >> b;
         }
   }

#define MEASURE(what) { \
   std::clock_t s = std::clock(); \
   what ; \
   std::clock_t f = std::clock(); \
   const double duration = double(f - s) / CLOCKS_PER_SEC; \
   std::cout << #what << ": " << duration << std::endl;  \
}

void
do_test()
   {
      otl_connect db;
      db.set_connection_mode( OTL_MSSQL_2005_ODBC_CONNECT );
      db.auto_commit_off();
      db.rlogon(
         "UID=test_user;PWD=CENSORED;DRIVER=SQL Native Client;SERVER=CENSORED;Database=CENSORED" );

      db.direct_exec(
         "create table #A (a varchar(64), b integer, "
            "constraint a_PK primary key(a,b) );"
      );

      MEASURE( fill_table(db); )
      MEASURE( load_table(db); )
   }

int
main()
   {
      try
         {
            do_test();
         }
      catch( const otl_exception & x )
         {
            std::cerr << "OTL Exception: " << x.what() << std::endl
                  << "SQLState: " << x.sqlstate << std::endl
                  << "STM_Text: " << x.stm_text << std::endl;

            return 1;
         }
      catch( const std::exception & x )
         {
            std::cerr << "Exception: " << x.what() << std::endl;

            return 1;
         }

      return 0;
   }

3 комментария:

Анонимный комментирует...

Я че-то не пойму. С MSSQL сервером нельзя разве общаться напрямую, по его протоколу, использую libMsSQL.so, чтобы не было лишних прослоек? Так умеет любой SQL сервер, что MySQL, что SQLite, что Postgres. Если промежуточные прослойки в виде разных ORM мешают работать быстро, то подключаешься напрямую и получаешь максимум того, что сервер БД может дать.

Кстати, из консоли MsSQL пробовал делать селекты? Те же времена или все супербыстро? Причем консоль лучше запустить на сервере БД и на локальной машине, чтобы проверить влияние сети. Если из консоли тоже все будет тормозить, я бы наваял вложенную процедуру, которая будет делать селект и возвращать свое время работы, чтобы уж совсем кроме самой базы данных на время выборки никто не влиял.

P.S. Тут недавно новость читал, что Microsoft выпустил в очередной раз драйвер-библиотеку под RedHat Linux для подключения к MsSQL серверу. :) Причем в бинарном виде, т.е. только для РедХата и подойдет и разобраться на каком уровне тормоза без доступа к исходникам тоже не получиться.

P.P.S. Толковый разговор получается для пятницы вечера. :)

Анонимный комментирует...

Еще одно замечание. Насколько тестовая программа соответствует тому, что происходит в реальном приложении? Наверняка ты не грузишь всю временную таблицу из базы. Если нужна только одна строка, то имеет смысл явно указать LIMIT 1 в запросе, у базы будет больше информации для оптимизации. В тестовой программе LIMIT не укажешь...

Евгений Охотников комментирует...

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

Понятно, что теперь нужно оценивать, где притормаживает сам сервер, где сеть, где промежуточные слои (в виде ODBC, OTL). Просто когда изначально видишь результаты замеров, то недоуменно разводишь руками -- столько уровней абстракции, и каждый вносит какую-то свою лепту.

PS. На счет "родной" библиотеки для доступа к MSSQL никогда не слышал. Про аналогичные вещи для MySQL, Postgres, Oracle знаю. А вот для MSSQL-я... Вроде были только какие-то ADO-шные интерфейсы. Но это COM, и хрен знает, насколько все это эффективнее простого ODBC.