SQL 계층
SQL 계층 (SQL Layer)
CockroachDB를 쓰면 개발자는 클러스터에 연결 문자열만 주면 바로 SQL 문장으로 작업을 시작할 수 있어요. 그런데 요청은 SQL로 들어와도 데이터는 결국 키-값(KV) 쌍으로 디스크에 저장되고 읽혀요. 그 사이를 이어주는 것이 바로 SQL 계층이에요. SQL 계층은 개발자에게 SQL API를 노출하면서, 상위 수준의 SQL 문장을 하위 키-값 저장소에 대한 읽기·쓰기 요청으로 변환해 트랜잭션 계층으로 넘겨줘요.
하위 계층들
SQL 계층은 다음과 같은 하위 계층으로 이루어져 있어요.
- SQL API — 사용자 인터페이스.
- 파서(Parser) — SQL 텍스트를 추상 구문 트리(AST)로 변환.
- 비용 기반 옵티마이저 — AST를 최적화된 논리적 쿼리 플랜으로 변환.
- 물리 플래너 — 논리적 쿼리 플랜을 클러스터의 노드 하나 이상에서 실행할 물리적 쿼리 플랜으로 변환.
- SQL 실행 엔진 — 하위 KV 저장소에 읽기·쓰기 요청을 만들어 물리적 플랜을 실행.
개요
클러스터의 각 노드는 대칭적으로 동작하므로, 개발자는 어떤 노드로든(즉 로드 밸런서와도 잘 맞게) 요청을 보낼 수 있어요. 요청을 받은 노드가 "게이트웨이 노드"가 되어 요청을 처리하고 클라이언트에 응답해요.
요청은 SQL 문장으로 도착하지만 데이터는 결국 KV 쌍으로 저장·읽히므로, SQL 계층은 SQL 문장을 KV 연산의 플랜으로 변환해 트랜잭션 계층에 넘겨줘요.
관계형 구조
개발자는 CockroachDB에 저장된 데이터를 행과 열로 이뤄진 관계형 구조로 경험해요. 행·열 집합은 테이블로, 테이블 모음은 데이터베이스로 정리돼요. CockroachDB 클러스터는 여러 데이터베이스를 담을 수 있어요. 외래 키 같은 제약 조건 같은 전형적인 관계형 기능도 제공해서, 애플리케이션 로직에 별도로 데이터 검증을 넣지 않아도 데이터베이스가 데이터의 일관된 구조를 보장하도록 신뢰할 수 있어요.
SQL API와 PostgreSQL 프로토콜
CockroachDB는 ANSI SQL 표준의 대부분을 구현해 관계형 구조를 드러내요. SQL API를 통해 개발자는 일반 SQL 데이터베이스처럼 ACID 의미론의 트랜잭션(BEGIN, COMMIT 등)에 접근할 수 있어요. SQL 쿼리는 PostgreSQL 와이어 프로토콜을 통해 클러스터에 도달해서, PostgreSQL 호환 드라이버와 ORM을 많이 지원해 애플리케이션 연결을 간단하게 만들어요.
SQL 파서, 플래너, 실행기
노드가 클라이언트로부터 SQL 요청을 받으면 문장을 파싱하고 최적화된 논리 쿼리 플랜을 만든 뒤 물리 쿼리 플랜으로 변환하고 마지막으로 물리 플랜을 실행해요.
파싱
SQL 쿼리는 지원 문법을 기술하는 yacc 파일에 대해 파싱되고, 각 쿼리의 SQL 버전은 추상 구문 트리(AST)로 변환돼요.
논리적 플래닝
AST는 다음 단계로 쿼리 플랜으로 변환돼요.
- AST를 상위 수준 논리 쿼리 플랜으로 변환. 이 과정에서 CockroachDB는 쿼리가 SQL 언어에서 유효한 문장인지, 테이블·변수 이름을 값으로 해석하는지,
0.6 + 0.4를1.0으로 대체하는 상수 접기(constant folding) 같은 불필요한 중간 계산을 제거하는지, 서브쿼리가 있을 때 중간 결과의 데이터 타입을 확정하는지 등의 **의미 분석(semantic analysis)**을 수행해요. - 논리 플랜을 항상 유효한 변환들로 단순화. 예를 들어
a BETWEEN b AND c를a >= b AND a <= c로 변환할 수 있어요. - 쿼리를 실행할 수 있는 많은 방법을 평가해 비용이 가장 적은 실행 플랜을 고르는 검색 알고리즘으로 논리 플랜을 최적화.
결과는 최적화된 논리 플랜이에요. 비용 기반 옵티마이저가 만든 논리 플랜을 보려면 EXPLAIN (OPT) 문장을 사용해요.
물리적 플래닝
물리 플래닝 단계는 range 지역성 정보를 바탕으로 어떤 노드가 쿼리 실행에 참여할지 정해요. 여기서 쿼리를 데이터가 저장된 곳 가까이에서 수행하도록 분산할지 결정해요. 구체적으로 논리 플랜을 물리적 SQL 연산자의 방향성 비순환 그래프(DAG)로 변환하는데, 이 연산자들은 EXPLAIN(DISTSQL) 문장으로 볼 수 있어요.
분산 계층이 단일 키 공간이라는 추상화를 제공하므로, SQL 계층은 어떤 range든 어떤 노드에서 읽기·쓰기 연산을 수행할 수 있어요. 덕분에 SQL 연산자는 게이트웨이 모드든 분산 모드든 동일하게 동작해요. 쿼리를 여러 노드에 분산할지 여부는 네트워크로 보내야 할 데이터 양을 추정하는 휴리스틱이 정해요. 적은 수의 행만 필요한 쿼리는 게이트웨이 노드에서 실행되고, 다른 쿼리는 여러 노드에 분산돼요.
예를 들어 쿼리가 분산되면 물리 플래닝 단계는 논리 플랜의 스캔 연산을 여러 물리적 TableReader 연산자로 나누는데, 각각이 스캔이 읽는 range를 포함하는 노드 하나씩 담당해요. 나머지 논리 연산(필터, 조인, 집계 등)은 같은 노드들에 배치돼 데이터에 최대한 가까운 곳에서 계산되도록 해요.
쿼리 실행
물리 플랜의 컴포넌트는 실행을 위해 하나 이상의 노드로 보내져요. 각 노드에서 CockroachDB는 쿼리의 일부를 계산하는 *논리 프로세서(logical processor)*를 생성해요. 노드 안이나 노드 사이의 논리 프로세서들은 *논리적 흐름(logical flow)*으로 데이터를 주고받아요. 쿼리의 결합 결과는 쿼리를 받은 첫 노드로 보내져 SQL 클라이언트로 전달돼요.
각 프로세서는 쿼리가 다루는 스칼라 값에 인코딩된 형태를 사용해요. 이는 SQL에서 쓰는 것과 다른 이진 형태라, SQL 쿼리에 나열된 값은 인코딩되어야 하고 프로세서 사이에서 통신되고 디스크에서 읽힌 데이터는 클라이언트에 보내기 전에 디코딩되어야 해요.
벡터화 쿼리 실행
벡터화 실행이 활성화되어 있으면 물리 플랜이 벡터화 실행 엔진에 처리되도록 보내져요. 벡터화 엔진은 디스크에서 테이블 데이터 배치를 읽어 행 형식에서 컬럼 형식으로 바꾸고, 이 컬럼 배치는 메모리에 저장돼 빠르게 접근할 수 있어요. 벡터화 엔진은 타입별 컬럼 배열을 빠르게 순회하는 특수한 사전 컴파일 함수를 사용해요. 입력 버퍼의 모든 컬럼을 처리한 뒤 컬럼형 출력을 다시 행 형식으로 바꿔 SQL 인터페이스에 돌려줘요.
인코딩
SQL 쿼리는 문자열로 쓰이지만 하위 계층은 주로 바이트를 다뤄요. 그래서 SQL 계층은 행 데이터를 SQL 문자열 표현에서 바이트로 변환하고, 하위 계층에서 돌아온 바이트를 클라이언트에 전달할 SQL 데이터로 변환해야 해요. 인덱스된 컬럼에서는 이 바이트 인코딩이 그 데이터 타입과 같은 정렬 순서를 보존하는 게 중요해요. CockroachDB가 결국 데이터를 정렬된 키-값 맵에 저장하므로, 데이터와 같은 순서로 바이트를 저장하면 KV 데이터를 효율적으로 스캔할 수 있어요. 반면 비인덱스 컬럼(예: PRIMARY KEY가 아닌 컬럼)은 공간을 덜 차지하지만 순서를 보존하지 않는 "값 인코딩(value encoding)"을 사용해요.
DistSQL
CockroachDB는 분산 데이터베이스이기 때문에 일부 쿼리를 위한 Distributed SQL(DistSQL) 최적화 도구를 개발했어요. 이 도구는 많은 range를 다루는 쿼리를 극적으로 빠르게 만들어줄 수 있어요.
비분산 쿼리에서는 조정 노드가 매칭되는 모든 행을 받아서 전체 데이터셋에 대해 계산을 수행해요. 하지만 DistSQL 호환 쿼리에서는 각 노드가 자기 행에 대해 계산을 수행하고, 전체 행 대신 결과만 조정 노드로 보내요. 조정 노드는 각 노드의 결과를 집계해 클라이언트에 단일 응답을 돌려줘요. 이렇게 하면 조정 노드로 가져오는 데이터 양이 극적으로 줄고, 데이터가 이미 저장된 노드에서 처리되므로 개별 노드의 저장 용량보다 큰 행 집합도 다룰 수 있어요.
스키마 변경
CockroachDB는 컬럼 추가나 보조 인덱스 생성 같은 스키마 변경을, 테이블이 스키마 변경 중에도 온라인 상태(읽기·쓰기 서빙)를 유지하는 프로토콜로 수행해요. 이 프로토콜은 클러스터의 서로 다른 노드가 비동기적으로 서로 다른 시점에 새 테이블 스키마로 전환되도록 허용해요.
스키마 변경 프로토콜은 각 스키마 변경을 원하는 효과를 낼 일련의 점진적 변경으로 분해해요. 예를 들어 보조 인덱스 추가는 시작과 끝 버전 사이에 두 개의 중간 스키마 버전이 필요해서, 인덱스가 읽기로 제공되기 전에 클러스터 전체의 쓰기에서 인덱스가 갱신되도록 보장해요. 스키마 변경 동안 데이터베이스가 일관된 상태를 유지하도록, 클러스터에서 언제나 최대 두 개의 연속 버전만 사용된다는 불변식을 강제해요. 이 방식은 Google의 Online, Asynchronous Schema Change in F1 논문에 기반해요.