diff --git a/_parts/part1.md b/_parts/part1.md deleted file mode 100644 index 7b2a365..0000000 --- a/_parts/part1.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Part 1 - Introduction and Setting up the REPL -date: 2017-08-30 ---- - -As a web developer, I use relational databases every day at my job, but they're a black box to me. Some questions I have: -- What format is data saved in? (in memory and on disk) -- When does it move from memory to disk? -- Why can there only be one primary key per table? -- How does rolling back a transaction work? -- How are indexes formatted? -- When and how does a full table scan happen? -- What format is a prepared statement saved in? - -In other words, how does a database **work**? - -To figure things out, I'm writing a database from scratch. It's modeled off sqlite because it is designed to be small with fewer features than MySQL or PostgreSQL, so I have a better hope of understanding it. The entire database is stored in a single file! - -# Sqlite - -There's lots of [documentation of sqlite internals](https://www.sqlite.org/arch.html) on their website, plus I've got a copy of [SQLite Database System: Design and Implementation](https://play.google.com/store/books/details?id=9Z6IQQnX1JEC). - -{% include image.html url="assets/images/arch1.gif" description="sqlite architecture (https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki)" %} - -A query goes through a chain of components in order to retrieve or modify data. The **front-end** consists of the: -- tokenizer -- parser -- code generator - -The input to the front-end is a SQL query. the output is sqlite virtual machine bytecode (essentially a compiled program that can operate on the database). - -The _back-end_ consists of the: -- virtual machine -- B-tree -- pager -- os interface - -The **virtual machine** takes bytecode generated by the front-end as instructions. It can then perform operations on one or more tables or indexes, each of which is stored in a data structure called a B-tree. The VM is essentially a big switch statement on the type of bytecode instruction. - -Each **B-tree** consists of many nodes. Each node is one page in length. The B-tree can retrieve a page from disk or save it back to disk by issuing commands to the pager. - -The **pager** receives commands to read or write pages of data. It is responsible for reading/writing at appropriate offsets in the database file. It also keeps a cache of recently-accessed pages in memory, and determines when those pages need to be written back to disk. - -The **os interface** is the layer that differs depending on which operating system sqlite was compiled for. In this tutorial, I'm not going to support multiple platforms. - -[A journey of a thousand miles begins with a single step](https://en.wiktionary.org/wiki/a_journey_of_a_thousand_miles_begins_with_a_single_step), so let's start with something a little more straightforward: the REPL. - -## Making a Simple REPL - -Sqlite starts a read-execute-print loop when you start it from the command line: - -```shell -~ sqlite3 -SQLite version 3.16.0 2016-11-04 19:09:39 -Enter ".help" for usage hints. -Connected to a transient in-memory database. -Use ".open FILENAME" to reopen on a persistent database. -sqlite> create table users (id int, username varchar(255), email varchar(255)); -sqlite> .tables -users -sqlite> .exit -~ -``` - -To do that, our main function will have an infinite loop that prints the prompt, gets a line of input, then processes that line of input: - -```c -int main(int argc, char* argv[]) { - InputBuffer* input_buffer = new_input_buffer(); - while (true) { - print_prompt(); - read_input(input_buffer); - - if (strcmp(input_buffer->buffer, ".exit") == 0) { - close_input_buffer(input_buffer); - exit(EXIT_SUCCESS); - } else { - printf("Unrecognized command '%s'.\n", input_buffer->buffer); - } - } -} -``` - -We'll define `InputBuffer` as a small wrapper around the state we need to store to interact with [getline()](http://man7.org/linux/man-pages/man3/getline.3.html). (More on that in a minute) -```c -typedef struct { - char* buffer; - size_t buffer_length; - ssize_t input_length; -} InputBuffer; - -InputBuffer* new_input_buffer() { - InputBuffer* input_buffer = (InputBuffer*)malloc(sizeof(InputBuffer)); - input_buffer->buffer = NULL; - input_buffer->buffer_length = 0; - input_buffer->input_length = 0; - - return input_buffer; -} -``` - -Next, `print_prompt()` prints a prompt to the user. We do this before reading each line of input. - -```c -void print_prompt() { printf("db > "); } -``` - -To read a line of input, use [getline()](http://man7.org/linux/man-pages/man3/getline.3.html): -```c -ssize_t getline(char **lineptr, size_t *n, FILE *stream); -``` -`lineptr` : a pointer to the variable we use to point to the buffer containing the read line. If it set to `NULL` it is mallocatted by `getline` and should thus be freed by the user, even if the command fails. - -`n` : a pointer to the variable we use to save the size of allocated buffer. - -`stream` : the input stream to read from. We'll be reading from standard input. - -`return value` : the number of bytes read, which may be less than the size of the buffer. - -We tell `getline` to store the read line in `input_buffer->buffer` and the size of the allocated buffer in `input_buffer->buffer_length`. We store the return value in `input_buffer->input_length`. - -`buffer` starts as null, so `getline` allocates enough memory to hold the line of input and makes `buffer` point to it. - -```c -void read_input(InputBuffer* input_buffer) { - ssize_t bytes_read = - getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin); - - if (bytes_read <= 0) { - printf("Error reading input\n"); - exit(EXIT_FAILURE); - } - - // Ignore trailing newline - input_buffer->input_length = bytes_read - 1; - input_buffer->buffer[bytes_read - 1] = 0; -} -``` - -Now it is proper to define a function that frees the memory allocated for an -instance of `InputBuffer *` and the `buffer` element of the respective -structure (`getline` allocates memory for `input_buffer->buffer` in -`read_input`). - -```c -void close_input_buffer(InputBuffer* input_buffer) { - free(input_buffer->buffer); - free(input_buffer); -} -``` - -Finally, we parse and execute the command. There is only one recognized command right now : `.exit`, which terminates the program. Otherwise we print an error message and continue the loop. - -```c -if (strcmp(input_buffer->buffer, ".exit") == 0) { - close_input_buffer(input_buffer); - exit(EXIT_SUCCESS); -} else { - printf("Unrecognized command '%s'.\n", input_buffer->buffer); -} -``` - -Let's try it out! -```shell -~ ./db -db > .tables -Unrecognized command '.tables'. -db > .exit -~ -``` - -Alright, we've got a working REPL. In the next part, we'll start developing our command language. Meanwhile, here's the entire program from this part: - -```c -#include -#include -#include -#include - -typedef struct { - char* buffer; - size_t buffer_length; - ssize_t input_length; -} InputBuffer; - -InputBuffer* new_input_buffer() { - InputBuffer* input_buffer = malloc(sizeof(InputBuffer)); - input_buffer->buffer = NULL; - input_buffer->buffer_length = 0; - input_buffer->input_length = 0; - - return input_buffer; -} - -void print_prompt() { printf("db > "); } - -void read_input(InputBuffer* input_buffer) { - ssize_t bytes_read = - getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin); - - if (bytes_read <= 0) { - printf("Error reading input\n"); - exit(EXIT_FAILURE); - } - - // Ignore trailing newline - input_buffer->input_length = bytes_read - 1; - input_buffer->buffer[bytes_read - 1] = 0; -} - -void close_input_buffer(InputBuffer* input_buffer) { - free(input_buffer->buffer); - free(input_buffer); -} - -int main(int argc, char* argv[]) { - InputBuffer* input_buffer = new_input_buffer(); - while (true) { - print_prompt(); - read_input(input_buffer); - - if (strcmp(input_buffer->buffer, ".exit") == 0) { - close_input_buffer(input_buffer); - exit(EXIT_SUCCESS); - } else { - printf("Unrecognized command '%s'.\n", input_buffer->buffer); - } - } -} -``` diff --git a/_parts/part10.md b/_parts/part10_kor.md similarity index 51% rename from _parts/part10.md rename to _parts/part10_kor.md index dc237c2..e2492c5 100644 --- a/_parts/part10.md +++ b/_parts/part10_kor.md @@ -1,19 +1,19 @@ --- -title: Part 10 - Splitting a Leaf Node +title: 제10 장 - 단말 노드 분할 date: 2017-10-09 --- -Our B-Tree doesn't feel like much of a tree with only one node. To fix that, we need some code to split a leaf node in twain. And after that, we need to create an internal node to serve as a parent for the two leaf nodes. +우리의 B-트리는 오직 단일 노드로 구성된 탓에 트리처럼 느껴지지 않습니다. 이 문제를 해결하기 위해서는 단말 노드를 둘로 나누는 코드가 필요합니다. 그다음, 두 단말 노드의 부모 노드가 될 내부 노드를 생성해야 합니다. -Basically our goal for this article is to go from this: +기본적으로 이번 장의 목표는 아래 그림에서 -{% include image.html url="assets/images/btree2.png" description="one-node btree" %} +{% include image.html url="assets/images/btree2.png" description="단일 노드 B-트리" %} -to this: +다음과 같이 되는 것입니다. -{% include image.html url="assets/images/btree3.png" description="two-level btree" %} +{% include image.html url="assets/images/btree3.png" description="레벨 2 B-트리" %} -First things first, let's remove the error handling for a full leaf node: +무엇보다 먼저, 가득 찬 단말 노드에 대한 에러 처리를 삭제하겠습니다. ```diff void leaf_node_insert(Cursor* cursor, uint32_t key, Row* value) { @@ -41,21 +41,21 @@ ExecuteResult execute_insert(Statement* statement, Table* table) { uint32_t key_to_insert = row_to_insert->id; ``` -## Splitting Algorithm +## 분할 알고리즘 -Easy part's over. Here's a description of what we need to do from [SQLite Database System: Design and Implementation](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en) +쉬운 것들은 모두 끝났습니다. 여기 [SQLite Database System: Design and Implementation](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en) 으로부터 가져온 우리가 해야 할 작업에 대한 설명입니다. -> If there is no space on the leaf node, we would split the existing entries residing there and the new one (being inserted) into two equal halves: lower and upper halves. (Keys on the upper half are strictly greater than those on the lower half.) We allocate a new leaf node, and move the upper half into the new node. +> 단말 노드에 공간이 없다면, 노드가 갖고 있는 기존 셀들과 새로운 셀(추가될)을 상부와 하부로 반반 나눕니다. (상부에 있는 키들은 하부의 키들보다 커야 합니다.) 우리는 새로운 단말 노드를 할당하고, 기존 노드의 상부를 새 노드로 옮기는 방식으로 구현합니다. -Let's get a handle to the old node and create the new node: +기존 노드를 가져오고 새 노드를 생성해봅시다. ```diff +void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) { + /* -+ Create a new node and move half the cells over. -+ Insert the new value in one of the two nodes. -+ Update parent or create a new parent. ++ 새 노드를 생성하고 절반의 셀을 이동합니다. ++ 두 노드 중 하나에 새 값을 삽입합니다. ++ 부모 노드를 갱신하거나 새로운 부모 노드를 생성합니다. + */ + + void* old_node = get_page(cursor->table->pager, cursor->page_num); @@ -64,13 +64,13 @@ Let's get a handle to the old node and create the new node: + initialize_leaf_node(new_node); ``` -Next, copy every cell into its new location: +다음으로, 모든 셀들을 적절한 위치에 복사합니다. ```diff + /* -+ All existing keys plus new key should be divided -+ evenly between old (left) and new (right) nodes. -+ Starting from the right, move each key to correct position. ++ 기존 노드의 모든 키들과 새 키를 기존 노드(좌측)와 ++ 새 노드(우측)에 균일하게 나눕니다. ++ 기존 노드의 우측부터 시작하여, 각 키를 올바른 위치로 이동합니다. + */ + for (int32_t i = LEAF_NODE_MAX_CELLS; i >= 0; i--) { + void* destination_node; @@ -92,15 +92,15 @@ Next, copy every cell into its new location: + } ``` -Update cell counts in each node's header: +각 노드의 헤더에서 셀 개수 필드를 갱신합니다. ```diff -+ /* Update cell count on both leaf nodes */ ++ /* 각 단말 노드의 셀 개수 갱신 */ + *(leaf_node_num_cells(old_node)) = LEAF_NODE_LEFT_SPLIT_COUNT; + *(leaf_node_num_cells(new_node)) = LEAF_NODE_RIGHT_SPLIT_COUNT; ``` -Then we need to update the nodes' parent. If the original node was the root, it had no parent. In that case, create a new root node to act as the parent. I'll stub out the other branch for now: +그런 다음 노드의 부모 노드를 갱신해야 합니다. 기존 노드가 루트인 경우 부모를 갖고 있지 않습니다. 이 경우 부모 노드 역할을 수행할 새 루트 노드를 생성합니다. 우선 분기문에 스텁을 사용하겠습니다. ```diff + if (is_node_root(old_node)) { @@ -112,23 +112,23 @@ Then we need to update the nodes' parent. If the original node was the root, it +} ``` -## Allocating New Pages +## 새로운 페이지 할당 -Let's go back and define a few new functions and constants. When we created a new leaf node, we put it in a page decided by `get_unused_page_num()`: +다시 돌아가서 몇 가지 새로운 함수와 상수를 정의하겠습니다. 우리는 새로운 단말 노드를 생성할 때, `get_unused_page_num()`에 의해 결정된 페이지에 넣을 것입니다. ```diff +/* -+Until we start recycling free pages, new pages will always -+go onto the end of the database file ++페이지 재 사용을 시작하기 전까지 항상 새 페이지는 ++데이터베이스 파일 끝에 저장됩니다. +*/ +uint32_t get_unused_page_num(Pager* pager) { return pager->num_pages; } ``` -For now, we're assuming that in a database with N pages, page numbers 0 through N-1 are allocated. Therefore we can always allocate page number N for new pages. Eventually after we implement deletion, some pages may become empty and their page numbers unused. To be more efficient, we could re-allocate those free pages. +현재 N 개 페이지가 있는 데이터베이스는 0번부터 N-1 번까지의 페이지가 할당되었다고 가정합니다. 그러므로 새로운 페이지를 위해서 항상 N 번 페이지를 할당하게 됩니다. 우리가 최종적으로 삭제 연산 구현을 마친 후에는, 몇몇 페이지는 빈 공간이 될 것이며 해당 페이지 번호를 사용하지 않게 됩니다. 우리는 효율을 위해서 빈 공간을 재사용 할 수 있습니다. -## Leaf Node Sizes +## 단말 노드 크기 -To keep the tree balanced, we evenly distribute cells between the two new nodes. If a leaf node can hold N cells, then during a split we need to distribute N+1 cells between two nodes (N original cells plus one new one). I'm arbitrarily choosing the left node to get one more cell if N+1 is odd. +트리의 균형을 유지하기 위해, 두 개의 새로운 노드에 셀들을 균등하게 분배하였습니다. 단말 노드가 N 개의 셀을 갖는 경우, N+1개의 셀들(N 개의 기존 셀과 하나의 새로운 셀)을 두 노드에 분배해야 합니다. 필자는 N+1이 홀수인 경우 왼쪽 노드가 한 개의 셀을 더 갖도록 선택했습니다. ```diff +const uint32_t LEAF_NODE_RIGHT_SPLIT_COUNT = (LEAF_NODE_MAX_CELLS + 1) / 2; @@ -136,22 +136,22 @@ To keep the tree balanced, we evenly distribute cells between the two new nodes. + (LEAF_NODE_MAX_CELLS + 1) - LEAF_NODE_RIGHT_SPLIT_COUNT; ``` -## Creating a New Root +## 새로운 루트 노드 생성 -Here's how [SQLite Database System](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en) explains the process of creating a new root node: +[SQLite Database System](https://play.google.com/store/books/details/Sibsankar_Haldar_SQLite_Database_System_Design_and?id=9Z6IQQnX1JEC&hl=en) 는 새로운 루트 노드를 만드는 과정을 다음과 같이 설명하였습니다. -> Let N be the root node. First allocate two nodes, say L and R. Move lower half of N into L and the upper half into R. Now N is empty. Add 〈L, K,R〉 in N, where K is the max key in L. Page N remains the root. Note that the depth of the tree has increased by one, but the new tree remains height balanced without violating any B+-tree property. +> N을 루트 노드라고 하겠습니다. 먼저 두 개의 노드 L과 R을 할당합니다. N의 하부를 L로, 상부를 R로 이동합니다. 이제 N은 비어 있게 됩니다. 〈L, K,R〉 을 N에 추가합니다. K는 L에서 최대 키 값입니다. 페이지 N은 루트 노드로 남습니다. 트리의 깊이가 1 증가했지만, 새로운 트리는 B+ 트리 특성을 위반하지 않고 높이 균형을 유지한다는 점에 유의하기 바랍니다. -At this point, we've already allocated the right child and moved the upper half into it. Our function takes the right child as input and allocates a new page to store the left child. +우리는 이미 우측 자식 노드를 할당하고 상부를 옮기는 작업을 마쳤습니다. 우리의 함수는 우측 자식 노드를 입력받고 왼쪽 자식 노드를 저장할 새로운 페이지를 할당합니다. ```diff +void create_new_root(Table* table, uint32_t right_child_page_num) { + /* -+ Handle splitting the root. -+ Old root copied to new page, becomes left child. -+ Address of right child passed in. -+ Re-initialize root page to contain the new root node. -+ New root node points to two children. ++ 루트 노드 분할 작업을 수행합니다. ++ 기존 루트 노드는 새 페이지에 복사되어 왼쪽 자식 노드가 됩니다. ++ 함수는 우측 자식 노드의 주소(페이지 번호)를 매개변수로 받습니다. ++ 루트 페이지가 새로운 루트 노드를 갖도록 재 설정합니다. ++ 새 루트 노드는 두 개의 자식 노드를 갖습니다. + */ + + void* root = get_page(table->pager, table->root_page_num); @@ -160,18 +160,18 @@ At this point, we've already allocated the right child and moved the upper half + void* left_child = get_page(table->pager, left_child_page_num); ``` -The old root is copied to the left child so we can reuse the root page: +기존 루트 노드는 왼쪽 자식에 복사되며, 루트 페이지를 재사용 할 수 있게 됩니다. ```diff -+ /* Left child has data copied from old root */ ++ /* 왼쪽 자식에 기존 루트 노드의 복사 데이터가 있습니다. */ + memcpy(left_child, root, PAGE_SIZE); + set_node_root(left_child, false); ``` -Finally we initialize the root page as a new internal node with two children. +마지막으로 루트 페이지를 두 개의 자식 노드를 갖는 내부 노드로 초기화합니다. ```diff -+ /* Root node is a new internal node with one key and two children */ ++ /* 루트 노드는 하나의 키와 두 개의 자식을 갖는 새로운 내부 노드입니다. */ + initialize_internal_node(root); + set_node_root(root, true); + *internal_node_num_keys(root) = 1; @@ -182,13 +182,13 @@ Finally we initialize the root page as a new internal node with two children. +} ``` -## Internal Node Format +## 내부 노드 형식 -Now that we're finally creating an internal node, we have to define its layout. It starts with the common header, then the number of keys it contains, then the page number of its rightmost child. Internal nodes always have one more child pointer than they have keys. That extra child pointer is stored in the header. +드디어 내부 노드를 만들게 되었으니, 노드의 형식 정의가 필요합니다. 내부 노드는 공통 헤더로 시작하고, 그다음에 갖고 있는 키의 수, 그리고 최우측 자식 노드의 페이지 번호를 갖습니다. 내부 노드는 항상 갖고 있는 키보다 한 개 많은 자식 노드 포인터를 갖습니다. 그 여분의 자식 노드 포인터(최우측 자식 노드 포인터)는 헤더에 저장됩니다. ```diff +/* -+ * Internal Node Header Layout ++ * 내부 노드 헤더 형식 + */ +const uint32_t INTERNAL_NODE_NUM_KEYS_SIZE = sizeof(uint32_t); +const uint32_t INTERNAL_NODE_NUM_KEYS_OFFSET = COMMON_NODE_HEADER_SIZE; @@ -200,11 +200,11 @@ Now that we're finally creating an internal node, we have to define its layout. + INTERNAL_NODE_RIGHT_CHILD_SIZE; ``` -The body is an array of cells where each cell contains a child pointer and a key. Every key should be the maximum key contained in the child to its left. +몸체는 자식 노드 포인터와 키를 포함하고 있는 셀들의 배열입니다. 모든 키는 왼쪽 자식 노드의 최대 키가 됩니다. ```diff +/* -+ * Internal Node Body Layout ++ * 내부 노드 몸체 형식 + */ +const uint32_t INTERNAL_NODE_KEY_SIZE = sizeof(uint32_t); +const uint32_t INTERNAL_NODE_CHILD_SIZE = sizeof(uint32_t); @@ -212,22 +212,22 @@ The body is an array of cells where each cell contains a child pointer and a key + INTERNAL_NODE_CHILD_SIZE + INTERNAL_NODE_KEY_SIZE; ``` -Based on these constants, here's how the layout of an internal node will look: +정의한 상수들을 바탕으로, 내부 노드는 다음과 같은 형식을 갖습니다. -{% include image.html url="assets/images/internal-node-format.png" description="Our internal node format" %} +{% include image.html url="assets/images/internal-node-format.png" description="우리의 내부 노드 형식" %} -Notice our huge branching factor. Because each child pointer / key pair is so small, we can fit 510 keys and 511 child pointers in each internal node. That means we'll never have to traverse many layers of the tree to find a given key! +많은 분기 계수(branching factor)에 주목하기 바랍니다. 우리의 내부 노드는 자식 노드 포인터와 키 쌍의 크기가 아주 작기 때문에 각 내부 노드마다 510개 키와 511개 포인터를 저장할 수 있습니다. 이는 주어진 키값을 찾기 위해 많은 층을 순회할 필요가 없다는 것을 의미합니다! -| # internal node layers | max # leaf nodes | Size of all leaf nodes | +| # 내부 노드 층 | 최대 # 단말 노드 | 모든 단말 노드의 크기 | |------------------------|---------------------|------------------------| | 0 | 511^0 = 1 | 4 KB | -| 1 | 511^1 = 512 | ~2 MB | -| 2 | 511^2 = 261,121 | ~1 GB | -| 3 | 511^3 = 133,432,831 | ~550 GB | +| 1 | 511^1 = 512 | ~2 MB | +| 2 | 511^2 = 261,121 | ~1 GB | +| 3 | 511^3 = 133,432,831 | ~550 GB | -In actuality, we can't store a full 4 KB of data per leaf node due to the overhead of the header, keys, and wasted space. But we can search through something like 500 GB of data by loading only 4 pages from disk. This is why the B-Tree is a useful data structure for databases. +실제로, 헤더, 키 그리고 낭비되는 공간 때문에 단말 노드 당 4KB의 데이터를 저장할 순 없습니다. 하지만 디스크에서 단 4페이지를 로드함으로써, 500GB 정도의 데이터를 탐색할 수 있게 됩니다. 이것이 데이터베이스에 B-트리가 유용한 이유입니다. -Here are the methods for reading and writing to an internal node: +다음은 내부 노드를 읽고 쓰기 위한 함수들입니다. ```diff +uint32_t* internal_node_num_keys(void* node) { @@ -259,7 +259,8 @@ Here are the methods for reading and writing to an internal node: +} ``` -For an internal node, the maximum key is always its right key. For a leaf node, it's the key at the maximum index: +내부 노드의 경우, 최대 키는 항상 우측 키입니다. +단말 노드의 경우는, 가장 큰 인덱스의 키입니다. ```diff +uint32_t get_node_max_key(void* node) { @@ -272,9 +273,9 @@ For an internal node, the maximum key is always its right key. For a leaf node, +} ``` -## Keeping Track of the Root +## 루트 추적 -We're finally using the `is_root` field in the common node header. Recall that we use it to decide how to split a leaf node: +드디어 공통 노드 헤더의 `is_root` 필드를 사용합니다. 단말 노드를 분할하는 방법을 결정하는 데 사용한다는 점을 상기하기 바립니다. ```c if (is_node_root(old_node)) { @@ -286,7 +287,7 @@ We're finally using the `is_root` field in the common node header. Recall that w } ``` -Here are the getter and setter: +게터와 세터는 다음과 같습니다. ```diff +bool is_node_root(void* node) { @@ -301,7 +302,7 @@ Here are the getter and setter: ``` -Initializing both types of nodes should default to setting `is_root` to false: +두 노드 유형의 초기화는 기본적으로 'is_root' 를 false로 설정합니다. ```diff void initialize_leaf_node(void* node) { @@ -317,10 +318,10 @@ Initializing both types of nodes should default to setting `is_root` to false: +} ``` -We should set `is_root` to true when creating the first node of the table: +테이블의 첫 번째 노드를 만들 때는 `is_root` 를 true로 설정해야 합니다. ```diff - // New database file. Initialize page 0 as leaf node. + // 새 데이터베이스 파일. 페이지 0을 단말 노드로 초기화합니다. void* root_node = get_page(pager, 0); initialize_leaf_node(root_node); + set_node_root(root_node, true); @@ -329,11 +330,11 @@ We should set `is_root` to true when creating the first node of the table: return table; ``` -## Printing the Tree +## 트리 출력 -To help us visualize the state of the database, we should update our `.btree` metacommand to print a multi-level tree. +데이터베이스의 상태를 시각화할 수 있도록 다중 레벨 트리를 출력하는 `.btree` 메타 명령을 추가합니다. -I'm going to replace the current `print_leaf_node()` function +현재의 `print_leaf_node()` 를 ```diff -void print_leaf_node(void* node) { @@ -346,7 +347,7 @@ I'm going to replace the current `print_leaf_node()` function -} ``` -with a new recursive function that takes any node, then prints it and its children. It takes an indentation level as a parameter, which increases with each recursive call. I'm also adding a tiny helper function to indent. +노드를 입력받아 그 자식들을 출력하는 새로운 재귀 함수로 대체하겠습니다. 이 재귀 함수는 들여쓰기 정도를 매개변수로 받으며, 재귀 호출에 따라 정도가 증가합니다. 추가로 들여 쓰기를 위한 작은 헬퍼 함수를 추가합니다. ```diff +void indent(uint32_t level) { @@ -387,7 +388,7 @@ with a new recursive function that takes any node, then prints it and its childr +} ``` -And update the call to the print function, passing an indentation level of zero. +그리고 0의 들여쓰기 정도를 매개 변수로 입력하여 출력 함수 호출 부를 수정합니다. ```diff } else if (strcmp(input_buffer->buffer, ".btree") == 0) { @@ -397,7 +398,7 @@ And update the call to the print function, passing an indentation level of zero. return META_COMMAND_SUCCESS; ``` -Here's a test case for the new printing functionality! +다음은 새 출력 기능을 위한 새로운 테스트 케이스입니다! ```diff + it 'allows printing out the structure of a 3-leaf-node btree' do @@ -429,12 +430,12 @@ Here's a test case for the new printing functionality! + " - 12", + " - 13", + " - 14", -+ "db > Need to implement searching an internal node", ++ "db > 내부 노드 탐색 구현이 필요합니다.", + ]) + end ``` -The new format is a little simplified, so we need to update the existing `.btree` test: +새로운 출력 형식이 약간 단순화되었으므로 기존의 `.btree` 테스트를 수정해야 합니다. ```diff "db > Executed.", @@ -453,7 +454,7 @@ The new format is a little simplified, so we need to update the existing `.btree end ``` -Here's the `.btree` output of the new test on its own: +새로운 테스트의 `.btree` 출력은 다음과 같습니다. ``` Tree: @@ -477,17 +478,17 @@ Tree: - 14 ``` -On the least indented level, we see the root node (an internal node). It says `size 1` because it has one key. Indented one level, we see a leaf node, a key, and another leaf node. The key in the root node (7) is is the maximum key in the first leaf node. Every key greater than 7 is in the second leaf node. +최소 들여쓰기 정도에서 루트 노드(내부 노드)가 보입니다. 키가 하나이므로 `size 1` 을 출력합니다. 다음 들여 쓰기에서 하나의 단말 노드와 하나의 키 그리고 또 다른 단말 노드가 보입니다. 루트 노드의 키(7)는 첫 번째 노드의 최대 키값입니다. 7보다 큰 모든 키는 두 번째 단말 노드에 위치합니다. -## A Major Problem +## 주요 문제 -If you've been following along closely you may notice we've missed something big. Look what happens if we try to insert one additional row: +여기까지 잘 따라왔다면 우리가 뭔가 큰 것을 놓쳤다는 것을 알아차릴 수 있을 것입니다. 행을 하나 더 삽입하는 경우 어떻게 되는지 보시기 바랍니다. ``` db > insert 15 user15 person15@example.com -Need to implement searching an internal node +내부 노드 탐색 구현이 필요합니다. ``` -Whoops! Who wrote that TODO message? :P +이런! 누가 TODO 메시지를 썼죠? :P -Next time we'll continue the epic B-tree saga by implementing search on a multi-level tree. +다음 장에서 다중 레벨 트리에 대한 구현을 통해 B-트리에 대한 여정을 계속 진행하겠습니다. diff --git a/_parts/part11.md b/_parts/part11_kor.md similarity index 53% rename from _parts/part11.md rename to _parts/part11_kor.md index bef16f7..eaa8db7 100644 --- a/_parts/part11.md +++ b/_parts/part11_kor.md @@ -1,16 +1,16 @@ --- -title: Part 11 - Recursively Searching the B-Tree +title: 제11 장 - B-트리 재귀 탐색 date: 2017-10-22 --- -Last time we ended with an error inserting our 15th row: +지난 장에서 15번째 행 삽입 오류와 함께 끝이 났었습니다. ``` db > insert 15 user15 person15@example.com -Need to implement searching an internal node +내부 노드 탐색 구현이 필요합니다. ``` -First, replace the code stub with a new function call. +먼저 코드 스텁을 새 함수 호출로 변경합니다. ```diff if (get_node_type(root_node) == NODE_LEAF) { @@ -23,20 +23,20 @@ First, replace the code stub with a new function call. } ``` -This function will perform binary search to find the child that should contain the given key. Remember that the key to the right of each child pointer is the maximum key contained by that child. +이 함수는 주어진 키를 갖는 자식 노드를 찾기 위해 이진 탐색을 수행합니다. 각 자식 노드 포인터 오른쪽에 위치한 키가 자식 노드가 갖는 최대 키임을 명심하기 바랍니다. -{% include image.html url="assets/images/btree6.png" description="three-level btree" %} +{% include image.html url="assets/images/btree6.png" description="레벨3 B-트리" %} -So our binary search compares the key to find and the key to the right of the child pointer: +따라서 우리의 이진 탐색은 찾을 키와 자식 노드 포인터 오른쪽에 있는 키를 비교합니다. ```diff +Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) { + void* node = get_page(table->pager, page_num); + uint32_t num_keys = *internal_node_num_keys(node); + -+ /* Binary search to find index of child to search */ ++ /* 이진 탐색으로 탐색할 자식의 인덱스를 찾습니다. */ + uint32_t min_index = 0; -+ uint32_t max_index = num_keys; /* there is one more child than key */ ++ uint32_t max_index = num_keys; /* 키 개수 + 1 개의 자식 노드가 있습니다. */ + + while (min_index != max_index) { + uint32_t index = (min_index + max_index) / 2; @@ -49,7 +49,7 @@ So our binary search compares the key to find and the key to the right of the ch + } ``` -Also remember that the children of an internal node can be either leaf nodes or more internal nodes. After we find the correct child, call the appropriate search function on it: +또한 내부 노드의 자식 노드는 단말 노드이거나 내부 노드 일 수 있습니다. 따라서 올바른 자식 노드를 찾은 후 유형에 맞는 적절한 탐색 함수를 호출합니다. ```diff + uint32_t child_num = *internal_node_child(node, min_index); @@ -63,9 +63,9 @@ Also remember that the children of an internal node can be either leaf nodes or +} ``` -# Tests +# 테스트 -Now inserting a key into a multi-node btree no longer results in an error. And we can update our test: +이제 다중 노드 B-트리에 키를 삽입해도 더 이상 오류가 발생하지 않을 것입니다. 따라서 다음과 같이 테스트를 수정합니다. ```diff " - 12", @@ -78,7 +78,7 @@ Now inserting a key into a multi-node btree no longer results in an error. And w end ``` -I also think it's time we revisit another test. The one that tries inserting 1400 rows. It still errors, but the error message is new. Right now, our tests don't handle it very well when the program crashes. If that happens, let's just use the output we've gotten so far: +추가로 다른 테스트를 해보는 게 좋을 것 같습니다. 이번에는 1400개 행을 삽입해보겠습니다. 수행 결과 에러가 발생하지만, 새로운 에러 메시지가 보입니다. 현재 우리의 테스트 코드가 프로그램이 충돌로 망가지는 경우 잘 처리하지 못하는 것으로 보입니다. 이렇게 된 이상 충돌 전까지의 출력만 사용하겠습니다. ```diff raw_output = nil @@ -95,7 +95,7 @@ I also think it's time we revisit another test. The one that tries inserting 140 pipe.close_write ``` -And that reveals that our 1400-row test outputs this error: +그 결과 1400 행 테스트에서 다음과 같은 에러 출력을 얻게 됩니다. ```diff end @@ -109,5 +109,5 @@ And that reveals that our 1400-row test outputs this error: end ``` -Looks like that's next on our to-do list! +우리의 다음 작업이 되겠습니다! diff --git a/_parts/part12.md b/_parts/part12_kor.md similarity index 62% rename from _parts/part12.md rename to _parts/part12_kor.md index fedc6a7..4ef8df3 100644 --- a/_parts/part12.md +++ b/_parts/part12_kor.md @@ -1,9 +1,9 @@ --- -title: Part 12 - Scanning a Multi-Level B-Tree +title: 제12 장 - 다중 레벨 B-Tree 순회 date: 2017-11-11 --- -We now support constructing a multi-level btree, but we've broken `select` statements in the process. Here's a test case that inserts 15 rows and then tries to print them. +이제 다중 레벨 B-트리를 만들 수 있게 되었습니다. 하지만 그 과정에서 우리의 `select` 문이 망가졌습니다. 다음은 15개 행을 삽입한 후 출력을 시도하는 테스트 케이스입니다. ```diff + it 'prints all rows in a multi-level tree' do @@ -36,7 +36,7 @@ We now support constructing a multi-level btree, but we've broken `select` state + end ``` -But when we run that test case right now, what actually happens is: +하지만 테스트 케이스를 실행했을 때 실제로 출력되는 것은 다음과 같습니다. ``` db > select @@ -44,11 +44,11 @@ db > select Executed. ``` -That's weird. It's only printing one row, and that row looks corrupted (notice the id doesn't match the username). +결과가 이상합니다. 한 행만 출력하며, 그 행조차 손상(ID와 사용자 ID의 불일치) 되었습니다. -The weirdness is because `execute_select()` begins at the start of the table, and our current implementation of `table_start()` returns cell 0 of the root node. But the root of our tree is now an internal node which doesn't contain any rows. The data that was printed must have been left over from when the root node was a leaf. `execute_select()` should really return cell 0 of the leftmost leaf node. +이상한 결과는 `execute_select()` 가 테이블의 처음에서 시작되고, 또 현재 `table_start()` 가 루트 노드의 0번 셀을 반환하도록 구현되어 발생합니다. 하지만 루트 노드는 아무행도 갖지 않는 내부 노드입니다. 따라서 루트 노드가 단말 노드였을 때 남겨진 것이 출력되는 것으로 보입니다. `execute_select()` 는 가장 왼쪽 단말 노드의 0번 셀을 제대로 반환해야 합니다. -So get rid of the old implementation: +따라서 기존 구현은 제거합니다. ```diff -Cursor* table_start(Table* table) { @@ -65,7 +65,7 @@ So get rid of the old implementation: -} ``` -And add a new implementation that searches for key 0 (the minimum possible key). Even if key 0 does not exist in the table, this method will return the position of the lowest id (the start of the left-most leaf node). +그리고 키 0(최소 키)을 찾는 새로운 구현을 추가합니다. 키 0이 테이블에 없어도, 이 함수는 가장 작은 id(가장 왼쪽 단말 노드의 시작)의 위치를 반환합니다. ```diff +Cursor* table_start(Table* table) { @@ -79,7 +79,7 @@ And add a new implementation that searches for key 0 (the minimum possible key). +} ``` -With those changes, it still only prints out one node's worth of rows: +변경작업에도 불구하고 한 노드의 행들만 출력됩니다. ``` db > select @@ -94,13 +94,13 @@ Executed. db > ``` -With 15 entries, our btree consists of one internal node and two leaf nodes, which looks something like this: +15개 행을 갖는 B-트리는 하나의 내부 노드와 두 개의 단말 노드로 구성되며, 다음과 같은 구조를 갖습니다. -{% include image.html url="assets/images/btree3.png" description="structure of our btree" %} +{% include image.html url="assets/images/btree3.png" description="우리의 B-트리 구조" %} -To scan the entire table, we need to jump to the second leaf node after we reach the end of the first. To do that, we're going to save a new field in the leaf node header called "next_leaf", which will hold the page number of the leaf's sibling node on the right. The rightmost leaf node will have a `next_leaf` value of 0 to denote no sibling (page 0 is reserved for the root node of the table anyway). +전체 테이블을 스캔하기 위해서는 첫 단말 노드의 끝에 도달한 후 두 번째 단말 노드로 이동해야 합니다. 이를 위해, 단말 노드 헤더에 "next_leaf" 필드를 추가하겠습니다. 이 필드는 오른쪽 형제 노드의 페이지 번호를 갖습니다. 가장 오른쪽의 단말 노드는 형제가 없음을 표시하기 위해 0의 `next_leaf` 값(페이지 0은 테이블의 루트 노드 용으로 예약된 값)을 갖습니다. -Update the leaf node header format to include the new field: +단말 노드 헤더 형식을 수정하여 새로운 필드를 포함시킵니다. ```diff const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t); @@ -116,25 +116,25 @@ Update the leaf node header format to include the new field: ``` -Add a method to access the new field: +새 필드에 접근하는 함수도 추가합니다. ```diff +uint32_t* leaf_node_next_leaf(void* node) { + return node + LEAF_NODE_NEXT_LEAF_OFFSET; +} ``` -Set `next_leaf` to 0 by default when initializing a new leaf node: +새 단말 노드를 초기화할 때, 기본적으로 `next_leaf` 를 0으로 설정합니다. ```diff @@ -322,6 +330,7 @@ void initialize_leaf_node(void* node) { set_node_type(node, NODE_LEAF); set_node_root(node, false); *leaf_node_num_cells(node) = 0; -+ *leaf_node_next_leaf(node) = 0; // 0 represents no sibling ++ *leaf_node_next_leaf(node) = 0; // 0 는 형제가 없음을 의미합니다. } ``` -Whenever we split a leaf node, update the sibling pointers. The old leaf's sibling becomes the new leaf, and the new leaf's sibling becomes whatever used to be the old leaf's sibling. +단말 노드를 분할할 때마다 형제 노드 포인터를 갱신합니다. 기존 단말 노드의 형제 노드는 새로운 단말 노드가 되고, 새 단말 노드의 형제는 기존 노드의 형제였던 노드가 됩니다. ```diff @@ -659,6 +671,8 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) { @@ -145,7 +145,7 @@ Whenever we split a leaf node, update the sibling pointers. The old leaf's sibli + *leaf_node_next_leaf(old_node) = new_page_num; ``` -Adding a new field changes a few constants: +새 필드 추가로 몇 가지 상수 값들이 바뀝니다. ```diff it 'prints constants' do script = [ @@ -164,7 +164,7 @@ Adding a new field changes a few constants: ]) ``` -Now whenever we want to advance the cursor past the end of a leaf node, we can check if the leaf node has a sibling. If it does, jump to it. Otherwise, we're at the end of the table. +이제 단말 노드의 끝에서 커서를 진행하는 경우, 단말 노드가 형제 노드를 갖는지 확인해야 합니다. 형제가 있으면 이동하고, 없으면 테이블의 끝임을 표시합니다. ```diff @@ -428,7 +432,15 @@ void cursor_advance(Cursor* cursor) { @@ -172,10 +172,10 @@ Now whenever we want to advance the cursor past the end of a leaf node, we can c cursor->cell_num += 1; if (cursor->cell_num >= (*leaf_node_num_cells(node))) { - cursor->end_of_table = true; -+ /* Advance to next leaf node */ ++ /* 다음 단말 노드로 진행 */ + uint32_t next_page_num = *leaf_node_next_leaf(node); + if (next_page_num == 0) { -+ /* This was rightmost leaf */ ++ /* 최우측 단말 노드 */ + cursor->end_of_table = true; + } else { + cursor->page_num = next_page_num; @@ -185,7 +185,7 @@ Now whenever we want to advance the cursor past the end of a leaf node, we can c } ``` -After those changes, we actually print 15 rows... +변경을 통해 실제로 15개의 행이 출력이 되는데... ``` db > select (1, user1, person1@example.com) @@ -207,12 +207,12 @@ Executed. db > ``` -...but one of them looks corrupted +...한 행이 이상합니다. ``` (1919251317, 14, on14@example.com) ``` -After some debugging, I found out it's because of a bug in how we split leaf nodes: +디버깅을 통해, 필자는 노드 분할 과정에서 버그 때문임을 찾아냈습니다. ```diff @@ -676,7 +690,9 @@ void leaf_node_split_and_insert(Cursor* cursor, uint32_t key, Row* value) { @@ -228,13 +228,13 @@ After some debugging, I found out it's because of a bug in how we split leaf nod } else { ``` -Remember that each cell in a leaf node consists of first a key then a value: +단말 노드의 각 셀은 키, 그다음 값으로 구성되어 있음을 기억하기 바랍니다. -{% include image.html url="assets/images/leaf-node-format.png" description="Original leaf node format" %} +{% include image.html url="assets/images/leaf-node-format.png" description="단말 노드 형식" %} -We were writing the new row (value) into the start of the cell, where the key should go. That means part of the username was going into the section for id (hence the crazy large id). +우리는 새로운 행(값)을 키가 저장되어야 할 셀의 시작 부분에 쓰고 있었습니다. 즉, 사용자 이름의 일부가 id 구역에 쓰인 것입니다. (따라서 엄청나게 큰 id가 출력 되었습니다.) -After fixing that bug, we finally print out the entire table as expected: +버그를 고친 후, 마침내 예상대로 테이블 전체를 출력합니다. ``` db > select @@ -257,6 +257,6 @@ Executed. db > ``` -Whew! One bug after another, but we're making progress. +휴! 버그가 속속 생기지만 그래도 잘 진행되고 있습니다. -Until next time. +그럼 다음 장에서 뵙겠습니다. diff --git a/_parts/part13.md b/_parts/part13_kor.md similarity index 59% rename from _parts/part13.md rename to _parts/part13_kor.md index 6957bb5..be66272 100644 --- a/_parts/part13.md +++ b/_parts/part13_kor.md @@ -1,20 +1,20 @@ --- -title: Part 13 - Updating Parent Node After a Split +title: 제13 장 - 분할 후 부모 노드 갱신 date: 2017-11-26 --- -For the next step on our epic b-tree implementation journey, we're going to handle fixing up the parent node after splitting a leaf. I'm going to use the following example as a reference: +B-트리 구현 여정의 다음 단계로, 단말 노드 분할 후 부모 노드를 갱신하는 작업을 진행하겠습니다. 다음 예제를 참조하여 진행하겠습니다. -{% include image.html url="assets/images/updating-internal-node.png" description="Example of updating internal node" %} +{% include image.html url="assets/images/updating-internal-node.png" description="내부 노드 갱신의 예" %} -In this example, we add the key "3" to the tree. That causes the left leaf node to split. After the split we fix up the tree by doing the following: +이 예에서는 키 "3" 을 트리에 추가합니다. 키 추가로 왼쪽 단말 노드는 분할됩니다. 분할 후 다음 절차를 수행하여 트리를 갱신합니다. -1. Update the first key in the parent to be the maximum key in the left child ("3") -2. Add a new child pointer / key pair after the updated key - - The new pointer points to the new child node - - The new key is the maximum key in the new child node ("5") - -So first things first, replace our stub code with two new function calls: `update_internal_node_key()` for step 1 and `internal_node_insert()` for step 2 +1. 부모 노드의 첫 번째 키를 왼쪽 자식 노드의 최대 키값("3")으로 갱신합니다. +2. 갱신된 키 뒤에 새로운 자식 포인터 / 키 쌍 추가합니다. + - 새로운 자식 포인터는 새로운 자식 노드를 가리킵니다. + - 새 키는 새 자식 노드의 최대 키값("5")입니다. + +무엇보다 먼저, 스텁 코드를 두 가지 새로운 함수 호출로 교체합니다. `update_internal_node_key()` 는 1번 절차, `internal_node_insert()` 는 2번 절차에 해당합니다. ```diff @@ -47,7 +47,7 @@ So first things first, replace our stub code with two new function calls: `updat } ``` -In order to get a reference to the parent, we need to start recording in each node a pointer to its parent node. +부모 노드에 대한 참조를 얻기 위해, 각 노드의 부모 포인터 필드가 부모 노드를 가리키도록 설정해야 합니다. ```diff +uint32_t* node_parent(void* node) { return node + PARENT_POINTER_OFFSET; } @@ -62,7 +62,7 @@ In order to get a reference to the parent, we need to start recording in each no } ``` -Now we need to find the affected cell in the parent node. The child doesn't know its own page number, so we can't look for that. But it does know its own maximum key, so we can search the parent for that key. +이제 부모 노드에서 갱신될 셀을 찾아야 합니다. 자식 노드는 자신의 페이지 번호를 알지 못하므로, 이를 이용해서 갱신될 셀을 찾을 수는 없습니다. 하지만 자신이 갖는 최대 키값은 알고 있기 때문에, 최대 키를 이용해서 부모 노드에서 갱신될 셀의 위치를 찾을 수 있습니다. ```diff +void update_internal_node_key(void* node, uint32_t old_key, uint32_t new_key) { @@ -71,23 +71,23 @@ Now we need to find the affected cell in the parent node. The child doesn't know } ``` -Inside `internal_node_find_child()` we'll reuse some code we already have for finding a key in an internal node. Refactor `internal_node_find()` to use the new helper method. +`internal_node_find_child()` 내부에서는 내부 노드에서 키를 찾는데 사용하던 코드를 재사용 하겠습니다. 새로운 헬퍼 함수를 사용해서 `internal_node_find()` 의 개선도 진행합니다. ```diff -Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) { - void* node = get_page(table->pager, page_num); +uint32_t internal_node_find_child(void* node, uint32_t key) { + /* -+ Return the index of the child which should contain -+ the given key. ++ 주어진 키를 포함하는 자식 노드의 ++ 인덱스를 반환합니다. + */ + uint32_t num_keys = *internal_node_num_keys(node); -- /* Binary search to find index of child to search */ -+ /* Binary search */ +- /* 이진 탐색으로 탐색할 자식의 인덱스를 찾습니다. */ ++ /* 이진 탐색 */ uint32_t min_index = 0; - uint32_t max_index = num_keys; /* there is one more child than key */ + uint32_t max_index = num_keys; /* 키 개수 + 1 개의 자식 노드가 있습니다. */ @@ -386,7 +394,14 @@ Cursor* internal_node_find(Table* table, uint32_t page_num, uint32_t key) { } @@ -107,13 +107,13 @@ Inside `internal_node_find_child()` we'll reuse some code we already have for fi case NODE_LEAF: ``` -Now we get to the heart of this article, implementing `internal_node_insert()`. I'll explain it in pieces. +이제 이번 장의 핵심인 `internal_node_insert()` 를 구현합니다. 한 부분 씩 설명하겠습니다. ```diff +void internal_node_insert(Table* table, uint32_t parent_page_num, + uint32_t child_page_num) { + /* -+ Add a new child/key pair to parent that corresponds to child ++ 새로운 자식/키 쌍을 적절한 위치에 추가합니다. + */ + + void* parent = get_page(table->pager, parent_page_num); @@ -130,11 +130,11 @@ Now we get to the heart of this article, implementing `internal_node_insert()`. + } ``` -The index where the new cell (child/key pair) should be inserted depends on the maximum key in the new child. In the example we looked at, `child_max_key` would be 5 and `index` would be 1. +새로운 셀(자식/키 쌍)이 삽입되어야 할 인덱스는 새로운 자식 노드의 최대 키에 따라 달라집니다. 위의 예를 보면, `child_max_key` 는 5가 되고 `index` 는 1이 됩니다. -If there's no room in the internal node for another cell, throw an error. We'll implement that later. +내부 노드에 새 셀을 삽입할 공간이 없다면 에러를 발생합니다. 그 부분은 나중에 구현하겠습니다. -Now let's look at the rest of the function: +이제 함수의 나머지 부분을 살펴보겠습니다. ```diff + @@ -142,13 +142,13 @@ Now let's look at the rest of the function: + void* right_child = get_page(table->pager, right_child_page_num); + + if (child_max_key > get_node_max_key(right_child)) { -+ /* Replace right child */ ++ /* 오른쪽 자식 교체 */ + *internal_node_child(parent, original_num_keys) = right_child_page_num; + *internal_node_key(parent, original_num_keys) = + get_node_max_key(right_child); + *internal_node_right_child(parent) = child_page_num; + } else { -+ /* Make room for the new cell */ ++ /* 새로운 셀을 위한 공간 생성 */ + for (uint32_t i = original_num_keys; i > index; i--) { + void* destination = internal_node_cell(parent, i); + void* source = internal_node_cell(parent, i - 1); @@ -160,24 +160,24 @@ Now let's look at the rest of the function: +} ``` -Because we store the rightmost child pointer separately from the rest of the child/key pairs, we have to handle things differently if the new child is going to become the rightmost child. +우리는 가장 오른쪽의 자식 포인터를 나머지 자식/키 쌍 들과 분리하여 저장했기 때문에, 새로운 자식이 가장 오른쪽 자식이 되는 경우를 다르게 처리해 줘야 합니다. -In our example, we would get into the `else` block. First we make room for the new cell by shifting other cells one space to the right. (Although in our example there are 0 cells to shift) +우리의 예는 `else` 블록으로 분기합니다. 가장 먼저 다른 셀들을 오른쪽으로 한 칸씩 옮겨 새로운 셀을 위한 공간을 만듭니다. (하지만 예제에서 이동이 필요한 셀들은 없습니다.) -Next, we write the new child pointer and key into the cell determined by `index`. +다음으로, 새로운 자식 포인터와 키 쌍을 `index` 로 결정된 셀에 저장합니다. -To reduce the size of testcases needed, I'm hardcoding `INTERNAL_NODE_MAX_CELLS` for now +테스트 케이스 크기를 줄이기 위해 `INTERNAL_NODE_MAX_CELLS` 을 하드코딩하여 설정하겠습니다. ```diff @@ -126,6 +126,8 @@ const uint32_t INTERNAL_NODE_KEY_SIZE = sizeof(uint32_t); const uint32_t INTERNAL_NODE_CHILD_SIZE = sizeof(uint32_t); const uint32_t INTERNAL_NODE_CELL_SIZE = INTERNAL_NODE_CHILD_SIZE + INTERNAL_NODE_KEY_SIZE; -+/* Keep this small for testing */ ++/* 테스트를 위해 작게 설정했습니다. */ +const uint32_t INTERNAL_NODE_MAX_CELLS = 3; ``` -Speaking of tests, our large-dataset test gets past our old stub and gets to our new one: +테스트 결과, 우리의 대량 데이터 테스트는 오래된 스텁 코드를 통과하여 새로운 스텁 코드 출력을 얻게 됩니다. ```diff @@ -65,7 +65,7 @@ describe 'database' do @@ -189,9 +189,9 @@ Speaking of tests, our large-dataset test gets past our old stub and gets to our ]) ``` -Very satisfying, I know. +매우 만족스럽습니다. -I'll add another test that prints a four-node tree. Just so we test more cases than sequential ids, this test will add records in a pseudorandom order. +4개 단말 노드 트리를 출력하는 또 다른 테스트를 추가하겠습니다. 이 테스트는 무작위로 추출된 순서의 레코드를 삽입해서 순차적인 삽입 보다 많은 경우를 테스트합니다. ```diff + it 'allows printing out the structure of a 4-leaf-node btree' do @@ -232,7 +232,7 @@ I'll add another test that prints a four-node tree. Just so we test more cases t + result = run_script(script) ``` -As-is, it will output this: +출력은 다음과 같습니다. ``` - internal (size 3) @@ -276,7 +276,7 @@ As-is, it will output this: db > ``` -Look carefully and you'll spot a bug: +잘 보면 버그가 보입니다. ``` - 5 - 6 @@ -284,9 +284,9 @@ Look carefully and you'll spot a bug: - key 1 ``` -The key there should be 7, not 1! +키는 1이 아닌 7이어야 합니다! -After a bunch of debugging, I discovered this was due to some bad pointer arithmetic. +여러 번의 디버깅 끝에, 필자는 잘못된 포인터 산술 연산이 원인임 찾아냈습니다. ```diff uint32_t* internal_node_key(void* node, uint32_t key_num) { @@ -295,8 +295,8 @@ After a bunch of debugging, I discovered this was due to some bad pointer arithm } ``` -`INTERNAL_NODE_CHILD_SIZE` is 4. My intention here was to add 4 bytes to the result of `internal_node_cell()`, but since `internal_node_cell()` returns a `uint32_t*`, this it was actually adding `4 * sizeof(uint32_t)` bytes. I fixed it by casting to a `void*` before doing the arithmetic. +`INTERNAL_NODE_CHILD_SIZE` 는 4입니다. 필자의 의도는 `internal_node_cell()` 의 결과에 4 바이트를 더하는 것입니다. 하지만 `internal_node_cell()` 는 `uint32_t*` 를 반환하므로 `4 * sizeof(uint32_t)` 가 더해지게 됩니다. 산술 연산 전에 `void*` 로 캐스팅하여 문제를 해결했습니다. -NOTE! [Pointer arithmetic on void pointers is not part of the C standard and may not work with your compiler](https://stackoverflow.com/questions/3523145/pointer-arithmetic-for-void-pointer-in-c/46238658#46238658). I may do an article in the future on portability, but I'm leaving my void pointer arithmetic for now. +주의! [void 포인터의 산술 연산을 C 표준이 아니므로 컴파일러에 따라 작동하지 않을 수도 있습니다.](https://stackoverflow.com/questions/3523145/pointer-arithmetic-for-void-pointer-in-c/46238658#46238658) 향후에 이식 관련된 글을 쓰게 될 것이니, 지금은 void 포인터 연산으로 남겨 두겠습니다. -Alright. One more step toward a fully-operational btree implementation. The next step should be splitting internal nodes. Until then! +좋습니다. 완전완 B-트리 구현을 위해 한 걸음 더 나아갔습니다. 다음 단계는 내부 노드를 분할하는 것입니다. 그럼 다음 장에서 뵙겠습니다. diff --git a/_parts/part1_kor.md b/_parts/part1_kor.md new file mode 100644 index 0000000..af054bd --- /dev/null +++ b/_parts/part1_kor.md @@ -0,0 +1,227 @@ +--- +title: 제1 장 - 소개 및 REPL 구축 +date: 2017-08-30 +--- + +웹 개발자로서, 매일 관계형 데이터베이스를 사용하지만, 작동원리를 이해하지는 못했습니다. 따라서 몇 가지 궁금증을 가지고 있었습니다. +- 데이터가 어떠한 형식으로 저장되는가? (메모리와 디스크에) +- 언제 메모리에서 디스크로 옮겨지는가? +- 왜 테이블마다 하나의 주키만을 가질 수 있는가? +- 어떻게 트랜잭션 롤백이 수행되는가? +- 인덱스는 어떻게 구성되는가? +- 전체 테이블 탐색(full table scan) 작업은 언제, 어떻게 수행되는가? +- 준비된 문장(prepared statement)은 어떠한 형식으로 저장되는가? + +즉, 데이터베이스는 어떻게 **작동** 하는가? + +궁금증을 해결하기 위해, 데이터베이스를 처음부터 만들어 볼 것입니다. MySQL이나 PostgreSQL 보다 적은 기능만으로 작게 설계된 sqlite를 본떠서 만들 것이며, 이 작업이 데이터베이스를 이해하는데 큰 도움이 될 것이라 생각합니다. 전체 데이터베이스는 하나의 파일에 작성합니다! + +# Sqlite + +웹사이트에는 [sqlite 내부 관련 문서](https://www.sqlite.org/arch.html)가 많이 있습니다. 참고로, 필자는 [SQLite Database System: Design and Implementation](https://play.google.com/store/books/details?id=9Z6IQQnX1JEC)의 사본을 갖고 있습니다. + +{% include image.html url="assets/images/arch1.gif" description="sqlite 구조 (https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki)" %} + +쿼리는 데이터를 가져오거나 수정하기 위해 구성 요소들의 사슬을 통과합니다. **전단부(front-end)** 는 다음으로 구성됩니다. +- 토크나이저(tokenizer) +- 파서(parser) +- 코드 생성기(code generator) + +전단부의 입력은 SQL 쿼리입니다. 출력은 sqlite 가상 머신 바이트코드 (기본적으로 데이터베이스에서 작동할 수 있는 컴파일된 프로그램)입니다. + +_후단부(back-end)_ 는 다음으로 구성됩니다. +- 가상 머신(virtual machine) +- B-트리(B-tree) +- 페이저(pager) +- 운영체제 인터페이스(os interface) + +**가상 머신(virtual machine)** 은 전단부에서 생성된 바이트코드를 명령어로 받아들입니다. 그 후, B-tree 자료구조에 저장된 하나 이상의 테이블 혹은 인덱스를 대상으로 작업을 수행합니다. 기본적으로 가상 머신은 바이트 코드 명령어 유형에 따라 분기하는 큰 스위치 문입니다. + +각각의 **B-트리(B-tree)** 는 많은 노드로 구성됩니다. 각 노드의 길이는 한 페이지입니다. B-tree는 페이저(pager)에게 명령하여, 디스크에서 페이지를 가져오거나 다시 디스크로 저장하도록 할 수 있습니다. + +**페이저(pager)** 는 페이지의 데이터를 읽거나 페이지에 데이터를 쓰는 명령을 받습니다. 데이터베이스 파일에서 적절한 오프셋에 위치한 데이터를 읽거나 쓰는 역할을 수행합니다. 또한 최근 접근 페이지에 대한 캐시를 메모리에 유지하고, 캐시 속 페이지가 디스크에 다시 쓰이는 시기를 결정합니다. + +**운영체제 인터페이스(os interface)** 는 sqlite가 컴파일된 운영체제에 따라 다른 층입니다. 여기서는 멀티 플랫폼을 지원하지는 않을 것입니다. + +[천 리 길도 한 걸음부터](https://en.wiktionary.org/wiki/a_journey_of_a_thousand_miles_begins_with_a_single_step), 그러므로 간단한 것부터 시작해보겠습니다. + +## 간단한 REPL 만들기 + +Sqlite를 명령줄에서 실행시키면, 입력-실행-출력 루프를 시작합니다. + +```shell +~ sqlite3 +SQLite version 3.16.0 2016-11-04 19:09:39 +Enter ".help" for usage hints. +Connected to a transient in-memory database. +Use ".open FILENAME" to reopen on a persistent database. +sqlite> create table users (id int, username varchar(255), email varchar(255)); +sqlite> .tables +users +sqlite> .exit +~ +``` + +이 작업을 위해, 메인 함수는 프롬프트를 출력하고, 한 줄의 입력을 가져오며 입력을 처리하는 무한 루프를 가질 것입니다. + +```c +int main(int argc, char* argv[]) { + InputBuffer* input_buffer = new_input_buffer(); + while (true) { + print_prompt(); + read_input(input_buffer); + + if (strcmp(input_buffer->buffer, ".exit") == 0) { + close_input_buffer(input_buffer); + exit(EXIT_SUCCESS); + } else { + printf("Unrecognized command '%s'.\n", input_buffer->buffer); + } + } +} +``` + +먼저, [getline()](http://man7.org/linux/man-pages/man3/getline.3.html) 함수와 상호작용하는데 필요한 정보들을 하나로 감싼 `InputBuffer` 를 정의합니다. (자세한 설명은 잠시 후에 하겠습니다.) +```c +typedef struct { + char* buffer; + size_t buffer_length; + ssize_t input_length; +} InputBuffer; + +InputBuffer* new_input_buffer() { + InputBuffer* input_buffer = (InputBuffer*)malloc(sizeof(InputBuffer)); + input_buffer->buffer = NULL; + input_buffer->buffer_length = 0; + input_buffer->input_length = 0; + + return input_buffer; +} +``` + +다음으로, 프롬프트를 사용자에게 출력하는 `print_prompt()` 를 정의합니다. 입력받기 전에 이 작업을 먼저 수행합니다. + +```c +void print_prompt() { printf("db > "); } +``` + +입력의 한 줄을 읽어오기 위해, [getline()](http://man7.org/linux/man-pages/man3/getline.3.html) 을 사용합니다. +```c +ssize_t getline(char **lineptr, size_t *n, FILE *stream); +``` +`lineptr` : 입력 저장 버퍼를 가리키는 포인터 변수의 포인터입니다. `NULL` 로 설정된 경우 함수 수행이 실패하더라도, `getline` 에 의해서 동적 메모리 할당이 되므로 사용자가 해제해야 합니다. + +`n` : 할당된 버퍼의 크기를 갖는 변수의 포인터입니다. + +`stream` : 읽어 올 입력 스트림 입니다. 여기서는, 표준 입력을 사용합니다. + +`return value` : 읽어온 바이트 수로서, 버퍼의 크기보다 작을 수도 있습니다. + +우리는 `getline` 함수에게 `input_buffer->buffer` 에 입력 줄을 저장하고 할당된 버퍼의 크기를 `input_buffer->buffer_length` 에 저장하도록 지시합니다. 반환 값은 `input_buffer->input_length` 에 저장합니다. + +`buffer`는 null로 초기화해서, `getline` 이 입력 줄을 저장하기에 충분한 버퍼를 동적 할당하고, 할당받은 버퍼를 `buffer` 가 가리키도록 합니다. + +```c +void read_input(InputBuffer* input_buffer) { + ssize_t bytes_read = + getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin); + + if (bytes_read <= 0) { + printf("Error reading input\n"); + exit(EXIT_FAILURE); + } + + // 후행 줄 바꿈 제거 + input_buffer->input_length = bytes_read - 1; + input_buffer->buffer[bytes_read - 1] = 0; +} +``` + +이제 `InputBuffer *` 인스턴스가 할당받은 메모리 공간과 구조체의 `buffer` 요소를 해제하는 함수를 정의합니다. (`getline` 함수는 `read_input` 함수에서 `input_buffer->buffer` 에 대한 메모리를 동적 할당 합니다.) + +```c +void close_input_buffer(InputBuffer* input_buffer) { + free(input_buffer->buffer); + free(input_buffer); +} +``` + +최종적으로, 명령어를 분석하고 실행합니다. 현재는 하나의 인식 가능한 명령어 `.exit` 만 존재합니다. 이 명령은 프로그램을 종료시킵니다. 나머지 명령은 에러 메시지를 출력하고 루프를 계속 진행합니다. + +```c +if (strcmp(input_buffer->buffer, ".exit") == 0) { + close_input_buffer(input_buffer); + exit(EXIT_SUCCESS); +} else { + printf("Unrecognized command '%s'.\n", input_buffer->buffer); +} +``` + +실행해 봅시다! +```shell +~ ./db +db > .tables +Unrecognized command '.tables'. +db > .exit +~ +``` + +좋습니다, 잘 작동하는 REPL을 만들었습니다. 다음 장에서, 우리의 명령어 개발을 시작하겠습니다. 이번 장의 전체 코드는 아래에 있습니다. + +```c +#include +#include +#include +#include + +typedef struct { + char* buffer; + size_t buffer_length; + ssize_t input_length; +} InputBuffer; + +InputBuffer* new_input_buffer() { + InputBuffer* input_buffer = malloc(sizeof(InputBuffer)); + input_buffer->buffer = NULL; + input_buffer->buffer_length = 0; + input_buffer->input_length = 0; + + return input_buffer; +} + +void print_prompt() { printf("db > "); } + +void read_input(InputBuffer* input_buffer) { + ssize_t bytes_read = + getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin); + + if (bytes_read <= 0) { + printf("Error reading input\n"); + exit(EXIT_FAILURE); + } + + // 후행 줄 바꿈 제거 + input_buffer->input_length = bytes_read - 1; + input_buffer->buffer[bytes_read - 1] = 0; +} + +void close_input_buffer(InputBuffer* input_buffer) { + free(input_buffer->buffer); + free(input_buffer); +} + +int main(int argc, char* argv[]) { + InputBuffer* input_buffer = new_input_buffer(); + while (true) { + print_prompt(); + read_input(input_buffer); + + if (strcmp(input_buffer->buffer, ".exit") == 0) { + close_input_buffer(input_buffer); + exit(EXIT_SUCCESS); + } else { + printf("Unrecognized command '%s'.\n", input_buffer->buffer); + } + } +} +``` diff --git a/_parts/part2.md b/_parts/part2_kor.md similarity index 61% rename from _parts/part2.md rename to _parts/part2_kor.md index 4b16b7c..73edb55 100644 --- a/_parts/part2.md +++ b/_parts/part2_kor.md @@ -1,19 +1,19 @@ --- -title: Part 2 - World's Simplest SQL Compiler and Virtual Machine +title: 제2 장 - 세상에서 가장 간단한 SQL 컴파일러 및 가상 머신 date: 2017-08-31 --- -We're making a clone of sqlite. The "front-end" of sqlite is a SQL compiler that parses a string and outputs an internal representation called bytecode. +sqlite를 본뜨는 작업을 진행 중입니다. sqlite의 전단 부는 문자열을 구문 분석하여 내부 표현 형태인 바이트코드로 출력하는 SQL 컴파일러입니다. -This bytecode is passed to the virtual machine, which executes it. +이 바이트 코드는 가상 머신으로 전달되고, 가상 머신은 바이트코드를 실행합니다. -{% include image.html url="assets/images/arch2.gif" description="SQLite Architecture (https://www.sqlite.org/arch.html)" %} +{% include image.html url="assets/images/arch2.gif" description="SQLite 구조 (https://www.sqlite.org/arch.html)" %} -Breaking things into two steps like this has a couple advantages: -- Reduces the complexity of each part (e.g. virtual machine does not worry about syntax errors) -- Allows compiling common queries once and caching the bytecode for improved performance +두 단계로 나누는 것은 두 가지 이점을 갖습니다. +- 각 부분의 복잡성을 낮추게 됩니다. (예: 가상 머신은 구문 오류를 고려하지 않아도 됩니다.) +- 자주 쓰이는 쿼리를 컴파일하고 바이트코드를 캐싱 하여 성능을 향상시킬 수 있습니다. -With this in mind, let's refactor our `main` function and support two new keywords in the process: +이를 염두에 두고, `main` 함수를 리팩토링하고 두 가지 새로운 키워드를 추가해보겠습니다: ```diff int main(int argc, char* argv[]) { @@ -52,13 +52,13 @@ With this in mind, let's refactor our `main` function and support two new keywor } ``` -Non-SQL statements like `.exit` are called "meta-commands". They all start with a dot, so we check for them and handle them in a separate function. +`.exit` 와 같은 Non-SQL 문장들은 "메타 명령(meta-commands)" 이라고 합니다. 모든 메타 명령은 점으로 시작하므로, 점을 검사하고 처리하는 작업을 별도의 함수로 만듭니다. -Next, we add a step that converts the line of input into our internal representation of a statement. This is our hacky version of the sqlite front-end. +다음으로, 입력받은 문장을 우리의 내부 표현 형태로 변환하는 단계를 추가합니다. 이것은 우리의 sqlite 전단부의 임시 버전입니다. -Lastly, we pass the prepared statement to `execute_statement`. This function will eventually become our virtual machine. +마지막으로 준비된 문장을 `execute_statement` 함수에 전달합니다. 최종적으로 이 함수는 우리의 가상 머신이 될 것입니다. -Notice that two of our new functions return enums indicating success or failure: +두 개의 새로운 함수들은 성공 혹은 실패의 열거형을 반환하는 것을 유의하시기 바랍니다: ```c typedef enum { @@ -69,9 +69,9 @@ typedef enum { typedef enum { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT } PrepareResult; ``` -"Unrecognized statement"? That seems a bit like an exception. But [exceptions are bad](https://www.youtube.com/watch?v=EVhCUSgNbzo) (and C doesn't even support them), so I'm using enum result codes wherever practical. The C compiler will complain if my switch statement doesn't handle a member of the enum, so we can feel a little more confident we handle every result of a function. Expect more result codes to be added in the future. +"인식되지 못한 문장(Unrecognized statement)"은 예외 처리를 위한 것처럼 보일 수 있습니다. 그러나 [예외 처리는 좋지 않습니다.](https://www.youtube.com/watch?v=EVhCUSgNbzo) (심지어 C는 지원하지 않습니다.) 그래서, 필자는 가능한 열거형 결과 코드를 사용하여 처리합니다. switch 문이 열거 형 멤버를 처리하지 않으면 C 컴파일러가 문제를 제기하므로 우리는 함수의 모든 결과를 처리하고 있음을 확신할 수 있습니다. 앞으로 더 많은 결과 코드가 추가될 것입니다. -`do_meta_command` is just a wrapper for existing functionality that leaves room for more commands: +`do_meta_command` 는 기존 기능을 단순히 감싼 함수로, 더 많은 명령들의 확장에 열려있습니다. ```c MetaCommandResult do_meta_command(InputBuffer* input_buffer) { @@ -83,7 +83,7 @@ MetaCommandResult do_meta_command(InputBuffer* input_buffer) { } ``` -Our "prepared statement" right now just contains an enum with two possible values. It will contain more data as we allow parameters in statements: +현재 "준비된 문장(prepared statement)" 은 두 가지 멤버 값을 갖는 열거형을 갖고 있습니다. 추후에, 매개 변수를 허용함에 따라 더 많은 데이터를 갖게 될 것입니다. ```c typedef enum { STATEMENT_INSERT, STATEMENT_SELECT } StatementType; @@ -93,7 +93,7 @@ typedef struct { } Statement; ``` -`prepare_statement` (our "SQL Compiler") does not understand SQL right now. In fact, it only understands two words: +현재 `prepare_statement` (우리의 "SQL 컴파일러")는 SQL을 이해하지 못합니다. 정확하게는 두 단어만을 이해합니다. ```c PrepareResult prepare_statement(InputBuffer* input_buffer, Statement* statement) { @@ -110,9 +110,9 @@ PrepareResult prepare_statement(InputBuffer* input_buffer, } ``` -Note that we use `strncmp` for "insert" since the "insert" keyword will be followed by data. (e.g. `insert 1 cstack foo@bar.com`) +"insert" 키워드 뒤에 데이터가 있으므로 `strncmp` 를 사용한다는 점을 유의하시기 바랍니다. (예: `insert 1 cstack foo@bar.com`) -Lastly, `execute_statement` contains a few stubs: +마지막으로 `execute_statement`는 몇 가지 스텁을 갖습니다. ```c void execute_statement(Statement* statement) { switch (statement->type) { @@ -126,9 +126,9 @@ void execute_statement(Statement* statement) { } ``` -Note that it doesn't return any error codes because there's nothing that could go wrong yet. +아직 잘못될 부분이 없기 때문에 어떠한 오류 코드도 반환되지 않음을 유의하시기 바랍니다. -With these refactors, we now recognize two new keywords! +개선 작업을 통해 두 개의 키워드를 인식하게 되었습니다! ```command-line ~ ./db db > insert foo bar @@ -145,7 +145,7 @@ db > .exit ~ ``` -The skeleton of our database is taking shape... wouldn't it be nice if it stored data? In the next part, we'll implement `insert` and `select`, creating the world's worst data store. In the mean time, here's the entire diff from this part: +데이터베이스가 모형을 갖춰가고 있습니다... 데이터를 저장하면 더 좋지 않을까요? 다음 장에서는 `insert` 와 `select` 를 구현하며, 최악의 데이터 저장소를 만들어볼 것입니다. 지금 까지 바뀐 부분을 다음과 같습니다. ```diff @@ -10,6 +10,23 @@ struct InputBuffer_t { diff --git a/_parts/part3.md b/_parts/part3_kor.md similarity index 73% rename from _parts/part3.md rename to _parts/part3_kor.md index cfb0d1e..8cb8da0 100644 --- a/_parts/part3.md +++ b/_parts/part3_kor.md @@ -1,15 +1,15 @@ --- -title: Part 3 - An In-Memory, Append-Only, Single-Table Database +title: 제3 장 - 메모리 내, 추가 전용, 단일 테이블 데이터베이스 date: 2017-09-01 --- -We're going to start small by putting a lot of limitations on our database. For now, it will: +데이터베이스에 많은 제한을 두어, 작은 규모로 시작해 보겠습니다. 우선, 다음과 같은 제한을 두겠습니다. -- support two operations: inserting a row and printing all rows -- reside only in memory (no persistence to disk) -- support a single, hard-coded table +- 두 가지 연산 만 지원: 행 삽입 및 모든 행 출력 +- 메모리에만 존재 (디스크에 지속 보존되지 않음) +- 하드 코딩 된 단일 테이블만 지원 -Our hard-coded table is going to store users and look like this: +하드 코딩 된 테이블은 사용자를 저장할 것이며, 형태는 다음과 같습니다. | column | type | |----------|--------------| @@ -17,15 +17,15 @@ Our hard-coded table is going to store users and look like this: | username | varchar(32) | | email | varchar(255) | -This is a simple schema, but it gets us to support multiple data types and multiple sizes of text data types. +단순한 스키마이지만, 여러 데이터 타입과 다양한 크기의 텍스트 데이터 타입을 지원합니다. -`insert` statements are now going to look like this: +`insert` 문은 다음과 같습니다. ``` insert 1 cstack foo@bar.com ``` -That means we need to upgrade our `prepare_statement` function to parse arguments +즉, 입력 인자들을 파싱 할 수 있도록 `prepare_statement` 함수를 개선해야 합니다. ```diff if (strncmp(input_buffer->buffer, "insert", 6) == 0) { @@ -41,7 +41,7 @@ That means we need to upgrade our `prepare_statement` function to parse argument if (strcmp(input_buffer->buffer, "select") == 0) { ``` -We store those parsed arguments into a new `Row` data structure inside the statement object: +파싱 된 입력 인자들은 새로운 `Row` 데이터 구조체 형태로 statement 객체 내부에 저장됩니다. ```diff +#define COLUMN_USERNAME_SIZE 32 @@ -54,21 +54,21 @@ We store those parsed arguments into a new `Row` data structure inside the state + typedef struct { StatementType type; -+ Row row_to_insert; // only used by insert statement ++ Row row_to_insert; // insert 문에서만 사용됩니다. } Statement; ``` -Now we need to copy that data into some data structure representing the table. SQLite uses a B-tree for fast lookups, inserts and deletes. We'll start with something simpler. Like a B-tree, it will group rows into pages, but instead of arranging those pages as a tree it will arrange them as an array. +이제 입력 데이터를 테이블을 표현하는 데이터 구조로 복사할 필요가 있습니다. SQLite의 경우 빠른 조회, 삽입 및 삭제를 위해 B-트리를 사용합니다. 우리는 좀 더 간단한 데이터 구조를 사용하여 진행하겠습니다. B-트리와 마찬가지로, 행을 페이지로 그룹화하지만, 페이지들을 트리 형태가 아닌 배열 형태로 처리하겠습니다. -Here's my plan: +계획은 다음과 같습니다. -- Store rows in blocks of memory called pages -- Each page stores as many rows as it can fit -- Rows are serialized into a compact representation with each page -- Pages are only allocated as needed -- Keep a fixed-size array of pointers to pages +- 페이지라는 메모리 블록에 행을 저장합니다. +- 각 페이지는 최대한 많은 행을 저장합니다. +- 각 페이지에 행들은 촘촘한 표현 형태로 직렬화되어 저장됩니다. +- 페이지는 필요한 경우에만 할당됩니다. +- 페이지는 고정 크기의 포인터 배열로 관리됩니다. -First we'll define the compact representation of a row: +먼저 행의 촘촘한 표현 형태를 정의합니다. ```diff +#define size_of_attribute(Struct, Attribute) sizeof(((Struct*)0)->Attribute) + @@ -81,7 +81,7 @@ First we'll define the compact representation of a row: +const uint32_t ROW_SIZE = ID_SIZE + USERNAME_SIZE + EMAIL_SIZE; ``` -This means the layout of a serialized row will look like this: +즉, 직렬화된 행의 형태는 다음과 같습니다. | column | size (bytes) | offset | |----------|--------------|--------------| @@ -90,7 +90,7 @@ This means the layout of a serialized row will look like this: | email | 255 | 36 | | total | 291 | | -We also need code to convert to and from the compact representation. +촘촘한 표현 형태로 변환하거나 재변환하는 코드도 필요합니다. ```diff +void serialize_row(Row* source, void* destination) { + memcpy(destination + ID_OFFSET, &(source->id), ID_SIZE); @@ -105,7 +105,7 @@ We also need code to convert to and from the compact representation. +} ``` -Next, a `Table` structure that points to pages of rows and keeps track of how many rows there are: +다음은 행의 페이지들을 가리키고 행의 수를 추적관리하는 `Table` 구조체입니다. ```diff +const uint32_t PAGE_SIZE = 4096; +#define TABLE_MAX_PAGES 100 @@ -118,19 +118,19 @@ Next, a `Table` structure that points to pages of rows and keeps track of how ma +} Table; ``` -I'm making our page size 4 kilobytes because it's the same size as a page used in the virtual memory systems of most computer architectures. This means one page in our database corresponds to one page used by the operating system. The operating system will move pages in and out of memory as whole units instead of breaking them up. +페이지 크기는 대부분의 컴퓨터 구조들이 가상 메모리 시스템에서 사용하는 것과 같은 4 킬로바이트로 만들었습니다. 이점은 우리 데이터베이스의 한 페이지가 운영체제의 한 페이지에 해당함을 의미합니다. 따라서, 운영체제는 페이지를 분할하지 않고 페이지를 메모리 내외로 이동시킬 것입니다. -I'm setting an arbitrary limit of 100 pages that we will allocate. When we switch to a tree structure, our database's maximum size will only be limited by the maximum size of a file. (Although we'll still limit how many pages we keep in memory at once) +100페이지 할당 제한은 임의로 설정 한 것입니다. 트리구조로 전환하게 되면 데이터베이스의 최대 크기는 파일 최대 크기에 의해서만 제한될 것입니다. (한 번에 메모리에 보관하는 페이지 수는 제한이 될 것입니다.) -Rows should not cross page boundaries. Since pages probably won't exist next to each other in memory, this assumption makes it easier to read/write rows. +행은 페이지 경계를 넘지 않아야 합니다. 페이지가 메모리에 연속적으로 존재하지 않기 때문에, 행을 좀 더 쉽게 읽고 쓸 수 있게 합니다. -Speaking of which, here is how we figure out where to read/write in memory for a particular row: +말이 나온 김에, 다음은 읽고 쓸 행의 메모리 위치를 찾는 방법입니다. ```diff +void* row_slot(Table* table, uint32_t row_num) { + uint32_t page_num = row_num / ROWS_PER_PAGE; + void* page = table->pages[page_num]; + if (page == NULL) { -+ // Allocate memory only when we try to access page ++ // 페이지에 접근하는 경우 메모리 할당 + page = table->pages[page_num] = malloc(PAGE_SIZE); + } + uint32_t row_offset = row_num % ROWS_PER_PAGE; @@ -139,7 +139,7 @@ Speaking of which, here is how we figure out where to read/write in memory for a +} ``` -Now we can make `execute_statement` read/write from our table structure: +이제 `execute_statement` 함수에서 우리의 테이블 구조를 읽고 쓰도록 만들 수 있습니다. ```diff -void execute_statement(Statement* statement) { +ExecuteResult execute_insert(Statement* statement, Table* table) { @@ -178,8 +178,7 @@ Now we can make `execute_statement` read/write from our table structure: } ``` -Lastly, we need to initialize the table, create the respective -memory release function and handle a few more error cases: +마지막으로 테이블 초기화 및 메모리 해제 함수를 생성하고 몇 가지 에러 처리를 합니다. ```diff + Table* new_table() { @@ -231,7 +230,7 @@ memory release function and handle a few more error cases: } ``` - With those changes we can actually save data in our database! + 변경을 통해 데이터베이스에 데이터를 저장할 수 있게 되었습니다! ```command-line ~ ./db db > insert 1 cstack foo@bar.com @@ -248,11 +247,11 @@ db > .exit ~ ``` -Now would be a great time to write some tests, for a couple reasons: -- We're planning to dramatically change the data structure storing our table, and tests would catch regressions. -- There are a couple edge cases we haven't tested manually (e.g. filling up the table) +이쯤에서, 테스트를 수행해보는 것이 좋을 것 같습니다. 여기서 테스트를 수행하는 것에는 몇 가지 이유가 있습니다. +- 앞으로 테이블에 저장되는 데이터 구조를 극적으로 변경할 것이며, 테스트를 통해 회귀 테스트 케이스를 얻는 것이 큰 도움이 될 것입니다. +- 수동으로 테스트하지 못할 몇 가지 엣지 케이스가 존재합니다. (예: 테이블 가득 채우기) -We'll address those issues in the next part. For now, here's the complete diff from this part: +다음 장에서 이 사항들을 다루어 보겠습니다. 지금까지 변경된 부분은 다음과 같습니다. ```diff @@ -2,6 +2,7 @@ #include @@ -290,7 +289,7 @@ We'll address those issues in the next part. For now, here's the complete diff f + +typedef struct { + StatementType type; -+ Row row_to_insert; //only used by insert statement ++ Row row_to_insert; // insert 문에서만 사용됩니다. +} Statement; + +#define size_of_attribute(Struct, Attribute) sizeof(((Struct*)0)->Attribute) @@ -333,7 +332,7 @@ We'll address those issues in the next part. For now, here's the complete diff f + uint32_t page_num = row_num / ROWS_PER_PAGE; + void *page = table->pages[page_num]; + if (page == NULL) { -+ // Allocate memory only when we try to access page ++ // 페이지에 접근하는 경우 메모리 할당 + page = table->pages[page_num] = malloc(PAGE_SIZE); + } + uint32_t row_offset = row_num % ROWS_PER_PAGE; diff --git a/_parts/part4.md b/_parts/part4_kor.md similarity index 74% rename from _parts/part4.md rename to _parts/part4_kor.md index 00a462d..be51955 100644 --- a/_parts/part4.md +++ b/_parts/part4_kor.md @@ -1,13 +1,13 @@ --- -title: Part 4 - Our First Tests (and Bugs) +title: 제4 장 - 첫 테스트 (그리고 버그) date: 2017-09-03 --- -We've got the ability to insert rows into our database and to print out all rows. Let's take a moment to test what we've got so far. +우리는 데이터베이스에 행을 삽입하고 모든 행을 출력해 볼 수 있게 되었습니다. 여기서, 지금까지 해온 것들을 테스트해보는 시간을 갖겠습니다. -I'm going to use [rspec](http://rspec.info/) to write my tests because I'm familiar with it, and the syntax is fairly readable. +필자가 사용에 익숙하기도 하며, 구문의 가독성도 좋은 [rspec](http://rspec.info/)을 사용하여 테스트를 진행하겠습니다. -I'll define a short helper to send a list of commands to our database program then make assertions about the output: +먼저, 데이터베이스에 명령 목록을 보내도록 간단한 헬퍼를 정의하고 출력에 대한 어설션을 만듭니다. ```ruby describe 'database' do @@ -42,7 +42,8 @@ describe 'database' do end ``` -This simple test makes sure we get back what we put in. And indeed it passes: +이 간단한 테스트는 우리가 넣은 값이 제대로 출력됨을 확인 시켜 줄 것입니다. 그리고 제대로 작동하여 테스트를 통과합니다. + ```command-line bundle exec rspec . @@ -51,7 +52,7 @@ Finished in 0.00871 seconds (files took 0.09506 seconds to load) 1 example, 0 failures ``` -Now it's feasible to test inserting a large number of rows into the database: +이제 데이터베이스에 많은 수의 행을 삽입해보는 테스트가 가능해집니다. ```ruby it 'prints error message when table is full' do script = (1..1401).map do |i| @@ -63,7 +64,7 @@ it 'prints error message when table is full' do end ``` -Running tests again... +다시 테스트를 진행합니다... ```command-line bundle exec rspec .. @@ -72,9 +73,9 @@ Finished in 0.01553 seconds (files took 0.08156 seconds to load) 2 examples, 0 failures ``` -Sweet, it works! Our db can hold 1400 rows right now because we set the maximum number of pages to 100, and 14 rows can fit in a page. +잘 작동합니다! 최대 페이지 수가 100이며 각각 14 개의 행을 적재할 수 있으므로, 현재 우리의 데이터베이스는 1400개의 행을 보유할 수 있습니다. -Reading through the code we have so far, I realized we might not handle storing text fields correctly. Easy to test with this example: +지금까지 진행된 코드를 살펴보던 중, 텍스트 필드 저장을 제대로 처리하지 못함을 깨달았습니다. 다음 예제로 쉽게 테스트해볼 수 있습니다. ```ruby it 'allows inserting strings that are the maximum length' do long_username = "a"*32 @@ -94,7 +95,7 @@ it 'allows inserting strings that are the maximum length' do end ``` -And the test fails! +그리고 테스트는 실패합니다! ```ruby Failures: @@ -108,7 +109,7 @@ Failures: # ./spec/main_spec.rb:48:in `block (2 levels) in ' ``` -If we try it ourselves, we'll see that there's some weird characters when we try to print out the row. (I'm abbreviating the long strings): +직접 실행해 보면, 행을 출력할 때 이상한 문자들이 있음을 알 수 있습니다. (긴 문자열은 줄여썼습니다.) ```command-line db > insert 1 aaaaa... aaaaa... Executed. @@ -118,7 +119,7 @@ Executed. db > ``` -What's going on? If you take a look at our definition of a Row, we allocate exactly 32 bytes for username and exactly 255 bytes for email. But [C strings](http://www.cprogramming.com/tutorial/c/lesson9.html) are supposed to end with a null character, which we didn't allocate space for. The solution is to allocate one additional byte: +왜 그럴까요? 행의 정의를 보면, 이름에는 정확히 32 바이트, 이메일에는 255바이트를 할당한 것을 볼 수 있습니다. 그러나 [C 문자열](http://www.cprogramming.com/tutorial/c/lesson9.html) 은 공백으로 끝나게 되어있고, 우리는 공백을 위한 공간을 할당하지 않았습니다. 해결책은 한 바이트를 추가 할당하는 것입니다. ```diff const uint32_t COLUMN_EMAIL_SIZE = 255; typedef struct { @@ -130,7 +131,7 @@ What's going on? If you take a look at our definition of a Row, we allocate exac } Row; ``` - And indeed that fixes it: + 그리고 정말로 문제를 해결합니다. ```ruby bundle exec rspec ... @@ -139,7 +140,7 @@ Finished in 0.0188 seconds (files took 0.08516 seconds to load) 3 examples, 0 failures ``` -We should not allow inserting usernames or emails that are longer than column size. The spec for that looks like this: +열 크기보다 긴 사용자 이름 또는 이메일을 삽입하는 것 역시 허용해서는 안 됩니다. 초과하는 케이스는 다음과 같습니다. ```ruby it 'prints error message if strings are too long' do long_username = "a"*33 @@ -158,7 +159,7 @@ it 'prints error message if strings are too long' do end ``` -In order to do this we need to upgrade our parser. As a reminder, we're currently using [scanf()](https://linux.die.net/man/3/scanf): +초과를 방지하기 위해서는 파서를 개선해야 합니다. 참고로, 현재는 [scanf()](https://linux.die.net/man/3/scanf)를 사용하고 있습니다. ```c if (strncmp(input_buffer->buffer, "insert", 6) == 0) { statement->type = STATEMENT_INSERT; @@ -172,9 +173,9 @@ if (strncmp(input_buffer->buffer, "insert", 6) == 0) { } ``` -But [scanf has some disadvantages](https://stackoverflow.com/questions/2430303/disadvantages-of-scanf). If the string it's reading is larger than the buffer it's reading into, it will cause a buffer overflow and start writing into unexpected places. We want to check the length of each string before we copy it into a `Row` structure. And to do that, we need to divide the input by spaces. +그러나 [scanf는 몇 가지 약점이 있습니다.](https://stackoverflow.com/questions/2430303/disadvantages-of-scanf) scanf가 읽은 문자열이 입력 버퍼의 크기보다 큰 경우, 버퍼 오버플로우가 발생하여 예기치 않은 위치에 쓰기 작업을 수행합니다. 따라서, 우리는 `Row` 구조체에 각 문자열을 복사하기 전에, 길이를 확인해야 합니다. 이를 위해서는 입력을 공백으로 나눌 필요가 있습니다. -I'm going to use [strtok()](http://www.cplusplus.com/reference/cstring/strtok/) to do that. I think it's easiest to understand if you see it in action: +[strtok()](http://www.cplusplus.com/reference/cstring/strtok/)를 사용해서 하도록 하겠습니다. 실제로 보면 쉽게 이해할 수 있을 것입니다. ```diff +PrepareResult prepare_insert(InputBuffer* input_buffer, Statement* statement) { @@ -219,11 +220,11 @@ I'm going to use [strtok()](http://www.cplusplus.com/reference/cstring/strtok/) } ``` -Calling `strtok` successively on the the input buffer breaks it into substrings by inserting a null character whenever it reaches a delimiter (space, in our case). It returns a pointer to the start of the substring. +연속적으로 `strtok` 를 호출하면 구분 문자(여기서는 공백에 해당)에 도달할 때마다 입력 버퍼에 null 문자를 삽입하여 하위 문자열로 분리합니다. -We can call [strlen()](http://www.cplusplus.com/reference/cstring/strlen/) on each text value to see if it's too long. +각 텍스트 값의 길이 초과 여부는 [strlen()](http://www.cplusplus.com/reference/cstring/strlen/)을 호출하여 확인할 수 있습니다. -We can handle the error like we do any other error code: +다른 에러 코드와 마찬가지로 에러 처리를 할 수 있습니다. ```diff enum PrepareResult_t { PREPARE_SUCCESS, @@ -244,7 +245,7 @@ We can handle the error like we do any other error code: continue; ``` -Which makes our test pass +파서 개선 작업은 테스트를 통과하게 만듭니다. ```command-line bundle exec rspec .... @@ -253,7 +254,7 @@ Finished in 0.02284 seconds (files took 0.116 seconds to load) 4 examples, 0 failures ``` -While we're here, we might as well handle one more error case: +하는 김에, 에러 케이스를 하나 더 처리하겠습니다. ```ruby it 'prints an error message if id is negative' do script = [ @@ -298,11 +299,11 @@ end continue; ``` -Alright, that's enough testing for now. Next is a very important feature: persistence! We're going to save our database to a file and read it back out again. +이제 테스트는 충분합니다. 다음 장은 매우 중요한 기능입니다. 지속성! 우리는 데이터 베이스를 파일에 저장하고 다시 읽어오도록 만들 것입니다. -It's gonna be great. +매우 근사한 작업이 될 것입니다. -Here's the complete diff for this part: +지금까지 변경된 부분은 다음과 같습니다. ```diff @@ -22,6 +22,8 @@ @@ -386,7 +387,7 @@ Here's the complete diff for this part: printf("Syntax error. Could not parse statement.\n"); continue; ``` -And we added tests: +그리고 추가된 테스트들입니다. ```diff +describe 'database' do + def run_script(commands) diff --git a/_parts/part5.md b/_parts/part5_kor.md similarity index 62% rename from _parts/part5.md rename to _parts/part5_kor.md index 497f3d0..06e825d 100644 --- a/_parts/part5.md +++ b/_parts/part5_kor.md @@ -1,11 +1,11 @@ --- -title: Part 5 - Persistence to Disk +title: 제5 장 - 디스크 지속성 date: 2017-09-08 --- -> "Nothing in the world can take the place of persistence." -- [Calvin Coolidge](https://en.wikiquote.org/wiki/Calvin_Coolidge) +> "세상에서 끈기를 대신할 수 있는 것은 없다." -- [캘빈 쿨리지](https://en.wikiquote.org/wiki/Calvin_Coolidge) -Our database lets you insert records and read them back out, but only as long as you keep the program running. If you kill the program and start it back up, all your records are gone. Here's a spec for the behavior we want: +우리의 데이터베이스가 레코드를 삽입하고 다시 읽어 올수 있게 되었습니다. 단, 프로그램이 계속 실행 중이어야만 합니다. 만약 프로그램을 종료하고 다시 시작한다면, 모든 레코드는 사라지게 됩니다. 우리는 다음과 같이 작동하길 원합니다. ```ruby it 'keeps data after closing connection' do @@ -29,15 +29,15 @@ it 'keeps data after closing connection' do end ``` -Like sqlite, we're going to persist records by saving the entire database to a file. +sqlite처럼 전체 데이터베이스를 파일에 저장하여 레코드가 지속되도록 해보겠습니다. -We already set ourselves up to do that by serializing rows into page-sized memory blocks. To add persistence, we can simply write those blocks of memory to a file, and read them back into memory the next time the program starts up. +우리는 페이지 크기의 메모리 블록으로 행을 직렬화 하였습니다. 이는 파일 저장 작업을 위한 준비를 이미 마친 것입니다. 지속성 추가를 위해서는, 단순히 메모리의 블록들을 파일에 쓰고 프로그램 시작 시 다시 읽어 오면 될 것입니다. -To make this easier, we're going to make an abstraction called the pager. We ask the pager for page number `x`, and the pager gives us back a block of memory. It first looks in its cache. On a cache miss, it copies data from disk into memory (by reading the database file). +좀 더 쉽게 하기 위해, 페이저라는 추상 객체를 만들겠습니다. 우리는 페이저에게 페이지 번호 `x`를 요청하고, 페이저는 메모리의 블록을 돌려줍니다. 페이저는 먼저 캐시를 탐색합니다. 캐시 미스가 발생하는 경우, 디스크에서 메모리로 블록을 복사합니다. (데이터베이스 파일을 읽음으로써) -{% include image.html url="assets/images/arch-part5.gif" description="How our program matches up with SQLite architecture" %} +{% include image.html url="assets/images/arch-part5.gif" description="우리의 프로그램과 SQLite 구조 간 비교" %} -The Pager accesses the page cache and the file. The Table object makes requests for pages through the pager: +페이저는 페이지 캐시와 파일에 접근합니다. 테이블 객체는 페이저를 통해 페이지들을 요청합니다. ```diff +typedef struct { @@ -53,11 +53,11 @@ The Pager accesses the page cache and the file. The Table object makes requests } Table; ``` -I'm renaming `new_table()` to `db_open()` because it now has the effect of opening a connection to the database. By opening a connection, I mean: +이제 `new_table()` 함수가 데이터베이스에 대한 연결을 여는 기능을 갖기 때문에 함수의 이름을 `db_open()` 으로 변경합니다. 연결을 여는 것은 다음을 뜻합니다. -- opening the database file -- initializing a pager data structure -- initializing a table data structure +- 데이터베이스 파일을 엽니다. +- 페이저 데이터 구조를 초기화합니다. +- 테이블 데이터 구조를 초기화합니다. ```diff -Table* new_table() { @@ -74,15 +74,15 @@ I'm renaming `new_table()` to `db_open()` because it now has the effect of openi } ``` -`db_open()` in turn calls `pager_open()`, which opens the database file and keeps track of its size. It also initializes the page cache to all `NULL`s. +`db_open()` 내부에서는 데이터 베이스 파일을 열고 파일의 크기를 확인하는 `pager_open()` 을 호출합니다. `pager_open()` 은 모든 페이지 캐시를 `NULL`로 초기화합니다. ```diff +Pager* pager_open(const char* filename) { + int fd = open(filename, -+ O_RDWR | // Read/Write mode -+ O_CREAT, // Create file if it does not exist -+ S_IWUSR | // User write permission -+ S_IRUSR // User read permission ++ O_RDWR | // 읽기/쓰기 모드 ++ O_CREAT, // 파일이 존재하지 않으면 파일 생성 ++ S_IWUSR | // 사용자 쓰기 권한 ++ S_IRUSR // 사용자 읽기 권한 + ); + + if (fd == -1) { @@ -104,14 +104,14 @@ I'm renaming `new_table()` to `db_open()` because it now has the effect of openi +} ``` -Following our new abstraction, we move the logic for fetching a page into its own method: +페이저 추상화를 위해, 페이지를 가져오는 로직을 하나의 함수로 생성합니다. ```diff void* row_slot(Table* table, uint32_t row_num) { uint32_t page_num = row_num / ROWS_PER_PAGE; - void* page = table->pages[page_num]; - if (page == NULL) { -- // Allocate memory only when we try to access page +- // 페이지에 접근하는 경우 메모리 할당 - page = table->pages[page_num] = malloc(PAGE_SIZE); - } + void* page = get_page(table->pager, page_num); @@ -121,7 +121,7 @@ Following our new abstraction, we move the logic for fetching a page into its ow } ``` -The `get_page()` method has the logic for handling a cache miss. We assume pages are saved one after the other in the database file: Page 0 at offset 0, page 1 at offset 4096, page 2 at offset 8192, etc. If the requested page lies outside the bounds of the file, we know it should be blank, so we just allocate some memory and return it. The page will be added to the file when we flush the cache to disk later. +`get_page()` 는 캐시 미스를 처리하는 로직을 갖습니다. 페이지는 데이터베이스 파일에 순서대로 저장된다고 가정합니다. (0번 페이지 오프셋 : 0, 1번 페이지 오프셋 : 4096, 2번 페이지 오프셋 : 8192 ...) 요청된 페이지가 파일의 범위를 벗어나는 경우, 아무것도 없음을 알기 때문에, 메모리 공간만 할당하고 반환하면 됩니다. 이 공간은 나중에 캐시를 디스크에 플러시 할 때 파일에 추가됩니다. ```diff @@ -133,11 +133,11 @@ The `get_page()` method has the logic for handling a cache miss. We assume pages + } + + if (pager->pages[page_num] == NULL) { -+ // Cache miss. Allocate memory and load from file. ++ // 캐시 미스. 메모리를 할당하고 파일에서 읽어옵니다. + void* page = malloc(PAGE_SIZE); + uint32_t num_pages = pager->file_length / PAGE_SIZE; + -+ // We might save a partial page at the end of the file ++ // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. + if (pager->file_length % PAGE_SIZE) { + num_pages += 1; + } @@ -158,11 +158,11 @@ The `get_page()` method has the logic for handling a cache miss. We assume pages +} ``` -For now, we'll wait to flush the cache to disk until the user closes the connection to the database. When the user exits, we'll call a new method called `db_close()`, which +현재는, 사용자가 데이터베이스 연결을 종료 할때 까지 캐시를 디스크에 플러시 하지 않고 기다립니다. 이제 사용자가 종료할 때 `db_close()` 라는 새로운 함수를 호출하도록 만들어 보겠습니다. 새로운 함수는 다음을 수행합니다. -- flushes the page cache to disk -- closes the database file -- frees the memory for the Pager and Table data structures +- 페이지 캐시를 디스크에 플러시 합니다. +- 데이터베이스 파일을 닫습니다. +- 페이저와 테이블 구조의 메모리를 해제합니다. ```diff +void db_close(Table* table) { @@ -178,8 +178,8 @@ For now, we'll wait to flush the cache to disk until the user closes the connect + pager->pages[i] = NULL; + } + -+ // There may be a partial page to write to the end of the file -+ // This should not be needed after we switch to a B-tree ++ // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. ++ // B-트리로 전환하면 이 작업은 필요하지 않게 됩니다. + uint32_t num_additional_rows = table->num_rows % ROWS_PER_PAGE; + if (num_additional_rows > 0) { + uint32_t page_num = num_full_pages; @@ -215,7 +215,7 @@ For now, we'll wait to flush the cache to disk until the user closes the connect return META_COMMAND_UNRECOGNIZED_COMMAND; ``` -In our current design, the length of the file encodes how many rows are in the database, so we need to write a partial page at the end of the file. That's why `pager_flush()` takes both a page number and a size. It's not the greatest design, but it will go away pretty quickly when we start implementing the B-tree. +현재 설계에서는, 파일의 길이로 데이터베이스에 몇 개의 행이 있는지 계산합니다. 그래서 파일 끝에 불완전한 페이지를 저장해야만 합니다. 이는 `pager_flush()` 가 페이지 번호와 크기를 함께 매개 변수로 갖는 이유입니다. 좋은 설계는 아니며, B-트리 구현을 시작하면 금방 사라질 것입니다. ```diff +void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) { @@ -241,7 +241,7 @@ In our current design, the length of the file encodes how many rows are in the d +} ``` -Lastly, we need to accept the filename as a command-line argument. Don't forget to also add the extra argument to `do_meta_command`: +마지막으로 파일명을 명령행 인자로 받아들여야 합니다. 또한 `do_meta_command` 에 인자를 추가해야 합니다. ```diff int main(int argc, char* argv[]) { @@ -263,7 +263,7 @@ Lastly, we need to accept the filename as a command-line argument. Don't forget - switch (do_meta_command(input_buffer)) { + switch (do_meta_command(input_buffer, table)) { ``` -With these changes, we're able to close then reopen the database, and our records are still there! +개선을 통해, 데이터베이스를 닫고 다시 열수 있으며, 입력한 레코드들도 그대로 남게 됩니다! ``` ~ ./db mydb.db @@ -282,23 +282,21 @@ db > .exit ~ ``` -For extra fun, let's take a look at `mydb.db` to see how our data is being stored. I'll use vim as a hex editor to look at the memory layout of the file: +추가로 재미 삼아 `mydb.db` 를 통해 데이터가 어떻게 저장되고 있는지 살펴보겠습니다. vim을 16진수 편집기로 사용하여 파일의 메모리 레이아웃을 살펴보겠습니다. ``` vim mydb.db :%!xxd ``` -{% include image.html url="assets/images/file-format.png" description="Current File Format" %} +{% include image.html url="assets/images/file-format.png" description="현재 파일 형식" %} -The first four bytes are the id of the first row (4 bytes because we store a `uint32_t`). It's stored in little-endian byte order, so the least significant byte comes first (01), followed by the higher-order bytes (00 00 00). We used `memcpy()` to copy bytes from our `Row` struct into the page cache, so that means the struct was laid out in memory in little-endian byte order. That's an attribute of the machine I compiled the program for. If we wanted to write a database file on my machine, then read it on a big-endian machine, we'd have to change our `serialize_row()` and `deserialize_row()` methods to always store and read bytes in the same order. +첫 4 바이트는 첫 번째 행의 id입니다. (`uint32_t` 를 사용함으로 4바이트입니다.) 리틀 엔디안 순서로 저장되어서, 최하위 바이트 (01) 가 먼저 나오고 상위 바이트 (00 00 00) 가 따라오게 됩니다. `Row` 구조체를 페이지 캐시로 복사할 때 `memcpy()` 를 사용하였습니다. 따라서 구조체는 리틀 엔디안 순서로 메모리에 저장됩니다. 이것은 필자가 프로그램 컴파일에 사용한 머신의 특성입니다. 만약 필자의 머신에서 작성된 데이터베이스 파일을 다른 빅 엔디안 머신에서 읽으려면, `serialize_row()` 와 `deserialize_row()` 함수가 항상 동일한 방식의 저장 방식을 사용하도록 수정해야 합니다. -The next 33 bytes store the username as a null-terminated string. Apparently "cstack" in ASCII hexadecimal is `63 73 74 61 63 6b`, followed by a null character (`00`). The rest of the 33 bytes are unused. +다음 33 바이트는 사용자 이름을 null 종료 문자열로 저장합니다. 보이는 것처럼, "cstack" 은 ASCII 16진수 값으로 `63 73 74 61 63 6b` 이며 null 문자 (`00`) 가 따라옵니다. 33 바이트 중 나머지는 사용되지 않습니다. -The next 256 bytes store the email in the same way. Here we can see some random junk after the terminating null character. This is most likely due to uninitialized memory in our `Row` struct. We copy the entire 256-byte email buffer into the file, including any bytes after the end of the string. Whatever was in memory when we allocated that struct is still there. But since we use a terminating null character, it has no effect on behavior. +다음 256 바이트는 동일한 방식으로 저장된 이메일 정보입니다. 여기를 보면, null 문자 이후에 랜덤 한 쓰레기 값을 볼 수 있습니다. 이는 메모리에서 `Row` 구조체를 초기화하지 않아 발생합니다. 우리는 문자열이 아닌 값들도 함께 포함된 256 바이트의 이메일 버퍼 전체를 파일에 복사한 것입니다. 따라서, 구조체 할당 전에 있던 무언가의 값이 그대로 기록된 것입니다. 하지만 null 종료 문자를 사용하기 때문에 작동에 영향을 미치지는 않았습니다. -**NOTE**: If we wanted to ensure that all bytes are initialized, it would -suffice to use `strncpy` instead of `memcpy` while copying the `username` -and `email` fields of rows in `serialize_row`, like so: +**참고**: 모든 바이트를 초기화하려면, `serialize_row` 에서 `username` 과 `email` 필드를 복사할 때 `memcpy` 대신 `strncpy` 를 사용하면 됩니다. ```diff void serialize_row(Row* source, void* destination) { @@ -310,15 +308,15 @@ and `email` fields of rows in `serialize_row`, like so: } ``` -## Conclusion +## 결론 -Alright! We've got persistence. It's not the greatest. For example if you kill the program without typing `.exit`, you lose your changes. Additionally, we're writing all pages back to disk, even pages that haven't changed since we read them from disk. These are issues we can address later. +좋습니다! 이제 지속성을 갖게 되었습니다. 하지만 훌륭하진 않습니다. 예를 들어 `.exit` 를 입력하지 않고 프로그램을 종료하는 경우 변경 내용이 손실됩니다. 또한 디스크에서 읽은 이후 변경되지 않은 페이지까지 디스크에 저장하고 있습니다. 이러한 문제들은 차후에 다루겠습니다. -Next time we'll introduce cursors, which should make it easier to implement the B-tree. +다음 장에서는 커서를 소개하겠습니다. 커서를 통해 B-트리를 쉽게 구현할 수 있을 것입니다. -Until then! +다음 장에서 뵙겠습니다! -## Complete Diff +## 변경된 부분 ```diff +#include +#include @@ -359,11 +357,11 @@ Until then! + } + + if (pager->pages[page_num] == NULL) { -+ // Cache miss. Allocate memory and load from file. ++ // 캐시 미스. 메모리를 할당하고 파일에서 읽어옵니다. + void* page = malloc(PAGE_SIZE); + uint32_t num_pages = pager->file_length / PAGE_SIZE; + -+ // We might save a partial page at the end of the file ++ // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. + if (pager->file_length % PAGE_SIZE) { + num_pages += 1; + } @@ -387,7 +385,7 @@ Until then! uint32_t page_num = row_num / ROWS_PER_PAGE; - void *page = table->pages[page_num]; - if (page == NULL) { -- // Allocate memory only when we try to access page +- // 페이지에 접근하는 경우 메모리 할당 - page = table->pages[page_num] = malloc(PAGE_SIZE); - } + void *page = get_page(table->pager, page_num); @@ -401,10 +399,10 @@ Until then! - table->num_rows = 0; +Pager* pager_open(const char* filename) { + int fd = open(filename, -+ O_RDWR | // Read/Write mode -+ O_CREAT, // Create file if it does not exist -+ S_IWUSR | // User write permission -+ S_IRUSR // User read permission ++ O_RDWR | // 읽기/쓰기 모드 ++ O_CREAT, // 파일이 존재하지 않으면 파일 생성 ++ S_IWUSR | // 사용자 쓰기 권한 ++ S_IRUSR // 사용자 읽기 권한 + ); + + if (fd == -1) { @@ -485,8 +483,8 @@ Until then! + pager->pages[i] = NULL; + } + -+ // There may be a partial page to write to the end of the file -+ // This should not be needed after we switch to a B-tree ++ // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. ++ // B-트리로 전환하면 이 작업은 필요하지 않게 됩니다. + uint32_t num_additional_rows = table->num_rows % ROWS_PER_PAGE; + if (num_additional_rows > 0) { + uint32_t page_num = num_full_pages; @@ -548,7 +546,7 @@ Until then! print_prompt(); ``` -And the diff to our tests: +그리고 변경된 테스트들입니다. ```diff describe 'database' do + before do diff --git a/_parts/part6.md b/_parts/part6_kor.md similarity index 58% rename from _parts/part6.md rename to _parts/part6_kor.md index ee3da27..d2f8c67 100644 --- a/_parts/part6.md +++ b/_parts/part6_kor.md @@ -1,40 +1,40 @@ --- -title: Part 6 - The Cursor Abstraction +title: 제6 장 - 커서 추상화 date: 2017-09-10 --- -This should be a shorter part than the last one. We're just going to refactor a bit to make it easier to start the B-Tree implementation. +이번 장은 지난 장 보다 분량이 적습니다. B-트리 구현을 쉽게 하기 위해 작은 개선 작업을 진행합니다. -We're going to add a `Cursor` object which represents a location in the table. Things you might want to do with cursors: +이번 장에서는 테이블의 위치를 표현하는 `Cursor` 객체를 추가하게 됩니다. 커서를 통해 수행하고자 하는 작업은 다음과 같습니다. -- Create a cursor at the beginning of the table -- Create a cursor at the end of the table -- Access the row the cursor is pointing to -- Advance the cursor to the next row +- 테이블의 시작을 가리키는 커서 생성 +- 테이블의 끝을 가리키는 커서 생성 +- 커서가 가리키는 행 접근 +- 커서를 다음 행으로 이동 -Those are the behaviors we're going to implement now. Later, we will also want to: +이러한 동작들을 지금부터 구현해 보겠습니다. 차후에는 다음과 같은 동작도 필요할 것입니다. -- Delete the row pointed to by a cursor -- Modify the row pointed to by a cursor -- Search a table for a given ID, and create a cursor pointing to the row with that ID +- 커서가 가리키는 행 삭제 +- 커서가 가리키는 행 수정 +- 주어진 ID를 통해 테이블을 검색하고 해당 행을 가리키는 커서 생성 -Without further ado, here's the `Cursor` type: +거두 절미하고 바로 시작하겠습니다. `Cursor` 데이터 타입은 다음과 같습니다. ```diff +typedef struct { + Table* table; + uint32_t row_num; -+ bool end_of_table; // Indicates a position one past the last element ++ bool end_of_table; // 마지막 행의 다음 위치를 가리키고 있음을 나타냅니다. +} Cursor; ``` -Given our current table data structure, all you need to identify a location in a table is the row number. +현재 테이블 데이터 구조를 고려하여, 테이블에서 위치를 식별하는 데 필요한 행번호를 멤버 변수로 갖습니다. -A cursor also has a reference to the table it's part of (so our cursor functions can take just the cursor as a parameter). +또한 테이블에 대한 참조를 멤버 변수로 갖습니다. (따라서 커서 관련 함수들이 단지 커서만을 매개변수로 받을 수 있게 됩니다.) -Finally, it has a boolean called `end_of_table`. This is so we can represent a position past the end of the table (which is somewhere we may want to insert a row). +마지막으로, 불 형식의 `end_of_table` 을 갖습니다. 커서가 마지막 행의 다음 위치를 가리키고 있음을 나타냅니다. (표의 마지막 행의 다음 위치는 행이 삽입될 곳입니다.) -`table_start()` and `table_end()` create new cursors: +`table_start()` 와 `table_end()` 는 새로운 커서를 생성합니다. ```diff +Cursor* table_start(Table* table) { @@ -56,7 +56,7 @@ Finally, it has a boolean called `end_of_table`. This is so we can represent a p +} ``` -Our `row_slot()` function will become `cursor_value()`, which returns a pointer to the position described by the cursor: +`row_slot()` 함수는 `cursor_value()` 로 변경되며, 이 함수는 커서가 갖고 있는 정보를 통해 행에 위치를 포인터로 반환합니다. ```diff -void* row_slot(Table* table, uint32_t row_num) { @@ -71,7 +71,7 @@ Our `row_slot()` function will become `cursor_value()`, which returns a pointer } ``` -Advancing the cursor in our current table structure is as simple as incrementing the row number. This will be a bit more complicated in a B-tree. +현재 우리의 테이블 구조에서는 간단히 행 번호를 증가시켜 커서를 이동할 수 있습니다. B-트리에서 이 작업은 좀 더 복잡해질 것입니다. ```diff +void cursor_advance(Cursor* cursor) { @@ -82,7 +82,7 @@ Advancing the cursor in our current table structure is as simple as incrementing +} ``` -Finally we can change our "virtual machine" methods to use the cursor abstraction. When inserting a row, we open a cursor at the end of table, write to that cursor location, then close the cursor. +마지막으로 "가상 머신" 함수들이 커서를 사용하도록 변경합니다. 행 삽입 시에는, 테이블 끝을 가리키는 커서를 생성하고 해당 위치에 행을 삽입한 후, 커서를 닫습니다. ```diff Row* row_to_insert = &(statement->row_to_insert); @@ -98,7 +98,7 @@ Finally we can change our "virtual machine" methods to use the cursor abstractio } ``` -When selecting all rows in the table, we open a cursor at the start of the table, print the row, then advance the cursor to the next row. Repeat until we've reached the end of the table. +테이블의 모든 행을 대상으로 SELECT 작업을 수행하는 경우, 테이블 시작 커서를 생성하고, 행을 출력한 후 커서를 다음 행으로 이동시킵니다. 이 작업을 테이블 끝에 도달할 때까지 반복 수행합니다. ```diff ExecuteResult execute_select(Statement* statement, Table* table) { @@ -119,9 +119,9 @@ When selecting all rows in the table, we open a cursor at the start of the table } ``` -Alright, that's it! Like I said, this was a shorter refactor that should help us as we rewrite our table data structure into a B-Tree. `execute_select()` and `execute_insert()` can interact with the table entirely through the cursor without assuming anything about how the table is stored. +좋습니다. 시작에서 말했듯이, 굉장히 짧은 개선 작업입니다. 이 작업은 테이블 구조를 B-트리로 변경할 때 큰 도움이 될 것입니다. 개선 작업을 통해, `execute_select()` 와 `execute_insert()` 는 테이블 저장 방식을 알지 못해도 커서를 통해 테이블과 상호작용할 수 있게 되었습니다. -Here's the complete diff to this part: +지금까지 변경된 부분은 다음과 같습니다. ```diff @@ -78,6 +78,13 @@ struct { } Table; @@ -129,7 +129,7 @@ Here's the complete diff to this part: +typedef struct { + Table* table; + uint32_t row_num; -+ bool end_of_table; // Indicates a position one past the last element ++ bool end_of_table; // 마지막 행의 다음 위치를 가리키고 있음을 나타냅니다. +} Cursor; + void print_row(Row* row) { diff --git a/_parts/part7.md b/_parts/part7.md deleted file mode 100644 index e2478cd..0000000 --- a/_parts/part7.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Part 7 - Introduction to the B-Tree -date: 2017-09-23 ---- - -The B-Tree is the data structure SQLite uses to represent both tables and indexes, so it's a pretty central idea. This article will just introduce the data structure, so it won't have any code. - -Why is a tree a good data structure for a database? - -- Searching for a particular value is fast (logarithmic time) -- Inserting / deleting a value you've already found is fast (constant-ish time to rebalance) -- Traversing a range of values is fast (unlike a hash map) - -A B-Tree is different from a binary tree (the "B" probably stands for the inventor's name, but could also stand for "balanced"). Here's an example B-Tree: - -{% include image.html url="assets/images/B-tree.png" description="example B-Tree (https://en.wikipedia.org/wiki/File:B-tree.svg)" %} - -Unlike a binary tree, each node in a B-Tree can have more than 2 children. Each node can have up to m children, where m is called the tree's "order". To keep the tree mostly balanced, we also say nodes have to have at least m/2 children (rounded up). - -Exceptions: -- Leaf nodes have 0 children -- The root node can have fewer than m children but must have at least 2 -- If the root node is a leaf node (the only node), it still has 0 children - -The picture from above is a B-Tree, which SQLite uses to store indexes. To store tables, SQLites uses a variation called a B+ tree. - -| | B-tree | B+ tree | -|-------------------------------|----------------|---------------------| -| Pronounced | "Bee Tree" | "Bee Plus Tree" | -| Used to store | Indexes | Tables | -| Internal nodes store keys | Yes | Yes | -| Internal nodes store values | Yes | No | -| Number of children per node | Less | More | -| Internal nodes vs. leaf nodes | Same structure | Different structure | - -Until we get to implementing indexes, I'm going to talk solely about B+ trees, but I'll just refer to it as a B-tree or a btree. - -Nodes with children are called "internal" nodes. Internal nodes and leaf nodes are structured differently: - -| For an order-m tree... | Internal Node | Leaf Node | -|------------------------|-------------------------------|---------------------| -| Stores | keys and pointers to children | keys and values | -| Number of keys | up to m-1 | as many as will fit | -| Number of pointers | number of keys + 1 | none | -| Number of values | none | number of keys | -| Key purpose | used for routing | paired with value | -| Stores values? | No | Yes | - -Let's work through an example to see how a B-tree grows as you insert elements into it. To keep things simple, the tree will be order 3. That means: - -- up to 3 children per internal node -- up to 2 keys per internal node -- at least 2 children per internal node -- at least 1 key per internal node - -An empty B-tree has a single node: the root node. The root node starts as a leaf node with zero key/value pairs: - -{% include image.html url="assets/images/btree1.png" description="empty btree" %} - -If we insert a couple key/value pairs, they are stored in the leaf node in sorted order. - -{% include image.html url="assets/images/btree2.png" description="one-node btree" %} - -Let's say that the capacity of a leaf node is two key/value pairs. When we insert another, we have to split the leaf node and put half the pairs in each node. Both nodes become children of a new internal node which will now be the root node. - -{% include image.html url="assets/images/btree3.png" description="two-level btree" %} - -The internal node has 1 key and 2 pointers to child nodes. If we want to look up a key that is less than or equal to 5, we look in the left child. If we want to look up a key greater than 5, we look in the right child. - -Now let's insert the key "2". First we look up which leaf node it would be in if it was present, and we arrive at the left leaf node. The node is full, so we split the leaf node and create a new entry in the parent node. - -{% include image.html url="assets/images/btree4.png" description="four-node btree" %} - -Let's keep adding keys. 18 and 21. We get to the point where we have to split again, but there's no room in the parent node for another key/pointer pair. - -{% include image.html url="assets/images/btree5.png" description="no room in internal node" %} - -The solution is to split the root node into two internal nodes, then create new root node to be their parent. - -{% include image.html url="assets/images/btree6.png" description="three-level btree" %} - -The depth of the tree only increases when we split the root node. Every leaf node has the same depth and close to the same number of key/value pairs, so the tree remains balanced and quick to search. - -I'm going to hold off on discussion of deleting keys from the tree until after we've implemented insertion. - -When we implement this data structure, each node will correspond to one page. The root node will exist in page 0. Child pointers will simply be the page number that contains the child node. - -Next time, we start implementing the btree! diff --git a/_parts/part7_kor.md b/_parts/part7_kor.md new file mode 100644 index 0000000..fb72496 --- /dev/null +++ b/_parts/part7_kor.md @@ -0,0 +1,88 @@ +--- +title: 제7 장 - B-트리 소개 +date: 2017-09-23 +--- + +B-트리는 SQLite가 테이블과 인덱스를 표현할 때 사용하는 데이터 구조로써, 매우 핵심적인 개념입니다. 이번 장에서는 오직 데이터 구조에 대해 소개하며, 코드 관련 내용은 없습니다. + +왜 트리가 데이터베이스에 적합한 데이터 구조일까요? 답은 다음과 같습니다. + +- 특정 값을 탐색하는 속도가 빠릅니다. (로그 시간) +- 찾은 값에 대해서 삽입 / 삭제하는 것이 빠릅니다. (다시 균형을 찾는데 거의 상수 시간) +- 일정 값의 범위를 순회하는 것이 빠릅니다. (해시 맵과는 달리) + +B-트리는 이진 트리와는 다릅니다. ("B"는 고안자 이름을 뜻하지만, "balanced(균형)"의 의미도 있습니다.) B-트리의 예는 다음과 같습니다. + +{% include image.html url="assets/images/B-tree.png" description="B-트리 예 (https://en.wikipedia.org/wiki/File:B-tree.svg)" %} + +이진 트리와는 달리, B-트리의 각 노드는 두 개 이상의 자식 노드를 가질 수 있습니다. 각 노드는 최대 m 개의 자식 노드를 가지며, m 은 트리의 "차수(order)" 라고 부릅니다. 트리의 균형을 유지하기 위해, 노드들은 적어도 m/2 (반올림) 개의 자식 노드를 가져야 합니다. + +예외: +- 단말 노드는 자식 노드를 갖지 않습니다. +- 루트 노드는 m 개 보다 작은 자식을 가질 수 있지만 반드시 2개 이상이어야 합니다. +- 루트 노드가 단말 노드인 경우 (유일 노드), 자식 노드를 갖지 않습니다. + +위의 그림은 SQLite가 인덱스 저장에 사용하는 B-트리입니다. 테이블 저장에는 B+ 트리라는 변형 자료구조를 사용합니다. + +| | B-트리 | B+ 트리 | +|-------------------------------|----------------|---------------------| +| 발음 | "비 트리" | "비 플러스 트리" | +| 사용하여 저장하는 것 | 인덱스 | 테이블 | +| 내부 노드의 키 저장 유무 | 예 | 예 | +| 내부 노드의 값 저장 유무 | 예 | 아니오 | +| 각 노드 별 자식 노드 수 | 적음 | 많음 | +| 내부 노드 vs. 단말 노드 | 동일 구조 | 다른 구조 | + +인덱스를 구현하기 전까지는 B+ 트리에 대해서만 이야기하지만, 편의상 B-트리로 부르겠습니다. + +자식을 갖는 노드를 "내부(internal)" 노드라고 합니다. 내부 노드와 단말 노드는 다른 구조를 갖습니다. + +| m 차수 트리의 경우... | 내부 노드(Internal Node) | 단말 노드(Leaf Node)| +|------------------------|-------------------------------|---------------------| +| 저장하는 것 | 키와 자식 노드 포인터 | 키와 값 | +| 키의 개수 | 최대 m-1 개 | 저장할 수 있는 만큼 | +| 포인터의 개수 | 키의 개수 + 1 개 | 없음 | +| 값의 개수 | 없음 | 키의 개수 | +| 키의 목적 | 탐색에 사용 | 값과 짝을 이룸 | +| 값 저장 유무 | 아니오 | 예 | + +예제를 통해 원소가 삽입될 때 B 트리가 어떻게 자라는지 보겠습니다. 간단한 설명을 위해 트리의 차수는 3으로 설정하겠습니다. 차수가 3인 트리는 다음과 같습니다. + +- 내부 노드는 최대 3개의 자식 노드를 갖습니다. +- 내부 노드는 최대 2개의 키를 갖습니다. +- 내부 노드는 적어도 2개의 자식 노드를 갖습니다. +- 내부 노드는 적어도 1개 이상의 키를 갖습니다. + +빈 B-트리는 루트 노드라는 단일 노드를 갖습니다. 루트 노드는 키/값 쌍의 개수가 0인 단말 노드로 시작합니다. + +{% include image.html url="assets/images/btree1.png" description="빈 B-트리" %} + +두 개의 키/값 쌍을 삽입하면, 정렬된 순서대로 단말 노드에 저장됩니다. + +{% include image.html url="assets/images/btree2.png" description="단일 노드 B-트리" %} + +단말 노드의 용량이 두 개의 키/값 쌍이라고 해보겠습니다. 여기서 또 다른 원소를 삽입하는 경우, 단말 노드를 분할하고 각 노드로 키/값 쌍을 절반 씩 나눕니다. 분할로 생성된 두 개의 노드는 새로운 내부 노드의 자식이 되고, 내부 노드는 루트 노드가 됩니다. + +{% include image.html url="assets/images/btree3.png" description="레벨 2 B-트리" %} + +내부 노드에는 키 1개와 자식 노드에 대한 2개의 포인터가 있습니다. 만약 5보다 작거나 같은 키를 탐색하는 경우, 왼쪽 자식 노드를 탐색합니다. 반대로 5보다 큰 키를 탐색하는 경우 오른쪽 자식 노드를 탐색합니다. + +이제 "2"번 키를 삽입해보겠습니다. 먼저 키가 존재한다면 위치해 있을 단말 노드를 찾기 시작합니다. 그렇게 왼쪽 단말 노드에 도착하게 됩니다. 해당 노드가 가득 찼으므로, 리프 노드를 분할하고 부모 노드에 새로운 가지를 만듭니다. + +{% include image.html url="assets/images/btree4.png" description="4개 노드를 갖는 B-트리" %} + +계속해서 키를 추가하겠습니다. 추가할 키는 18과 21번입니다. 다시 분할이 필요한 지점에 생겼습니다. 하지만 부모 노드에 다른 키/포인터 쌍을 위한 공간이 없습니다. + +{% include image.html url="assets/images/btree5.png" description="단말 노드가 꽉찬 경우" %} + +해결 방법은 루트 노드를 두 개의 내부 노드로 분할하고, 내부 노드의 부모 노드로 새로운 루트 노드를 생성하는 것입니다. + +{% include image.html url="assets/images/btree6.png" description="레벨 3 B-트리" %} + +B-트리의 깊이는 루트 노드를 분할할 때만 증가합니다. 이를 통해, 모든 단말 노드는 같은 깊이와 비슷한 수의 키/값 쌍을 갖습니다. 따라서, 트리는 균형을 유지하며 빠른 탐색을 가능케 합니다. + +B-트리에서 키를 삭제하는 것은 삽입을 구현한 후 다루도록 하겠습니다. + +B-트리 데이터 구조를 구현하면 각 노드는 한 페이지에 해당됩니다. 루트 노드는 0번 페이지에 해당됩니다. 자식 노드 포인터는 자식 노드에 해당하는 페이지 번호를 저장하는 것으로 간단하게 구현될 것입니다. + +다음 장에서 B-트리 구현을 시작하겠습니다! diff --git a/_parts/part8.md b/_parts/part8_kor.md similarity index 70% rename from _parts/part8.md rename to _parts/part8_kor.md index 1228a01..3966fc2 100644 --- a/_parts/part8.md +++ b/_parts/part8_kor.md @@ -1,41 +1,41 @@ --- -title: Part 8 - B-Tree Leaf Node Format +title: 제8 장 - B-트리 단말 노드 형식 date: 2017-09-25 --- -We're changing the format of our table from an unsorted array of rows to a B-Tree. This is a pretty big change that is going to take multiple articles to implement. By the end of this article, we'll define the layout of a leaf node and support inserting key/value pairs into a single-node tree. But first, let's recap the reasons for switching to a tree structure. +정렬되지 않은 행 배열에서 B-트리로 테이블의 형식을 변경하고 있습니다. 이것은 여러 장으로 다뤄질 만큼 굉장히 큰 변경 작업입니다. 이번 장을 통해, 단말 노드의 형식을 정의하고 단일 노드 트리에 대한 키/값 쌍 삽입을 구현하게 됩니다. 먼저, 트리 구조로 바꾸는 이유를 상기해보며 시작하겠습니다. -## Alternative Table Formats +## 다양한 테이블 형식들 -With the current format, each page stores only rows (no metadata) so it is pretty space efficient. Insertion is also fast because we just append to the end. However, finding a particular row can only be done by scanning the entire table. And if we want to delete a row, we have to fill in the hole by moving every row that comes after it. +현재 형식을 사용하면, 각 페이지가 행들만을 저장하기 때문에 (메타데이터는 저장하지 않음) 공간 효율성이 매우 높습니다. 삽입 연산의 경우 단지 끝에 추가하면 됨으로 매우 빠릅니다. 하지만, 특정 행을 찾기 위해서는 전체 테이블을 탐색해야 합니다. 또한 행을 삭제하는 경우 뒤에 오는 행들을 일일이 옮겨 빈틈을 채우는 작업이 필요합니다. -If we stored the table as an array, but kept rows sorted by id, we could use binary search to find a particular id. However, insertion would be slow because we would have to move a lot of rows to make space. +만약 배열로 저장하지만 id 값으로 행의 정렬을 유지한다면, 특정 id를 이진 탐색으로 찾을 수 있을 것입니다. 그러나, 삽입 시 많은 행을 이동해야 하기 때문에 매우 느릴 것입니다. -Instead, we're going with a tree structure. Each node in the tree can contain a variable number of rows, so we have to store some information in each node to keep track of how many rows it contains. Plus there is the storage overhead of all the internal nodes which don't store any rows. In exchange for a larger database file, we get fast insertion, deletion and lookup. +이러한 이유로, 트리 구조를 사용합니다. 트리의 각 노드는 행의 개수를 가변적으로 갖게 됩니다. 그래서 각 노드가 몇 개의 행을 갖고 있는지 추적하기 위한 정보를 저장해야 합니다. 추가로 모든 내부 노드들이 어떠한 행도 저장하지 않아 발생하는 공간 낭비가 있습니다. 하지만 큰 데이터베이스 파일을 갖는 대신, 빠른 삽입, 삭제 그리고 탐색 연산을 갖게 됩니다. -| | Unsorted Array of rows | Sorted Array of rows | Tree of nodes | -|---------------|------------------------|----------------------|---------------| -| Pages contain | only data | only data | metadata, primary keys, and data | -| Rows per page | more | more | fewer | -| Insertion | O(1) | O(n) | O(log(n)) | -| Deletion | O(n) | O(n) | O(log(n)) | -| Lookup by id | O(n) | O(log(n)) | O(log(n)) | +| | 정렬되지 않은 행 배열 | 정렬된 행 배열 | 노드들의 트리 | +|------------------|------------------------|----------------------|--------------------------| +| 페이지가 갖는 값 | 오직 데이터 | 오직 데이터 | 메타데이터, 주키, 데이터 | +| 페이지 별 행 | 많음 | 많음 | 적음 | +| 삽입 | O(1) | O(n) | O(log(n)) | +| 삭제 | O(n) | O(n) | O(log(n)) | +| id를 통한 탐색 | O(n) | O(log(n)) | O(log(n)) | -## Node Header Format +## 노드 헤더 형식 -Leaf nodes and internal nodes have different layouts. Let's make an enum to keep track of node type: +단말 노드와 내부 노드는 다른 형식을 갖습니다. 노드 유형을 관리하기 위해 열거형을 만들겠습니다. ```diff +typedef enum { NODE_INTERNAL, NODE_LEAF } NodeType; ``` -Each node will correspond to one page. Internal nodes will point to their children by storing the page number that stores the child. The btree asks the pager for a particular page number and gets back a pointer into the page cache. Pages are stored in the database file one after the other in order of page number. +각 노드는 하나의 페이지에 해당합니다. 내부 노드는 자식 노드에 해당하는 페이지 번호를 저장하여 자식 노드를 가리킵니다. B-트리는 페이저에 특정 페이지 번호를 요청하고, 페이지 캐시 내 해당 페이지의 포인터를 얻습니다. 페이지는 페이지 번호 순서대로 데이터베이스 파일에 차례로 저장됩니다. -Nodes need to store some metadata in a header at the beginning of the page. Every node will store what type of node it is, whether or not it is the root node, and a pointer to its parent (to allow finding a node's siblings). I define constants for the size and offset of every header field: +노드는 페이지 시작 부분에 있는 헤더에 일부 메타데이터를 저장해야 합니다. 모든 노드는 노드 유형, 루트 노드 여부, 그리고 부모 노드에 대한 포인터를 갖습니다. (형제 노드를 찾기 위해서 사용됩니다.) 다음과 같이 헤더 필드의 크기와 오프셋을 상수로 정의합니다. ```diff +/* -+ * Common Node Header Layout ++ * 공통 노드 헤더 형식 + */ +const uint32_t NODE_TYPE_SIZE = sizeof(uint8_t); +const uint32_t NODE_TYPE_OFFSET = 0; @@ -47,13 +47,13 @@ Nodes need to store some metadata in a header at the beginning of the page. Ever + NODE_TYPE_SIZE + IS_ROOT_SIZE + PARENT_POINTER_SIZE; ``` -## Leaf Node Format +## 단말 노드 형식 -In addition to these common header fields, leaf nodes need to store how many "cells" they contain. A cell is a key/value pair. +공통 헤더 필드 외에도 단말 노드는 갖고 있는 "셀" 수를 정의해야 합니다. 셀은 키/값 쌍을 말합니다. ```diff +/* -+ * Leaf Node Header Layout ++ * 단말 노드 헤더 형식 + */ +const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t); +const uint32_t LEAF_NODE_NUM_CELLS_OFFSET = COMMON_NODE_HEADER_SIZE; @@ -61,11 +61,11 @@ In addition to these common header fields, leaf nodes need to store how many "ce + COMMON_NODE_HEADER_SIZE + LEAF_NODE_NUM_CELLS_SIZE; ``` -The body of a leaf node is an array of cells. Each cell is a key followed by a value (a serialized row). +단말 노드의 본체는 셀의 배열입니다. 각 셀은 키 뒤에 값 (직렬화된 행)을 갖습니다. ```diff +/* -+ * Leaf Node Body Layout ++ * 단말 노드 본체 형식 + */ +const uint32_t LEAF_NODE_KEY_SIZE = sizeof(uint32_t); +const uint32_t LEAF_NODE_KEY_OFFSET = 0; @@ -78,17 +78,17 @@ The body of a leaf node is an array of cells. Each cell is a key followed by a v + LEAF_NODE_SPACE_FOR_CELLS / LEAF_NODE_CELL_SIZE; ``` -Based on these constants, here's what the layout of a leaf node looks like currently: +정의한 상수들을 바탕으로, 단말 노드는 다음과 같은 형식을 갖습니다. -{% include image.html url="assets/images/leaf-node-format.png" description="Our leaf node format" %} +{% include image.html url="assets/images/leaf-node-format.png" description="우리의 단말 노드 형식" %} -It's a little space inefficient to use an entire byte per boolean value in the header, but this makes it easier to write code to access those values. +헤더에서 불 값을 하나의 바이트 공간에 저장하는 것은 비효율적입니다. 하지만 값들을 접근하는 코드를 작성하기 쉽게 만듭니다. -Also notice that there's some wasted space at the end. We store as many cells as we can after the header, but the leftover space can't hold an entire cell. We leave it empty to avoid splitting cells between nodes. +추가로 마지막에 낭비되는 공간이 있습니다. 우리는 헤더 필드 이후부터 가능한 많은 셀을 저장합니다. 하지만 하나의 셀을 저장하기에 불충분한 크기의 공간이 남게 됩니다. 필자는 하나의 셀이 두 노드로 분할되지 않도록 하기 위해 해당 공간을 빈 공간으로 남겨 두었습니다. -## Accessing Leaf Node Fields +## 단말 노드 필드 접근 -The code to access keys, values and metadata all involve pointer arithmetic using the constants we just defined. +키, 값 그리고 메타데이터에 접근하는 모든 코드는 정의한 상수를 사용한 포인터 산술 연산입니다. ```diff +uint32_t* leaf_node_num_cells(void* node) { @@ -111,11 +111,11 @@ The code to access keys, values and metadata all involve pointer arithmetic usin + ``` -These methods return a pointer to the value in question, so they can be used both as a getter and a setter. +이러한 함수들은 요청한 값의 포인터를 반환하므로, 게터와 세터로 모두 사용할 수 있습니다. -## Changes to Pager and Table Objects +## 페이저와 테이블 객체 변경 -Every node is going to take up exactly one page, even if it's not full. That means our pager no longer needs to support reading/writing partial pages. +모든 노드는 꽉 차지는 않더라도 정확히 한 페이지씩 차지하게 됩니다. 이것은 페이저가 불완전 페이지에 대한 읽기/쓰기를 고려할 필요가 없음을 의미합니다. ```diff -void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) { +void pager_flush(Pager* pager, uint32_t page_num) { @@ -149,8 +149,8 @@ Every node is going to take up exactly one page, even if it's not full. That mea pager->pages[i] = NULL; } -- // There may be a partial page to write to the end of the file -- // This should not be needed after we switch to a B-tree +- // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. +- // B-트리로 전환하면 이 작업은 필요하지 않게 됩니다. - uint32_t num_additional_rows = table->num_rows % ROWS_PER_PAGE; - if (num_additional_rows > 0) { - uint32_t page_num = num_full_pages; @@ -166,7 +166,7 @@ Every node is going to take up exactly one page, even if it's not full. That mea printf("Error closing db file.\n"); ``` -Now it makes more sense to store the number of pages in our database rather than the number of rows. The number of pages should be associated with the pager object, not the table, since it's the number of pages used by the database, not a particular table. A btree is identified by its root node page number, so the table object needs to keep track of that. +이제 데이터베이스에 행 수를 저장하기 보다 페이지 수를 저장하는 것이 더 합리적입니다. 페이지 수는 특정 테이블이 아닌 데이터베이스에서 사용하기 때문에 테이블 객체가 아닌 페이저 객체에 포함되어야 합니다. B-트리는 루트 노드 페이지 번호를 통해 식별되므로, 테이블 객체가 루트 노드 페이지 정보를 관리해야 합니다. ```diff const uint32_t PAGE_SIZE = 4096; @@ -218,9 +218,9 @@ Now it makes more sense to store the number of pages in our database rather than pager->pages[i] = NULL; ``` -## Changes to the Cursor Object +## 커서 객체 변경 -A cursor represents a position in the table. When our table was a simple array of rows, we could access a row given just the row number. Now that it's a tree, we identify a position by the page number of the node, and the cell number within that node. +커서는 테이블에서 위치를 나타냅니다. 테이블이 단순 행 배열이었을 때, 행 번호를 통해 쉽게 접근이 가능했습니다. 이제는 트리로 변경되었고, 노드의 페이지 번호와 해당 노드 내 셀 번호로 위치를 식별합니다. ```diff typedef struct { @@ -228,7 +228,7 @@ A cursor represents a position in the table. When our table was a simple array o - uint32_t row_num; + uint32_t page_num; + uint32_t cell_num; - bool end_of_table; // Indicates a position one past the last element + bool end_of_table; // 마지막 행의 다음 위치를 가리키고 있음을 나타냅니다. } Cursor; ``` @@ -292,17 +292,17 @@ A cursor represents a position in the table. When our table was a simple array o } ``` -## Insertion Into a Leaf Node +## 단말 노드 삽입 -In this article we're only going to implement enough to get a single-node tree. Recall from last article that a tree starts out as an empty leaf node: +이번 장에서는 단일 노드 트리에 대한 구현을 진행합니다. 먼저 트리가 빈 단말 노드로 시작함을 다시 떠올려 보기 바랍니다. -{% include image.html url="assets/images/btree1.png" description="empty btree" %} +{% include image.html url="assets/images/btree1.png" description="빈 B-트리" %} -Key/value pairs can be added until the leaf node is full: +키/값 쌍은 단말 노드가 가득 찰 때까지 추가될 수 있습니다. -{% include image.html url="assets/images/btree2.png" description="one-node btree" %} +{% include image.html url="assets/images/btree2.png" description="단일 노드 B-트리" %} -When we open the database for the first time, the database file will be empty, so we initialize page 0 to be an empty leaf node (the root node): +데이터베이스를 처음 열면 파일이 비어있으므로, 0번 페이지를 빈 단말 노드 (루트 노드)로 초기화합니다. ```diff Table* db_open(const char* filename) { @@ -315,7 +315,7 @@ When we open the database for the first time, the database file will be empty, s + table->root_page_num = 0; + + if (pager->num_pages == 0) { -+ // New database file. Initialize page 0 as leaf node. ++ // 새 데이터베이스 파일. 페이지 0을 단말 노드로 초기화합니다. + void* root_node = get_page(pager, 0); + initialize_leaf_node(root_node); + } @@ -324,7 +324,7 @@ When we open the database for the first time, the database file will be empty, s } ``` -Next we'll make a function for inserting a key/value pair into a leaf node. It will take a cursor as input to represent the position where the pair should be inserted. +다음으로 키/값 쌍을 단말 노드에 삽입하는 함수를 생성합니다. 쌍이 삽입될 위치를 나타내는 커서를 입력으로 받습니다. ```diff +void leaf_node_insert(Cursor* cursor, uint32_t key, Row* value) { @@ -332,13 +332,13 @@ Next we'll make a function for inserting a key/value pair into a leaf node. It w + + uint32_t num_cells = *leaf_node_num_cells(node); + if (num_cells >= LEAF_NODE_MAX_CELLS) { -+ // Node full ++ // 노드가 가득 찬 경우 + printf("Need to implement splitting a leaf node.\n"); + exit(EXIT_FAILURE); + } + + if (cursor->cell_num < num_cells) { -+ // Make room for new cell ++ // 새로운 셀을 위한 공간 생성 + for (uint32_t i = num_cells; i > cursor->cell_num; i--) { + memcpy(leaf_node_cell(node, i), leaf_node_cell(node, i - 1), + LEAF_NODE_CELL_SIZE); @@ -352,9 +352,9 @@ Next we'll make a function for inserting a key/value pair into a leaf node. It w + ``` -We haven't implemented splitting yet, so we error if the node is full. Next we shift cells one space to the right to make room for the new cell. Then we write the new key/value into the empty space. +아직 분할이 구현되지 않았으므로 노드가 가득 차면 에러를 발생합니다. 다음으로 새로운 셀의 공간을 만들기 위해 기존 셀들을 오른쪽으로 한 칸씩 이동시킵니다. 그런 다음 빈 공간에 새 키/값을 씁니다. -Since we assume the tree only has one node, our `execute_insert()` function simply needs to call this helper method: +단일 노드 트리로 가정했기 때문에 `execute_insert()` 함수는 이 헬퍼 함수를 호출하기만 하면 됩니다. ```diff ExecuteResult execute_insert(Statement* statement, Table* table) { @@ -374,13 +374,13 @@ Since we assume the tree only has one node, our `execute_insert()` function simp free(cursor); ``` -With those changes, our database should work as before! Except now it returns a "Table Full" error much sooner, since we can't split the root node yet. +변경을 통해, 데이터베이스는 전과 같이 작동합니다! 루트 노드를 아직 분할할 수 없기 때문에 중간에 "Table Full" 에러를 반환하는 것을 제외하면 말이죠. -How many rows can the leaf node hold? +그렇다면, 단말 노드는 몇 개의 행을 저장할 수 있을까요? -## Command to Print Constants +## 상수 출력 명령 -I'm adding a new meta command to print out a few constants of interest. +몇 가지 주요한 상숫값을 출력하기 위해 메타 명령을 추가합니다. ```diff +void print_constants() { @@ -405,7 +405,7 @@ I'm adding a new meta command to print out a few constants of interest. } ``` -I'm also adding a test so we get alerted when those constants change: +또한 테스트를 추가하여 상숫값이 다른 경우 경고가 발생토록 합니다. ```diff + it 'prints constants' do + script = [ @@ -427,11 +427,11 @@ I'm also adding a test so we get alerted when those constants change: + end ``` -So our table can hold 13 rows right now! +우리 테이블은 지금 13 행을 수용할 수 있습니다! -## Tree Visualization +## 트리 시각화 -To help with debugging and visualization, I'm also adding a meta command to print out a representation of the btree. +또한, 디버깅과 시각화를 돕기 위해 B-트리 형태를 출력하는 메타 명령을 추가합니다. ```diff +void print_leaf_node(void* node) { @@ -463,7 +463,7 @@ To help with debugging and visualization, I'm also adding a meta command to prin } ``` -And a test +그리고 테스트도 추가합니다. ```diff + it 'allows printing out the structure of a one-node btree' do @@ -488,15 +488,15 @@ And a test + end ``` -Uh oh, we're still not storing rows in sorted order. You'll notice that `execute_insert()` inserts into the leaf node at the position returned by `table_end()`. So rows are stored in the order they were inserted, just like before. +아직 행을 정렬된 순서로 저장하진 않습니다. `execute_insert()` 함수 가 `table_end()` 함수가 반환한 위치에 단말 노드를 삽입하고 있음을 주목하기 바랍니다. 따라서, 이전과 마찬가지로 삽입된 순서대로 저장됩니다. -## Next Time +## 다음 장에서 -This all might seem like a step backwards. Our database now stores fewer rows than it did before, and we're still storing rows in unsorted order. But like I said at the beginning, this is a big change and it's important to break it up into manageable steps. +퇴보한 것처럼 보일 수 있습니다. 이전 보다 적은 행을 저장하고, 여전히 행은 정렬되지 않습니다. 하지만 처음에 말했듯이, 굉장히 큰 변경 작업으로써 다루기 쉬운 단계들로 나눠 접근 하는 것이 중요합니다. -Next time, we'll implement finding a record by primary key, and start storing rows in sorted order. +다음 장에서는, 기본 키를 통한 레코드 탐색을 구현하며 정렬된 순서대로 행을 저장할 것입니다. -## Complete Diff +## 변경된 부분 ```diff @@ -62,29 +62,101 @@ const uint32_t ROW_SIZE = ID_SIZE + USERNAME_SIZE + EMAIL_SIZE; @@ -524,13 +524,13 @@ Next time, we'll implement finding a record by primary key, and start storing ro - uint32_t row_num; + uint32_t page_num; + uint32_t cell_num; - bool end_of_table; // Indicates a position one past the last element + bool end_of_table; // 마지막 행의 다음 위치를 가리키고 있음을 나타냅니다. } Cursor; +typedef enum { NODE_INTERNAL, NODE_LEAF } NodeType; + +/* -+ * Common Node Header Layout ++ * 공통 노드 헤더 형식 + */ +const uint32_t NODE_TYPE_SIZE = sizeof(uint8_t); +const uint32_t NODE_TYPE_OFFSET = 0; @@ -542,7 +542,7 @@ Next time, we'll implement finding a record by primary key, and start storing ro + NODE_TYPE_SIZE + IS_ROOT_SIZE + PARENT_POINTER_SIZE; + +/* -+ * Leaf Node Header Layout ++ * 단말 노드 헤더 형식 + */ +const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t); +const uint32_t LEAF_NODE_NUM_CELLS_OFFSET = COMMON_NODE_HEADER_SIZE; @@ -550,7 +550,7 @@ Next time, we'll implement finding a record by primary key, and start storing ro + COMMON_NODE_HEADER_SIZE + LEAF_NODE_NUM_CELLS_SIZE; + +/* -+ * Leaf Node Body Layout ++ * 단말 노드 본체 형식 + */ +const uint32_t LEAF_NODE_KEY_SIZE = sizeof(uint32_t); +const uint32_t LEAF_NODE_KEY_OFFSET = 0; @@ -697,7 +697,7 @@ Next time, we'll implement finding a record by primary key, and start storing ro + table->root_page_num = 0; + + if (pager->num_pages == 0) { -+ // New database file. Initialize page 0 as leaf node. ++ // 새 데이터베이스 파일. 페이지 0을 단말 노드로 초기화합니다. + void* root_node = get_page(pager, 0); + initialize_leaf_node(root_node); + } @@ -741,8 +741,8 @@ Next time, we'll implement finding a record by primary key, and start storing ro pager->pages[i] = NULL; } -- // There may be a partial page to write to the end of the file -- // This should not be needed after we switch to a B-tree +- // 파일의 끝에 불완전한 페이지를 저장할 수도 있습니다. +- // B-트리로 전환하면 이 작업은 필요하지 않게 됩니다. - uint32_t num_additional_rows = table->num_rows % ROWS_PER_PAGE; - if (num_additional_rows > 0) { - uint32_t page_num = num_full_pages; @@ -780,13 +780,13 @@ Next time, we'll implement finding a record by primary key, and start storing ro + + uint32_t num_cells = *leaf_node_num_cells(node); + if (num_cells >= LEAF_NODE_MAX_CELLS) { -+ // Node full ++ // 노드가 가득 찬 경우 + printf("Need to implement splitting a leaf node.\n"); + exit(EXIT_FAILURE); + } + + if (cursor->cell_num < num_cells) { -+ // Make room for new cell ++ // 새로운 셀을 위한 공간 생성 + for (uint32_t i = num_cells; i > cursor->cell_num; i--) { + memcpy(leaf_node_cell(node, i), leaf_node_cell(node, i - 1), + LEAF_NODE_CELL_SIZE); diff --git a/_parts/part9.md b/_parts/part9_kor.md similarity index 69% rename from _parts/part9.md rename to _parts/part9_kor.md index cf1be57..c83ab75 100644 --- a/_parts/part9.md +++ b/_parts/part9_kor.md @@ -1,11 +1,11 @@ --- -title: Part 9 - Binary Search and Duplicate Keys +title: 제9 장 - 이진 탐색 및 중복 키 date: 2017-10-01 --- -Last time we noted that we're still storing keys in unsorted order. We're going to fix that problem, plus detect and reject duplicate keys. +지난 장에서 여전히 키가 정렬되지 않은 순서대로 저장되고 있음을 확인했습니다. 이번 장에서는 그 문제를 해결하며, 추가로 중복된 키를 검사하고 거부하도록 합니다. -Right now, our `execute_insert()` function always chooses to insert at the end of the table. Instead, we should search the table for the correct place to insert, then insert there. If the key already exists there, return an error. +현재, `execute_insert()` 함수는 항상 테이블 끝에 삽입을 수행합니다. 끝에 삽입하는 대신에, 삽입할 올바른 위치를 찾고, 해당 위치에 삽입하도록 해야 합니다. 만약 키가 이미 있는 경우 에러를 반환해야 합니다. ```diff ExecuteResult execute_insert(Statement* statement, Table* table) { @@ -31,7 +31,7 @@ ExecuteResult execute_insert(Statement* statement, Table* table) { leaf_node_insert(cursor, row_to_insert->id, row_to_insert); ``` -We don't need the `table_end()` function anymore. +더 이상 `table_end()` 함수는 필요하지 않습니다. ```diff -Cursor* table_end(Table* table) { @@ -48,13 +48,13 @@ We don't need the `table_end()` function anymore. -} ``` -We'll replace it with a method that searches the tree for a given key. +주어진 키로 트리를 탐색하는 함수로 대체합니다. ```diff +/* -+Return the position of the given key. -+If the key is not present, return the position -+where it should be inserted ++주어진 키를 갖는 노드의 위치를 반환합니다. ++키를 갖는 노드가 없는 경우, ++삽입될 위치를 반환합니다. +*/ +Cursor* table_find(Table* table, uint32_t key) { + uint32_t root_page_num = table->root_page_num; @@ -69,7 +69,7 @@ We'll replace it with a method that searches the tree for a given key. +} ``` -I'm stubbing out the branch for internal nodes because we haven't implemented internal nodes yet. We can search the leaf node with binary search. +아직 내부 노드를 구현하지 않았기 때문에 내부 노드를 위한 분기문은 스텁을 사용합니다. 리프 노드에 대해서는 이진 탐색을 수행합니다. ```diff +Cursor* leaf_node_find(Table* table, uint32_t page_num, uint32_t key) { @@ -80,7 +80,7 @@ I'm stubbing out the branch for internal nodes because we haven't implemented in + cursor->table = table; + cursor->page_num = page_num; + -+ // Binary search ++ // 이진 탐색 + uint32_t min_index = 0; + uint32_t one_past_max_index = num_cells; + while (one_past_max_index != min_index) { @@ -102,12 +102,12 @@ I'm stubbing out the branch for internal nodes because we haven't implemented in +} ``` -This will either return -- the position of the key, -- the position of another key that we'll need to move if we want to insert the new key, or -- the position one past the last key +이 함수는 다음 값들 중 하나를 반환합니다. +- 키의 위치 +- 새로운 키 삽입 시 이동이 필요한 또 다른 키의 위치 +- 마지막 키 다음 위치 -Since we're now checking node type, we need functions to get and set that value in a node. +이제 노드의 유형을 확인하기 때문에, 노드에서 유형 값을 가져오고 설정할 수 있는 함수가 필요합니다. ```diff +NodeType get_node_type(void* node) { @@ -121,9 +121,9 @@ Since we're now checking node type, we need functions to get and set that value +} ``` -We have to cast to `uint8_t` first to ensure it's serialized as a single byte. +단일 바이트로 저장되도록, `uint8_t` 로 먼저 캐스팅해야 합니다. -We also need to initialize node type. +또한 노드 유형을 초기화 합니다. ```diff -void initialize_leaf_node(void* node) { *leaf_node_num_cells(node) = 0; } @@ -133,7 +133,7 @@ We also need to initialize node type. +} ``` -Lastly, we need to make and handle a new error code. +마지막으로, 새로운 에러 코드를 만들어 처리합니다. ```diff -enum ExecuteResult_t { EXECUTE_SUCCESS, EXECUTE_TABLE_FULL }; @@ -156,7 +156,7 @@ Lastly, we need to make and handle a new error code. break; ``` -With these changes, our test can change to check for sorted order: +변경 작업으로, 테스트가 정렬된 순서를 확인하도록 변경합니다. ```diff "db > Executed.", @@ -173,7 +173,7 @@ With these changes, our test can change to check for sorted order: end ``` -And we can add a new test for duplicate keys: +그리고 키 중복 상황에 대한 테스트를 추가합니다. ```diff + it 'prints an error message if there is a duplicate id' do @@ -194,4 +194,4 @@ And we can add a new test for duplicate keys: + end ``` -That's it! Next up: implement splitting leaf nodes and creating internal nodes. +끝났습니다! 다음 장: 단말 노드 분할 및 새 내부 노드 생성 구현 diff --git a/index.md b/index.md deleted file mode 100644 index 8a23ba0..0000000 --- a/index.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: How Does a Database Work? ---- - -- What format is data saved in? (in memory and on disk) -- When does it move from memory to disk? -- Why can there only be one primary key per table? -- How does rolling back a transaction work? -- How are indexes formatted? -- When and how does a full table scan happen? -- What format is a prepared statement saved in? - -In short, how does a database **work**? - -I'm building a clone of [sqlite](https://www.sqlite.org/arch.html) from scratch in C in order to understand, and I'm going to document my process as I go. - -# Table of Contents -{% for part in site.parts %}- [{{part.title}}]({{site.baseurl}}{{part.url}}) -{% endfor %} - -> "What I cannot create, I do not understand." -- [Richard Feynman](https://en.m.wikiquote.org/wiki/Richard_Feynman) - -{% include image.html url="assets/images/arch2.gif" description="sqlite architecture (https://www.sqlite.org/arch.html)" %} \ No newline at end of file diff --git a/index_kor.md b/index_kor.md new file mode 100644 index 0000000..a522453 --- /dev/null +++ b/index_kor.md @@ -0,0 +1,23 @@ +--- +title: 데이터베이스는 어떻게 작동하는가? +--- + +- 데이터가 어떠한 형식으로 저장되는가? (메모리와 디스크에) +- 언제 메모리에서 디스크로 옮겨지는가? +- 왜 테이블마다 하나의 주키만을 가질 수 있는가? +- 어떻게 트랜잭션 롤백이 수행되는가? +- 인덱스는 어떻게 구성되는가? +- 전체 테이블 탐색(full table scan) 작업은 언제, 어떻게 수행되는가? +- 준비된 문장(prepared statement)은 어떠한 형식으로 저장되는가? + +즉, 데이터베이스는 어떻게 **작동** 하는가? + +필자는 데이터베이스를 이해하기 위해, C언어로 sqlite를 바닥부터 본뜨는 작업을 진행하며, 모든 진행 과정을 문서로 기록합니다. + +# 목차 +{% for part in site.parts %}- [{{part.title}}]({{site.baseurl}}{{part.url}}) +{% endfor %} + +> "내가 만들어낼 수 없다면, 이해하지 못한 것이다." -- [리처드 파인만](https://en.m.wikiquote.org/wiki/Richard_Feynman) + +{% include image.html url="assets/images/arch2.gif" description="sqlite 구조 (https://www.sqlite.org/arch.html)" %} \ No newline at end of file