Як бази даних зберігають ключові значення індексу (на диску) для полів змінної довжини?


16

Контекст

Це питання стосується детальних відомостей про реалізацію індексів як у системах баз даних SQL, так і в системах NoSQL. Фактична структура індексу (B + дерево, хеш, SSTable тощо) не має значення, оскільки питання стосується конкретно ключів, що зберігаються в одному вузлі будь-якої з цих реалізацій.

Фон

У базах даних SQL (наприклад, MySQL) та NoSQL (CouchDB, MongoDB тощо), коли ви створюєте індекс на стовпчику або в полі даних документа JSON, те, що ви насправді викликаєте базу даних, створює по суті відсортований список усіх ці значення разом із файлом зміщуються в основний файл даних, де живе запис, що відноситься до цього значення.

(Для простоти, я можу відмахувати рукою інші езотеричні деталі конкретних імпульсів)

Простий класичний приклад SQL

Розглянемо стандартну таблицю SQL, у якій є простий 32-розрядний int первинний ключ, на якому ми створюємо індекс, і в кінцевому підсумку з індексом на диску цілих клавіш буде відсортовано та пов'язано з 64-бітовим зміщенням у файл даних, де життя записів, наприклад:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

Дискове представлення клавіш в індексі виглядає приблизно так:

[4-bytes][8-bytes] --> 12 bytes for each indexed value

Дотримуючись стандартних правил щодо оптимізації вводу / виводу диска за допомогою файлових систем та систем баз даних, скажімо, що ви зберігаєте ключі у блоках 4 КБ на диску, що означає:

4096 bytes / 12 bytes per key = 341 keys per block

Ігноруючи загальну структуру індексу (B + дерево, хеш, відсортований список тощо), ми читаємо і записуємо блоки з 341 клавішами одночасно в пам'ять і повертаємося на диск за потребою.

Приклад запиту

Скажімо, інформація з попереднього розділу, скажімо, надходить запит для "id = 2", класичний пошук індексу БД відбувається таким чином:

  1. Прочитайте корінь індексу (у цьому випадку 1 блок)
  2. Двійковий пошук відсортованого блоку, щоб знайти ключ
  3. Отримайте зміщення файлу даних зі значення
  4. Шукайте запис у файлі даних за допомогою зміщення
  5. Поверніть дані абоненту

Налаштування питань ...

Добре, ось де питання зібралося ...

Крок №2 - це найважливіша частина, яка дозволяє виконувати ці запити в час (час реєстрації) ... інформація повинна бути сортована, АЛЕ ви повинні бути здатними швидко список ... докладніше конкретно, ви повинні мати можливість переходити до чітко визначених зрушень за бажанням, щоб прочитати значення ключа індексу на цій позиції.

Після читання в блоці ви повинні бути в змозі перейти на 170-ту позицію негайно, прочитати ключове значення і побачити, що те, що ви шукаєте, це GT або LT, це положення (і так далі, і так далі ...)

Єдиний спосіб, коли ви зможете перейти навколо даних у блоці, як це, якби розміри ключових значень були всі чітко визначені, як у нашому прикладі вище (4-байт, а потім 8-байт на ключ).

ПИТАННЯ

Гаразд, ось, де я застрягаю в ефективному дизайні індексів ... для стовпчиків varchar у базах даних SQL або, більш конкретно, абсолютно вільних полів у базах даних документів, таких як CouchDB або NoSQL, де будь-яке поле, яке ви хочете індексувати, може бути будь-яким length, як ви реалізуєте ключові значення, які знаходяться всередині блоків структури індексу, з яких ви будуєте свої індекси?

Наприклад, скажімо, що ви використовуєте послідовний лічильник для ідентифікатора в CouchDB і індексуєте твіти ... через кілька місяців у вас з'являться значення, які переходять від "1" до "100 000 000 000".

Скажімо, ви будуєте індекс на базі даних в перший день, коли в базі даних є лише 4 твіти, CouchDB може спокуситись використовувати наступну конструкцію для ключових значень усередині індексних блоків:

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

У якийсь момент це розривається, і вам потрібна змінна кількість байтів, щоб зберігати ваше ключове значення в індексах.

Сенс стає ще більш яскравим, якщо ви вирішите проіндексувати дійсно поле змінної довжини, наприклад "tweet_message" чи щось таке.

Оскільки самі ключі мають абсолютно змінну довжину, а база даних не має можливості розумно здогадатися про якийсь "максимальний розмір ключа", коли індекс створюється та оновлюється, як ці ключі насправді зберігаються всередині блоків, що представляють сегменти індексів у цих базах даних ?

Очевидно, якщо ваші клавіші змінного розміру і ви читаєте в блоці клавіш, ви не тільки не маєте поняття, скільки клавіш насправді знаходиться в блоці, але й поняття не маєте, як перейти до середини списку, щоб зробити двійкові. пошук на них.

Ось тут я все спіймаю.

Маючи статичні типи полів у класичних базах даних SQL (наприклад, bool, int, char тощо), я розумію, що індекс може заздалегідь визначити довжину ключа та дотримуватися його ... але в цьому світі зберігання даних документів я спантеличено, наскільки вони ефективно моделюють ці дані на диску, щоб їх все одно можна було сканувати в O (час входу) і буде вдячний за будь-яке уточнення тут.

Будь ласка, дайте мені знати, чи потрібні роз'яснення!

Оновлення (відповідь Грега)

Будь ласка, дивіться мої коментарі, додані до відповіді Грега. Після тижня додаткових досліджень, я думаю, що він насправді натрапив на дивовижно просте і ефективне припущення про те, що на практиці це просто-просто реалізувати та використовувати, забезпечуючи великі виграші від продуктивності, уникаючи дезаріалізації ключових цінностей, які вас не цікавлять.

Я розглядав 3 окремі реалізації СУБД (CouchDB, kivaloo та InnoDB), і всі вони справляються з цією проблемою, десеріалізуючи весь блок у внутрішній структурі даних перед тим, як шукати значення в середовищі їх виконання (erlang / C).

Це те, що я вважаю таким яскравим щодо пропозиції Грега; нормальний розмір блоку 2048 зазвичай матиме 50 або менше компенсацій, що призводить до дуже малого блоку чисел, який потрібно прочитати.

Оновлення (потенційні недоліки пропозиції Грега)

Щоб найкраще продовжити цей діалог із собою, я зрозумів наступні недоліки цього ...

  1. Якщо кожен "блок" має заголовок із зміщеними даними, ви не зможете дозволити коригувати розмір блоку в конфігурації пізніше вниз по дорозі, оскільки ви можете в кінцевому підсумку прочитати дані, які не почалися з заголовка правильно або блоком, який містив кілька заголовків.

  2. Якщо ви індексуєте величезні ключові значення (скажімо, хтось намагається проіндексувати стовпчик char (8192) або blob (8192)), можливо, ключі не вміщуються в один блок і їх потрібно переливати через два блоки поряд . Це означає, що ваш перший блок мав би зміщення заголовка, а другий блок негайно починався б з основних даних.

Рішенням у всьому цьому є наявність фіксованого розміру блоку бази даних, який не регулюється, і розробка навколо нього структур даних блоку заголовків ... наприклад, ви фіксуєте всі розміри блоків до 4 КБ (як правило, найоптимальніший у будь-якому випадку) і пишете дуже маленький заголовок блоку, що включає "тип блоку" на початку. Якщо це нормальний блок, то негайно після заголовка блоку має бути заголовок зрушення. Якщо його тип "переповнення", то одразу після заголовка блоку вводяться необроблені ключові дані.

Оновлення (потенційний дивовижний верх)

Після того, як блок зчитується у вигляді ряду байтів, а зсуви розшифровуються; технічно ви можете просто закодувати ключ, який ви шукаєте, для необмежених байтів, а потім зробити прямі порівняння у потоці байтів.

Після того, як ключ, який ви шукаєте, знайдений, вказівник можна розшифрувати та дотримуватися.

Ще один дивовижний побічний ефект ідеї Грега! Потенціал оптимізації часу процесора тут достатньо великий, щоб встановити фіксований розмір блоку, можливо, варто того, щоб отримати все це.


Для всіх, хто цікавиться цією темою, головний розробник Redis зіткнувся з цією проблемою, намагаючись реалізувати неіснуючий компонент "дисковий магазин" для Redis. Спочатку він вибрав "досить великий" статичний розмір ключа 32-байт, але зрозумів потенціал проблем і замість цього вирішив перейти зі зберіганням хеш-клавіш (sha1 або md5) просто для того, щоб мати послідовний розмір. Це вбиває можливість робити діапазони запитів, але це добре балансує дерево FWIW. Деталі тут redis.hackyhack.net/2011-01-12.html
Ріяд Калла

Ще трохи інформації я знайшов. Схоже, SQLite має обмеження на те, наскільки великі клавіші можуть отримати, або він фактично урізує значення ключа на деякій верхній межі, а решту поміщає на «сторінку переповнення» на диску. Це може зробити запити про величезні клавіші жахливими, як подвійні введення-виведення. Прокрутіть униз до розділу "Сторінки дерева" тут sqlite.org/fileformat2.html
Ріяд Калла,

Відповіді:


7

Ви можете зберігати свій індекс у списку компенсацій фіксованого розміру у блоці, що містить ваші ключові дані. Наприклад:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(ну, ключові дані будуть відсортовані за реальним прикладом, але ви розумієте).

Зауважте, що це не обов'язково відображає, як реально побудовані блоки індексів у будь-якій базі даних. Це просто приклад того , як ви могли б організувати блок даних індексу , де дані ключа змінної довжини.


Грег, я ще не вибрав вашу відповідь як дефоктозну відповідь, тому що сподіваюся на ще зворотній зв'язок, а також на ще одне дослідження інших СУБД (я додаю свої коментарі до початкового запитання). Поки найпоширенішим підходом здається верхнє обмеження, а потім решта ключа в таблиці переповнення, яка перевіряється лише тоді, коли потрібен повний ключ. Не такий елегантний. Ваше рішення має певну елегантність до цього, що мені подобається, але в крайньому випадку, коли клавіші розміщують наш розмір сторінки, ваш спосіб все одно потребує таблиці переповнення або просто не дозволяє.
Ріяд Калла

Мені не вистачало місця ... Якщо коротко, якщо дизайнер db міг би жити з деякими жорсткими обмеженнями щодо розміру ключа, я вважаю, що ваш підхід є найбільш ефективним та гнучким. Приємне поєднання простору та ефективності процесора. Таблиці переповнення є більш гнучкими, але можуть бути приємними для додавання випадкових вводу-виводу у пошуки ключів, які постійно переповнюються. Дякую за внесок у це!
Ріяд Калла

Грег, я все більше замислююся над цим, дивлячись на альтернативні рішення, і я думаю, що ти прибив це ідеєю зміщення заголовка. Якщо б у вас мало блоків, ви могли б уникнути 8-бітових (1-байтових) компенсацій, а більші блоки 16-бітних було б найбезпечнішим навіть до 128 КБ або 256 КБ блоків, що повинно бути розумним (припустимо, 4 або 8 байт-ключі). Великий виграш полягає в тому, наскільки дешево і швидко ви можете прочитати дані компенсації та скільки десяриалізації ви заощадите в результаті. Відмінна пропозиція, ще раз дякую.
Ріяд Калла

Це також підхід , який використовується в UpscaleDB: upscaledb.com/about.html#varlength
Матьє Rodic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.