스키마 개요

이 페이지에서는 Spanner 스키마 요구사항, 스키마를 사용하여 계층적 관계를 만드는 방법, 스키마 기능에 대해 논의합니다. 또한 상하 관계에서 테이블을 쿼리할 때 쿼리 성능을 향상시킬 수 있는 인터리브 처리된 테이블을 소개합니다.

스키마는 테이블, 뷰, 색인, 함수 등의 데이터베이스 객체를 포함하는 네임스페이스입니다. 스키마를 사용하면 객체를 구성하고, 미세 조정된 액세스 제어 권한을 적용하고, 이름 충돌을 방지할 수 있습니다. Spanner에서 각 데이터베이스에 대해 스키마를 정의해야 합니다.

또한 추가적으로 세분화를 수행하고 여러 지리적 리전에 분산된 데이터베이스 테이블에 행을 저장할 수 있습니다. 자세한 내용은 지역 파티셔닝 개요를 참조하세요.

강력한 유형 데이터

Spanner의 데이터는 강력한 유형입니다. 데이터 유형에는 스칼라 및 복합 유형이 있으며, 이는 GoogleSQL의 데이터 유형PostgreSQL 데이터 유형에 설명되어 있습니다.

기본 키 선택

Spanner 데이터베이스는 하나 이상의 테이블을 포함할 수 있습니다. 테이블은 행과 열로 구성됩니다. 테이블 스키마는 하나 이상의 테이블 열을 각 행을 고유하게 식별하는 테이블의 기본 키로 정의합니다. 기본 키는 항상 빠른 행 조회를 위해 색인으로 생성됩니다. 테이블의 기존 행을 업데이트하거나 삭제하려면 테이블에 기본 키가 있어야 합니다. 기본 키 열이 없는 테이블에는 행이 1개만 있을 수 있습니다. GoogleSQL 언어 데이터베이스만 기본 키가 없는 테이블을 포함할 수 있습니다.

애플리케이션에 이미 기본 키로 사용하기에 적합한 필드가 있는 경우가 있습니다. 예를 들어 Customers 테이블의 경우 기본 키 역할을 잘 수행하는 애플리케이션이 제공하는 CustomerId가 있을 수 있습니다. 다른 경우에는 행을 삽입할 때 기본 키를 생성해야 할 수 있습니다. 이는 일반적으로 비즈니스 의미가 없는 고유한 정수 값(서로게이트 기본 키)일 수 있습니다.

어떤 경우이든 기본 키를 선택할 때는 핫스팟을 만들지 않도록 주의해야 합니다. 예를 들어, 단조 증가하는 정수가 포함된 레코드를 키로 삽입할 경우 항상 키 공간 끝에 삽입됩니다. 이는 바람직하지 않은 방식입니다. Spanner는 키 범위를 기준으로 서버 간에 데이터를 구분하기 때문에 단일 서버에서 삽입이 이루어져서 핫스팟이 생성되기 때문입니다. 여러 서버로 부하를 분산시켜 핫스팟을 피할 수 있는 다음과 같은 방법이 있습니다.

상하 테이블 관계

Spanner에서 상하 관계를 정의하는 방법에는 테이블 인터리브 처리외래 키 등 두 가지가 있습니다.

Spanner의 테이블 인터리브 처리는 많은 상하 관계에 적합한 옵션입니다. 인터리브 처리를 사용하면 Spanner가 하위 행을 스토리지의 상위 행과 물리적으로 같은 위치에 배치합니다. 같은 위치에 배치하면 성능이 크게 향상될 수 있습니다. 예를 들어 Customers 테이블과 Invoices 테이블이 있는데 애플리케이션이 고객의 모든 인보이스를 자주 가져온다면 InvoicesCustomers의 인터리브 처리된 하위 테이블로 정의할 수 있습니다. 이렇게 하면 독립된 테이블 2개 간의 데이터 지역성 관계를 선언할 수 있습니다. 그러면 Spanner에서 하나의 Customers 행에 하나 이상의 Invoices 행을 저장하도록 지정됩니다.

하위 테이블을 상위 테이블에 인터리브 처리된 것으로 선언하는 DDL을 사용하고 상위 테이블 기본 키를 하위 테이블 복합 기본 키의 첫 번째 부분으로 포함하여 하위 테이블을 상위 테이블에 연결합니다. 인터리브 처리에 대한 자세한 내용은 이 페이지 뒷부분의 인터리브 처리된 테이블 만들기를 참조하세요.

외래 키는 보다 일반적인 상하 솔루션이며 추가 사용 사례를 해결합니다. 기본 키 열로 제한되지 않으며 테이블에는 여러 외래 키 관계가 일부 관계의 상위 요소와 다른 관계의 하위 요소 모두로 포함될 수 있습니다. 그러나 외래 키 관계가 스토리지 레이어에서 테이블의 동일한 위치를 암시하지 않습니다.

상위/하위 관계를 인터리브 처리된 테이블 또는 외래 키 중 하나로 표시하도록 선택하는 것이 좋습니다. 외래 키와 인터리브 처리된 테이블의 비교에 대한 자세한 내용은 외래 키 개요를 참조하세요.

인터리브 처리된 테이블의 기본 키

인터리브 처리의 경우 모든 테이블에 기본 키가 있어야 합니다. 한 테이블을 다른 테이블의 인터리브 처리된 하위 항목으로 선언할 경우에는 상위 항목의 기본 키에 대한 모든 구성요소를 동일한 순서로 포함하고 그리고 일반적으로 하나 이상의 추가적인 하위 테이블 열을 포함하는 복합 기본 키가 이 테이블에 있어야 합니다.

Cloud Spanner는 기본 키 값을 기준으로 정렬된 순서로 행을 저장하되, 상위 행 사이에 하위 행을 삽입합니다. 이 페이지의 뒷부분에서 인터리브 처리된 테이블 만들기의 인터리브 처리된 행 그림을 참조하세요.

요약하자면, Spanner는 관련된 표의 행을 물리적으로 같은 장소에 배치할 수 있습니다. 스키마 예시는 이 같은 물리적 배치가 어떤 모습인지 보여줍니다.

데이터베이스 분할

인터리브 처리된 상하 관계의 계층 구조는 최대 7개까지 정의할 수 있습니다. 즉, 독립된 7개의 테이블 행을 같은 장소에 배치할 수 있습니다. 테이블의 데이터 크기가 작으면 단일 Spanner 서버로도 데이터베이스 처리가 가능할 수 있습니다. 그러나 관련 테이블이 많아져서 각 서버의 리소스 한도에 도달하면 어떻게 될까요? Spanner는 분산 데이터베이스로, 데이터베이스가 증가함에 따라 '분할'이라는 단위로 데이터를 나눕니다. 분할은 독립적으로 서로 간에 이동할 수 있으며 여러 물리적 위치에 있을 수 있는 여러 서버로 할당될 수 있습니다. 분할은 연속된 행 범위를 포함합니다. 이 범위의 시작 키와 종료 키를 "분할 경계"라고 합니다. Spanner는 크기 및 부하에 따라 분할 경계를 자동으로 추가 및 삭제하고 이에 따라 데이터베이스 내 분할 개수가 바뀝니다.

부하 기반 분할

Spanner가 읽기 핫스팟을 완화하기 위해 부하 기반 분할을 수행하는 방법에 대한 예로, 데이터베이스에 테이블의 다른 모든 행보다 더 자주 읽히는 10개의 행이 있다고 가정해 보겠습니다. Spanner는 10개 행 사이에 분할 경계를 추가하여 행을 읽을 때마다 단일 서버의 리소스가 소비되지 않고 각 행이 서로 다른 서버에서 처리되도록 할 수 있습니다.

일반적으로 스키마 설계 권장사항을 따를 경우, Spanner는 핫스팟을 완화하여 인스턴스의 리소스가 포화되거나 새 분할 경계를 추가할 수 없을 때까지 일기 처리량이 몇 분마다 개선되도록 할 수 있습니다(인터리브 처리된 하위 요소가 없는 단일 행만 처리하는 분할이 있기 때문).

이름이 지정된 스키마

이름이 지정된 스키마는 비슷한 데이터를 함께 관리하는 데 도움이 됩니다. Google Cloud 콘솔에서 객체를 빠르게 찾고, 권한을 적용하고, 이름 지정 충돌을 방지하는 데 도움이 됩니다.

이름이 지정된 스키마는 다른 데이터베이스 객체와 마찬가지로 DDL을 사용하여 관리됩니다.

Spanner 이름이 지정된 스키마를 사용하면 정규화된 이름(FQN)을 사용해서 데이터를 쿼리할 수 있습니다. FQN을 사용하면 스키마 이름과 객체 이름을 조합하여 데이터베이스 객체를 식별할 수 있습니다. 예를 들어 웨어하우스 비즈니스 단위에 대해 warehouse라는 스키마를 만들 수 있습니다. 이 스키마를 사용하는 테이블에는 product, order, customer information이 있습니다. 또는 이행 비즈니스 단위를 위해 fulfillment라는 스키마를 만들 수 있습니다. 이 스키마에는 또한 product, order, customer information이라는 테이블이 포함될 수 있습니다. 첫 번째 예시에서 FQN은 warehouse.product이고 두 번째 예시에서 FQN은 fulfillment.product입니다. 이렇게 하면 여러 객체가 동일한 이름을 공유하는 상황에서 혼란을 방지할 수 있습니다.

CREATE SCHEMA DDL에서 테이블 객체에는 FQN(예: sales.customers)과 축약 이름(예: sales)이 모두 부여됩니다.

다음 데이터베이스 객체는 이름이 지정된 스키마를 지원합니다.

  • TABLE
    • CREATE
    • INTERLEAVE IN [PARENT]
    • FOREIGN KEY
    • SYNONYM
  • VIEW
  • INDEX
  • FOREIGN KEY
  • SEQUENCE

이름이 지정된 스키마에 대한 자세한 내용은 이름이 지정된 스키마 관리를 참조하세요.

이름이 지정된 스키마에 미세 조정된 액세스 제어 사용

이름이 지정된 스키마를 사용하면 스키마의 각 객체에 대해 스키마 수준의 액세스를 부여할 수 있습니다. 이는 액세스 부여 당시 존재하는 스키마 객체에 적용됩니다. 나중에 추가되는 객체에는 액세스를 따로 부여해야 합니다.

미세 조정된 액세스 제어는 테이블, 열, 행과 같이 데이터베이스 객체의 전체 그룹에 대한 액세스를 제한합니다.

자세한 내용은 이름 지정된 스키마에 미세 조정된 액세스 제어 권한 부여를 참조하세요.

스키마 예시

이 섹션의 스키마 예시는 인터리브 처리를 사용하거나 사용하지 않고 상위 및 하위 테이블을 만드는 방법과 그에 따른 데이터의 물리적 레이아웃을 보여줍니다.

상위 테이블 만들기

음악 애플리케이션을 만드는 중에 가수 데이터 행을 저장하는 표가 필요하다고 가정해 보겠습니다.

행이 5개이고 열이 4개인 가수 테이블

이 테이블은 굵은 선 왼쪽에 SingerId라는 기본 키 열이 하나 있으며 행 및 열로 구성되어 있습니다.

다음 DDL로 테이블을 정의할 수 있습니다.

GoogleSQL

CREATE TABLE Singers (
SingerId   INT64 NOT NULL PRIMARY KEY,
FirstName  STRING(1024),
LastName   STRING(1024),
SingerInfo BYTES(MAX),
);

PostgreSQL

CREATE TABLE singers (
singer_id   BIGINT PRIMARY KEY,
first_name  VARCHAR(1024),
last_name   VARCHAR(1024),
singer_info BYTEA
);

이 스키마 예시에서 다음 사항에 주목하세요.

  • Singers는 데이터베이스 계층의 루트에 있는 테이블입니다(다른 테이블의 인터리브 처리된 하위 항목으로 정의되지 않았기 때문).
  • GoogleSQL 언어 데이터베이스의 경우에는 기본 키 열이 일반적으로 NOT NULL로 주석 처리됩니다. 단, 키 열에 NULL 값을 허용하려면 이 주석을 생략할 수 있습니다. 자세한 내용은 키 열을 참조하세요.
  • 기본 키에 포함되지 않은 열을 키가 아닌 열이라고 하며 이 열에는 NOT NULL 주석이 선택적으로 추가될 수 있습니다.
  • GoogleSQL에서 STRING 또는 BYTES 유형을 사용하는 열은 필드에 저장될 수 있는 최대 유니코드 문자 수를 나타내는 길이와 함께 정의되어야 합니다. PostgreSQL varcharcharacter varying 유형은 길이 사양이 선택사항입니다. 자세한 내용은 GoogleSQL 언어 데이터베이스에 대한 스칼라 데이터 유형 및 PostgreSQL 언어 데이터베이스에 대한 PostgreSQL 데이터 유형을 참조하세요.

Singers 테이블의 실제 행 레이아웃은 어떤 모습일까요? 다음 다이어그램에서는 기본 키로 저장된 Singers 테이블의 행을 보여줍니다('Singers(1)' 다음에 'Singers(2)'가 오는 순서이며 여기서 괄호 안의 숫자는 기본 키 값임).

기본 키 순서로 저장된 테이블의 행 예시.

이전 다이어그램은 여러 서버에 할당된 분할의 데이터와 함께 Singers(3)Singers(4)로 키가 지정된 행 사이의 분할 경계 예시도 보여줍니다. 이 테이블이 커짐에 따라 Singers 데이터의 행이 여러 위치에 저장될 수 있습니다.

상하 테이블 만들기

이제 음악 애플리케이션에 각 가수의 앨범에 대한 몇 가지 기본적인 데이터를 추가하려는 경우를 가정해 보겠습니다.

행이 5개이고 열이 3개인 앨범 테이블

Albums의 기본 키는 각 앨범을 해당 가수와 연결하는 SingerIdAlbumId라는 2개의 열로 구성됩니다. 다음의 스키마 예시에서는 AlbumsSingers 테이블 모두를 데이터베이스 계층의 루트에 정의하여 이 둘을 형제 테이블로 만듭니다.

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)

GoogleSQL

CREATE TABLE Singers (
 SingerId   INT64 NOT NULL PRIMARY KEY,
 FirstName  STRING(1024),
 LastName   STRING(1024),
 SingerInfo BYTES(MAX),
);

CREATE TABLE Albums (
SingerId     INT64 NOT NULL,
AlbumId      INT64 NOT NULL,
AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

PostgreSQL

CREATE TABLE singers (
singer_id   BIGINT PRIMARY KEY,
first_name  VARCHAR(1024),
last_name   VARCHAR(1024),
singer_info BYTEA
);

CREATE TABLE albums (
singer_id     BIGINT,
album_id      BIGINT,
album_title   VARCHAR,
PRIMARY KEY (singer_id, album_id)
);

SingersAlbums 행의 실제 레이아웃은 다음 다이어그램과 같은 모습이며, 인접 기본 키로 Albums 테이블의 행이 저장된 후 인접 기본 키로 Singers의 행이 저장됩니다.

실제 행의 레이아웃

이 스키마에서 한 가지 중요한 점은 Singers 테이블과 Albums 테이블은 최상위 수준의 테이블이기 때문에 Spanner가 둘 사이에 데이터 지역성 관계를 가정하지 않는다는 점입니다. 데이터베이스가 커짐에 따라 Spanner는 모든 행 사이에 분할 경계를 추가할 수 있습니다. 즉, Albums 테이블의 행이 Singers 테이블의 행과 다른 분할에 위치할 수 있고 두 분할이 서로 간에 독립적으로 이동할 수 있습니다.

애플리케이션의 필요에 따라 Albums 데이터가 Singers 데이터와 다른 분할에 위치하도록 허용해도 좋습니다. 그러나 서로 다른 리소스 간에 읽기 및 업데이트를 조정해야 하기 때문에 성능 저하가 발생할 수 있습니다. 그러나 애플리케이션이 특정 가수의 모든 앨범에 대한 정보를 자주 검색해야 할 경우, AlbumsSingers의 인터리브 처리된 하위 테이블로 만들어서 기본 키 측정기준에 따라 두 테이블의 행을 같은 위치에 배치하는 것이 좋습니다. 다음의 예시에서 이에 대해 더 자세히 설명합니다.

인터리브 처리된 테이블 만들기

인터리브 처리된 테이블은 사용자가 다른 테이블의 인터리브 처리된 하위 항목으로 선언하여 하위 테이블의 행을 관련된 상위 행과 물리적으로 함께 저장하려는 경우에 사용하는 테이블입니다. 앞에서 언급한 것처럼 상위 테이블 기본 키는 하위 테이블 복합 기본 키의 첫 번째 부분이어야 합니다.

테이블을 교체하면 영구적으로 유지됩니다. 인터리빙은 실행취소할 수 없습니다. 대신 테이블을 다시 만들고 데이터를 테이블로 이전해야 합니다.

음악 애플리케이션을 설계하면서 앱이 Singers 행을 액세스할 때 Albums 테이블의 행에 자주 액세스해야 한다는 것을 알게 되었다고 가정해 보겠습니다. 예를 들어 Singers(1) 행에 액세스할 때 Albums(1, 1)Albums(1, 2) 행에도 액세스해야 합니다. 이 경우 SingersAlbums는 긴밀한 데이터 지역성 관계를 가져야 합니다. AlbumsSingers의 인터리브 처리된 하위 테이블로 만들어서 이와 같은 데이터 지역성 관계를 선언할 수 있습니다.

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)

다음 스키마에서 굵게 표시된 줄은 AlbumsSingers의 인터리브 처리된 테이블로 만드는 방법을 보여줍니다.

GoogleSQL

CREATE TABLE Singers (
 SingerId   INT64 NOT NULL PRIMARY KEY,
 FirstName  STRING(1024),
 LastName   STRING(1024),
 SingerInfo BYTES(MAX),
 );

CREATE TABLE Albums (
 SingerId     INT64 NOT NULL,
 AlbumId      INT64 NOT NULL,
 AlbumTitle   STRING(MAX),
 ) PRIMARY KEY (SingerId, AlbumId),
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

PostgreSQL

CREATE TABLE singers (
 singer_id   BIGINT PRIMARY KEY,
 first_name  VARCHAR(1024),
 last_name   VARCHAR(1024),
 singer_info BYTEA
 );

CREATE TABLE albums (
 singer_id     BIGINT,
 album_id      BIGINT,
 album_title   VARCHAR,
 PRIMARY KEY (singer_id, album_id)
 )
 INTERLEAVE IN PARENT singers ON DELETE CASCADE;

이 스키마에 대한 참고 사항:

  • 하위 테이블 Albums의 기본 키의 첫 번째 부분인 SingerId는 또한 해당 상위 테이블 Singers의 기본 키이기도 합니다.
  • ON DELETE CASCADE 주석은 상위 테이블의 행이 삭제될 때 해당 하위 행도 자동으로 삭제된다는 의미입니다. 그러나 하위 테이블에 이 주석이 없거나 주석이 ON DELETE NO ACTION인 경우, 하위 행을 먼저 삭제해야 상위 행을 삭제할 수 있습니다.
  • 인터리브 처리된 행은 우선 상위 테이블의 행을 기준으로 정렬된 후 상위 요소의 기본 키를 공유하는 하위 테이블의 인접 행을 기준으로 정렬됩니다. 즉, 'Singers(1)', 'Albums(1, 1)', 'Albums(1, 2)'의 순서입니다.
  • Singers 행 및 모든 해당 Albums 행의 크기가 분할 크기 한도 아래로 유지되고 이러한 Albums 행 어디에도 핫스팟이 없는 한 이 데이터베이스가 분할될 경우 각 가수와 해당 앨범 데이터 간의 데이터 지역성 관계가 유지됩니다.
  • 하위 행을 삽입하려면 먼저 상위 행이 있어야 합니다. 상위 행은 데이터베이스에 이미 존재하거나 동일 트랜잭션에서 하위 행이 삽입되기 전에 삽입될 수 있습니다.

Albums 행이 Singers 행 사이에 인터리브 처리됨

인터리브 처리된 테이블의 계층 만들기

SingersAlbums 간의 상하 관계를 추가 하위 테이블로 확장할 수 있습니다. 예를 들어 각 앨범의 트랙 목록을 저장하기 위해 Albums의 하위 테이블로 Songs라는 인터리브 처리된 테이블을 만들 수 있습니다.

행이 6개이고 열이 4개인 곡 테이블

Songs는 해당 계층에서 상위 수준에 있는 테이블의 모든 기본 키를 포함하는 기본 키를 가져야 합니다(즉, SingerIdAlbumId).

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

GoogleSQL

CREATE TABLE Singers (
 SingerId   INT64 NOT NULL PRIMARY KEY,
 FirstName  STRING(1024),
 LastName   STRING(1024),
 SingerInfo BYTES(MAX),
);

CREATE TABLE Albums (
 SingerId     INT64 NOT NULL,
 AlbumId      INT64 NOT NULL,
 AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
 INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
 SingerId     INT64 NOT NULL,
 AlbumId      INT64 NOT NULL,
 TrackId      INT64 NOT NULL,
 SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
 INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

PostgreSQL

CREATE TABLE singers (
 singer_id   BIGINT PRIMARY KEY,
 first_name  VARCHAR(1024),
 last_name   VARCHAR(1024),
 singer_info BYTEA
 );

CREATE TABLE albums (
 singer_id     BIGINT,
 album_id      BIGINT,
 album_title   VARCHAR,
 PRIMARY KEY (singer_id, album_id)
 )
 INTERLEAVE IN PARENT singers ON DELETE CASCADE;

CREATE TABLE songs (
 singer_id     BIGINT,
 album_id      BIGINT,
 track_id      BIGINT,
 song_name     VARCHAR,
 PRIMARY KEY (singer_id, album_id, track_id)
 )
 INTERLEAVE IN PARENT albums ON DELETE CASCADE;

다음 다이어그램은 인터리브 처리된 행의 실제 뷰를 보여줍니다.

Songs가 Albums 안에 인터리브 처리되고 Albums는 Singers 사이에 인터리브 처리됨

이 예시에서 가수 수가 늘어남에 따라 Spanner는 가수와 해당 앨범 및 노래 데이터 간에 데이터 지역성을 유지하기 위해 가수 사이에 분할 경계를 추가합니다. 그러나 가수 행 및 해당 하위 행의 크기가 분할 크기 한도를 초과하거나 하위 행에서 핫스팟이 감지되는 경우 Spanner가 해당 핫스팟 행과 그 아래에 있는 모든 하위 행을 분리하기 위해 분할 경계를 추가하려고 시도합니다.

요약하자면, 상위 테이블과 모든 하위 테이블은 스키마 내에서 테이블의 계층을 형성합니다. 계층 내의 각 테이블은 논리적으로 독립되어 있지만 이런 방식으로 테이블을 물리적으로 인터리브 처리하면 테이블을 효과적으로 미리 결합하여 관련 행에 함께 액세스할 수 있고 스토리지 액세스를 최소화함으로써 성능을 개선할 수 있습니다.

인터리브 처리된 테이블과 조인

가능하다면 인터리빙된 테이블 내의 데이터를 기본 키로 조인합니다. 인터리브 처리된 각 행은 일반적으로 상위 행과 동일한 분할에 저장되므로, Spanner는 로컬에서 기본 키로 조인을 수행하여 스토리지 액세스와 네트워크 트래픽을 최소화할 수 있습니다. 다음 예시에서는 SingersAlbums가 기본 키인 SingerId로 조인됩니다.

GoogleSQL

SELECT s.FirstName, a.AlbumTitle
FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;

PostgreSQL

SELECT s.first_name, a.album_title
FROM singers AS s JOIN albums AS a ON s.singer_id = a.singer_id;

키 열

이 섹션에는 키 열에 대한 몇 가지 참고 사항이 포함되어 있습니다.

테이블 키 변경

테이블의 키는 바뀔 수 없습니다. 즉, 기존 테이블에 키 열을 추가하거나 기존 테이블에서 키 열을 삭제할 수 없습니다.

기본 키에 NULL 저장

GoogleSQL에서 기본 키 열에 NULL을 저장하려면 스키마에 해당 열에 대해 NOT NULL 절을 생략합니다. (PostgreSQL 언어 데이터베이스는 기본 키 열에서 NULL을 지원하지 않습니다.)

다음은 기본 키 열 SingerId에서 NOT NULL 절을 생략하는 예시입니다. SingerId가 기본 키이므로 해당 열에 NULL을 저장하는 행이 하나 뿐일 수 있습니다.

CREATE TABLE Singers (
  SingerId   INT64 PRIMARY KEY,
  FirstName  STRING(1024),
  LastName   STRING(1024),
);

기본 키 열의 null 사용 가능 속성은 상위 테이블과 하위 테이블 선언 간에 일치해야 합니다. 이 예시에서는 Singers.SingerId에서 생략되어 있으므로 Albums.SingerId 열에 NOT NULL이 허용되지 않습니다.

CREATE TABLE Singers (
  SingerId   INT64 PRIMARY KEY,
  FirstName  STRING(1024),
  LastName   STRING(1024),
);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

허용되지 않는 유형

다음 열은 ARRAY 유형이 될 수 없습니다.

  • 테이블의 키 열.
  • 색인의 키 열.

멀티테넌시를 위한 설계

서로 다른 고객에게 속한 데이터를 저장할 경우 멀티테넌시를 구현할 수 있습니다. 예를 들어, 음악 서비스의 경우 각각의 레코드 라벨의 콘텐츠를 개별적으로 저장하는 것이 좋습니다.

기본 멀티테넌시 지원

멀티테넌시 지원을 설계하는 기본적인 방법은 고객별로 데이터베이스를 따로 만드는 것입니다. 이 예시에는 데이터베이스마다 고유한 Singers 테이블이 있습니다.

데이터베이스 1: Ackworth Records
SingerId FirstName LastName
1MarcRichards
2CatalinaSmith
데이터베이스 2: Cama Records
SingerId FirstName LastName
1AliceTrentor
2GabrielWright
데이터베이스 3: Eagan Records
SingerId FirstName LastName
1BenjaminMartinez
2HannahHarris

스키마 관리 멀티테넌시

Spanner에서 멀티테넌시를 설계하는 또 다른 방법은 단일 데이터베이스의 단일 테이블에 모든 고객을 포함하고 각 고객에 대해 서로 다른 기본 키 값을 사용하는 것입니다. 예를 들어 테이블에 CustomerId 키 열을 포함할 수 있습니다. CustomerId를 첫 번째 키 열로 만들면 각 고객의 데이터 지역성이 양호해집니다. 그러면 Spanner가 데이터베이스 분할을 효과적으로 사용하여 데이터 크기 및 부하 패턴에 따라 성능을 최대화할 수 있습니다. 다음 예시에는 고객별로 Singers 테이블이 하나씩 있습니다.

Spanner 멀티테넌시 데이터베이스
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
21AliceTrentor
22GabrielWright
31BenjaminMartinez
32HannahHarris

각 테넌트마다 별도의 데이터베이스가 있어야 하는 경우, 다음과 같은 제약에 유의해야 합니다.

  • 인스턴스당 데이터베이스 수와 데이터베이스당 테이블 및 색인 수에 한도가 있습니다. 고객 수에 따라 별도의 데이터베이스나 표를 갖는 것이 불가능할 수 있습니다.
  • 새 표와 인터리브 처리되지 않은 색인을 추가하려면 시간이 오래 걸릴 수 있습니다. 스키마가 새 표와 색인의 추가에 의존하도록 설계되어 있다면 원하는 성능을 얻지 못할 수도 있습니다.

별도의 데이터베이스를 만들려면 각 데이터베이스의 주당 스키마의 변동 횟수가 적도록 데이터베이스에 표를 분산한다면 더 성공적인 결과를 얻을 수 있습니다.

애플리케이션 고객마다 별도의 테이블과 색인을 만들 경우 모든 테이블과 색인을 같은 데이터베이스에 넣지 마세요. 그 대신, 여러 데이터베이스에 분할하여 다수의 색인을 만들어 성능 문제를 완화하세요.

멀티테넌시를 위한 다른 데이터 관리 패턴과 애플리케이션 설계에 대한 자세한 내용은 Spanner에서 멀티테넌시 구현을 참조하세요.