У вас есть база данных MS SQL Server, которую нужно перенести на другой физический комп. Вы уже сделали бэкап и радостно приступаете к восстановлению. Но тут обнаруживается, что на том компе, куда нужно перенести базу, установлена более старая версия MS SQL Server. Stack Overflow уверяет вас, что всё плохо. Но так ли это на самом деле?


Конечно, перенос базы из более новой версии в старую — это не классический и не самый правильный сценарий работы. Но зачастую базы данных создаются такими, что они поддерживают все более новые версии SQL, начиная с какой-то, например с 2008 R2, т.к. прямая совместимость у MS SQL более чем отличная. И, например, ваш клиент уже поставил себе MS SQL 2016, а у вас на тестовом сервере для разработки стоит MS SQL 2014. А вы хотите развернуть себе базу клиента, чтобы разобраться, где у него путаница с данными.

Microsoft открестились от проблемы — мол нет у них обратной совместимости, и баста. Резервную копию, созданную в более новом сервере, невозможно восстановить на более старом сервере. Да, у них есть инструменты типа DTS, копирование базы, экспорт-импорт и т.п. Но они настолько неудобны и громоздки, что нормальный перенос большой БД с множеством таблиц сделать с их помощью не особо удобно. Во всяком случае лично у меня не получилось.

Да, можно сгенерировать SQL-скрипты всей базы, включая данные. Но представьте, у вас в базе куча блоб-полей с большими данными, и вообще размер всей базы 500+ ГБ. Представляете, сколько будет занимать такой скрипт, сколько времени он будет генерироваться и исполняться.

Итак, задача — в точности воссоздать базу данных (структуру и данные) из новой версии MS SQL сервера в более старой версии. Я придумал довольно простое решение, которым хочу поделиться. Конечно, данное решение обладает значительным количеством ограничений, но всё же на мой взгляд это лучше, чем DTS и скрипты.

Ограничение номер один заключается в том, что вам нужен доступ через MS SQL Management Studio к обоим серверам — старому и новому. Если это не возможно, то должна быть возможность на той машине, откуда нужно перенести базу, установить ту версию SQL, в которую нужно перенести базу, чтобы перенести базу сначала в эту версию локально, а потом уже перетащить её через бэкап или непосредственно через *df файлы базы данных (через Detach/Attach) на новую машину (версия SQL Server'а в этом случае уже будет совпадать).

Еще одним ограничением является то, что вам будет необходим скрипт схемы базы данных (всех объектов, включая таблицы, индексы, констрейнты, хранимые процедуры, триггеры и т.п.) без данных, причем инструкции создания Foreign Key Constraints должны в этом скрипте идти в самом конце, отдельно от скрипта создания самих таблиц.

Опишу кратко сам алгоритм переноса данных. Все действия выполняются в сеансе Management Studio, подключенной к серверу, на который нужно перенести базу.

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

2) Скриптом схемы базы данных создаем все объекты базы (таблицы, индексы, представления, триггеры, хранимые процедуры и функции), но без создания Foreign Key Constraints. Создавать FK на этом этапе нельзя, т.к. они будут мешать вставке данных.

3) Подключаем базу данных, из которой будем переносить данные, в качестве Linked Server'а, чтобы можно было использовать в запросах к новой базе данных обращения к старой базе данных.

EXEC sp_addlinkedserver     
   @server=N'LinkedServerAlias',   
   @srvproduct=N'',  
   @provider=N'SQLNCLI',   
   @datasrc=N'LinkedServerHost\LinkedServerName';  

EXEC sp_addlinkedsrvlogin 'LinkedServerUser', 'false', null, 'RealUser', 'RealUserPassword';

4) Т.к. структуры баз совпадают, воспользуемся встроенной хранимой процедурой sp_msforeachtable, которая позволяет выполнить запрос над каждой таблицей БД, чтобы сгенерировать скрипт переноса данных из старой базы в новую через запрос вида

INSERT INTO ? 
  SELECT * FROM ?

Вместо знака вопроса sp_msforeachtable подставляет имя каждой таблицы и выполняет запрос несколько раз (по одному разу на каждую таблицу).

Здесь я натолкнулся на самое большое количество граблей.

а) Проблема номер один заключается в том, что для таблиц с IDENTITY-полями необходимо вызывать:

SET IDENTITY_INSERT ON; 
--INSERT INTO ... (сама вставка);
SET IDENTITY_INSERT OFF;

б) Проблема номер два заключается в том, что на таблицах, в которых нет IDENTITY-полей, делать данный вызов нельзя, поэтому требуется динамически определять, есть в таблице IDENITY-колонка или нет.

Это можно сделать таким запросом:

SELECT * FROM INFORMATION_SCHEMA.COLUMNS 
WHERE (TABLE_NAME='SomeTable') AND (COLUMNPROPERTY(object_id('dbo.SomeTable'), COLUMN_NAME, 'IsIdentity') = 1)

в) Проблема номер три заключается в том, что, как оказалось, в режиме IDENITY_INSERT ON нельзя делать

INSERT INTO ... SELECT * FROM ...

, а нужно перечислять конкретные поля.

Перечислить поля таблицы в строку можно таким запросом:

SELECT SUBSTRING(
    (SELECT ', ' + QUOTENAME(COLUMN_NAME)
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = 'SomeTable'
        ORDER BY ORDINAL_POSITION
        FOR XML path('')),
    3,
    200000);

4) Генерируем скрипт вставки по все таблицы:

Процедура генерации скрипта
EXEC sp_msforeachtable N'
DECLARE @command varchar(MAX);
DECLARE @name varchar(200);
SET @name=''?'';
SET @name = SUBSTRING(@name, 8, LEN(@name)-8);

SET @command = '''';
SELECT @command= 
 SUBSTRING(
    (SELECT '', '' + QUOTENAME(COLUMN_NAME)
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = '''' + @name + ''''
        ORDER BY ORDINAL_POSITION
        FOR XML path('''')),
    3,
    200000);

SET @command = ''INSERT INTO ''+ @name +'' (''+ @command + '') SELECT '' + @command + '' FROM '' + ''LinkedServerAlias.SourceDatabase.'' + ''?'';

SET @command=
''IF EXISTS (select * from INFORMATION_SCHEMA.COLUMNS where (TABLE_NAME='''''' + @Name + '''''') AND (COLUMNPROPERTY(object_id(''''dbo.''+@Name+''''''), COLUMN_NAME, ''''IsIdentity'''') = 1)) SET IDENTITY_INSERT '' + @name + '' ON; '' +@command;
SET @command=@command+'';'' +
''IF EXISTS (select * from INFORMATION_SCHEMA.COLUMNS where (TABLE_NAME='''''' + @Name + '''''') AND (COLUMNPROPERTY(object_id(''''dbo.''+@Name+''''''), COLUMN_NAME, ''''IsIdentity'''') = 1)) SET IDENTITY_INSERT '' + @name + '' OFF;'';


PRINT (@command);
--EXEC(@command); // Если раскомментировать, скрипт будет сразу исполняться, а не только выводиться на экран
'


5) Исполняем сгенерированный скрипт переноса данных

6) Исполняем скрипт на создание всех Foreign Key Constraints (теперь уже можно).

7) Готово! Вы перенесли базу из нового сервера SQL в старый, хоть это и считалось невозможным. Причем перенос осуществляется всего лишь раза в полтора медленнее, чем скорость передачи данных по сети, т.е. довольно быстро.

8) Прибираемся за собой (отключаем Linked Server):

EXEC sp_droplinkedsrvlogin 'LinkedServerUser', null;
sp_dropserver 'LinkedServerAlias';

Ограничения метода.

1) Подобным методом не получится перенести таблицы, в которых есть колонки с типом XML.
Наверняка есть множество других ограничений, т.к. в той базе, которую я переносил подобным способом, не использовалось множество возможностей SQL-сервера. Вы можете написать об ограничениях в комментариях, и я дополню ими статью.

Спасибо за внимание! Надеюсь, кому-то поможет.

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


  1. x893
    30.08.2018 03:36

    Можно и Foreign Key Constraints сразу, только надо добавить анализ таблиц на связи. Но совсем грустно, если Foreign внутри одной таблицы.


    1. alan008 Автор
      30.08.2018 07:38

      Было несколько таблиц с FK внутри одной таблицы и несколько пар таблиц с взаимообратными FK


  1. CatBoris
    30.08.2018 05:04

    Создать пустую базу, скопировать схему с помощью RedGate SQL Compare, потом скопировать данные с помощью RedGate SQL Data Compare. Насколько я помню, можно даже из бэкап файла сравнение/копирование производить.
    Если надо один раз, то можно и триальными версиями воспользоваться.


    1. darkdaskin
      30.08.2018 20:22
      +1

      Аналогичные инструменты встроены в Visual Studio, в том числе бесплатную Community Edition.


      1. VitalKoshalew
        01.09.2018 01:12

        Придерусь: VS Community Edition бесплатна только для малого бизнеса.


  1. unfilled
    30.08.2018 06:04

    Индексы лучше создавать после переноса данных — это ускорит сам перенос.
    На таблицу-приёмник лучше накладывать блокировку TABLOCK, а БД-приёмник должна быть с моделью восстановления simple или bulk-logged, чтобы sql server мог выполнять insert с минимальным протоколированием.
    С identity-колонками по-любому геморрой.

    >>7) Готово! Вы перенесли базу из нового сервера SQL в старый, хоть это и считалось невозможным. Причем перенос осуществляется примерно со скоростью передачи данных по сети, т.е. очень быстро.
    1) это не считалось невозможным;
    2) это гораздо медленнее чем передача данных по сети, особенно в таком исполнении, как в статье.


    1. alan008 Автор
      30.08.2018 07:36

      1) У меня была модель SIMPLE как раз, забыл об этом написать.
      2) 10 ГБ база у меня скопировалась за 20 минут по 100 Мегабитному каналу (со скоростью сети было бы не менее 13 минут), так что явно не "гораздо медленнее"


      1. unfilled
        30.08.2018 08:25

        10 ГБ — это объём данных? Или размер mdf-файла?


        1. alan008 Автор
          30.08.2018 08:56

          Реальный объем данных был около 9 ГБ и около 10% reserved space.


    1. port443
      30.08.2018 18:17

      Да, при сложных индексах можно сильно выиграть.

      И ещё момент странный: почему constraints отнесены на применение после переноса данных, а триггеры — нет? Триггеры же могут быть достаточно сложными.


      1. alan008 Автор
        31.08.2018 00:26

        У меня в базе триггеров было мало и они были примитивные. Так то да, можно было их тоже потом создать. Кстати триггер на INSERT сработает всего один раз после INSERT...SELECT FROM


  1. firedragon
    30.08.2018 09:14

    Попробуйте создать базу с моделью восстановления simple и compatibility version совпадающей с вашей установкой.
    Дальше практически в несколько кликов перенесите данные через мастер импорта.
    После отсоедините файлы и перенесите на другую машину.
    Это действовало для 2013 Sharepoint'a.


  1. alexhott
    30.08.2018 14:02

    Обращение напрямую к прилинкованному серверу не всегда эффективно, иногда лучше использовать openqwery. По крайней мере выборки по условиям выполняются в разы быстрее.
    1) Подобным методом не получится перенести таблицы, в которых есть колонки с типом XML.
    Вот тут как раз openqwery тоже поможет — выполняете запрос на доноре с преобразованием XML в текст. Инсертите потом его спокойно в поля XML.


  1. alexhott
    30.08.2018 14:07

    Теперь надо все в одну процедуру оформить
    exec db_copy старая база, новая база


  1. kantuz001
    30.08.2018 16:42

    У меня лишь один вопрос, зачем?


    1. alan008 Автор
      30.08.2018 16:45

      Я же написал. Есть "шаблон" базы (голая схема без данных), который может работать в разных версиях сервера SQL. У каждого клиента свои данные в базе и своя версия SQL. Клиент хотел передать мне бэкап базы, чтобы я посмотрел ошибку в данных. Но меня нет настолько новой версии SQL, как установлена у клиента, я хочу загнать его базу в свой, более старый SQL.
      Бывают и другие похожие потребности, например необходимость миграции базы с компа на комп внутри предприятия (например, из-за роста размера БД), когда на одном компе установлен новый лицензионный SQL, а на второй — старый лицензионный SQL.


  1. MichaelSamteladze
    30.08.2018 17:13

    Подобные проблемы Я всегда решаю с помощю RedGate Sql Compare & Data Compare


  1. port443
    30.08.2018 18:20

    А что если разбить создание схемы на два этапа, и добавить identity после вставки данных? Мне проверить негде, но alter table сможет корректно добавить identity?


    1. alan008 Автор
      30.08.2018 20:40

      Это не возможно.


      You can't alter the existing columns for identity.


      You have 2 options,


      Create a new table with identity & drop the existing table


      Create a new column with identity & drop the existing column


  1. reci
    30.08.2018 18:20

    Я просто оставлю это здесь
    Стандартная генерация скриптов, «Schema + Data». Выручало не раз при даунгрейде версии сервера.



  1. alan008 Автор
    30.08.2018 20:45

    Спасибо за наводку! Для схемы подойдет. Скриптовать сами данные на базе размером в десятки и сотни гигабайт выглядит как ужас-ужас.


    1. reci
      30.08.2018 23:23

      Пробовал скриптовать базу в 90 ГБ. Спешить было особо некуда, оставил на ночь. К утру скрипты успешно создались, а вот с запуском пришлось повозиться. А если ещё изменить что-то в скрипте надо — то да, ужас-ужас :)

      Но для небольших баз (до ~4 Гб) вполне себе вариант.