← Posts

Database · Storage · Index

블록 - DB는 바이트를 모른다

4바이트를 읽으려고 8KB를 읽는다. 낭비처럼 보이는 이 단위가 뒤에 나올 거의 모든 것의 바닥이다.

목차
  1. 최소 단위가 있다
  2. 이름이 둘이다
  3. 블록 안에는 뭐가 있나
  4. 그래서 생기는 일 - 같은 블록이면 공짜다
  5. 반대쪽도 있다 - 한 바이트를 써도 블록 하나다
  6. 꽉 차면 - 쪼개거나, 안 쪼개거나
  7. 행이 블록보다 크면
  8. 크기는 왜 하필 그건가
  9. 일부러 비워둔다
  10. 정리

주문 하나의 번호가 궁금해서 4바이트짜리 컬럼 하나를 조회했는데, 디스크는 8KB를 읽었다. 2000배다. 버그가 아니라 덜 읽을 방법이 아예 없다.

여기까지가 트랜잭션부터 MVCC까지, “같이 건드리면 무엇이 깨지나”였다. 여기서부터는 조회 성능이다. 그런데 그 전에 바닥을 하나 깔아야 한다. 뒤에 나올 것 대부분이 이 위에 선다.

최소 단위가 있다

앞에서 말한 그 조회는 이렇게 생겼다.

sql
SELECT 주문번호 FROM orders WHERE id = 7;

받고 싶은 건 4바이트다. 그런데 디스크는 바이트 하나를 못 읽는다. “여기서부터 4바이트만 주세요”라는 요청 자체가 성립하지 않는다. 정해진 덩어리 단위로만 오간다.

그 단위가 여러 층에 걸쳐 있다.

단위
디스크섹터. 전통적으로 512바이트, 요즘은 4KB
운영체제보통 4KB
DB4~16KB. 여기가 우리 얘기다

DB는 아래 층 단위를 그대로 쓰지 않고 자기 단위를 따로 갖는다. 그리고 그 단위로 읽고, 쓰고, 캐시하고, 잠근다. DB가 세상을 보는 눈금이다.

이름이 둘이다

여기서 헷갈리는 게 하나 있다. 같은 것을 제품마다 다르게 부른다.

뭐라고 부르나기본 크기
Oracle블록8KB
PostgreSQL페이지8KB
MySQL (InnoDB)페이지16KB
SQL Server페이지8KB

같은 것이다. 다른 개념이 아니라 그냥 이름이 갈린 것뿐이다.

참고

이 시리즈에서는 블록이라고 부른다. 하나로 정하지 않으면 문장마다 “블록/페이지”를 밟게 되고, 그건 읽는 사람 몫이 된다. 다만 페이지 분할만 예외다. 그건 굳어진 이름이라 억지로 바꾸면 검색이 안 된다.

블록 안에는 뭐가 있나

block-anatomyblock-anatomy

헤더가 앞에 있다. 이 블록이 뭔지, 어디까지 찼는지 같은 정보다. 그리고 행들이 들어간다. 뒤에 빈 공간이 남는다.

여기서 두 가지가 나온다.

8KB를 다 쓰지 못한다. 헤더가 먹고, 빈 공간이 남는다. 실제 데이터는 그보다 적다.

행 하나는 한 블록 안에 들어간다. 이게 기본 규칙이다. 행이 블록 경계에 걸쳐 반씩 나뉘지 않는다.

그래서 생기는 일 - 같은 블록이면 공짜다

한 행을 읽어도 블록 하나를 통째로 읽는다. 그러면 그 블록에 같이 있던 나머지 행들도 이미 메모리에 와 있다. 공짜로 딸려온 것이다.

이게 이 글의 전부다. 그리고 뒤에 나올 거의 모든 것의 바닥이다.

  • 다음 글의 B-tree가 왜 그렇게 뚱뚱한지 - 어차피 블록 하나를 통째로 읽을 거면 그 블록을 값으로 꽉 채우는 게 이득이다
  • 인덱스가 왜 빠른지 - 필요한 값들이 한 블록에 모여 있어서다
  • 무작위 키가 왜 나쁜지 - 매번 다른 블록을 열어야 해서다

전부 같은 문장의 다른 얼굴이다. 같이 읽힐 것들을 같은 블록에 모아라.

반대쪽도 있다 - 한 바이트를 써도 블록 하나다

읽기만 그런 게 아니다.

8바이트를 바꾸려면 그 블록 8KB를 통째로 다시 써야 한다. 덜 쓸 방법이 없다. 읽을 때와 같은 이유다.

낭비처럼 보이는데, 조회에서는 이게 이득이었다. 쓰기에서는 그냥 손해다. 같은 성질이 방향에 따라 이득도 되고 손해도 된다.

B-tree와 LSM-tree 글에서 이 손해를 정면으로 다룬다. 그리고 그걸 못 견딘 사람들이 만든 게 LSM-tree다.

꽉 차면 - 쪼개거나, 안 쪼개거나

블록에 자리가 없는데 새로 넣어야 한다. 어떻게 되나? 그 구조가 순서를 지켜야 하느냐에 갈린다.

테이블은 순서가 없다. 어느 행이 어느 블록에 있든 상관없다. 그러니 꽉 찬 블록을 만났으면 그냥 자리 있는 다른 블록에 넣는다. 쪼갤 이유가 없다.

인덱스는 정렬돼 있어야 한다. 그게 인덱스가 빠른 이유의 전부다. 1005는 반드시 10041006 사이에 있어야 하는데 그 블록이 꽉 찼다면, 아무 데나 던져둘 수가 없다. 블록을 둘로 쪼개고 절반씩 나눠 담은 뒤 그 사이에 끼운다. 이게 페이지 분할이다.

한 번의 삽입이 블록 여러 개를 건드리는 일이 된다. 그래서 값이 어디에 꽂히느냐가 성능을 가른다. 이건 키 설계 편에서 다시 만난다.

참고

“테이블은 안 쪼갠다”에 예외가 있다. 테이블 자체를 기본키 순으로 정렬해 저장하는 DB가 있다(MySQL InnoDB가 그렇다). 그러면 테이블이 곧 인덱스라 테이블도 쪼개진다. 규칙은 “테이블이냐 인덱스냐”가 아니라 “순서를 지켜야 하느냐” 다.

행이 블록보다 크면

TEXT 컬럼에 긴 글을 넣었다. 행 하나가 8KB를 넘는다. “행은 한 블록에 들어간다”는 규칙이 깨진다.

DB마다 방법이 다르지만 결국 둘 중 하나다. 쪼개서 여러 블록에 나눠 담거나, 큰 값만 밖으로 빼고 블록에는 가리키는 표시만 남긴다.

어느 쪽이든 그 행을 읽으려면 블록을 두 번 이상 읽는다. 한 번이면 될 걸 두 번 한다.

크기는 왜 하필 그건가

8KB냐 16KB냐는 거래다.

크면 한 번에 많이 가져온다. 훑는 조회에 유리하다. 대신 한 행만 필요할 때 버리는 게 많고, 메모리에 올릴 때도 그만큼 자리를 먹는다.

작으면 낭비가 적다. 대신 블록 개수가 늘어 관리할 게 많아지고, 큰 행이 안 들어간다.

주의

거의 항상 기본값을 그대로 쓴다. 바꾸려면 대개 DB를 다시 만들어야 하고, 바꿔서 얻는 것보다 잃는 게 크다. 이 절은 “크기를 튜닝하라”는 게 아니라 왜 이 숫자가 나왔는지 알아두라는 것이다. 느린 조회의 원인이 블록 크기인 경우는 사실상 없다.

일부러 비워둔다

블록을 100% 꽉 채우면 문제가 생긴다. 나중에 그 행이 커지면 갈 데가 없다.

주소 컬럼에 서울이라고 넣었다가 서울특별시 강남구 …로 바꾸면 행이 커진다. 블록이 꽉 차 있으면 그 행을 다른 블록으로 통째로 옮긴다. 그리고 원래 자리에는 “저기로 갔다”는 표시만 남는다. 이제 그 행을 읽으려면 두 블록을 읽는다.

그래서 DB는 블록을 채울 때 일부러 여유를 남긴다. 나중에 커질 자리를 미리 비워두는 것이다. 공간을 버리고 나중의 이사를 산다.

정리

  • 디스크는 바이트를 모른다. 최소 단위가 있고, DB는 자기 단위를 따로 갖는다. 그게 블록이다.
  • Oracle은 블록, 나머지는 대개 페이지라고 부른다. 같은 것이다. 이 시리즈는 블록으로 부른다.
  • 한 행을 읽어도 블록 하나, 한 바이트를 써도 블록 하나다. 조회에선 이게 이득이고 쓰기에선 손해다.
  • 그래서 뒤에 나올 게 전부 한 문장으로 모인다. 같이 읽힐 것들을 같은 블록에 모아라.
  • 블록이 꽉 차면 쪼개고, 행이 블록보다 크면 나눠 담고, 행이 커질 자리는 미리 비워둔다.
  • 크기는 안 건드린다. 왜 그 숫자인지만 알면 된다.
참고

다음 글은 인덱스다. B-tree가 왜 그렇게 생겼는지, 왜 이진트리가 아닌지는 사실 이 글 하나로 설명된다.

관련 글