|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Страница указателей имеет простой заголовок, который включает в себя: тип страницы и номер следующей страницы указателей данной таблицы. Остальная часть страницы заполнена массивом из четырехбайтовых чисел, которые являются номерами страниц и составляют данную таблицу Страница учета транзакций (Transaction inventory page).Таблица 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 ) и опишем ее более подробно.
/* Page types */ #define pag_data 5 /* Data page */ Указывается, что страница данных идентифицируется номером 5.
#ifdef _CRAY #define MIN_PAGE_SIZE 4096 #else #define MIN_PAGE_SIZE 1024 #endif #define MAX_PAGE_SIZE 16384 #define DEFAULT_PAGE_SIZE 4096 В данном листинге определяются размеры страницы: её максимальный и минимальный размер, а так же размер по умолчанию.
/* Basic page header */ Основной заголовок страницы typedef struct pag { SCHAR pag_type; SCHAR pag_flags; USHORT pag_checksum; ULONG pag_generation; ULONG pag_seqno; /*WAL seqno of last update*/ ULONG pag_offset; /*WAL offset of last update*/ } *PAG; Определяется: тип страницы, флаги, контрольная сумма, номер последней измененной последовательности и смещение последнего изменения.
/* Data Page */ Определяется заголовок страницы данных typedef struct dpg { struct pag dpg_header; SLONG dpg_sequence; USHORT dpg_relation; USHORT dpg_count; struct dpg_repeat { USHORT dpg_offset; USHORT dpg_length; } dpg_rpt [1]; } *DPG; Сначала идет стандартный заголовок страницы, далее следует номер последовательности в отношении, номер отношения, количество записей на странице. Далее следует повторяющаяся структура { смещение, длина } фрагмента записи.
/* 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 {
В стандартном заголовке для нас особо интересно поле flag , если как описано выше, оно установлено в <1>, то это значит что таблица указателей является последней для рассматриваемого отношения (таблицы), и соответственно мы можем узнать все необходимые нам данные. За стандартным заголовком страницы, идет заголовок страницы указателей, где соответственно указывается: номер данной страницы, в последовательности страниц указателя для данного отношения (таблицы); номер следующей страницы для данного отношения; количество активных слотов (то есть записей о том, какие страницы используются); номер отношения (таблицы); наименьший доступный слот и максимальный доступный слот. Далее идет вектор данных, в котором непосредственно и перечислены страницы. Всё вышеописанное можно наглядно посмотреть с помощью программы IBSurgeon Viewer . Итак, что мы знаем в данный момент: • структуру таблицы - каким образом расположены в ней поля и их размер; Таким образом, у нас выполнены необходимые и достаточные условия, чтобы попробовать восстановить утерянные данные. Восстановление данныхИ так мы можем приступать к самому ответственному этапу. Для удобства работы можно порезать файл базы данных на множество страниц и вычленить страницы с необходимой нам таблицей. Для начала пусть это будет любая таблица с текстовыми данными, например справочник товаров и т.д. Просмотрев его любым текстовым редактором можно заметить, что в данном файле находятся утерянные текстовые данные. Но возникает другой вопрос, как эти данные вытащить из базы, если заголовок со смещением и размером записи утерян навсегда. Возможно придумать массу алгоритмов с помощью которых можно восстановить утерянные данные, я предлагаю, на мой взгляд, самый простой, да и по вычислительным возможностям современных компьютеров восстановление происходит относительно быстро. В качестве примера данного утверждения могу сказать, что восстановление 2000 записей (не текстовая таблица) на Athlon XP 2000, программой написанной на Delphi , заняло порядка 10 секунд. Суть алгоритма состоит в том, что нам известно следующее: • размер записи в байтах; Возьмем на рассмотрение самый простой метод, варьированием параметров которого можно добиваться увеличения производительности программы восстановления во много раз. Начиная с конца страницы, берем несколько байт (для каждой таблицы можно отдельно посчитать необходимый минимум) и просто пытаемся раскодировать, если после декодирования у нас не получился размер равный размеру записи, то делаем шаг приращения (советую в один байт, пока не найден кандидат в запись, потом шаг можно пересмотреть), и повторяем процедуру заново. В случае, если размеры совпали, то мы получили кандидата на правильную запись в первом приближении, однако, она должна удовлетворять многим условиям. Прежде всего, её заголовок не должен содержать ничего криминального, после этого проверяем на правильность данных, которые несет в себе запись. При проверке стоит учитывать несколько факторов, которые напрямую зависят от предметной области базы данных: • Если первичным ключом таблицы является поле с автоинкриментом, то соответственно оно не должно быть более чем значение генератора, так же, если не определено иное оно не может быть отрицательным; • Даты, если это, например, простенькая складская программа, не могут быть в далёком прошлом и далёком будущем; • Цена на товар не может быть отрицательной либо слишком большой; Таким образом, кроме вышеперечисленных, можно выдумать ещё массу реальных ограничений, которые можно наложить на данные. В качестве примера могу сказать, что при восстановлении 2000 записей имеющими дубликаты оказались только 4. Так как их количество оказалось не сильно велико, то основываясь на знаниях, что именно должно было указано в этих полях, лично я не стал утруждать себя и возится с версиями записей. В результате проделанной работы мы получаем из базы всё что нужно. Для простоты восстановления можно просто приостановить работу всех триггеров и с помощью sql запросов добавить недостающие данные. ГОТОВО!!!! |
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||