В процессе развития проекта периодически появляется необходимость обмена данными между серверами баз данных. Предположим, у нас есть источник данных в виде SQL Server и удалённый PostgreSQL сервер, на котором эти данные должны оказаться. После добавления удалённого сервера в качестве linked server, можно делать запросы вида:


INSERT INTO RemotePG...RemoteTable (RecordID, RecordName) VALUES (1,'Test string');

Проблема в том, что такие запросы выполняются очень долго. Если перед нами стоит задача выгрузить десятки и сотни тысяч записей, то время на выполнение стремится к бесконечности. Рассмотрим два с половиной способа вставить данные в таблицу на linked server и сравним время выполнения.



Создание нового linked server:

Чтобы создать linked server, у вас уже должен быть источник данных ODBC. В моём случае имя linked server и источника ODBC совпадают.


USE [master]
GO

-- положим имя linked server в переменную
declare @ServerName nvarchar(200)
SET @ServerName=N'RemotePG'

-- добавим удалённый сервер
EXEC master.dbo.sp_addlinkedserver @server = @ServerName, @srvproduct=@ServerName, @provider=N'MSDASQL', @datasrc=@ServerName

-- добавим пользователя с удалённого сервера
-- именно под этой учётной записью будут выполнятся запросы на удалённой машине
EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname=@ServerName,@useself=N'False',@locallogin=NULL,@rmtuser=N'remote_user',@rmtpassword='password'

-- разрешим удалённый вызов процедур
EXEC master.dbo.sp_serveroption @server=@ServerName, @optname=N'rpc', @optvalue=N'true'
EXEC master.dbo.sp_serveroption @server=@ServerName, @optname=N'rpc out', @optvalue=N'true'


Способ номер 1


Пусть на удалённой машине есть простая таблица:


CREATE TABLE RemoteTable (RecordID INT, RecordName VARCHAR(200));

Посмотрим на время выполнения простого запроса:


-- отметим время начала
SELECT getdate();
GO

-- объявим и инициализируем переменную
DECLARE @i INT;
SET @i=0;

-- вставим тысячу строк напрямую в удалённую таблицу
WHILE @i<1000
BEGIN
    INSERT INTO RemotePG...RemoteTable (RecordID, RecordName) VALUES (1,'Test string');
    SET @i=@i+1;
END
GO

-- отметим время окончания
SELECT getdate();
GO

В моём случае это заняло 2 минуты 52 секунды. Примерно 6 записей в секунду. Небыстро. Для справки: удалённая машина находится на канале около 5 Мбит/сек и средним пингом 16 мс.


Способ номер 2


В случае с удалённым сервером SQL Server позволяет использовать конструкцию вида:


EXECUTE ('sql запрос на удалённой машине') AT LinkedServerName;

Чтобы это было возможным, в настройках Linked Server должны быть разрешены удалённые вызовы процедур (RPC = remote procedure call). В этом случае запрос выполняется непосредственно на удалённой машине.
Посмотрим, как это скажется на времени выполнения:


-- отметим время начала
SELECT getdate();
GO

-- объявим и инициализируем переменную
DECLARE @i INT;
SET @i=0;

-- вставим тысячу строк исполняя запрос удалённо
WHILE @i<1000
BEGIN
    EXECUTE ('INSERT INTO RemoteTable (RecordID, RecordName) VALUES (1,''Test string'');') AT RemotePG;
    SET @i=@i+1;
END
GO

-- отметим время окончания
SELECT getdate();
GO

Время выполнения 17.25 секунд, уже лучше, но попробуем уменьшить это время.


Способ номер 2.5


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


-- отметим время начала
SELECT getdate();
GO

-- объявим и инициализируем переменные
DECLARE @sql VARCHAR(max);
DECLARE @i INT;

SET @i=0;

-- напишем начало скрипта
SET @sql='INSERT INTO RemoteTable (RecordID, RecordName) VALUES ';

-- добавим в скрипт данные для вставки
WHILE @i<1000
BEGIN
    SELECT @sql=@sql+'(1,''Test string''),';
    SET @i=@i+1;
END

-- заменим последний символ ',' на ';'
SELECT @sql=SUBSTRING(@sql,1,LEN(@sql)-1)+';'

-- выполним запрос удалённо
EXECUTE (@sql) AT RemotePG;
GO

-- отметим время окончания
SELECT getdate();
GO

Запрос будет представлять из себя длинную строку вида:


INSERT INTO RemoteTable (RecordID, RecordName) VALUES (1,'Test string'),(1,'Test string') ... (1,'Test string');

Такой запрос, в тех же условиях выполнился за 217 миллисекунд. Что примерно в 800 раз быстрее первоначального варианта.


P. S. Данные для вставки в таблицу специально упрощены.

Поделиться с друзьями
-->

Комментарии (36)


  1. easyman
    19.07.2016 19:53
    +2

    Хотел спросить, что будет, когда превысите максимальную длину запроса.
    Отвечаю сам: Нет лимита. ( There has been no hard-wired upper limit for several releases now. )


    1. BalinTomsk
      20.07.2016 00:19

      — Нет лимита

      То на что вы указали — лимит на сам сервер PostgreSQL, если скрипт будете выполнять непосредтвенно на серверe.

      На стороне MSSQL есть лимит на переменную VARCHAR(max), а еше лимиты на фреймворк использумый для передачи данных.


      1. TimsTims
        20.07.2016 10:49

        А еще в MSSQL нельзя просто так используя INSERT INTO вставлять более 1000 записей за раз. Вместо этого приходится использовать конструкцию BULK INSERT, либо другие хитрые INSERT into table SELECT from


    1. QuickJoey
      20.07.2016 10:07

      в реальной жизни данные вставляются блоками по 500 записей. в таком случае, в случае обрыва, можно продолжить с того места, где закончил. а не пытаться запихнуть 1млн. записей разом.


  1. amaksr
    19.07.2016 20:10

    Интересный способ связывать MS SQL и Postgres, не знал про него.
    Но в остальном как-то маловато для статьи на Хабре.


    1. QuickJoey
      20.07.2016 09:49

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


  1. MinamotoSoft
    19.07.2016 20:56

    Метод правильный — выводы ошибочные.
    Основная разница между 1 и 2-2.5 вариантами — колличество соединений.
    В первом случае у вас перед вставкой каждой строки открывается соединение, после вставки — закрывается. Вот в этом и кроется «тормоз» так как сама процедура установления соединения достаточно медленная.
    Ну а последний вариант — это уже не совсем «SQL». Это больше похоже на «блочную работу» по типу как у MySqlDump. И этот метод станет не таким эффективным если размер «блока» будет мегабайт 100-1000. (из-за большого времени на физический транспорт) А вот если сформировать «блок» в виде текста, заархивировать, передать мультипоточным протоколом, а принимающая сторона распакует и выполнит на «принимающем» сервере — это будет куда более эффективный метод.
    Главный «враг» во всем этом это Ваши 16мс задержки между хостами.


    1. QuickJoey
      20.07.2016 09:57

      начну с конца. про главного врага и да и нет. я специально писал про удалённый сервер, но в реальности ситуация с локальным точно такая же. мне кажется, главный враг — это ожидание подтверждения транзакции со стороны mssq. про количество соединений, если честно не догадался проверить, вы утверждаете, что в одной транзакции mssql каждый раз открывает новое соединение?
      блоки, которые сейчас используются в бою, это примерно 30Мб раз в час, на сотню узлов PostgeSQL, и примерно 150Мб два раза в сутки, на ту же сотню хостов.
      способ с архивацией — передачей — распаковкой хорош, но хотелось обойтись без ещё одной детальки, которая может сломаться.


    1. QuickJoey
      20.07.2016 10:55

      https://habrahabr.ru/post/305982/#comment_9707862


  1. vdem
    19.07.2016 20:56
    +1

    Не знаю как насчет удаленной БД, но и на локальной вставка большого количества записей по одной будет занимать немалое время. А вариант с одним запросом INSERT, вставляющим тысячу строк за один раз — я полагаю, ни для кого ни секрет.


    1. QuickJoey
      20.07.2016 09:58

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


      1. vdem
        20.07.2016 10:52

        Все же не соглашусь :) Удаленное выполнение запроса в Вашем примере увеличивает эффективность всего в 10 раз, в то время как вставка многих записей одним INSERT увеличивает эффективность в 80 раз (ориентировался по приведенным оценкам времени выполнения). Так что пунктом 2 следовало бы описать вставку многих записей одним запросом, а пунктом 2.5 — удаленное выполнение.


  1. SabMakc
    19.07.2016 21:44
    +1

    Не пробовали вариант с INSERT… SELECT…?
    Должно быстро отработать и нет необходимости формирования запроса «вручную».


    1. MinamotoSoft
      19.07.2016 23:15

      Эта штука точно сработает внутри одного сервера. Как она сработает со «связанными» — предсказать сложно. Надо проверять ;)


    1. QuickJoey
      20.07.2016 10:52

      попробовал, результат такой же, как и в первом случае 2 минуты 49 секунд на 1000 строк.

      insert into RemotePG...RemoteTable (RecordID, RecordName)
      select top 1000 RecordID, RecordName
      from LocalTable with(nolock)
      


  1. avloss
    20.07.2016 00:40

    мне кажется даже если это сработает — то в реальной жизни такой поход упрется в максимальную длину запроса, и это случится на живом сервере в самый не подходящий момент. К тому страдает читаемость кода и логов. Спасибо за статью!


    1. QuickJoey
      20.07.2016 10:15

      это не если, а вполне работает, выше ответил. удалённые сервера, это тупенькие машинки, 2Гб оперативки, на целеронах, используются как POS терминалы в розничной сети.


  1. DarkOrion
    20.07.2016 07:26

    Насколько оперативно новые записи из MSSQL должны появляться в Postgres? Если раз в день/часс, то почему бы не попробовать написать ETL и грузить с помощью SSIS?


    1. QuickJoey
      20.07.2016 10:04

      есть несколько блоков данных, условно медленные и быстрые, быстрые — раз в час, медленные два раза в сутки. проблема в том, что хостов, на которых эти данные нужны, больше сотни. таблиц, которые таким образом синхронизируются примерно два десятка.
      а чем SSIS пакет будет принципиально отличаться? когда-то давно пробовал SSIS с единственным mysql, скорость была похожа, как если бы я просто исполнял запросы из консоли.


      1. DarkOrion
        20.07.2016 10:20

        Ну формально SSIS как и раз и создан под задачи, которые вы описываете — мигрировать данные между системами. Он по дефолту заточен под большие объемы, использует пакетные операции и т.п. Конкретный адаптер\драйвер для системы (в вашем случае — Postgres) обычно тоже можно затюнить под конкретную задачу\профиль данных (размер блока и т.п.).
        Соответственно пишите ETL (если вы не трансформируете данные, то это вообще можно мастером сделать), настраиваете два джоба (часовой и полусуточный) и готово.
        Конкретных бенчмарков к сожалению не могу предоставить. Попробуйте на одной таблице, в теории должно быть как минимум не хуже вашего решения.


        1. QuickJoey
          20.07.2016 10:50

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


        1. QuickJoey
          20.07.2016 11:50

          попробовал, цедит в час по чайной ложке: за 15 минут около 9к. записей. при этом в свойствах data transformation вроде бы прописано, что 1000 записей за раз, в 100 потоков. плюс, SSIS не видит источников данных из 64-битного драйвера, что неудобно, потому что у меня все источники заведены там.


          1. semI-PACK
            20.07.2016 19:37

            Тоже столкнулся с такой задачей. Данные на линкед сервер шли очень медленно. Таблица с 65 млн записей скопировалась бы по расчетам только через 300 суток непрерывной записи. Решение нашли тоже через SSIS. В access базу пишутся через ssis пакет по 1 млн данные (если больше аксесс тупить начинает), затем другой ssis выбирает из него данные в другой сервер (но уже напрямую).


            1. QuickJoey
              20.07.2016 19:43

              а конечный сервер при этом какой? и сколько времени занимает весь процесс?


              1. semI-PACK
                21.07.2016 10:45

                на обоих серверах MSSQL, на перенос 1 млн уходит около 10 мин


          1. DarkOrion
            21.07.2016 12:43

            Очень странно. У нас SSIS перегружает Oracle > MS SQL по 4 млн.записей в минуту, причем это ограничено в основном скоростью работы view на стороне Oracle (из физических таблиц — 5+ млн.записей в минуту). Таблицы не очень широкие, атрибутов по 10-20.


            1. QuickJoey
              21.07.2016 17:56

              общее с комментарием выше – таргет сервер mssql, видимо дело в этом.


              1. DarkOrion
                22.07.2016 01:18

                А какие версии у Postgres/MSSQL? Подниму у себя, погоняю. Непонятно, где затык. Скорость вашего метода 2.5 имхо в том, что данные одной строкой передаются, по сути файлом. Можно попробовать вставить ssisом из csv.


                1. QuickJoey
                  22.07.2016 09:54

                  версии более-менее всё равно, 9.5 Postgres, и любой выше 2000 у MSSQL. главное в скорости, что MSSQL не следит за подтверждением транзакции на удалённом сервере. отправил, там что-то исполнилось, здесь пришло «запрос исполнился».
                  csv вряд ли даст какой-нибудь прирост, но появится лишний шаг (или два), которые тоже могу сломаться. и проблема, мне кажется, не в том откуда выбирать, выбирает быстро, вставляет медленно.


  1. Danik-ik
    20.07.2016 10:45

    В mssql есть прекрасный и быстрый инструмент для импорта- экспорта. За давностью лет могу ошибиться в названиях, но если в enterprise manager зайти в раздел packages — то это самое оно.
    Быстро
    Любые odbc источники
    Можно планировать запуск
    Можно строить сложные последовательности обработки
    И НЕ НАДО ДУМАТЬ ОБ ЭКРАНИРОВАНИИ ДАННЫХ!!!
    Это был mssql 2000. Вряд ли сегодня стало много хуже.


    1. Danik-ik
      20.07.2016 11:40

      кстати, мы использовали это для систематической синхронизации двух баз данных управленческого учёта в процессе постепенного перехода на новое ПО: старой, несвязной, с двоящимися записями на карат aka db2 и новой на MSSQL. Абсолютно разные структуры таблиц, проверки, устранение дублирования, обеспечение связности данных… При этом визуальное наглядное представление и автоматическая работа. На тот момент кроме MS никто, кажется, такого полноценного инструмента не имел.

      Целиковый запрос м.б. и побыстрее, но экранирование данных игнорировать нельзя.


      1. QuickJoey
        20.07.2016 11:55

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


  1. lokks
    20.07.2016 16:46

    А вы не пробовали то же самое хотя бы для теста сделать с помощью Foreign data wrapper?
    Насколько всё будет хуже/лучше для простого COPY из внешних таблиц?


    1. QuickJoey
      20.07.2016 16:48

      нет, не пробовал, потому что в моём случае со стороны Postgres это делать неудобно.


  1. radya100
    20.07.2016 17:02

    Talend — возможно лучшее решение, чем SSIS для данной задачи/ Есть бесплатный www.talend.com/download


    1. QuickJoey
      21.07.2016 17:58

      там куча инструментов, и по названию так сразу не понятно, чем они занимаются. а качать по 700Мб, чтобы в этом разобраться…
      может быть вы напишите что именно пробовали и как настраивали? хотя, ощущение, что краткого описания не получится, там полноценная статья.