조건 함수

조건 함수 (Conditional Functions)

조건에 따라 분기하거나 값을 고르는 함수 모음이에요. 조건식 결과를 직접 활용하고, NULL 처리와 CASE 문을 이해하는 데 도움이 돼요.

출처: 문서

본문

개요 (Overview)

조건 결과 직접 사용하기

조건식은 항상 0, 1 또는 NULL로 결과가 나와요. 그래서 조건 결과를 이렇게 직접 사용할 수 있어요:

SELECT left < right AS is_small
FROM LEFT_RIGHT
┌─is_small─┐
│     ᴺᵁᴸᴸ │
│        1 │
│        0 │
│        0 │
│     ᴺᵁᴸᴸ │
└──────────┘

조건식의 NULL 값

조건식에 NULL 값이 관여하면 결과도 NULL이 돼요.

SELECT
    NULL < 1,
    2 < NULL,
    NULL < NULL,
    NULL = NULL
┌─less(NULL, 1)─┬─less(2, NULL)─┬─less(NULL, NULL)─┬─equals(NULL, NULL)─┐
│ ᴺᵁᴸᴸ          │ ᴺᵁᴸᴸ          │ ᴺᵁᴸᴸ             │ ᴺᵁᴸᴸ               │
└───────────────┴───────────────┴──────────────────┴────────────────────┘

그래서 타입이 Nullable이라면 쿼리를 조심스럽게 구성해야 해요. 다음 예는 multiIf에 equals 조건을 추가하지 못해 생기는 문제를 보여줘요.

SELECT
    left,
    right,
    multiIf(left < right, 'left is smaller', left > right, 'right is smaller', 'Both equal') AS faulty_result
FROM LEFT_RIGHT
┌─left─┬─right─┬─faulty_result────┐
│ ᴺᵁᴸᴸ │     4 │ Both equal       │
│    1 │     3 │ left is smaller  │
│    2 │     2 │ Both equal       │
│    3 │     1 │ right is smaller │
│    4 │  ᴺᵁᴸᴸ │ Both equal       │
└──────┴───────┴──────────────────┘

CASE 문

ClickHouse의 CASE 표현식은 SQL CASE 연산자와 유사한 조건 로직을 제공해요. 조건을 평가하고 첫 번째로 일치하는 조건에 따라 값을 반환해요. ClickHouse는 두 가지 형태의 CASE를 지원해요:

  1. CASE WHEN ... THEN ... ELSE ... END

이 형태는 완전한 유연성을 제공하며 내부적으로 multiIf 함수를 사용해 구현돼요. 각 조건은 독립적으로 평가되고, 표현식은 비상수 값(non-constant values)을 포함할 수 있어요.

SELECT
    number,
    CASE
        WHEN number % 2 = 0 THEN number + 1
        WHEN number % 2 = 1 THEN number * 10
        ELSE number
    END AS result
FROM system.numbers
WHERE number < 5;
┌─number─┬─result─┐
│      0 │      1 │
│      1 │     10 │
│      2 │      3 │
│      3 │     30 │
│      4 │      5 │
└────────┴────────┘
  1. CASE <expr> WHEN <val1> THEN ... WHEN <val2> THEN ... ELSE ... END

이 더 간결한 형태는 상수 값 매칭에 최적화되어 있으며 내부적으로 caseWithExpression()을 사용해요.

예를 들어 다음은 유효해요:

SELECT
    number,
    CASE number
        WHEN 0 THEN 100
        WHEN 1 THEN 200
        ELSE 0
    END AS result
FROM system.numbers
WHERE number < 3;
┌─number─┬─result─┐
│      0 │    100 │
│      1 │    200 │
│      2 │      0 │
└────────┴────────┘

이 형태는 반환 표현식이 상수일 필요가 없어요.

SELECT
    number,
    CASE number
        WHEN 0 THEN number + 1
        WHEN 1 THEN number * 10
        ELSE number
    END
FROM system.numbers
WHERE number < 3;
┌─number─┬─caseWithExpr⋯0), number)─┐
│      0 │                        1 │
│      1 │                       10 │
│      2 │                        2 │
└────────┴──────────────────────────┘
주의사항 (Caveats)

ClickHouse는 어떤 조건도 평가하기 전에 CASE 표현식(또는 multiIf 같은 내부 등가물)의 결과 타입을 결정해요. 이는 반환 표현식이 서로 다른 타임존이나 숫자 타입처럼 타입이 다를 때 중요해요.

  • 결과 타입은 모든 분기 중 가장 큰 호환 타입을 기준으로 선택돼요.
  • 타입이 선택되면 나머지 모든 분기는 런타임에 실행되지 않더라도 이 타입으로 암묵적으로 캐스팅돼요.
  • 타임존이 타입 시그니처의 일부인 DateTime64 같은 타입에서는 놀라운 동작이 발생할 수 있어요: 다른 분기가 다른 타임존을 지정하더라도 처음 만난 타임존이 모든 분기에 사용될 수 있어요.

예를 들어 아래에서 모든 행은 첫 번째 매칭 분기의 타임존, 즉 Asia/Kolkata로 타임스탬프를 반환해요.

SELECT
    number,
    CASE
        WHEN number = 0 THEN fromUnixTimestamp64Milli(0, 'Asia/Kolkata')
        WHEN number = 1 THEN fromUnixTimestamp64Milli(0, 'America/Los_Angeles')
        ELSE fromUnixTimestamp64Milli(0, 'UTC')
    END AS tz
FROM system.numbers
WHERE number < 3;
┌─number─┬──────────────────────tz─┐
│      0 │ 1970-01-01 05:30:00.000 │
│      1 │ 1970-01-01 05:30:00.000 │
│      2 │ 1970-01-01 05:30:00.000 │
└────────┴─────────────────────────┘

여기서 ClickHouse는 여러 개의 DateTime64(3, <timezone>) 반환 타입을 봐요. 처음 보는 DateTime64(3, 'Asia/Kolkata')를 공통 타입으로 추론하고 나머지 분기를 이 타입으로 암묵적으로 캐스팅해요. 이는 의도한 타임존 형식을 보존하기 위해 문자열로 변환해 해결할 수 있어요:

SELECT
    number,
    multiIf(
        number = 0, formatDateTime(fromUnixTimestamp64Milli(0), '%F %T', 'Asia/Kolkata'),
        number = 1, formatDateTime(fromUnixTimestamp64Milli(0), '%F %T', 'America/Los_Angeles'),
        formatDateTime(fromUnixTimestamp64Milli(0), '%F %T', 'UTC')
    ) AS tz
FROM system.numbers
WHERE number < 3;
┌─number─┬─tz──────────────────┐
│      0 │ 1970-01-01 05:30:00 │
│      1 │ 1969-12-31 16:00:00 │
│      2 │ 1970-01-01 00:00:00 │
└────────┴─────────────────────┘

clamp

값을 지정한 최소·최대 경계 안에 제한해요. 값이 최소값보다 작으면 최소값을, 최대값보다 크면 최대값을 반환해요. 그 외에는 값 자체를 반환해요. 모든 인자는 비교 가능한 타입이어야 해요. 결과 타입은 모든 인자 중 가장 큰 호환 타입이에요.

구문 (Syntax)

clamp(value, min, max)

인자 (Arguments)

  • value — 클램프할 값.
  • min — 최소 경계.
  • max — 최대 경계.

반환 값 (Returned value)

[min, max] 범위로 제한된 값.

예제 (Examples)

SELECT clamp(5, 1, 10) AS result;

응답: 5

SELECT clamp(-3, 0, 7) AS result;

응답: 0

SELECT clamp(15, 0, 7) AS result;

응답: 7

greatest

인자 중 가장 큰 값을 반환해요. NULL 인자는 무시돼요.

  • 배열의 경우 사전식으로 가장 큰 배열을 반환해요.
  • DateTime 타입의 경우 결과 타입은 가장 큰 타입으로 승격돼요(예: DateTime32와 혼합되면 DateTime64).

버전 24.12에서 NULL 값을 무시하도록 하위 호환되지 않는 변경이 도입됐어요. 이전에는 인자 중 하나가 NULL이면 NULL을 반환했어요. 이전 동작을 유지하려면 least_greatest_legacy_null_behavior 설정(기본값: false)을 true로 설정하세요.

구문 (Syntax)

greatest(x1[, x2, ...])

인자 (Arguments)

  • x1[, x2, ...] — 비교할 하나 또는 여러 값. 모든 인자는 비교 가능한 타입이어야 해요. Any

반환 값 (Returned value)

인자 중 가장 큰 값으로, 가장 큰 호환 타입으로 승격된 값. Any

예제 (Examples)

SELECT greatest(1, 2, toUInt8(3), 3.) AS result, toTypeName(result) AS type;

응답: {result: 3, type: Float64}

SELECT greatest(['hello'], ['there'], ['world']);

응답: ['world']

if

조건 분기를 수행해요.

  • 조건 cond가 0이 아닌 값으로 평가되면 then 표현식의 결과를 반환해요.
  • cond가 0 또는 NULL로 평가되면 else 표현식의 결과를 반환해요.

short_circuit_function_evaluation 설정은 단락 평가(short-circuit evaluation) 사용 여부를 제어해요. 활성화되면 then 표현식은 cond가 참인 행에서만, else 표현식은 cond가 거짓인 행에서만 평가돼요. 예를 들어 단락 평가로 다음 쿼리를 실행할 때 0으로 나누기 예외가 발생하지 않아요:

SELECT if(number = 0, 0, intDiv(42, number)) FROM numbers(10)

thenelse는 유사한 타입이어야 해요.

구문 (Syntax)

if(cond, then, else)

인자 (Arguments)

  • cond — 평가되는 조건. UInt8, Nullable(UInt8) 또는 NULL
  • thencond가 참일 때 반환되는 표현식.
  • elsecond가 거짓이거나 NULL일 때 반환되는 표현식.

반환 값 (Returned value)

조건 cond에 따른 then 또는 else 표현식의 결과.

예제 (Examples)

SELECT if(1, 2 + 2, 2 + 6) AS res;

응답: 4

least

인자 중 가장 작은 값을 반환해요. NULL 인자는 무시돼요.

  • 배열의 경우 사전식으로 가장 작은 배열을 반환해요.
  • DateTime 타입의 경우 결과 타입은 가장 큰 타입으로 승격돼요(예: DateTime32와 혼합되면 DateTime64).

버전 24.12에서 NULL 값을 무시하도록 하위 호환되지 않는 변경이 도입됐어요. 이전 동작을 유지하려면 least_greatest_legacy_null_behavior 설정(기본값: false)을 true로 설정하세요.

구문 (Syntax)

least(x1[, x2, ...])

인자 (Arguments)

  • x1[, x2, ...] — 비교할 하나 또는 여러 값. 모든 인자는 비교 가능한 타입이어야 해요. Any

반환 값 (Returned value)

인자 중 가장 작은 값으로, 가장 큰 호환 타입으로 승격된 값. Any

예제 (Examples)

SELECT least(1, 2, toUInt8(3), 3.) AS result, toTypeName(result) AS type;

응답: {result: 1, type: Float64}

SELECT least(['hello'], ['there'], ['world']);

응답: ['hello']

multiIf

쿼리에서 CASE 연산자를 더 간결하게 작성할 수 있게 해줘요. 각 조건을 순서대로 평가해요. 참(0이 아니고 NULL이 아닌) 첫 번째 조건에 해당하는 분기 값을 반환해요. 조건 중 어느 것도 참이 아니면 else 값을 반환해요. short_circuit_function_evaluation 설정은 단락 평가 사용 여부를 제어해요. 활성화되면 then_i 표현식은 ((NOT cond_1) AND ... AND (NOT cond_{i-1}) AND cond_i)가 참인 행에서만 평가돼요. 예를 들어 단락 평가로 다음 쿼리에서 0으로 나누기 예외가 발생하지 않아요:

SELECT multiIf(number = 2, intDiv(1, number), number = 5) FROM numbers(10)

모든 분기와 else 표현식은 공통 상위 타입(common supertype)을 가져야 해요. NULL 조건은 거짓으로 처리돼요.

구문 (Syntax)

multiIf(cond_1, then_1, cond_2, then_2, ..., else)

별칭 (Aliases): caseWithoutExpression, caseWithoutExpr

인자 (Arguments)

  • cond_Nthen_N을 반환할지 제어하는 N번째 평가 조건. UInt8, Nullable(UInt8) 또는 NULL
  • then_Ncond_N이 참일 때의 함수 결과.
  • else — 조건 중 어느 것도 참이 아닐 때의 함수 결과.

반환 값 (Returned value)

일치하는 cond_N에 대한 then_N의 결과를 반환하고, 그 외에는 else 조건을 반환해요.

예제 (Examples)

쿼리:

CREATE TABLE LEFT_RIGHT (left Nullable(UInt8), right Nullable(UInt8)) ENGINE = Memory;
INSERT INTO LEFT_RIGHT VALUES (NULL, 4), (1, 3), (2, 2), (3, 1), (4, NULL);

SELECT
    left,
    right,
    multiIf(left < right, 'left is smaller', left > right, 'left is greater', left = right, 'Both equal', 'Null value') AS result
FROM LEFT_RIGHT;

응답:

┌─left─┬─right─┬─result──────────┐
│ ᴺᵁᴸᴸ │     4 │ Null value      │
│    1 │     3 │ left is smaller │
│    2 │     2 │ Both equal      │
│    3 │     1 │ left is greater │
│    4 │  ᴺᵁᴸᴸ │ Null value      │
└──────┴───────┴─────────────────┘

더 알아보기 (Learn more)