From: Vadim Pavlov <pvm_job(at)mail.ru>
Newsgroups: email
Date: Mon, 1 Dec 2004 14:31:37 +0000 (UTC)
Subject: Восстановление данных в СУБД Interbase
Delete from mytable; Commit:..
(или восстановление данных в СУБД Interbase )
Случилось страшное, кровь матери пролил сын обезумевший.
(c)Кто-то древний
Предисловие
Данная статья написана на основе статей: "The Interbase On - Disk
Structure" by Ann Harrison ; "Structure of a Data Page" by Paul Beach,
а так же включает в себя мой горький опыт в удалении важных данных, а
потом в их успешном восстановлении. Я никоим образом не собираюсь
нарушать права ни выше перечисленных авторов, ни компаний, чьи
зарегистрированные торговые знаки перечисленные в данной статье.
Эта статья была написана Вадимом Павловым в 2004г. с соответствующим
наложением авторских прав. Вы можете опубликовать её по своему
усмотрению, единственным условием является опубликование данного
абзаца.
По любым вопросам, касающимся данной статьи можно связаться с автором
по адресу: pvm _ job(at)mail.ru, либо пообщаться на форуме, в чате
сайта http://penguin.photon.ru
Концептуализация
Итак, всё что можно было сделать плохого мы уже сами своими
собственными руками сделали. Но не стоит сильно отчаиваться, в
некоторых случаях можно помочь беде.
Много было уже рассказано и написано о том, как полезен бэкап и
насколько часто его стоит делать. Но внештатные ситуации бывают
всегда, и восстановление целого рабочего дня небольшой фирмы может
оказаться трудоёмким и затруднительным процессом. Поэтому стоит
немного подумать и посмотреть, а не проще ли попробовать восстановить
удаленные данные.
Поиск в интернет, о возможности восстановления данных, не дал нам
никаких результатов, поэтому будем делать всё сами.
Мы в данной статье будем рассматривать возможность восстановления
данных в СУБД Interbase 6. x , Firebird 1. x для Windows (для других
операционных систем возможны небольшие отличия в дисковой структуре).
Сначала немного формализуем задачу.
Что мы имеем:
1. Реляционную базу данных с несколькими тысячами записей, которые
необходимо восстановить.
2. Устойчивое желание восстановить базу и немного терпения.
Как вам, наверное, уже известно, сразу после удаления какой либо
информации из базы данных Interbase , файлы базы не уменьшаются,
значит, вся информация осталась, а были только удалены ссылки на место
хранения необходимой нам информации (записей).
После удаления информации о местоположении записей оказывается так,
что утерянная информация находится в файле вперемешку с . Тем более
что, каждой записи может содержаться несколько версий.
Самое главное для успешного восстановления - быстрота вывода базы
данных из эксплуатации. Чем раньше это будет сделано, тем выше
вероятность восстановления утерянной информации.
Не нужно останавливать офис на неопределенный срок, достаточно просто
скопировать базу поближе к себе и приступить к работе.
Для удобства можно воспользоваться утилитой IBSurgeon Viewer
(http://www.ib-aid.com), а также любым клиентом sql для Interbase , вполне
подойдет ibconsole , wisql , isql и т.д. А так же нам очень помогут
статьи The InterBase On - Disk Structure By Ann Harrison , Borland
Developers Conference San Diego 2000, Structure of a Data Page By Paul
Beach .
С задачей мы определились, установили необходимое программное
обеспечение и запаслись статьями о структуре файла данных Interbase .
В данной статье мы рассмотрим все основные и необходимые моменты
данных документов для восстановления информации из базы. Так же Вам
придется применить навыки программирования, либо обратиться за
помощью.
Итак, приступим.
Дисковая структура базы данных Interbase
Для понимания процесса восстановления и возможности такового изложим
немного теории.
База данных состоит из одного или нескольких файлов. Файл базы данных,
в свою очередь, состоит из набора (далее просто страниц) фиксированной
длины, размер которых указывается при создании базы данных, каждый
файл базы данных содержит в себе страницы одинакового размера.
Стандартной является страница 4096 байтов. Достоверность данного
утверждения можно проверить, поделив размер файла на размер страницы.
У Вас получится количество страниц содержащихся в базе. Для удобства
работы с каждой страницей в отдельности можно разрезать данный файл на
множество файлов содержащих в себе только одну станицу, либо
воспользоваться программой IBSurgeon Viewer .
Страница заголовок (Header page) это первый блок в первом файле в базе
данных. Когда Interbase присоединяется к базе данных он считывает
первый килобайт файла. Заголовок содержит в себе критичную информацию
о базе данных. Она включает в себя номер версии дисковой структуры (On
Disk Structure - ODS) и размер страницы. После того, как сервер
Interbase установил, что версия дисковой структуры, которая
используется в файле им поддерживается, он перечитывает
страницу-заголовок, используя правильный размер страницы и узнает
необходимую информацию, такую как имена и диапазон страниц,
содержащихся во вторичных файлах базы данных, следующую доступную
транзакцию и последнюю, интересующую нас, транзакцию.
На последующем шаге ищется ядро системных таблиц и строится внутреннее
представление базы данных.
Страница указателей ( Pointer page )
Страница-заголовок содержит информацию о месте положения первой
страницы указателей для таблицы rdb$pages . Системная таблица
rdb$pages позволяет interbase найти критичные страницы, включающие
страницы данных для таблиц. Система читает первую страницу указателей
и использует её для поиска первой страницы данных rdb$pages . Из неё
затем можно найти страницы данных и индексов других системных таблиц.
Страница указателей имеет простой заголовок, который включает в себя:
тип страницы и номер следующей страницы указателей данной таблицы.
Остальная часть страницы заполнена массивом из четырехбайтовых чисел,
которые являются номерами страниц и составляют данную таблицу
Таблица rdb$pages содержит записи не только о страницах данных, а
также о страницах учета транзакций. Подобно странице указателей
страница учета транзакций состоит из простого заголовка, который
включает в себя тип страницы и номер следующей страницы учета
транзакций. Оставшаяся часть страницы является массивом двухбитовых
записей, которые отражают состояние транзакций в системе. Ноль
указывает на то, что транзакция не была начата, активна или без
подтверждения или отката. Единица указывает на то, что транзакция была
подтверждена. Двойка указывает на то, что транзакция была
опровергнута. Тройка указывает на то, что транзакция в состоянии
(неопределенное состояние, которое существует в середине двухфазового
подтверждения транзакции).
Чтобы определить состояние транзакции Interbase использует номер
транзакции, как индекс в массиве транзакций и просматривает состояние
соответствующих битов. Алгоритм более сложный, так как зависит от
заголовка страницы. В случае если одна транзакция проверяет состояние
другой транзакции и обнаруживает, что она обозначена как активная, а
фактически является - (транзакции проверяется по средством проверки
таблицы блокировки) - то она меняет состояние транзакции с активной на
откаченную.
Страница распределения места (Space inventory page)
Последней из страниц является страница распределения места. Страница
распределения места показывает какие страницы являются распределенными
() и если она зарезервирована, то свободна ли она и насколько.
Страница распределения места находится сразу за страницей-заголовком.
Она подобно всем страницам имеет заголовок, в котором определен тип
страницы. Остальная часть страницы - массив 1-битных кластеров,
который соответствует страницам в базе данных. Каждая страница вне
зависимости от типа включена в страницу распределения пространства (за
исключением страницы-заголовка). Заголовок страницы распределения
места не включает в себя указатель на следующую страницу распределения
места, так же эти страницы не перечислены в таблице rdb$pages . Они
расположены на определенных интервалах. Таким образом, Interbase
вычисляет расположение следующей страницы в зависимости от размера
страницы базы данных и длины заголовка.
Когда страница добавляется в таблицу или индекс, Interbase меняет её
состояние в странице распределения места. Ошибка "orphan page"
возникает, когда сервер находится в середине процесса распределения
новой страницы. Запись в страницу распределения места сделана,
указанная страница выделена, но новая страница не была записана и
осталась как .
Страница генераторов (Generator page)
Страница генераторов последняя из страниц. Страница генераторов
состоит из заголовка и четырехбайтовых записей, которые представляют
состояние генераторов. Индексом в этом массиве служит порядковый номер
генератора. Таблица rdb$pages содержит в себе записи о страницах
генераторов.
Страница основного индекса (Index root page)
Любые таблицы, включая те, у которых нет индексов, имеют страницу
основного индекса. Страница основного индекса отожествляется с
вершиной каждого индекса определенного для таблицы. На этой странице
должен быть описан (перечислены поля индексации) каждый индекс таблицы
БД. Также здесь указывается полезность индекса, которая может быть
рассчитана как отношение числа различающихся индексных полей внутри
индекса и среднего количества записей. Среднее количество записей -
это число страниц БД, занятых данной таблицей, делённой на
максимальное количество записей на странице. Полезность индекса -
чрезвычайно важный показатель, на основании которого InterBase
оптимизирует выполнение запросов.
Страница индекса (index page)
Заголовок страницы индекса включает тип страницы и номер страницы
следующей на данном уровне.
Индексами называется древовидная структура. На странице основного
индекса указывается верхняя страница индекса. Она содержит записи,
указывающие на точки следующих уровней индексных страниц.
Страница данных (Data page)
Страница данных содержит следующие данные: записи, фрагменты записей,
фрагменты, запасные версии, изменения, поля типа blob и относящиеся к
данному полю структуры. Заголовок страницы, в частности, содержит тип
страницы, номер отношения (relation , таблицы) и номер следующей
страницы, содержащую данную таблицу. В отличие от других страниц,
страницы данных содержат значимую структуру, следующую сразу за
заголовком. Эта структура имеет название индекс записей. Последняя
часть db_key записи является смещением в массиве индексов записей
снизу страницы. Этот индекс содержит актуальное положение и размер
сохраненной на странице строки таблицы.
Строки индекса расположены после заголовка до конца страницы. Записи
таблицы сохраняется снизу вверх. Когда они встречаются, страница
объявляется полной. В случае с резервированием места для изменений
(режим по умолчанию), страница будет заполнена до определенной точки,
оставшееся место будет использовано для создания новых версий записей
на текущей странице.
Эти структуры мы рассмотрим далее более подробно.
Индекс должен содержать длину сохраненной записи, несмотря на то, что
все записи на странице принадлежат одной и той же таблице, поэтому, в
теории должны быть одной длины. Дело в том, что записи компрессируются
перед тем, как будут сохранены. Алгоритм сжатия самый простой -
кодирование переменной длины, которое предназначено для отлова общих
мест нулевых колонок и последовательных пробелов.
Индекс содержит смещение, поэтому запись может перемещаться по
странице без воздействия на её основной идентификатор - её db_key.
Когда Interbase обнаруживает, что страница стала фрагментированной в
следствии того что некоторые записи были удалены, то он сдвигает все
оставшиеся данные вместе, делая одно большое пространство вместо
небольших маленьких фрагментов. Это обычные действия базы данных.
Как же выглядит строка таблицы на этом уровне? Во-первых, идет
фиксированной длины заголовок, который включает в себя номер
транзакции, которая создала эту версию записи и формат версии для этой
записи. Если существует старая версия записи, то заголовок также
содержит указатель на неё. В заголовке также указан тип записи:
обычная запись, фрагментированная запись, фрагмент или поле blob .
Маленькие blob -поля часто расположены на странице вместе с записью, к
которой они относятся. Если запись фрагментирована, то в заголовке
указывается на фрагмент.
Последняя часть служебных данных в записи - добавление к записи
переменной длины битов (кратной 8), определяющие значения NULL в
соответствующих полях.
Столбцы таблицы расположены в соответствии со значением rdb$field_id
таблицы rdb$relation_fields, которая описывает их. Если порядок,
определенный значением поля rdb$position отличается, то
высокоуровневый механизм приводит их в соответствующий порядок.
Высокоуровневая функция также смотрит на версию формата записи и
использует ее для нахождения соответствующего формата в rdb$formats.
Все записи будут переведены в наиболее подходящий формат во время
перемещения из страницы в кэш записей. До тех пор, пока запись не
будет изменена, она не будет перезаписана, даже если её формат
устарел.
Если запись является сохраненной ( back version ), её основная версия
будет содержать флаг, который показывает какая из них является
изменением. Изменения - это набор различий, которые могут быть
применены к основной записи для создания её сохраненной ( back )
версии. Формат изменения очень похож на кодирование переменной длины,
с байтами указывающим количество заменяемых символов и количество
сохраняемых символов.
Записи не фрагментируются до тех пор, пока сжатый размер данных меньше
размера страницы. Когда запись модифицируется её сжатый размер может
увеличиваться, до тех пор, пока не останется места на странице, в
данном случае она будет фрагментироваться. Фрагментация значительно
влияет на производительность, так как чтение фрагментированной записи
требует выборку, как минимум двух страниц. Запись фрагментированных
строк может требовать четыре или более страниц для использования
стратегии , которая требует, чтобы изменения на странице были записаны
в соответствующем порядке.
Страница BLOB (Data page)
Страницы, полностью занятые двоичными данными ( BLOB ), можно при
определённых условиях выделить в отдельный тип страниц БД, которые не
отражаются на странице указателей.
Также страница двоичных данных может быть страницей указателей на
другие страницы двоичных данных. Для каждого созданного поля BLOB
создаётся запись, содержащая информацию о расположении данных поля и
данные о содержимом, которые могут быть полезны при чтении. Механизм
хранения таких полей определяется их размером и бывает трёх типов
(0,1,2).
* Механизм 0 . Поле умещается на одной странице БД вместе с записью.
* Механизм 1 . Когда поле не умещается на одной странице БД вместе с
записью, поле записывается в специализированные страницы, а в поле
двоичных данных на странице с остальными полями данных записи будет
помещен массив указателей на занятые полем страницы
* Механизм 2. Когда места на начальной странице не хватает даже для
того, чтобы записать туда массив указателей, InterBase создаст
страницу (страницы) указателей на страницы с полем BLOB .
Восстановление информации
В самом простом случае для восстановления данных нам нужно знать лишь
структуру страниц данных. Всю остальную полезную информацию мы сможем
получить с помощью sql запросов к системным таблицам Interbase , что
существенно ускорит и упростит процесс восстановления информации и,
соответственно, продлит и укрепит наш сон.
Подробный формат страницы данных и записи
Рассмотрим формат страницы данных и записи более подробно.
Выборочно информацию возьмем из файла ods . h ( on disk structure ) из
поставки Interbase ( firebird ) и опишем ее более подробно.
Сначала идет стандартный заголовок страницы, далее следует номер
последовательности в отношении, номер отношения, количество записей на
странице. Далее следует повторяющаяся структура { смещение, длина }
фрагмента записи.
/* Record header */
Заголовок записи
typedef struct rhd {
SLONG rhd_transaction;/* transaction id */
SLONG rhd_b_page; /* back pointer */
USHORT rhd_b_line; /* back line */
USHORT rhd_flags; /* flags, etc */
UCHAR rhd_format; /* format version */
#ifdef _CRAY
UCHAR rhd_pad [7];
#endif
UCHAR rhd_data [1];
} *RHD;
В заголовке записи определен номер транзакции, указатель на старую
версию записи, флаги, версию формата записи и непосредственно данные.
Так же из файла ods . h можно узнать структуру фрагментированных
записей и структуру записей поля blob . Но в данной статье мы не будем
их рассматривать.
Далее определяются флаги записей
#define rhd _ deleted 1 Запись логически удалена
#define rhd_chain 2 Запись является старой версией
#define rhd_fragment 4 Запись является фрагментом
#define rhd_incomplete 8 Запись неполная
#define rhd_blob 16 Поле типа blob
#define rhd_delta 32 Предыдущая версия, различия только
#define rhd_large 64 Объект является большим
#define rhd_damaged 128 Объект известен , как поврежденный
#define rhd_gc_active 256 Мусор, мертвая версия записи
Как говорилось, ранее данные в записях сжаты методом кодирования
переменной длины ( RLE ). Суть метода заключается в том, что
положительное число указывает на количество следующих байт, которые
непосредственно следует прочитать, отрицательное же число указывает,
что следующий байт нужно повторить abs ( n ) (где abs - модуль числа)
раз.
Заголовок хранит информацию о транзакции, которая требуется для
реализации multi - generational архитектуры, в нормальной записи
заголовок будет только содержать указатель на старую версию.
После заголовка и перед данными идет вектор нулевых бит на каждое поле
(добитое до 8 (битовой) байтовой границы) в таблице. Если флаг
установлен, тогда поле равно нулю, пока данные установлены в 0 для
улучшения компрессии. Это означает что заголовок состоит из
эффективной длины 16 байт и 3 байта были добавлены для выравнивания.
Байты для нулевого битового вектора добавляются по необходимости, в
зависимости от количества полей, определенных в таблице.
В результате того, что резервируется место для версий, когда
заканчивается место на странице, остается возможность того, что новая
версия записи будет сохранена на этой же странице.
При восстановлении данных не следует забывать о внутреннем формате
представления чисел, даты и времени и кодировке строк, дабы заранее не
разочароваться в возможности восстановления.
Напомню вам, что на платформе intel при записи числа типа word (два
байта), сначала записывается младший байт, а затем старший, например
число 0 x 5684 будет представлено на диске, как 84:56. Аналогично
сохраняются и числа типа long .
Формат даты является необычным по сравнению со стандартными типами
записи дат, поэтому немного остановимся на его описании.
Для хранения даты и времени в Interbase существует тип date, его
внутреннее представление таково. Это запись из двух 32-разрядных
знаковых целых чисел. В первом числе хранится число дней, прошедших с
17 ноября 1858, а во втором - время в десятых долях миллисекунды,
прошедшее после полуночи.
Для перевода в стандартный тип Delphi - TDateTime, который объявлен
как TDateTime = type Double, где целая часть - это число дней,
прошедших с 30 декабря 1899, а дробная часть - время, прошедшее после
полуночи (.0 = 0:00; .25 = 6:00; .5 = 12:00; .75 = 18:00) можно
воспользоваться простой формулой
DateTime := Days - IBDateDelta + MSec10 / MSecsPerDay10, где
Days - количество дней в формате Interbase ;
IBDateDelta = 15018 - разница в днях между датами Delphi и Interbase;
MSec 10 - время в десятых долях миллисекунды, прошедшее после
полуночи;
MSecsPerDay10 = Количество миллисекунд в сутках * 10
На данный момент мы в достаточной степени узнали необходимую
информацию о том, каким образом и где хранится информация в
препарируемой нами базе данных. Изучив подробно все структуры
используемые СУБД было бы возможно написать программу и воочию
посмотреть и восстановить все данные своими руками. Но, как говорится,
, я же предпочитаю воспользоваться стандартной программной isq ( wisq
, кому как нравится) и движком interbase (ведь не зря его писали?)
если, конечно база данных не повреждена, и тем самым ускорить процесс
восстановления информации.
SQL- запросы
Далее мы рассмотрим sql -запросы, которые необходимы нам для
восстановления утерянной информации.
select rdb$relation_fields.rdb$field_name,
rdb$relation_fields.rdb$field_id, rdb$fields.rdb$field_length,
rdb$types.rdb$type_name from rdb$relation_fields left join rdb$fields
on rdb$relation_fields.rdb$field_source= rdb$fields.rdb$field_name
left join rdb$types on rdb$types.rdb$type=rdb$fields.rdb$field_type
where (rdb$relation_name=' MYTABLE ') and (rdb$types.rdb$field_name=
'RDB$FIELD_TYPE')
Данный запрос дает нам ответ на один из главных вопросов, как
расположены поля в записи таблицы MYTABLE , какой имеют размер и тип
В первом столбце указано название поля, во втором его порядок в
записи, в третьем длина поля в байтах, а в четвертом тип поля.
Осталось только узнать в каких страницах расположена таблица, которую
мы пытаемся восстановить.
select rdb$relation_id,rdb$relation_name, RDB$PAGE_NUMBER,
rdb$page_type from rdb$pages left join RDB$relations on rdb$pages.
RDB$RELATION_ID=RDB$relations.RDB$RELATION_ID where
rdb$relation_name=' MYTABLE ';
В первой колонке указан номер отношения (таблицы), во второй название
таблицы, в третьей страницы, на которых расположена таблица, а в
четвертой тип таблицы.
На самом деле, , мы только из базы номера страниц указателя и индекса,
а не полностью список всех страниц. Так что, засучим рукава и будем
работать далее. Страница индексов для нас никоем образом не важна, а
формат страницы указателя был вкратце описан выше.
Остановимся на нем более подробно.
typedef struct ppg {
struct pag ppg_header;
SLONG ppg_sequence; /* Sequence number in relation */
SLONG ppg_next; /* Next pointer page in relation */
USHORT ppg_count; /* Number of slots active */
USHORT ppg_relation; /* Relation id */
USHORT ppg_min_space;/* Lowest slot with space available */
USHORT ppg_max_space;/* Highest slot with space available */
SLONG ppg_page [1]; /* Data page vector */ } *PPG;
#define ppg_eof 1 /* Last pointer page in relation */
В стандартном заголовке для нас особо интересно поле flag , если как
описано выше, оно установлено в <1>, то это значит что таблица
указателей является последней для рассматриваемого отношения
(таблицы), и соответственно мы можем узнать все необходимые нам
данные.
За стандартным заголовком страницы, идет заголовок страницы
указателей, где соответственно указывается: номер данной страницы, в
последовательности страниц указателя для данного отношения (таблицы);
номер следующей страницы для данного отношения; количество активных
слотов (то есть записей о том, какие страницы используются); номер
отношения (таблицы); наименьший доступный слот и максимальный
доступный слот.
Далее идет вектор данных, в котором непосредственно и перечислены
страницы.
Всё вышеописанное можно наглядно посмотреть с помощью программы
IBSurgeon Viewer.
Итак, что мы знаем в данный момент:
* структуру таблицы - каким образом расположены в ней поля и их
размер;
* номера страниц данных, в которых находится наша таблица;
* структуру страниц данных.
Таким образом, у нас выполнены необходимые и достаточные условия,
чтобы попробовать восстановить утерянные данные.
Восстановление данных
И так мы можем приступать к самому ответственному этапу. Для удобства
работы можно порезать файл базы данных на множество страниц и
вычленить страницы с необходимой нам таблицей. Для начала пусть это
будет любая таблица с текстовыми данными, например справочник товаров
и т.д. Просмотрев его любым текстовым редактором можно заметить, что в
данном файле находятся утерянные текстовые данные. Но возникает другой
вопрос, как эти данные вытащить из базы, если заголовок со смещением и
размером записи утерян навсегда. Возможно придумать массу алгоритмов с
помощью которых можно восстановить утерянные данные, я предлагаю, на
мой взгляд, самый простой, да и по вычислительным возможностям
современных компьютеров восстановление происходит относительно быстро.
В качестве примера данного утверждения могу сказать, что
восстановление 2000 записей (не текстовая таблица) на Athlon XP 2000,
программой написанной на Delphi , заняло порядка 10 секунд.
Суть алгоритма состоит в том, что нам известно следующее:
* размер записи в байтах;
* записи начинаются с конца страницы и сжаты методом rle ;
* записи расположены не по всей странице;
* на любые данные, находящиеся в таблице, можно наложить
соответствующие ограничения.
Возьмем на рассмотрение самый простой метод, варьированием параметров
которого можно добиваться увеличения производительности программы
восстановления во много раз.
Начиная с конца страницы, берем несколько байт (для каждой таблицы
можно отдельно посчитать необходимый минимум) и просто пытаемся
раскодировать, если после декодирования у нас не получился размер
равный размеру записи, то делаем шаг приращения (советую в один байт,
пока не найден кандидат в запись, потом шаг можно пересмотреть), и
повторяем процедуру заново.
В случае, если размеры совпали, то мы получили кандидата на правильную
запись в первом приближении, однако, она должна удовлетворять многим
условиям. Прежде всего, её заголовок не должен содержать ничего
криминального, после этого проверяем на правильность данных, которые
несет в себе запись.
При проверке стоит учитывать несколько факторов, которые напрямую
зависят от предметной области базы данных:
* Если первичным ключом таблицы является поле с автоинкриментом, то
соответственно оно не должно быть более чем значение генератора, так
же, если не определено иное оно не может быть отрицательным;
* Даты, если это, например, простенькая складская программа, не
могут быть в далёком прошлом и далёком будущем;
* Цена на товар не может быть отрицательной либо слишком большой;
Таким образом, кроме вышеперечисленных, можно выдумать ещё массу
реальных ограничений, которые можно наложить на данные.
В качестве примера могу сказать, что при восстановлении 2000 записей
имеющими дубликаты оказались только 4.
Так как их количество оказалось не сильно велико, то основываясь на
знаниях, что именно должно было указано в этих полях, лично я не стал
утруждать себя и возится с версиями записей.
В результате проделанной работы мы получаем из базы всё что нужно.
Для простоты восстановления можно просто приостановить работу всех
триггеров и с помощью sql запросов добавить недостающие данные.
ГОТОВО!!!!
1221 Прочтений • [Восстановление данных в СУБД Interbase (interbase database crash restore trouble)] [08.05.2012] [Комментариев: 0]