데이터 조작

데이터 조작 (Data Manipulation)

이 섹션에서는 데이터를 삽입, 업데이트, 삭제, 조회하기 위해 CQL이 지원하는 문을 설명할게요.

출처: 문서

본문

SELECT

데이터를 조회하는 것은 SELECT 문으로 합니다:

select_statement::= SELECT [ JSON | DISTINCT ] ( select_clause | '*' )
	FROM `table_name`
	[ WHERE `where_clause` ]
	[ GROUP BY `group_by_clause` ]
	[ ORDER BY `ordering_clause` ]
	[ PER PARTITION LIMIT (`integer` | `bind_marker`) ]
	[ LIMIT (`integer` | `bind_marker`) ]
	[ ALLOW FILTERING ]
select_clause::= `selector` [ AS `identifier` ] ( ',' `selector` [ AS `identifier` ] )*
selector::== `column_name`
	| `term`
	| CAST '(' `selector` AS `cql_type` ')'
	| `function_name` '(' [ `selector` ( ',' `selector` )* ] ')'
	| COUNT '(' '*' ')'
where_clause::= `relation` ( AND `relation` )*
relation::= column_name operator term
	'(' column_name ( ',' column_name )* ')' operator tuple_literal
	TOKEN '(' column_name ( ',' column_name )* ')' operator term
operator::= '=' | '<' | '>' | '<=' | '>=' | '!=' | IN | CONTAINS | CONTAINS KEY
group_by_clause::= column_name ( ',' column_name )*
ordering_clause::= column_name [ ASC | DESC ] ( ',' column_name [ ASC | DESC ] )*

예를 들어:

SELECT name, occupation FROM users WHERE userid IN (199, 200, 207);
SELECT JSON name, occupation FROM users WHERE userid = 199;
SELECT name AS user_name, occupation AS user_occupation FROM users;

SELECT time, value
FROM events
WHERE event_type = 'myEvent'
  AND time > '2011-02-03'
  AND time <= '2012-01-01'

SELECT COUNT (*) AS user_count FROM users;

SELECT 문은 테이블의 한 행 이상에 대해 하나 이상의 컬럼을 읽습니다. 요청과 일치하는 행의 결과 집합을 반환하는데, 각 행은 쿼리에 해당하는 선택에 대한 값을 포함합니다. 추가로 집계를 포함한 함수를 결과에 적용할 수 있어요.

SELECT 문은 적어도 선택 절(selection clause)과 선택이 실행되는 테이블 이름을 포함합니다. CQL은 조인이나 서브쿼리를 실행하지 않으며, select 문은 단일 테이블에만 적용됩니다. select 문은 쿼리 결과를 더 좁힐 수 있는 where 절도 가질 수 있어요. 추가 절은 결과를 정렬하거나 제한할 수 있습니다. 마지막으로 전체 클러스터 필터링을 요구하는 쿼리는 모든 쿼리에 ALLOW FILTERING을 추가할 수 있습니다. 가상 테이블의 경우 CASSANDRA-18238부터, 보통 ALLOW FILTERING이 필요한 쿼리에서 이를 지정할 필요가 없습니다. 자세한 내용은 가상 테이블 문서를 참고하세요.

선택 절 (Selection clause)

select_clause는 어떤 컬럼이 쿼리되고 결과 집합에 반환될지 결정합니다. 이 절은 반환 전에 결과에 적용할 변환을 적용할 수도 있어요. 선택 절은 특정 selector의 쉼표로 구분된 목록 또는, 테이블에 정의된 모든 컬럼을 선택하는 와일드카드 문자(*)로 구성됩니다.

Selectors

selector는 다음 중 하나일 수 있어요:

  • 선택된 테이블의 컬럼 이름 — 해당 컬럼의 값을 가져옴.
  • term — 보통 함수 같은 다른 selector 안에 중첩되어 사용됩니다(term이 직접 선택되면 결과 집합의 해당 컬럼은 반환되는 모든 행에 대해 단순히 이 term의 값을 갖습니다).
  • casting — 중첩된 selector를 (호환되는) 타입으로 변환할 수 있게 해줌.
  • 함수 호출 — 인자 자체가 selector입니다. 자세한 내용은 함수 섹션 참고.
  • COUNT 함수에 대한 특수 호출 COUNT(*) — 모든 non-null 결과를 셈.

Aliases

모든 최상위 selector는 (AS를 사용해) 별칭을 붙일 수 있습니다. 그렇게 하면 결과 집합의 해당 컬럼 이름은 별칭이 됩니다. 예를 들어:

// Without alias
SELECT int_as_blob(4) FROM t;

//  int_as_blob(4)
// ----------------
//  0x00000004

// With alias
SELECT int_as_blob(4) AS four FROM t;

//  four
// ------------
//  0x00000004

현재 별칭은 문의 WHERE나 ORDER BY 절에서 인식되지 않습니다. 원래 컬럼 이름을 사용해야 해요.

WRITETIME, MAXWRITETIME 및 TTL 함수

선택은 다른 곳에서 허용되지 않는 세 가지 특수 함수를 지원합니다: WRITETIME, MAXWRITETIME, TTL. 모든 함수는 인자를 하나만 받으며, 컬럼 이름입니다. 컬럼이 컬렉션이나 UDT이면 WRITETIME(phones[2..4])WRITETIME(user.name) 같은 요소 선택자를 추가할 수 있어요. 이 함수들은 각 컬럼에 대해 내부적으로 저장된 메타 정보를 검색합니다:

  • WRITETIME은 컬럼의 값 타임스탬프를 저장합니다.
  • MAXWRITETIME은 컬럼 값의 가장 큰 타임스탬프를 저장합니다. non-collection 및 non-UDT 컬럼의 경우 MAXWRITETIMEWRITETIME과 동등합니다. 다른 경우에는 컬럼 값 중 가장 큰 타임스탬프를 반환합니다.
  • TTL은 컬럼의 값이 만료되도록 설정된 경우 남은 수명(초)을 저장하고, 그렇지 않으면 값은 null입니다.

WRITETIMETTL 함수는 non-frozen 컬렉션 또는 non-frozen 사용자 정의 타입 같은 멀티셀 컬럼에 사용될 수 있어요. 그 경우 함수는 선택된 각 셀에 대한 타임스탬프 또는 TTL 목록을 반환합니다.

WHERE 절

WHERE 절은 어떤 행이 쿼리되는지 지정합니다. PRIMARY KEY 컬럼이나 이차 인덱스가 정의된 컬럼에 대해, 설정된 값과 함께 관계(relationship)를 지정해요.

쿼리에서 모든 관계가 허용되는 것은 아닙니다. 예를 들어 파티션 키에는 동등성(equality)만 허용됩니다. IN 절은 하나 이상의 값에 대한 동등성으로 간주됩니다. TOKEN 절은 파티션 키 비동등성(non-equality)을 쿼리하는 데 사용할 수 있어요. WHERE 절에서 파티션 키는 클러스터링 컬럼보다 먼저 지정되어야 합니다. 클러스터링 컬럼의 관계는 순서를 정하기 위해 연속된 행 집합을 지정해야 합니다.

예를 들어:

CREATE TABLE posts (
    userid text,
    blog_title text,
    posted_at timestamp,
    entry_title text,
    content text,
    category int,
    PRIMARY KEY (userid, blog_title, posted_at)
);

다음 쿼리는 허용됩니다:

SELECT entry_title, content FROM posts
 WHERE userid = 'john doe'
   AND blog_title='John''s Blog'
   AND posted_at >= '2012-01-01' AND posted_at < '2012-01-31';

하지만 다음은 연속된 행 집합을 선택하지 않으므로(그리고 이차 인덱스가 설정되지 않았다고 가정) 허용되지 않습니다:

// Needs a blog_title to be set to select ranges of posted_at

SELECT entry_title, content FROM posts
 WHERE userid = 'john doe'
   AND posted_at >= '2012-01-01' AND posted_at < '2012-01-31';

관계를 지정할 때 TOKEN 함수를 PARTITION KEY 컬럼에 적용해 쿼리할 수 있습니다. 행은 값이 아니라 PARTITION_KEY의 토큰에 기반해 선택됩니다.

키의 토큰은 사용 중인 파티셔너에 따라 달라지며, 특히 RandomPartitioner는 의미 있는 순서를 산출하지 않습니다. 또한 ordering 파티셔너는 항상 토큰 값을 바이트로 정렬합니다(그래서 파티션 키가 int 타입이어도 특히 token(-1) > token(0)입니다).

예를 들어:

SELECT * FROM posts
 WHERE token(userid) > token('tom') AND token(userid) < token('bob');

IN 관계는 파티션 키의 마지막 컬럼 또는 전체 기본 키의 마지막 컬럼에만 허용됩니다.

튜플 표기법을 사용해 CLUSTERING COLUMNS를 관계에서 "그룹화"하는 것도 가능합니다.

예를 들어:

SELECT * FROM posts
 WHERE userid = 'john doe'
   AND (blog_title, posted_at) > ('John''s Blog', '2012-01-01');

이 쿼리는 클러스터링 순서에서 blog_title이 "John's Blog"이고 posted_at이 '2012-01-01'인 행보다 뒤에 정렬되는 모든 행을 반환합니다. 특히 blog_title > 'John''s Blog'인 한 posted_at ⇐ '2012-01-01'인 행도 반환됩니다.

이 예제에서는 그렇지 않을 것입니다:

SELECT * FROM posts
 WHERE userid = 'john doe'
   AND blog_title > 'John''s Blog'
   AND posted_at > '2012-01-01';

튜플 표기법은 클러스터링 컬럼의 IN 절에도 사용될 수 있어요:

SELECT * FROM posts
 WHERE userid = 'john doe'
   AND (blog_title, posted_at) IN (('John''s Blog', '2012-01-01'), ('Extreme Chess', '2014-06-01'));

CONTAINS 연산자는 컬렉션 컬럼(lists, sets, maps)에만 사용될 수 있어요. map의 경우 CONTAINS는 map 값에 적용됩니다. CONTAINS KEY 연산자는 map 컬럼에만 사용될 수 있고 map 키에 적용됩니다.

결과 그룹화

GROUP BY 옵션은 일련의 컬럼에 대해 같은 값을 공유하는 모든 선택된 행을 단일 행으로 압축할 수 있습니다.

GROUP BY 옵션을 사용해 행을 파티션 키 또는 클러스터링 컬럼 수준에서 그룹화할 수 있어요. 결과적으로 GROUP BY 옵션은 정의된 순서의 기본 키 컬럼만 인자로 받습니다. 기본 키 컬럼이 동등성 제한으로 제한되면 GROUP BY 절에 포함되지 않습니다.

집계 함수는 각 그룹에 대해 별도의 값을 생성합니다. GROUP BY 절이 지정되지 않으면 집계 함수는 모든 행에 대해 단일 값을 생성해요.

GROUP BY가 있는 문에서 집계 함수 없이 컬럼이 선택되면 각 그룹에서 처음 만난 값이 반환됩니다.

결과 정렬

ORDER BY 절은 반환된 결과의 순서를 선택합니다. 인자는 컬럼 이름 목록과 각 컬럼의 순서(ASC는 오름차순, DESC는 내림차순)입니다. 가능한 순서는 테이블에 정의된 클러스터링 순서에 의해 제한됩니다:

  • 테이블이 특정 CLUSTERING ORDER 없이 정의된 경우, 순서는 클러스터링 컬럼이 정의하는 대로 또는 그 반대입니다.
  • 그렇지 않으면 순서는 CLUSTERING ORDER 옵션이 정의하는 대로와 그 반대입니다.

결과 제한

SELECT 문의 LIMIT 옵션은 쿼리가 반환하는 행 수를 제한합니다. PER PARTITION LIMIT 옵션은 쿼리가 주어진 파티션에 대해 반환하는 행 수를 제한합니다. 두 유형의 제한 모두 같은 문에서 사용할 수 있어요.

필터링 허용

기본적으로 CQL은 모든 파티션의 전체 스캔을 수반하지 않는 select 쿼리만 허용합니다. 모든 파티션이 스캔되면 결과 반환은 테이블의 데이터 양에 비례하는 상당한 지연 시간을 겪을 수 있어요. ALLOW FILTERING 옵션은 전체 스캔을 명시적으로 실행합니다. 따라서 쿼리의 성능은 예측할 수 없습니다.

예를 들어 출생 연도와 거주 국가가 있는 다음 사용자 프로필 테이블을 생각해 봅시다. 출생 연도에는 이차 인덱스가 정의되어 있습니다.

CREATE TABLE users (
    username text PRIMARY KEY,
    firstname text,
    lastname text,
    birth_year int,
    country text
);

CREATE INDEX ON users(birth_year);

다음 쿼리는 유효합니다:

// All users are returned
SELECT * FROM users;

// All users with a particular birth year are returned
SELECT * FROM users WHERE birth_year = 1981;

두 경우 모두 쿼리 성능은 반환되는 데이터 양에 비례합니다. 첫 번째 쿼리는 모든 사용자가 선택되므로 모든 행을 반환합니다. 두 번째 쿼리는 이차 인덱스에서 정의한 행만을 반환하는데, 이차 인덱스는 노드별 구현입니다. 결과는 클러스터의 노드 수에 따라 달라지며, 저장된 데이터 양에 간접적으로 비례합니다. 노드 수는 항상 저장된 사용자 프로필 수보다 몇 자릿수 작을 것입니다. 두 쿼리 모두 매우 큰 결과 집합을 반환할 수 있지만, LIMIT 절을 추가하면 지연 시간을 줄일 수 있어요.

다음 쿼리는 거부됩니다:

SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR';

Cassandra는 결과가 작더라도 많은 양의 데이터를 스캔해야 하지 않을 것을 보장할 수 없어요. 데이터셋이 작고 성능이 합리적일 것임을 안다면 ALLOW FILTERING을 추가해 쿼리 실행을 허용하세요:

SELECT * FROM users WHERE birth_year = 1981 AND country = 'FR' ALLOW FILTERING;

INSERT

행에 데이터를 삽입하는 것은 INSERT 문으로 합니다:

insert_statement::= INSERT INTO table_name ( names_values | json_clause )
	[ IF NOT EXISTS ]
	[ USING update_parameter ( AND update_parameter )* ]
names_values::= names VALUES tuple_literal
json_clause::= JSON string [ DEFAULT ( NULL | UNSET ) ]
names::= '(' column_name ( ',' column_name )* ')'

예를 들어:

INSERT INTO NerdMovies (movie, director, main_actor, year)
   VALUES ('Serenity', 'Joss Whedon', 'Nathan Fillion', 2005)
   USING TTL 86400;

INSERT INTO NerdMovies JSON '{"movie": "Serenity", "director": "Joss Whedon", "year": 2005}';

INSERT 문은 테이블의 주어진 행에 대해 하나 이상의 컬럼을 씁니다. 행은 PRIMARY KEY로 식별되므로 최소한 하나의 컬럼을 지정해야 합니다. 삽입할 컬럼 목록은 VALUES 문법으로 제공되어야 합니다. JSON 문법을 사용할 때 VALUES는 선택사항입니다. 자세한 내용은 JSON 지원 섹션을 참고하세요. INSERT의 모든 업데이트는 원자적으로 그리고 격리되어 적용됩니다.

SQL과 달리 INSERT는 기본적으로 행의 기존 존재를 확인하지 않습니다. 이전에 존재하는 행이 없으면 행이 생성되고, 그렇지 않으면 업데이트됩니다. 또한 어떤 동작이 발생했는지 알 수 있는 방법이 없어요.

IF NOT EXISTS 조건은 행이 존재하지 않을 때 삽입을 제한할 수 있습니다. 다만 IF NOT EXISTS를 사용하면 Paxos가 사용되므로 무시할 수 없는 성능 비용이 발생하며, 드물게 사용해야 합니다.

update_parameter에 대한 정보는 UPDATE 섹션을 참고하세요. 또한 INSERT는 카운터를 지원하지 않지만 UPDATE는 지원한다는 점을 유의하세요.

UPDATE

행 업데이트는 UPDATE 문으로 합니다:

update_statement ::=    UPDATE table_name
                        [ USING update_parameter ( AND update_parameter )* ]
                        SET assignment( ',' assignment )*
                        WHERE where_clause
                        [ IF ( EXISTS | condition ( AND condition)*) ]
update_parameter ::= ( TIMESTAMP | TTL ) ( integer | bind_marker )
assignment: simple_selection'=' term
                | column_name'=' column_name ( '+' | '-' ) term
                | column_name'=' list_literal'+' column_name
simple_selection ::= column_name
                        | column_name '[' term']'
                        | column_name'.' field_name
condition ::= `simple_selection operator term

예를 들어:

UPDATE NerdMovies USING TTL 400
   SET director   = 'Joss Whedon',
       main_actor = 'Nathan Fillion',
       year       = 2005
 WHERE movie = 'Serenity';

UPDATE UserActions
   SET total = total + 2
   WHERE user = B70DE1D0-9908-4AE3-BE34-5573E5B09F14
     AND action = 'click';

UPDATE 문은 테이블의 주어진 행에 대해 하나 이상의 컬럼을 씁니다. WHERE 절은 업데이트할 행을 선택하는 데 사용되며 PRIMARY KEY의 모든 컬럼을 포함해야 합니다. 기본 키가 아닌 컬럼은 SET 키워드를 사용해 설정됩니다. UPDATE 문에서 같은 파티션 키 내의 모든 업데이트는 원자적으로 그리고 격리되어 적용됩니다.

SQL과 달리 UPDATE는 기본적으로 행의 기존 존재를 확인하지 않습니다. 이전에 존재하는 행이 없으면 행이 생성되고, 그렇지 않으면 업데이트됩니다. 또한 어떤 동작이 발생했는지 알 수 있는 방법이 없어요.

IF 조건은 특정 조건이 충족되면 행이 업데이트될지 여부를 선택하는 데 사용할 수 있어요. 다만 IF NOT EXISTS 조건처럼 무시할 수 없는 성능 비용이 발생할 수 있습니다.

SET 할당에 관해서:

  • c = c + 3은 유일하게 허용되는 연산인 증가/감소 카운터입니다. = 기호 뒤의 컬럼 이름은 = 기호 앞의 것과 같아야 해요. 증가/감소는 카운터에만 허용됩니다. 자세한 내용은 카운터 섹션 참고.
  • id = id + <some-collection>id[value1] = value2는 컬렉션용입니다. 컬렉션 섹션 참고.
  • id.field = 3은 non-frozen 사용자 정의 타입의 필드 값을 설정하는 것입니다. UDT 섹션 참고.

업데이트 파라미터

UPDATEINSERT 문은 다음 파라미터를 지원합니다:

  • TTL: 삽입된 값에 대한 선택적 Time To Live(초)를 지정합니다. 설정하면 삽입된 값은 지정된 시간 후 자동으로 데이터베이스에서 제거됩니다. TTL은 컬럼 자체가 아니라 삽입된 값에 관련된다는 점을 유의하세요. 즉 나중에 컬럼을 업데이트하면 TTL도 재설정됩니다(그 업데이트에 지정된 어떤 TTL로). 기본적으로 값은 만료되지 않습니다. TTL 0은 TTL이 없는 것과 동등합니다. 테이블에 default_time_to_live가 있으면 TTL 0이 삽입되거나 업데이트된 값에서 TTL을 제거합니다. TTL null은 TTL 0으로 삽입하는 것과 동등합니다.

UPDATE, INSERT, DELETE, BATCH 문은 다음 파라미터를 지원합니다:

  • TIMESTAMP: 연산의 타임스탬프를 설정합니다. 지정하지 않으면 조정자는 문 실행 시작 시점의 현재 시간(마이크로초)을 타임스탬프로 사용합니다. 보통 이것이 적절한 기본값입니다.

DELETE

행 또는 행의 일부를 삭제하는 것은 DELETE 문을 사용합니다:

delete_statement::= DELETE [ simple_selection ( ',' simple_selection ) ]
	FROM table_name
	[ USING update_parameter ( AND update_parameter )* ]
	WHERE where_clause
	[ IF ( EXISTS | condition ( AND condition)*) ]

예를 들어:

DELETE FROM NerdMovies USING TIMESTAMP 1240003134
 WHERE movie = 'Serenity';

DELETE phone FROM Users
 WHERE userid IN (C73DE1D3-AF08-40F3-B124-3FF3E5109F22, B70DE1D0-9908-4AE3-BE34-5573E5B09F14);

DELETE 문은 컬럼과 행을 삭제합니다. DELETE 키워드 바로 뒤에 컬럼 이름이 제공되면, WHERE 절이 나타내는 행에서 해당 컬럼만 삭제됩니다. 그렇지 않으면 전체 행이 제거됩니다.

WHERE 절은 어떤 행이 삭제될지 지정합니다. IN 연산자를 사용하면 한 문으로 여러 행을 삭제할 수 있어요. 부등식 연산자(예: >=)를 사용하면 행 범위를 삭제할 수 있습니다.

DELETE는 업데이트와 동일한 의미론으로 TIMESTAMP 옵션을 지원합니다.

DELETE 문에서 같은 파티션 키 내의 모든 삭제는 원자적으로 그리고 격리되어 적용됩니다.

DELETE 연산은 UPDATE 및 INSERT 문과 유사하게 IF 절을 사용해 조건부일 수 있습니다. 다만 INSERT 및 UPDATE 문과 마찬가지로 Paxos가 사용되므로 무시할 수 없는 성능 비용이 발생하며, 드물게 사용해야 합니다.

BATCH

BATCH 문으로 여러 INSERT, UPDATE, DELETE를 그룹화해 단일 문으로 실행할 수 있습니다:

batch_statement ::=     BEGIN [ UNLOGGED | COUNTER ] BATCH
                        [ USING update_parameter( AND update_parameter)* ]
                        modification_statement ( ';' modification_statement )*
                        APPLY BATCH
modification_statement ::= insert_statement | update_statement | delete_statement

예를 들어:

BEGIN BATCH
   INSERT INTO users (userid, password, name) VALUES ('user2', 'ch@ngem3b', 'second user');
   UPDATE users SET password = 'ps22dhds' WHERE userid = 'user3';
   INSERT INTO users (userid, password) VALUES ('user4', 'ch@ngem3c');
   DELETE name FROM users WHERE userid = 'user1';
APPLY BATCH;

BATCH 문은 여러 수정 문(삽입/업데이트와 삭제)을 단일 문으로 그룹화합니다. 여러 목적으로 사용됩니다:

  • 여러 업데이트를 배칭할 때 클라이언트-서버 간(그리고 때로 서버 조정자-리플리카 간) 네트워크 왕복을 절약합니다.
  • 주어진 파티션 키에 속하는 BATCH의 모든 업데이트는 격리되어 수행됩니다.
  • 기본적으로 배치의 모든 연산은 모든 변경이 결국 완료되도록(또는 어떤 것도 완료되지 않도록) logged로 수행됩니다. 자세한 내용은 UNLOGGED 배치 참고.

다음에 유의하세요:

  • BATCH 문은 UPDATE, INSERT, DELETE 문만 포함할 수 있습니다(예를 들어 다른 배치는 안 됩니다).
  • 배치는 SQL 트랜잭션의 완전한 대응물이 아닙니다.
  • 각 연산에 대해 타임스탬프가 지정되지 않으면 모든 연산이 같은 타임스탬프(자동 생성 또는 배치 수준에서 제공된 타임스탬프)로 적용됩니다. 타임스탬프 동률의 경우 Cassandra의 충돌 해결 절차 때문에 연산이 BATCH 문에 나열된 순서와 다른 순서로 적용될 수 있어요. 특정 연산 순서를 강제하려면 연산별 타임스탬프를 지정해야 합니다.
  • 단일 파티션에 대한 LOGGED 배치는 최적화로 UNLOGGED 배치로 변환됩니다.

UNLOGGED 배치

기본적으로 Cassandra는 배치 로그를 사용해 배치의 모든 연산이 결국 완료되거나 어떤 것도 완료되지 않도록 보장합니다(다만 연산은 단일 파티션 내에서만 격리됩니다).

배치가 여러 파티션에 걸치면 배치 원자성에 성능 패널티가 있습니다. 이 패널티를 감수하고 싶지 않다면 UNLOGGED 옵션으로 Cassandra가 batchlog를 건너뛰도록 지시할 수 있어요. UNLOGGED 옵션을 사용하면 실패한 배치가 부분적으로만 적용된 채로 남을 수 있습니다.

COUNTER 배치

배치된 카운터 업데이트에는 COUNTER 옵션을 사용하세요. Cassandra의 다른 업데이트와 달리 카운터 업데이트는 idempotent가 아닙니다.

더 알아보기 (Learn more)