파이프 연산자(Pipe Operators)

파이프 연산자(Pipe Operators)

파이프 연산자는 GoogleSQL의 파이프 구문과 유사하게, 쿼리를 위에서 아래로 읽히는 변환의 선형 체인으로 작성할 수 있게 해줘요. 각 연산자는 |> 토큰으로 시작하며, 그 앞의 쿼리 결과를 입력으로 받아 하나의 변환을 더 적용해요.

출처: 문서

본문

파이프 연산자는 위에서 아래로 읽히는 변환의 선형 체인으로 쿼리를 작성할 수 있게 해줘요. GoogleSQL의 파이프 구문과 유사해요.

FROM orders
|> WHERE cancelled = 0
|> AGGREGATE sum(amount) AS total GROUP BY customer
|> ORDER BY total DESC
|> LIMIT 3

모든 SELECT 쿼리 뒤에는 파이프 연산자의 체인이 이어질 수 있어요. 각 연산자는 |> 토큰으로 시작하고, 그 앞의 쿼리 결과를 입력으로 받아 하나의 변환을 더 적용해요. 각 연산자 안에서는 일반적인 ClickHouse 구문이 사용돼요. 파이프 연산자는 구문 확장이에요. 각 연산자는 그 앞의 쿼리를 서브쿼리로 감싸므로, 결과 AST는 중첩 서브쿼리로 작성된 동등한 쿼리의 AST와 같아요. 위 쿼리는 다음과 동등해요.

SELECT * FROM
(
    SELECT customer, sum(amount) AS total FROM
    (
        SELECT * FROM
        (
            SELECT * FROM orders
        )
        WHERE cancelled = 0
    )
    GROUP BY customer
)
ORDER BY total DESC
LIMIT 3

FROM queries (FROM 쿼리)

쿼리는 FROM 절로 시작할 수 있으며, 그런 쿼리에서는 SELECT 절이 선택적이에요. 생략하면 SELECT *가 작성된 것처럼 동작해요.

FROM orders;
FROM orders WHERE amount > 100;
FROM orders |> WHERE amount > 100;

테이블 별칭은 일반 SELECT 쿼리의 FROM 절과 마찬가지로 AS 키워드를 쓰거나 쓰지 않고 작성할 수 있어요: FROM orders o WHERE o.amount > 100. 유일한 예외는 별칭이 select라는 단어 하나로 작성된 경우인데, 테이블 뒤에서 명시적인 SELECT 절을 시작하게 되어 별칭으로 취급되지 않아요. select라는 이름의 테이블은 영향을 받지 않고 자체 별칭을 유지해요: FROM select s WHERE s.id = 1. 괄호 안의 서브쿼리도 FROM 절로 시작할 수 있는데, 이로 인해 (from IN ('a'))는 모호해져요. 이는 from이라는 열에 대한 표현식 from IN ('a')로 읽힐 수도 있고, IN이라는 테이블 함수에 대한 서브쿼리 SELECT * FROM IN('a')로 읽힐 수도 있어요. 열로 읽는 것이 더 오래된 방식이라 우선해요. 즉 from이라는 단어로 시작하고 연산자가 뒤따르는 표현식으로 읽히는 괄호 내용은 서브쿼리가 아니라 항상 그 표현식이에요. 다른 해석을 얻으려면 SELECT 절을 명시적으로 작성하세요: 1 IN (SELECT * FROM in). 마지막 테이블의 샘플 오프셋이 쿼리 레벨 OFFSET으로 읽힐 수도 있을 때는 SELECT 절을 생략할 수 없어요. FROM t SAMPLE 1/10 OFFSET 5에서 OFFSETSAMPLE에 속하지만, FROM t SAMPLE 1/10 SELECT * OFFSET 5에서는 쿼리 레벨 OFFSET이기 때문이에요. 이 둘을 구분하려면 명시적인 SELECT가 필요해요. 쿼리가 쿼리 레벨 OFFSET이 앞에 오지 못하는 절로 이어질 때는 모호함이 없으며 SELECT 절은 평소처럼 선택적이에요: FROM t SAMPLE 1/10 OFFSET 5 WHERE x > 0, FROM t SAMPLE 1/10 OFFSET 5 JOIN dim USING (id).

Operators (연산자)

WHERE

|> WHERE condition은 입력 행을 필터링해요. 집계 후에 적용되면 HAVING처럼 동작해요.

FROM orders
|> AGGREGATE sum(amount) AS total GROUP BY customer
|> WHERE total > 100

SELECT

|> SELECT [DISTINCT] expr1 [AS alias1], ...는 나열된 표현식만 출력 열로 남겨요.

FROM orders |> SELECT customer, amount * 2 AS doubled

일반 쿼리의 SELECT 절과 같은 위치에서 표현식 목록 끝에 끝 쉼표(trailing comma)가 허용돼요. 여기서는 쿼리 끝이나 다음 |> 연산자로 이어질 수 있어요: FROM orders |> SELECT customer, amount, |> LIMIT 1. 이는 EXTENDAGGREGATE 연산자에도 동일하게 적용돼요.

EXTEND

|> EXTEND expr1 [AS alias1], ...는 나열된 표현식을 입력 열에 추가해요. SELECT *, expr1 AS alias1, ...와 동등해요.

FROM orders |> EXTEND amount * 10 AS big

SET

|> SET column1 = expr1, ...는 나열된 열의 값을 대체해요. SELECT * REPLACE (expr1 AS column1, ...)와 동등해요.

FROM orders |> SET amount = amount + 1000

DROP

|> DROP column1, ...는 나열된 열을 제거해요. SELECT * EXCEPT (column1, ...)와 동등해요.

FROM orders |> DROP cancelled

AS

|> AS alias는 다음 연산자의 입력에 별칭을 부여해서 해당 연산자에서 참조할 수 있게 해줘요. 주로 조인에 유용해요.

FROM orders
|> AGGREGATE sum(amount) AS total GROUP BY customer
|> AS agg
|> JOIN orders AS o ON agg.customer = o.customer

AGGREGATE

|> AGGREGATE agg1 [AS alias1], ... [GROUP BY expr1 [AS alias1], ...]는 입력 행을 집계해요. 출력 열은 그룹핑 열 다음에 집계 열 순서로 돼요. GROUP BY가 없으면 전체 입력이 단일 행으로 집계돼요.

FROM orders |> AGGREGATE count() AS c, sum(amount) AS total GROUP BY customer;
FROM orders |> AGGREGATE count() AS c;

DISTINCT

|> DISTINCT는 중복 행을 제거해요. SELECT DISTINCT *와 동등해요.

ORDER BY

|> ORDER BY expr1 [ASC/DESC], ...는 입력 행을 정렬해요. ORDER BY ALL, WITH FILL, INTERPOLATE를 포함한 전체 ORDER BY 절 구문이 지원돼요.

FROM orders |> ORDER BY amount DESC;
FROM orders |> SELECT customer, amount |> ORDER BY ALL;
FROM points |> ORDER BY x WITH FILL FROM 1 TO 10 INTERPOLATE (y AS y + 1)

LIMIT and OFFSET

|> LIMIT length [OFFSET offset]|> OFFSET offset은 행 수를 제한해요.

FROM orders |> ORDER BY amount DESC |> LIMIT 3 OFFSET 1

JOIN and ARRAY JOIN

|> [GLOBAL] [ANY/ALL/ASOF/SEMI/ANTI] [INNER/LEFT/RIGHT/FULL/CROSS] JOIN table [ON expr | USING (columns)]는 입력을 다른 테이블, 서브쿼리, 또는 테이블 함수와 조인해요. 모든 종류의 JOINARRAY JOIN이 지원되며, 단일 연산자가 FROM 절처럼 여러 조인을 포함할 수 있어요.

FROM customers
|> AS c
|> LEFT JOIN orders AS o ON c.name = o.customer
|> ARRAY JOIN tags

모든 연산자는 새로운 서브쿼리 스코프이므로 테이블 별칭은 같은 연산자 안에서만 보여요(ON 조건에서). 이후 연산자들은 SELECT * 이후처럼 조인 결과의 결합된 열을 보게 돼요. 크로스 조인의 쉼표 표기도 지원되며, 연산자의 입력이 왼쪽이 돼요: FROM customers |> AS c |> , orders. 다른 조인과 마찬가지로 joined_subquery_requires_alias 설정이 활성화되어 있으면(기본값) 입력에 별칭이 필요해요. 일반 쿼리의 FROM 절에서처럼, 쉼표(크로스) 조인은 ARRAY JOIN 바로 뒤에는 지원되지 않아요. ARRAY JOIN 뒤의 쉼표는 항상 그 표현식 목록에 속해요.

UNION, INTERSECT, and EXCEPT

|> UNION [ALL/DISTINCT] (query1) [, (query2), ...], |> INTERSECT [ALL/DISTINCT] ..., |> EXCEPT [ALL/DISTINCT] ...는 입력을 다른 쿼리의 결과와 결합해요.

FROM orders
|> SELECT customer
|> UNION ALL (FROM customers |> SELECT name)
|> DISTINCT

단일 쿼리의 경우 피연산자 주변의 괄호는 선택적이지만, 집합 연산(set operation) 뒤에 다른 파이프 연산자로 체인이 계속될 때는 필수예요. 그렇지 않으면 다음 연산자가 마지막 피연산자에 적용되는지 전체 결과에 적용되는지가 불분명해지기 때문이에요.

Notes (참고 사항)

  • 쿼리의 WITH 절은 스칼라 별칭과 CTE 모두에 대해 이후의 모든 파이프 연산자에서 계속 보여요: WITH 10 AS threshold FROM t |> WHERE x < threshold.
  • INSERT ... SELECT에서 INSERT 앞에 작성된 WITH 절은 가장 바깥쪽 생성된 SELECT에 붙고, enable_global_with_statement 설정(기본 활성화)을 통해 해석 중에 안쪽 파이프 단계에 도달해요. 이것은 손으로 작성한 중첩 서브쿼리에 도달하는 방식과 같아요. 해당 설정이 비활성화되면 INSERT 스코프의 WITH에서 온 별칭과 CTE는 손으로 작성한 서브쿼리 안에서처럼 파이프 단계 안에서 보이지 않아요.
  • 다른 SELECT 쿼리와 마찬가지로, 파이프 연산자로 생성된 쿼리는 SETTINGS 절로 끝날 수 있으며, 이는 그 생성된 쿼리에 붙어요: FROM t |> LIMIT 1 SETTINGS max_threads = 1SELECT * FROM (SELECT * FROM t) LIMIT 1 SETTINGS max_threads = 1과 같아요. 이것은 서브쿼리, CREATE VIEW, view 테이블 함수처럼 쿼리 설정을 위한 별도 패스가 없는 곳에서도 동작해요. 체인 중간의 SETTINGS 절은 해당 단계에 남아 다음 연산자의 서브쿼리가 돼요. 괄호로 묶인 피연산자가 있는 집합 연산 뒤에는 끝의 SETTINGS가 허용되지 않아요. 서브쿼리가 있는 동등한 쿼리도 그 위치에 SETTINGS 절을 가질 수 없어요.
  • 첫 번째 파이프 연산자 앞 쿼리의 SETTINGS 절은 그 쿼리에 남아, 생성된 래퍼의 서브쿼리가 돼요. 서브쿼리의 설정은 그 서브쿼리가 해석될 때 적용되므로 일반 설정은 계속 동작해요.
  • 파이프 연산자는 집합 연산을 포함한 그 앞의 전체 쿼리에 바인딩돼요. SELECT 1 UNION ALL SELECT 2 |> AGGREGATE count()에서 집계는 UNION ALL의 결과에 적용돼요. 파이프 연산자 뒤 쿼리를 UNION으로 계속하려면 |> UNION 연산자나 괄호를 사용해요.
  • 파이프 연산자는 SELECT 쿼리가 예상되는 곳 어디에서나 사용할 수 있어요: 서브쿼리, INSERT ... SELECT(INSERT INTO t FROM src |> ... 형식 포함), CREATE VIEW, view 테이블 함수 등.
  • 열의 제자리 이름 변경은 별도 연산자로 제공되지 않아요. |> SELECT * EXCEPT (old_name), old_name AS new_name 또는 SETDROP 연산자를 사용해요.

더 알아보기 (Learn more)