데이터 타입
데이터 타입 (Data Types)
CQL은 타입이 있는 언어로, 네이티브 타입, 컬렉션 타입, 사용자 정의 타입, 튜플 타입, 커스텀 타입을 포함한 풍부한 데이터 타입을 지원해요.
cql_type::= native_type| collection_type| user_defined_type | tuple_type | custom_type
출처: 문서
본문
네이티브 타입 (Native types)
CQL이 지원하는 네이티브 타입은 다음과 같아요:
native_type::= ASCII | BIGINT | BLOB | BOOLEAN | COUNTER | DATE
| DECIMAL | DOUBLE | DURATION | FLOAT | INET | INT |
SMALLINT | TEXT | TIME | TIMESTAMP | TIMEUUID | TINYINT |
UUID | VARCHAR | VARINT | VECTOR
다음 표는 네이티브 데이터 타입에 대한 추가 정보와 각 타입이 지원하는 상수 종류를 보여줍니다:
| 타입 | 지원되는 상수 | 설명 |
|---|---|---|
| ascii | string | ASCII 문자 문자열 |
| bigint | integer | 64비트 부호 있는 long |
| blob | blob | 임의의 바이트(검증 없음) |
| boolean | boolean | true 또는 false |
| counter | integer | 카운터 컬럼(64비트 부호 있는 값). 자세한 내용은 counters 참고 |
| date | integer, string | (대응하는 시간 값이 없는) 날짜. 자세한 내용은 아래 dates 참고 |
| decimal | integer, float | 가변 정밀도 십진수 |
| double | integer, float | 64비트 IEEE-754 부동소수 |
| duration | duration | 나노초 정밀도의 기간. 자세한 내용은 아래 durations 참고 |
| float | integer, float | 32비트 IEEE-754 부동소수 |
| inet | string | IPv4(4바이트) 또는 IPv6(16바이트) IP 주소. inet 상수는 없으며, IP 주소는 문자열로 입력해야 한다는 점에 유의 |
| int | integer | 32비트 부호 있는 int |
| smallint | integer | 16비트 부호 있는 int |
| text | string | UTF8 인코딩 문자열 |
| time | integer, string | 나노초 정밀도의 (대응하는 날짜 값이 없는) 시간. 자세한 내용은 아래 times 참고 |
| timestamp | integer, string | 밀리초 정밀도의 타임스탬프(날짜와 시간). 자세한 내용은 아래 timestamps 참고 |
| timeuuid | uuid | "충돌 없는" 타임스탬프로 주로 쓰이는 Version 1 UUID. timeuuid-functions도 참고 |
| tinyint | integer | 8비트 부호 있는 int |
| uuid | uuid | (어떤 버전이든) UUID |
| varchar | string | UTF8 인코딩 문자열 |
| varint | integer | 임의 정밀도 정수 |
| vector | float | 고정 길이의 non-null flat한 float 배열. CASSANDRA-18504가 이 데이터 타입을 {cass-50}에 추가 |
카운터 (Counters)
카운터 타입은 카운터 컬럼을 정의하는 데 사용됩니다. 카운터 컬럼은 값이 64비트 부호 있는 정수이고, 증가와 감소 두 가지 연산이 지원되는 컬럼이에요(문법은 UPDATE 문 참고). 카운터의 값은 설정할 수 없습니다. 카운터는 처음 증가/감소되기 전까지는 존재하지 않고, 첫 증가/감소는 이전 값이 0이었던 것처럼 이루어집니다.
카운터에는 여러 중요한 제한 사항이 있어요:
- 테이블의 PRIMARY KEY의 일부인 컬럼에는 사용할 수 없습니다.
- 카운터를 포함하는 테이블은 카운터만 포함할 수 있어요. 즉 테이블의 PRIMARY KEY 밖의 모든 컬럼이 카운터 타입이거나, 그 어느 것도 카운터 타입이 아니어야 합니다.
- 카운터는 만료(expiration)를 지원하지 않습니다.
- 카운터 삭제는 지원되지만, 카운터를 처음 삭제할 때만 올바르게 동작한다는 것이 보장됩니다. 다시 말해 삭제한 카운터를 다시 업데이트하지 말아야 합니다(그렇게 하면 올바른 동작이 보장되지 않아요).
- 카운터 업데이트는 본질적으로 idempotent가 아닙니다. 중요한 결과로, 카운터 업데이트가 예기치 않게 실패하면(타임아웃 또는 조정자 노드와의 연결 손실) 클라이언트는 업데이트가 적용됐는지 여부를 알 방법이 없어요. 특히 업데이트를 재생하면 과대 집계(over count)로 이어질 수도 있고 그렇지 않을 수도 있습니다.
타임스탬프 다루기
timestamp 타입의 값은 에포크(epoch)라고 알려진 표준 기준 시간(1970년 1월 1일 00:00:00 GMT) 이후의 밀리초 수를 나타내는 64비트 부호 있는 정수로 인코딩됩니다.
타임스탬프는 정수 값으로 입력하거나 ISO 8601 날짜를 나타내는 문자열로 입력할 수 있어요. 예를 들어 2011년 3월 2일 04:05:00 AM GMT에 대한 다음 값들은 모두 유효한 타임스탬프 값입니다:
- 1299038700000
- '2011-02-03 04:05+0000'
- '2011-02-03 04:05:00+0000'
- '2011-02-03 04:05:00.000+0000'
- '2011-02-03T04:05+0000'
- '2011-02-03T04:05:00+0000'
- '2011-02-03T04:05:00.000+0000'
위의 +0000은 RFC 822 4자리 시간대 지정이며, +0000은 GMT를 가리켜요. 미국 태평양 표준시는 -0800입니다. 원한다면 시간대를 생략할 수 있고('2011-02-03 04:05:00'), 생략하면 조정 Cassandra 노드가 구성된 시간대로 날짜가 해석됩니다. 하지만 시간대 구성이 예상대로라는 것에 의존하는 데는 본질적인 어려움이 있으므로, 가능하면 타임스탬프에 항상 시간대를 지정하는 것이 권장돼요.
하루 중 시간도 생략할 수 있고('2011-02-03' 또는 '2011-02-03+0000'), 이 경우 하루 중 시간은 지정되거나 기본인 시간대에서 00:00:00이 됩니다. 다만 날짜 부분만 관련 있다면 date 타입 사용을 고려해 보세요.
Date 타입
date 타입의 값은 범위의 중심(2^31)에 "에포크"가 있는 일수를 나타내는 32비트 부호 없는 정수로 인코딩됩니다. 에포크는 1970년 1월 1일이에요.
타임스탬프처럼 날짜는 정수로 입력하거나 날짜 문자열로 입력할 수 있습니다. 후자의 경우 형식은 yyyy-mm-dd여야 해요(예: '2011-02-03').
Time 타입
time 타입의 값은 자정 이후의 나노초 수를 나타내는 64비트 부호 있는 정수로 인코딩됩니다.
타임스탬프처럼 시간은 정수로 입력하거나 시간을 나타내는 문자열로 입력할 수 있어요. 후자의 경우 형식은 hh:mm:ss[.fffffffff]여야 합니다(초 이하 정밀도는 선택적이고, 제공되면 나노초보다 작을 수 있어요). 예를 들어 다음은 시간에 대한 유효한 입력입니다:
- '08:12:54'
- '08:12:54.123'
- '08:12:54.123456'
- '08:12:54.123456789'
Duration 타입
duration 타입의 값은 가변 길이의 3개 부호 있는 정수로 인코딩됩니다. 첫 번째 정수는 월 수, 두 번째는 일 수, 세 번째는 나노초 수입니다. 이는 한 달의 일 수가 달라질 수 있고, 일광 절약 시간에 따라 하루가 23시간 또는 25시간일 수 있기 때문입니다. 내부적으로 월 수와 일 수는 32비트 정수로, 나노초 수는 64비트 정수로 디코딩됩니다.
duration은 다음과 같이 입력할 수 있어요:
(quantity unit)+형태(예:12h30m). 단위는:- y: 년(12개월)
- mo: 월(1개월)
- w: 주(7일)
- d: 일(1일)
- h: 시간(3,600,000,000,000 나노초)
- m: 분(60,000,000,000 나노초)
- s: 초(1,000,000,000 나노초)
- ms: 밀리초(1,000,000 나노초)
- us 또는 µs: 마이크로초(1000 나노초)
- ns: 나노초(1 나노초)
- ISO 8601 형식:
P[n]Y[n]M[n]DT[n]H[n]M[n]S또는P[n]W - ISO 8601 대체 형식:
P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]
예를 들어:
INSERT INTO RiderResults (rider, race, result)
VALUES ('Christopher Froome', 'Tour de France', 89h4m48s);
INSERT INTO RiderResults (rider, race, result)
VALUES ('BARDET Romain', 'Tour de France', PT89H8M53S);
INSERT INTO RiderResults (rider, race, result)
VALUES ('QUINTANA Nairo', 'Tour de France', P0000-00-00T89:09:09);
Duration 컬럼은 테이블의 PRIMARY KEY에 사용할 수 없어요. 이 제한은 duration이 순서를 매길 수 없기 때문입니다. 날짜 맥락 없이는 1mo가 29d보다 큰지 알 수 없어요.
duration 타입은 일광 절약 시간을 지원하기 위해 만들어졌으므로 1d duration은 24h와 같지 않습니다.
컬렉션 (Collections)
CQL은 세 가지 종류의 컬렉션, 즉 maps, sets, lists를 지원해요. 이 컬렉션들의 타입은 다음과 같이 정의됩니다:
collection_type::= MAP '<' cql_type',' cql_type'>'
| SET '<' cql_type '>'
| LIST '<' cql_type'>'
그 값들은 컬렉션 리터럴을 사용해 입력할 수 있어요:
collection_literal::= map_literal | set_literal | list_literal
map_literal::= '\{' [ term ':' term (',' term : term)* ] '}'
set_literal::= '\{' [ term (',' term)* ] '}'
list_literal::= '[' [ term (',' term)* ] ']'
다만 컬렉션 리터럴 안에서는 bind_marker도 NULL도 지원되지 않는다는 점에 유의하세요.
주목할 만한 특성
컬렉션은 비교적 적은 양의 데이터를 저장/비정규화하기 위한 것입니다. "특정 사용자의 전화번호", "이메일에 적용된 레이블" 같은 것에 잘 작동해요. 하지만 항목이 무한정 늘어날 것으로 예상되는 경우("사용자가 보낸 모든 메시지", "센서가 기록한 이벤트" 등) 컬렉션은 적절하지 않고 특정 테이블(클러스터링 컬럼 포함)을 사용해야 합니다. 구체적으로 (frozen이 아닌) 컬렉션은 다음과 같은 주목할 만한 특성과 한계가 있어요:
- 개별 컬렉션은 내부적으로 인덱싱되지 않습니다. 이는 컬렉션의 단일 요소에 접근하더라도 전체 컬렉션을 읽어야 함을 의미해요(그리고 읽기는 내부적으로 페이징되지 않습니다).
- sets와 maps에 대한 삽입 연산은 내부적으로 read-before-write를 유발하지 않지만, lists의 일부 연산은 그렇습니다. 또한 일부 list 연산은 본질적으로 idempotent가 아니어서(자세한 내용은 아래 lists 섹션 참고) 타임아웃 시 재시도가 문제가 됩니다. 따라서 가능하면 sets를 lists보다 선호하는 것이 좋아요.
이러한 한계 중 일부가 미래에 제거되거나 개선될 수도 있지만, (단일) 컬렉션을 사용해 많은 양의 데이터를 저장하는 것은 안티패턴이라는 점을 유의하세요.
Maps
map은 (정렬된) 키-값 쌍의 집합으로, 키는 고유하고 map은 키로 정렬됩니다. 다음으로 map을 정의하고 삽입할 수 있어요:
CREATE TABLE users (
id text PRIMARY KEY,
name text,
favs map<text, text> // A map of text keys, and text values
);
INSERT INTO users (id, name, favs)
VALUES ('jsmith', 'John Smith', { 'fruit' : 'Apple', 'band' : 'Beatles' });
// Replace the existing map entirely.
UPDATE users SET favs = { 'fruit' : 'Banana' } WHERE id = 'jsmith';
또한 map은 다음을 지원합니다:
- 하나 이상의 요소 업데이트 또는 삽입:
UPDATE users SET favs['author'] = 'Ed Poe' WHERE id = 'jsmith';
UPDATE users SET favs = favs + { 'movie' : 'Cassablanca', 'band' : 'ZZ Top' } WHERE id = 'jsmith';
- 하나 이상의 요소 제거(요소가 없으면 제거는 no-op이지만 오류는 발생하지 않음):
DELETE favs['author'] FROM users WHERE id = 'jsmith';
UPDATE users SET favs = favs - { 'movie', 'band'} WHERE id = 'jsmith';
map에서 여러 요소를 제거할 때는 키 집합을 제거한다는 점에 유의하세요.
마지막으로 INSERT와 UPDATE 모두 TTL이 허용되지만, 두 경우 모두 설정된 TTL은 새로 삽입/업데이트된 요소에만 적용됩니다. 즉:
UPDATE users USING TTL 10 SET favs['color'] = 'green' WHERE id = 'jsmith';
는 TTL을 { 'color' : 'green' } 레코드에만 적용하고, map의 나머지는 그대로 둡니다.
Sets
set은 (정렬된) 고유 값의 컬렉션이에요. 다음으로 set을 정의하고 삽입할 수 있습니다:
CREATE TABLE images (
name text PRIMARY KEY,
owner text,
tags set<text> // A set of text values
);
INSERT INTO images (name, owner, tags)
VALUES ('cat.jpg', 'jsmith', { 'pet', 'cute' });
// Replace the existing set entirely
UPDATE images SET tags = { 'kitten', 'cat', 'lol' } WHERE name = 'cat.jpg';
또한 set은 다음을 지원합니다:
- 하나 또는 여러 요소 추가(set이므로 이미 존재하는 요소를 삽입하는 것은 no-op):
UPDATE images SET tags = tags + { 'gray', 'cuddly' } WHERE name = 'cat.jpg';
- 하나 또는 여러 요소 제거(요소가 없으면 제거는 no-op이지만 오류는 발생하지 않음):
UPDATE images SET tags = tags - { 'cat' } WHERE name = 'cat.jpg';
마지막으로 set의 경우 TTL은 새로 삽입된 값에만 적용됩니다.
Lists
위에서 언급하고 이 섹션 끝에서 더 자세히 논의하듯, list에는 사용하기 전에 고려해야 할 한계와 특정 성능 고려 사항이 있어요. 일반적으로 list 대신 set을 쓸 수 있다면 항상 set을 선호하세요.
list는 요소가 목록 내 위치에 따라 정렬되는 (정렬된) non-unique 값의 컬렉션입니다. 다음으로 list를 정의하고 삽입할 수 있어요:
CREATE TABLE plays (
id text PRIMARY KEY,
game text,
players int,
scores list<int> // A list of integers
)
INSERT INTO plays (id, game, players, scores)
VALUES ('123-afde', 'quake', 3, [17, 4, 2]);
// Replace the existing list entirely
UPDATE plays SET scores = [ 3, 9, 4] WHERE id = '123-afde';
또한 list는 다음을 지원합니다:
- list에 값 추가(append) 및 앞에 추가(prepend):
UPDATE plays SET players = 5, scores = scores + [ 14, 21 ] WHERE id = '123-afde';
UPDATE plays SET players = 6, scores = [ 3 ] + scores WHERE id = '123-afde';
Warning append와 prepend 연산은 본질적으로 idempotent가 아닙니다. 그래서 특히 이 연산 중 하나가 타임아웃되면 연산을 재시도하는 것이 안전하지 않으며, 값을 두 번 추가/앞에 추가하게 될 수도 있고 그렇지 않을 수도 있어요.
- 그 위치에 기존 요소가 있는 list의 특정 위치에 값 설정. list에 그 위치가 없으면 오류 발생:
UPDATE plays SET scores[1] = 7 WHERE id = '123-afde';
- 그 위치에 기존 요소가 있는 list의 특정 위치에서 요소 제거. list에 그 위치가 없으면 오류 발생. 또한 이 연산은 list에서 요소를 제거하므로 list 크기가 하나 줄고, 이후 모든 요소의 위치가 하나씩 앞으로 이동합니다:
DELETE scores[1] FROM plays WHERE id = '123-afde';
- list에서 특정 값의 모든 발생 삭제(특정 요소가 list에 전혀 없으면 단순히 무시되고 오류는 발생하지 않음):
UPDATE plays SET scores = scores - [ 12, 21 ] WHERE id = '123-afde';
Warning 위치로 설정 및 제거하고 특정 값의 발생을 제거하는 것은 내부적으로 read-before-write를 유발합니다. 이 연산들은 일반 업데이트보다 느리게 실행되고 더 많은 리소스를 사용해요(자체 비용이 있는 조건부 쓰기는 제외).
마지막으로 list의 경우 TTL은 새로 삽입된 값에만 적용됩니다.
벡터 다루기
벡터는 특정 데이터 타입의 non-null 값으로 이루어진 고정 크기 시퀀스예요. list와 같은 리터럴을 사용합니다.
다음으로 벡터를 정의, 삽입, 업데이트할 수 있어요:
CREATE TABLE plays (
id text PRIMARY KEY,
game text,
players int,
scores vector<int, 3> // A vector of 3 integers
)
INSERT INTO plays (id, game, players, scores)
VALUES ('123-afde', 'quake', 3, [17, 4, 2]);
// Replace the existing vector entirely
UPDATE plays SET scores = [ 3, 9, 4] WHERE id = '123-afde';
벡터의 개별 값을 변경할 수 없고, 벡터의 개별 요소를 선택할 수도 없다는 점에 유의하세요.
사용자 정의 타입 (UDTs)
CQL은 사용자 정의 타입(UDT)의 정의를 지원합니다. 이러한 타입은 아래 설명하는 create_type_statement, alter_type_statement, drop_type_statement를 사용해 생성, 수정, 제거할 수 있어요. 하지만 한 번 생성되면 UDT는 단순히 이름으로 참조됩니다:
user_defined_type::= udt_name
udt_name::= [ keyspace_name '.' ] identifier
UDT 생성
새 사용자 정의 타입을 만드는 것은 다음으로 정의되는 CREATE TYPE 문으로 이루어집니다:
create_type_statement::= CREATE TYPE [ IF NOT EXISTS ] udt_name
'(' field_definition ( ',' field_definition)* ')'
field_definition::= identifier cql_type
UDT에는 (그 타입의 컬럼을 선언하는 데 사용되는) 이름이 있고, 이름이 있고 타입이 지정된 필드의 집합입니다. 필드 이름은 컬렉션 또는 다른 UDT를 포함한 어떤 타입이든 될 수 있어요. 예를 들어:
CREATE TYPE phone (
country_code int,
number text,
);
CREATE TYPE address (
street text,
city text,
zip text,
phones map<text, phone>
);
CREATE TABLE user (
name text PRIMARY KEY,
addresses map<text, frozen<address>>
);
UDT에 대해 기억해야 할 것:
- 이미 존재하는 타입을 생성하려 하면
IF NOT EXISTS옵션을 사용하지 않는 한 오류가 발생합니다. 사용하면 타입이 이미 존재할 때 문은 no-op이 돼요. - 타입은 생성된 키스페이스에 본질적으로 묶여 있으며, 그 키스페이스에서만 사용할 수 있어요. 생성 시 타입 이름이 키스페이스 이름으로 접두사가 붙으면 그 키스페이스에 생성됩니다. 그렇지 않으면 현재 키스페이스에 생성됩니다.
- Cassandra부터 UDT는 대부분의 경우 frozen이어야 하므로 위 테이블 정의에
frozen<address>가 있습니다.
UDT 리터럴
사용자 정의 타입이 생성된 후 값은 UDT 리터럴을 사용해 입력할 수 있어요:
udt_literal::= '{' identifier ':' term ( ',' identifier ':' term)* '}'
즉 UDT 리터럴은 map 리터럴과 같지만 키가 타입의 필드 이름입니다. 예를 들어 이전 섹션에서 정의한 테이블에 다음과 같이 삽입할 수 있어요:
INSERT INTO user (name, addresses)
VALUES ('z3 Pr3z1den7', {
'home' : {
street: '1600 Pennsylvania Ave NW',
city: 'Washington',
zip: '20500',
phones: { 'cell' : { country_code: 1, number: '202 456-1111' },
'landline' : { country_code: 1, number: '...' } }
},
'work' : {
street: '1600 Pennsylvania Ave NW',
city: 'Washington',
zip: '20500',
phones: { 'fax' : { country_code: 1, number: '...' } }
}
}
);
유효하려면 UDT 리터럴은 그 리터럴의 타입이 정의한 필드만 포함할 수 있지만, 일부 필드는 생략할 수 있어요(그 필드들은 NULL로 설정됩니다).
UDT 변경
기존 사용자 정의 타입은 ALTER TYPE 문으로 수정할 수 있어요:
alter_type_statement::= ALTER TYPE [ IF EXISTS ] udt_name alter_type_modification
alter_type_modification::= ADD [ IF NOT EXISTS ] field_definition
| RENAME [ IF EXISTS ] identifier TO identifier (AND identifier TO identifier )*
타입이 존재하지 않으면 문은 오류를 반환합니다. 단 IF EXISTS를 사용하면 연산이 no-op이 됩니다. 다음과 같이 할 수 있어요:
- 타입에 새 필드 추가(
ALTER TYPE address ADD country text). 그 새 필드는 추가 전에 생성된 타입의 모든 값에 대해 NULL이 됩니다. 새 필드가 존재하면 문은 오류를 반환하지만,IF NOT EXISTS를 사용하면 연산이 no-op이 됩니다. - 타입의 필드 이름 바꾸기. 필드가 존재하지 않으면 문은 오류를 반환하지만,
IF EXISTS를 사용하면 연산이 no-op이 됩니다.
ALTER TYPE address RENAME zip TO zipcode;
UDT 삭제
DROP TYPE 문으로 기존 사용자 정의 타입을 삭제할 수 있어요:
drop_type_statement::= DROP TYPE [ IF EXISTS ] udt_name
타입을 삭제하면 그 타입이 즉시, 되돌릴 수 없게 제거됩니다. 하지만 다른 타입, 테이블, 함수가 여전히 사용 중인 타입을 삭제하려 하면 오류가 발생합니다.
삭제된 타입이 존재하지 않으면 IF EXISTS를 사용하지 않는 한 오류가 반환되며, IF EXISTS를 사용하면 연산이 no-op입니다.
튜플 (Tuples)
CQL은 튜플과 튜플 타입도 지원합니다(요소가 서로 다른 타입일 수 있어요). 기능적으로 튜플은 익명 필드가 있는 익명 UDT라고 생각할 수 있습니다. 튜플 타입과 튜플 리터럴은 다음과 같이 정의됩니다:
tuple_type::= TUPLE '<' cql_type( ',' cql_type)* '>'
tuple_literal::= '(' term( ',' term )* ')'
다음과 같이 생성할 수 있어요:
CREATE TABLE durations (
event text,
duration tuple<int, text>,
);
INSERT INTO durations (event, duration) VALUES ('ev1', (3, 'hours'));
컬렉션이나 UDT 같은 다른 구성 타입과 달리, 튜플은 항상 frozen이며(frozen 키워드가 필요 없음) 튜플의 일부 요소만 업데이트하는 것은 불가능합니다(전체 튜플을 업데이트하지 않고서는). 또한 튜플 리터럴은 그 튜플의 타입에 선언된 값과 항상 같은 수의 값을 가져야 해요(일부 값은 null일 수 있지만 명시적으로 null로 선언해야 합니다).
커스텀 타입 (Custom Types)
커스텀 타입은 주로 하위 호환성 목적으로 존재하며, 사용은 권장되지 않습니다. 그 사용법은 복잡하고 사용자 친화적이지 않으며, 다른 제공 타입(특히 사용자 정의 타입)이 거의 항상 충분해야 합니다.
커스텀 타입은 다음과 같이 정의됩니다:
custom_type::= string
커스텀 타입은 서버측 AbstractType 클래스를 확장하고 Cassandra가 로드할 수 있는(따라서 Cassandra를 실행하는 모든 노드의 CLASSPATH에 있어야 하는) Java 클래스의 이름을 포함하는 문자열이에요. 그 클래스는 타입에 대해 어떤 값이 유효한지, 클러스터링 컬럼으로 사용될 때 어떻게 정렬되는지를 정의합니다. 다른 어떤 목적으로든 커스텀 타입의 값은 blob과 같으며, 특히 blob 리터럴 문법으로 입력할 수 있어요.
더 알아보기 (Learn more)
- 정의 — 상수와 항
- 함수 — timeuuid 등 타입 관련 함수
- 데이터 조작(DML) — UPDATE 문 문법