ClickHouse の前に薄い API 層を置き、HTTP パラメーターを受け取ってクエリを組み立て、結果を返す構成はよく見られます。ClickHouse 26.8 では、名前付き HTTP ハンドラー、結果の加工、フレーミングフォーマットを使って、その多くを ClickHouse 自体で直接実現できます。
この記事では、これらの機能を使い、英国の不動産価格データセットを題材にストリーミング HTTP API を構築します。API にビジネスロジック、オーケストレーション、アプリケーション固有のバリデーションがあるなら、アプリケーション層は引き続き必要です。しかし、内容を制御した ClickHouse クエリを公開するだけなら、アーキテクチャの層を 1 つ減らせます。
英国不動産価格データセットの準備
英国不動産価格データセットには、1995年以降に英国で売買された不動産の詳細が収められています。 まずテーブルを作成します:
CREATE TABLE uk_price_paid
(
price UInt32,
date Date,
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
type Enum8(
'terraced' = 1,
'semi-detached' = 2,
'detached' = 3,
'flat' = 4,
'other' = 0
),
is_new UInt8,
duration Enum8(
'freehold' = 1,
'leasehold' = 2,
'unknown' = 0
),
addr1 String,
addr2 String,
street LowCardinality(String),
locality LowCardinality(String),
town LowCardinality(String),
district LowCardinality(String),
county LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2);元の CSV にはヘッダーがないため、ClickHouse のスキーマ推論は各列に c1 から c16 までの名前を割り当てます。この位置に基づく列名を使い、取り込みながら元のフィールドを変換します:
INSERT INTO uk_price_paid
SELECT
toUInt32(c2) AS price,
toDate(c3) AS date,
splitByChar(' ', c4)[1] AS postcode1,
splitByChar(' ', c4)[2] AS postcode2,
transform(
c5,
['T', 'S', 'D', 'F', 'O'],
['terraced', 'semi-detached', 'detached', 'flat', 'other']
) AS type,
c6 = 'Y' AS is_new,
transform(
c7,
['F', 'L', 'U'],
['freehold', 'leasehold', 'unknown']
) AS duration,
c8 AS addr1,
c9 AS addr2,
c10 AS street,
c11 AS locality,
c12 AS town,
c13 AS district,
c14 AS county
FROM url(
'http://prod1.publicdata.landregistry.gov.uk.s3-website-eu-west-1.amazonaws.com/pp-complete.csv',
'CSV'
)
SETTINGS
schema_inference_make_columns_nullable = 0,
max_http_get_redirects = 10;データを探索するクエリ
データの概要をつかむために、いくつかクエリを実行して調べます。まずは取引件数と、最初と最後の取引を調べます:
SELECT
count() AS transactions,
min(date) AS first_transaction,
max(date) AS last_transaction
FROM uk_price_paid;┌─transactions─┬─first_transaction─┬─last_transaction─┐
│ 30452463 │ 1995-01-01 │ 2025-07-31 │
└──────────────┴───────────────────┴──────────────────┘次のクエリは、データが占めるディスク容量を計算します:
SELECT formatReadableSize(sum(bytes_on_disk)) AS size_on_disk
FROM system.parts
WHERE database = 'default' AND table = 'uk_price_paid' AND active;┌─size_on_disk─┐
│ 339.08 MiB │
└──────────────┘2024年1月1日以降の平均売却価格が最も高い町と地区を調べたい場合は、次のクエリで求められます:
SELECT town, district, count() AS sales, round(avg(price)) AS average_price
FROM uk_price_paid
WHERE date >= '2024-01-01'
GROUP BY ALL
HAVING sales >= 100
ORDER BY average_price DESC
LIMIT 20;┌─town───────────────┬─district───────────────┬─sales─┬─average_price─┐
│ LONDON │ CITY OF WESTMINSTER │ 3830 │ 2627535 │
│ LONDON │ CITY OF LONDON │ 367 │ 2523498 │
│ LONDON │ KENSINGTON AND CHELSEA │ 2694 │ 2149896 │
│ PURFLEET-ON-THAMES │ THURROCK │ 126 │ 1813003 │
│ VIRGINIA WATER │ RUNNYMEDE │ 114 │ 1600253 │
│ LONDON │ CAMDEN │ 3200 │ 1331681 │
│ LEATHERHEAD │ ELMBRIDGE │ 111 │ 1273186 │
│ BARNET │ ENFIELD │ 116 │ 1235428 │
│ COBHAM │ ELMBRIDGE │ 330 │ 1222086 │
│ RADLETT │ HERTSMERE │ 228 │ 1219858 │
│ LONDON │ RICHMOND UPON THAMES │ 708 │ 1204016 │
│ BEACONSFIELD │ BUCKINGHAMSHIRE │ 384 │ 1160016 │
│ LONDON │ HOUNSLOW │ 660 │ 1157176 │
│ TRING │ DACORUM │ 365 │ 1082008 │
│ LONDON │ HAMMERSMITH AND FULHAM │ 3312 │ 1059507 │
│ ESHER │ ELMBRIDGE │ 418 │ 1017787 │
│ RICHMOND │ RICHMOND UPON THAMES │ 886 │ 988095 │
│ WEYBRIDGE │ ELMBRIDGE │ 612 │ 980195 │
│ LEATHERHEAD │ GUILDFORD │ 162 │ 973938 │
│ ASCOT │ BRACKNELL FOREST │ 147 │ 962569 │
└────────────────────┴────────────────────────┴───────┴───────────────┘API ユーザーの設定
API エンドポイントへのクエリをすべて実行する api_user を作成します。
このユーザーの既定の出力フォーマットは JSONEachRow、既定の上限は 10 件です:
CREATE USER api_user
IDENTIFIED WITH no_password
HOST LOCAL
SETTINGS default_format = 'JSONEachRow', limit = 10;リクエストで別の上限を指定しない限り、limit 設定により、このユーザーへ返る結果はすべて 10 行までになります。
次に、このユーザーが uk_price_paid テーブルをクエリできるようにします:
GRANT SELECT ON uk_price_paid TO api_user;名前付き HTTP エンドポイントの作成
HTTP エンドポイントは、26.8 で導入されたCREATE HANDLER文で作成できます。
ハンドラーには URL と、実行するクエリが必要です。受け付ける HTTP メソッドも指定でき、指定しない場合は GET が既定になります。
Note
対応するメソッドは GET、POST、PUT、DELETE です。
次のハンドラーは 2024年1月1日以降に最も高額な地域を返し、/api/expensive-areas への GET リクエストで呼び出せます:
CREATE HANDLER expensive_areas
URL '/api/expensive-areas'
METHODS (GET)
AS
SELECT town, district, count() AS sales, round(avg(price)) AS average_price
FROM uk_price_paid
WHERE date >= '2024-01-01'
GROUP BY ALL
HAVING sales >= 100
ORDER BY average_price DESC;Note
ハンドラーの作成時に検査されるのはクエリの構文だけで、意味解析は行われません。たとえば、参照しているテーブルや列が存在するかどうかは、API エンドポイントが呼び出されるまで検査されません。
また、このハンドラーのクエリには LIMIT 句がありません。API ユーザーの limit 設定は、HTTP レベルのフィルタリングと並べ替えの後、最終結果に適用されます。これにより既定では 10 行が上限になり、リクエストごとに上書きできます。
このエンドポイントは cURL で呼び出せます:
curl --silent --user 'api_user:' 'http://localhost:8123/api/expensive-areas'{"town":"LONDON","district":"CITY OF WESTMINSTER","sales":3830,"average_price":2627535}
{"town":"LONDON","district":"CITY OF LONDON","sales":367,"average_price":2523498}
{"town":"LONDON","district":"KENSINGTON AND CHELSEA","sales":2694,"average_price":2149896}
{"town":"PURFLEET-ON-THAMES","district":"THURROCK","sales":126,"average_price":1813003}
{"town":"VIRGINIA WATER","district":"RUNNYMEDE","sales":114,"average_price":1600253}
{"town":"LONDON","district":"CAMDEN","sales":3200,"average_price":1331681}
{"town":"LEATHERHEAD","district":"ELMBRIDGE","sales":111,"average_price":1273186}
{"town":"BARNET","district":"ENFIELD","sales":116,"average_price":1235428}
{"town":"COBHAM","district":"ELMBRIDGE","sales":330,"average_price":1222086}
{"town":"RADLETT","district":"HERTSMERE","sales":228,"average_price":1219858}まずはこれで動きますが、このクエリは日付と売買件数の値がハードコードされているため用途が限られます。 次はこの点を解決します。
型付きパラメーター
通常のクエリと同じように、ハンドラーのクエリにもパラメーターを含められます。 次のハンドラーのクエリは、指定された町、日付、価格で取引を絞り込みます:
CREATE HANDLER town_sales
URL '/api/town-sales'
METHODS (GET)
AS
SELECT
date, price, type, duration,
concat(postcode1, ' ', postcode2) AS postcode,
district, street, addr1, addr2
FROM uk_price_paid
WHERE town = upperUTF8({town:String})
AND date >= {from:Date}
AND price >= {minimum_price:UInt32}
ORDER BY price DESC;API エンドポイントは次のように、パラメーターをクエリ文字列で渡して呼び出します:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales' \
--data-urlencode 'town=London' \
--data-urlencode 'from=2024-01-01' \
--data-urlencode 'minimum_price=1000000'{"date":"2024-03-20","price":164300000,"type":"other","duration":"leasehold","postcode":"E14 5GX","district":"TOWER HAMLETS","street":"WATER STREET","addr1":"15","addr2":""}
{"date":"2024-03-20","price":164300000,"type":"other","duration":"leasehold","postcode":"E14 5GX","district":"TOWER HAMLETS","street":"WATER STREET","addr1":"15","addr2":""}
{"date":"2024-03-20","price":161890000,"type":"other","duration":"leasehold","postcode":"E14 5GX","district":"TOWER HAMLETS","street":"WATER STREET","addr1":"UNIT D1.1, 14","addr2":""}
{"date":"2024-12-13","price":138900000,"type":"other","duration":"leasehold","postcode":"NW1 4NT","district":"CITY OF WESTMINSTER","street":"INNER CIRCLE","addr1":"THE HOLME COTTAGE","addr2":""}
{"date":"2024-01-31","price":129706651,"type":"other","duration":"leasehold","postcode":"SW1A 2WH","district":"CITY OF WESTMINSTER","street":"THE MALL","addr1":"ADMIRALTY ARCH HOTEL","addr2":""}
{"date":"2025-03-31","price":124519556,"type":"other","duration":"freehold","postcode":"WC2E 7PS","district":"CITY OF WESTMINSTER","street":"TAVISTOCK STREET","addr1":"15","addr2":""}
{"date":"2024-01-23","price":115000000,"type":"other","duration":"freehold","postcode":"SW1Y 4SP","district":"CITY OF WESTMINSTER","street":"HAYMARKET","addr1":"HAYMARKET HOUSE, 28 - 29","addr2":"FIRST FLOOR"}
{"date":"2025-01-22","price":109500000,"type":"other","duration":"freehold","postcode":"EC2R 8EJ","district":"CITY OF LONDON","street":"POULTRY","addr1":"1","addr2":""}
{"date":"2024-07-05","price":101000000,"type":"other","duration":"freehold","postcode":"EC1R 5EN","district":"CAMDEN","street":"BACKHILL","addr1":"6","addr2":""}
{"date":"2024-11-30","price":93836616,"type":"other","duration":"leasehold","postcode":" ","district":"WANDSWORTH","street":"NINE ELMS LANE","addr1":"BUILDING A01 EMBASSY GARDENS","addr2":""}パラメーターは POST リクエストのボディにフォームフィールドとして渡すこともできますが、この記事では POST エンドポイントは扱いません。
URL パス内の型付きパラメーター
パラメーターは URL パスでも指定できます。
次のハンドラーは、指定された URL から town と district_path を取り出します:
CREATE HANDLER town_sales_path
URL REGEXP '/api/town-sales/(?P<town>[^/]+)(?P<district_path>(?:/[^/]+)?)'
METHODS (GET)
AS
SELECT date, price, type, duration,
concat(postcode1, ' ', postcode2) AS postcode,
town, district, street, addr1, addr2
FROM uk_price_paid
WHERE town = upperUTF8({town:String})
AND (
{district_path:String} = ''
OR district = upperUTF8(substring({district_path:String}, 2))
);町は必須ですが、地区は省略できます。地区を指定しない場合、クエリは町だけで絞り込みます。
次のクエリはロンドンの売買を取得します:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON'{"date":"2010-10-15","price":420000,"type":"terraced","duration":"freehold","postcode":" ","town":"LONDON","district":"ISLINGTON","street":"ST CLEMENTS STREET","addr1":"1","addr2":""}
{"date":"2010-12-17","price":250000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"TOWER HAMLETS","street":"BALTIMORE WHARF","addr1":"1","addr2":"APARTMENT 703"}
{"date":"2012-12-06","price":320000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"BARNET","street":"LICHFIELD GROVE","addr1":"1","addr2":"FLAT 2"}
{"date":"2023-06-13","price":240000,"type":"other","duration":"leasehold","postcode":" ","town":"LONDON","district":"CITY OF LONDON","street":"GREAT ST THOMAS APOSTLE","addr1":"1 - 7","addr2":"COMMERCIAL UNIT 2"}
{"date":"2013-04-30","price":250000,"type":"semi-detached","duration":"freehold","postcode":" ","town":"LONDON","district":"GREENWICH","street":"MYRA STREET","addr1":"1 UNITY MEWS","addr2":""}
{"date":"2009-02-05","price":554000,"type":"detached","duration":"freehold","postcode":" ","town":"LONDON","district":"HACKNEY","street":"ANDRE STREET","addr1":"10","addr2":""}
{"date":"2022-12-15","price":1630000,"type":"other","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"BLOOMSBURY WAY","addr1":"10","addr2":"EIGHTH FLOOR"}
{"date":"2011-07-11","price":610000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"WILMOT PLACE","addr1":"10","addr2":"GROUND FLOOR FLAT"}
{"date":"2022-12-15","price":1630000,"type":"other","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"BLOOMSBURY WAY","addr1":"10","addr2":"NINTH FLOOR"}
{"date":"2023-08-04","price":537500,"type":"other","duration":"leasehold","postcode":" ","town":"LONDON","district":"ISLINGTON","street":"DRAYTON PARK","addr1":"100","addr2":"PARKING SPACE 35"}たとえばカムデンに絞り込みたい場合は、次のようにします:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON/CAMDEN'{"date":"2002-09-25","price":900000,"type":"detached","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"KILBURN HIGH ROAD","addr1":"1 - 4","addr2":"UNITS"}
{"date":"2002-03-19","price":235000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"GREENCROFT GARDENS","addr1":"106","addr2":"FLAT 7"}
{"date":"2002-05-31","price":248000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"CROFTDOWN ROAD","addr1":"11","addr2":"FIRST FLOOR FLAT"}
{"date":"2004-03-12","price":285000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"CROFTDOWN ROAD","addr1":"11","addr2":"SECOND FLOOR FLAT"}
{"date":"2002-05-17","price":215000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"HAVERSTOCK HILL","addr1":"119","addr2":"STUDIO B"}
{"date":"2002-06-26","price":239000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"FITZROY STREET","addr1":"12 - 16","addr2":"FLAT 83"}
{"date":"2003-08-18","price":290000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"FITZROY STREET","addr1":"12 - 16","addr2":"FLAT 88"}
{"date":"2004-09-17","price":625000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"WEDDERBURN ROAD","addr1":"13","addr2":"FIRST FLOOR FLAT"}
{"date":"2005-03-08","price":290000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"ABBEY ROAD","addr1":"132","addr2":"BASEMENT FLAT"}
{"date":"2003-06-27","price":310000,"type":"flat","duration":"leasehold","postcode":" ","town":"LONDON","district":"CAMDEN","street":"ABBEY ROAD","addr1":"132","addr2":"SECOND FLOOR FLAT"}結果を加工する汎用設定
26.8 より前から、ClickHouse はクエリ本文に手を入れずに結果を加工するパラメーターとして limit と offset に対応していました。
26.8 では新しいクエリ設定(filter、select、sort、order、page)に加え、出力データフォーマットを明示的に上書きする output_format が追加されました。他の新しい設定は圧縮と入力フォーマットに関するもので、この読み取り専用 API の例では扱いません。
これらの設定の使い方を、order と limit から見ていきます:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=price DESC' \
--data-urlencode 'limit=3'{"date":"2017-07-31","price":594300000,"type":"other","duration":"leasehold","postcode":"W1U 8EW","town":"LONDON","district":"CITY OF WESTMINSTER","street":"BAKER STREET","addr1":"55","addr2":"UNIT 53"}
{"date":"2018-02-08","price":569200000,"type":"other","duration":"freehold","postcode":"W1J 7BT","town":"LONDON","district":"CITY OF WESTMINSTER","street":"STANHOPE ROW","addr1":"2","addr2":""}
{"date":"2019-11-20","price":542540820,"type":"other","duration":"freehold","postcode":"NW5 2HB","town":"LONDON","district":"CAMDEN","street":"FORTESS ROAD","addr1":"36","addr2":""}上の例のとおり、order は SQL の並べ替え式を受け取ります。sort のほうは、より簡潔で URL に向いた構文を使います。
フィールド名の前に - を付けると降順になります:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'sort=-price' \
--data-urlencode 'limit=3'結果を絞り込んで、1,000,000 ポンド超で売却された物件だけを返すこともできます:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date' \
--data-urlencode 'limit=3' \
--data-urlencode 'filter=price > 1000000'{"date":"1995-01-06","price":1250000,"type":"semi-detached","duration":"freehold","postcode":"W9 1AL","town":"LONDON","district":"CITY OF WESTMINSTER","street":"","addr1":"WHITE LODGE, 19A","addr2":""}
{"date":"1995-01-06","price":1252500,"type":"semi-detached","duration":"leasehold","postcode":"NW1 7SR","town":"LONDON","district":"CAMDEN","street":"PRINCE ALBERT ROAD","addr1":"7","addr2":""}
{"date":"1995-01-06","price":1600000,"type":"terraced","duration":"freehold","postcode":"SW10 9SW","town":"LONDON","district":"KENSINGTON AND CHELSEA","street":"HARLEY GARDENS","addr1":"2","addr2":""}フィルターは複数指定できます。次のクエリは、2024年以降に 1,000,000 ポンド超で売却された物件を返します。2 つ目の並べ替えフィールドも追加します:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date, price DESC' \
--data-urlencode 'limit=3' \
--data-urlencode 'filter=price > 1000000' \
--data-urlencode "filter=date >= '2024-01-01'"{"date":"2024-01-02","price":6000000,"type":"flat","duration":"leasehold","postcode":"NW8 7HN","town":"LONDON","district":"CITY OF WESTMINSTER","street":"ST JOHNS WOOD ROAD","addr1":"60","addr2":"APARTMENT 94"}
{"date":"2024-01-02","price":2400000,"type":"flat","duration":"leasehold","postcode":"SW19 5EF","town":"LONDON","district":"MERTON","street":"HIGH STREET WIMBLEDON","addr1":"EAGLE HOUSE","addr2":"7"}
{"date":"2024-01-02","price":1470000,"type":"flat","duration":"leasehold","postcode":"E14 9LX","town":"LONDON","district":"TOWER HAMLETS","street":"PARK DRIVE","addr1":"1","addr2":"APARTMENT 5402"}あるいは、両方のフィルター条件を AND でつなぐこともできます:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date, price DESC' \
--data-urlencode 'limit=3' \
--data-urlencode "filter=price > 1000000 AND date >= '2024-01-01'"select を使うと、返す列を選べます:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date, price DESC' \
--data-urlencode 'limit=3' \
--data-urlencode "filter=price > 1000000 AND date >= '2024-01-01'" \
--data-urlencode "select=date,price,postcode"{"date":"2024-01-02","price":6000000,"postcode":"NW8 7HN"}
{"date":"2024-01-02","price":2400000,"postcode":"SW19 5EF"}
{"date":"2024-01-02","price":1470000,"postcode":"E14 9LX"}page を使うと、結果をページ送りできます。次のリクエストは続きの 3 件を取得します。日付と価格が同じ行でも順序が定まるように、並べ替えのフィールドを追加しています:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date, price DESC, postcode, district, street, addr1, addr2' \
--data-urlencode 'limit=3' \
--data-urlencode 'page=2' \
--data-urlencode "filter=price > 1000000 AND date >= '2024-01-01'" \
--data-urlencode "select=date,price,postcode"{"date":"2024-01-02","price":1300000,"postcode":"E1W 1AG"}
{"date":"2024-01-02","price":1300000,"postcode":"NW1 1NB"}
{"date":"2024-01-02","price":1300000,"postcode":"W6 8JN"}出力フォーマットも変更できます:
curl --silent --user 'api_user:' --get 'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'order=date, price DESC' \
--data-urlencode 'limit=3' \
--data-urlencode "filter=price > 1000000 AND date >= '2024-01-01'" \
--data-urlencode 'output_format=JSONCompactColumns'[
["2024-01-02", "2024-01-02", "2024-01-02"],
[6000000, 2400000, 1470000],
["flat", "flat", "flat"],
["leasehold", "leasehold", "leasehold"],
["NW8 7HN", "SW19 5EF", "E14 9LX"],
["LONDON", "LONDON", "LONDON"],
["CITY OF WESTMINSTER", "MERTON", "TOWER HAMLETS"],
["ST JOHNS WOOD ROAD", "HIGH STREET WIMBLEDON", "PARK DRIVE"],
["60", "EAGLE HOUSE", "1"],
["APARTMENT 94", "7", "APARTMENT 5402"]
]動的なフィルタリング
26.8 リリースでは、新しい設定 http_allow_filters_as_unrecognized_url_parameters も追加されました。この設定を有効にすると、ClickHouse は認識できない URL パラメーターを WHERE のフィルターとして解釈します。
api_user に対しては次のように有効化します:
ALTER USER api_user
ADD SETTINGS http_allow_filters_as_unrecognized_url_parameters = 1;これにより、先ほど filter 設定で書いたクエリを簡潔にできます:
curl --silent --user 'api_user:' --get "http://localhost:8123/api/town-sales/LONDON" \
--data-urlencode 'order=date,price DESC' \
--data-urlencode 'limit=3' \
--data-urlencode 'price>1000000' \
--data-urlencode 'date>=2024-01-01'ハンドラーの一覧
作成したハンドラーは system.handlers テーブルで確認できます:
SELECT name, url, methods
FROM system.handlers
ORDER BY name;┌─name────────────┬─url───────────────────────────────────────────────────────────┬─methods─┐
│ expensive_areas │ /api/expensive-areas │ ['GET'] │
│ town_sales │ /api/town-sales │ ['GET'] │
│ town_sales_path │ /api/town-sales/(?P<town>[^/]+)(?P<district_path>(?:/[^/]+)?) │ ['GET'] │
└─────────────────┴───────────────────────────────────────────────────────────────┴─────────┘テーブルを API エンドポイントにする
独自のエンドポイントを作るだけでなく、テーブルそのものを API エンドポイントにすることもできます。
そのためには、グローバル設定 http_allow_path_requests を有効にする必要があります:
config.d/http-path-requests.yaml
http_allow_path_requests: 1http_allow_path_requests はサーバーを再起動しないと変更できないため、ClickHouse を再起動して設定を反映します。
api_user 側でも、追加の設定を有効にする必要があります:
ALTER USER api_user
ADD SETTINGS
http_allow_table_as_file = 1,
http_allow_database_as_path = 1;http_allow_table_as_fileは、パスの最後の要素を table、table.format、または table.format.compression として解釈します。http_allow_database_as_pathは、先頭の /database/ パス要素を現在のデータベースとして解釈します。
これが済めば、ロンドンで最も高額な物件を CSV 形式で返すクエリを次のように書けます:
curl --silent --user 'api_user:' 'http://localhost:8123/default/uk_price_paid.csv?town=LONDON&sort=-price&limit=3'594300000,"2017-07-31","W1U","8EW","other",0,"leasehold","55","UNIT 53","BAKER STREET","","LONDON","CITY OF WESTMINSTER","GREATER LONDON"
569200000,"2018-02-08","W1J","7BT","other",0,"freehold","2","","STANHOPE ROW","","LONDON","CITY OF WESTMINSTER","GREATER LONDON"
542540820,"2019-11-20","NW5","2HB","other",0,"freehold","36","","FORTESS ROAD","","LONDON","CAMDEN","GREATER LONDON"出力フォーマットは、テーブル名の接尾辞ではなく format パラメーターで制御することもできます。また、独自エンドポイントと同じように、select パラメーターで返すフィールドを指定できます:
curl --silent \
--user 'api_user:' \
--get 'http://localhost:8123/default/uk_price_paid' \
--data-urlencode 'town=LONDON' \
--data-urlencode 'sort=-price' \
--data-urlencode 'limit=3' \
--data-urlencode 'select=date,price,postcode1' \
--data-urlencode 'format=JSONEachRow'{"date":"2017-07-31","price":594300000,"postcode1":"W1U"}
{"date":"2018-02-08","price":569200000,"postcode1":"W1J"}
{"date":"2019-11-20","price":542540820,"postcode1":"NW5"}HTTP レスポンスのストリーミング
HTTP レスポンスのストリームには、データ、進捗、合計(totals)、プロファイルイベント、サーバーログ、例外を載せられるようになりました。
フレーミングフォーマット(framing format)は、クエリの通常の出力を型付きパケットのストリームで包みます。次の例では、data パケットに JSONEachRow の結果が入り、progress パケットは ClickHouse が行った処理量を報告します。簡潔にするため、プロファイルイベントのパケットは無効にしています。
curl --no-buffer --silent --user 'api_user:' --get \
'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'sort=-price' \
--data-urlencode 'limit=3' \
--data-urlencode 'select=date,price,postcode' \
--data-urlencode 'framing_output_format=JSONEachPacketString' \
--data-urlencode 'send_profile_events=0'{"packet":"data","data":"{\"date\":\"2017-07-31\",\"price\":594300000,\"postcode\":\"W1U 8EW\"}\n{\"date\":\"2018-02-08\",\"price\":569200000,\"postcode\":\"W1J 7BT\"}\n{\"date\":\"2019-11-20\",\"price\":542540820,\"postcode\":\"NW5 2HB\"}\n"}
{"packet":"progress","progress":{"read_rows":"2867200","read_bytes":"39810980","total_rows_to_read":"2867200","result_rows":"3","result_bytes":"902","elapsed_ns":"10055000","memory_usage":"1860249"}}packet フィールドが各パケットの内容を示します。ここでは、1 つ目のパケットにクエリ結果が、2 つ目に最終的な進捗カウンターが入っています。各パケットが届いた時点で表示されるように、curl の --no-buffer オプションを使っています。
フレーミング出力フォーマットに EventStream を指定すると、クエリのレスポンスを Server-Sent Events(SSE)として返すこともできます:
curl --no-buffer --silent --user 'api_user:' --get \
'http://localhost:8123/api/town-sales/LONDON' \
--data-urlencode 'sort=-price' \
--data-urlencode 'limit=3' \
--data-urlencode 'select=date,price,postcode' \
--data-urlencode 'framing_output_format=EventStream' \
--data-urlencode 'send_profile_events=0'event: data
data: eyJkYXRlIjoiMjAxNy0wNy0zMSIsInByaWNlIjo1OTQzMDAwMDAsInBvc3Rjb2RlIjoiVzFVIDhFVyJ9CnsiZGF0ZSI6IjIwMTgtMDItMDgiLCJwcmljZSI6NTY5MjAwMDAwLCJwb3N0Y29kZSI6IlcxSiA3QlQifQp7ImRhdGUiOiIyMDE5LTExLTIwIiwicHJpY2UiOjU0MjU0MDgyMCwicG9zdGNvZGUiOiJOVzUgMkhCIn0K
event: progress
data: {"read_rows":"2867200","read_bytes":"39810980","total_rows_to_read":"2867200","result_rows":"3","result_bytes":"902","elapsed_ns":"9820000","memory_usage":"1862345"}data イベントの値は Base64 でエンコードされています。SSE は行指向の UTF-8 テキストプロトコルですが、ClickHouse の出力は複数行、不正な UTF-8、任意のバイナリデータを含み得ます。そのため ClickHouse は各 data パケットのペイロードを Base64 でエンコードします。これで 1 つの SSE フィールドに安全に載せられ、クライアント側では元のバイト列に復元できます。progress などの他のイベントは、そのまま読める JSON です。
前の例はどちらも処理が速すぎて、ClickHouse が途中の進捗パケットを返す前に終わってしまいます。途中の進捗を見るために、次のクエリではデータセット全体を走査し、年ごとの売買件数、平均価格、価格の中央値を計算します:
curl --no-buffer --silent --user 'api_user:' --get \
'http://localhost:8123/' \
--data-urlencode 'query=SELECT toYear(date) AS year, type, count() AS sales, round(avg(price)) AS average_price, quantileExact(0.5)(price) AS median_price FROM uk_price_paid GROUP BY year, type ORDER BY year, type' \
--data-urlencode 'framing_output_format=EventStream' \
--data-urlencode 'send_profile_events=0' \
--data-urlencode 'interactive_delay=50000' \
--data-urlencode 'max_threads=1'interactive_delay=50000 で進捗パケットを 50 ミリ秒ごとに送れるようにし、max_threads=1 でクエリを遅くしてパケットが見えるようにしています。これらの設定はデモのためのものです。本番環境では使わないでください。
event: progress
data: {"read_rows":"1177362","read_bytes":"8241534","total_rows_to_read":"6501466","elapsed_ns":"51300000"}
event: progress
data: {"read_rows":"14466268","read_bytes":"101263876","total_rows_to_read":"23712297","elapsed_ns":"252277000"}
event: progress
data: {"read_rows":"30452463","read_bytes":"213167241","total_rows_to_read":"30452463","elapsed_ns":"399507000"}
event: data
data: eyJ5ZWFy...Cg==
event: progress
data: {"read_rows":"30452463","read_bytes":"213167241","total_rows_to_read":"30452463","result_rows":"10","result_bytes":"917","elapsed_ns":"409242000","memory_usage":"19570057"}進捗パケットは、ClickHouse がテーブルを走査している間に届きます。これは集計クエリなので、data パケットは ClickHouse が最終的なグループを計算し終えた終盤に届きます。
API リクエストの観測
名前付きハンドラーへのリクエストと、テーブルを直接指す API パスへのリクエストは system.query_log に記録されます。最近成功した HTTP リクエストは次のクエリで確認できます:
SYSTEM FLUSH LOGS;
SELECT event_time, http_handler_name, http_request_url, query_duration_ms, read_rows, result_rows
FROM system.query_log
WHERE (type = 'QueryFinish') AND (http_request_url != '')
ORDER BY event_time DESC
LIMIT 3;┌──────────event_time─┬─http_handler_name─┬─http_request_url───────────┬─query_duration_ms─┬─read_rows─┬─result_rows─┐
│ 2026-09-02 12:28:58 │ town_sales │ /api/town-sales │ 12 │ 2300102 │ 10 │
│ 2026-09-02 12:28:16 │ │ /default/uk_price_paid.csv │ 26 │ 26946287 │ 3 │
│ 2026-09-02 12:28:12 │ town_sales_path │ /api/town-sales/LONDON │ 15 │ 647168 │ 13958 │
└─────────────────────┴───────────────────┴────────────────────────────┴───────────────────┴───────────┴─────────────┘名前付きハンドラーの場合、http_handler_name にリクエストを処理したハンドラーが入ります。テーブルを直接指す API リクエストでは空ですが、http_request_url にはクエリ文字列を除いたリクエストパスが記録されます。
26.8 では新しいシステムテーブル system.user_query_log も導入され、ユーザーはクエリログ全体へのアクセス権なしに自分のクエリ履歴を見られます。これにより、api_user が実行したすべての API リクエストは次のように取得できます:
./clickhouse client --user api_user <<'SQL'
SELECT
event_time, http_handler_name, http_request_url,
query_duration_ms, read_rows, result_rows
FROM system.user_query_log
WHERE type = 'QueryFinish' AND http_request_url != ''
ORDER BY event_time DESC
LIMIT 3 FORMAT Pretty;
SQLハンドラーへのアクセス制御
ハンドラーの URL は HTTP サーバーに到達できるどのユーザーからでもリクエストできますが、そのクエリは認証されたユーザーの権限で実行されます。ハンドラー単位で呼び出しを許可する個別の権限はありません。
実際に確かめるために、uk_price_paid へのアクセス権を持たない別の API ユーザーを作成します:
CREATE USER restricted_api_user
IDENTIFIED WITH no_password
HOST LOCAL
SETTINGS default_format = 'JSONEachRow', limit = 10;付与されている権限は次のコマンドで確認できます:
SHOW GRANTS FOR restricted_api_user;Ok.
0 rows in set. Elapsed: 0.002 sec.このユーザーで expensive_areas ハンドラーを呼び出してみます:
curl --silent --user 'restricted_api_user:' 'http://localhost:8123/api/expensive-areas'次の出力が返ります:
Code: 497. DB::Exception: restricted_api_user: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON default.uk_price_paid. (ACCESS_DENIED) (version 26.9.1.466 (official build))ハンドラーはテーブルへのクエリだけでなく、ビューへのクエリも包めます。テーブル全体を公開せずにクエリでアクセスさせる便利な方法です。
たとえば、元の取引データではなく、expensive_areas の背後にある集計結果だけを公開するビューを作成できます。
ビューを所有するユーザーを作成します。このユーザーは uk_price_paid テーブルをクエリできますが、HOST NONE により誰もこのアカウントで直接ログインできません:
CREATE USER api_view_owner
IDENTIFIED WITH no_password
HOST NONE;
GRANT SELECT ON uk_price_paid
TO api_view_owner;次に、許可するクエリを含む DEFINER ビューを作成します:
CREATE VIEW expensive_areas_api
DEFINER = api_view_owner
SQL SECURITY DEFINER
AS
SELECT town, district, count() AS sales, round(avg(price)) AS average_price
FROM uk_price_paid
WHERE date >= '2024-01-01'
GROUP BY ALL
HAVING sales >= 100;SQL SECURITY DEFINER を付けると、ビューの元になるクエリは呼び出し元ではなく api_view_owner の権限で実行されます。
続いて、このビューへのアクセス権を restricted_api_user に付与します:
GRANT SELECT ON expensive_areas_api
TO restricted_api_user;これで、最初のハンドラーが uk_price_paid を直接ではなく expensive_areas_api をクエリするように書き換えられます:
ALTER HANDLER expensive_areas
AS
SELECT *
FROM expensive_areas_api
ORDER BY average_price DESC;制限付きユーザーからハンドラーを呼び出せるようになりました:
curl --silent --user 'restricted_api_user:' 'http://localhost:8123/api/expensive-areas'{"town":"LONDON","district":"CITY OF WESTMINSTER","sales":3830,"average_price":2627535}
{"town":"LONDON","district":"CITY OF LONDON","sales":367,"average_price":2523498}
{"town":"LONDON","district":"KENSINGTON AND CHELSEA","sales":2694,"average_price":2149896}
{"town":"PURFLEET-ON-THAMES","district":"THURROCK","sales":126,"average_price":1813003}
{"town":"VIRGINIA WATER","district":"RUNNYMEDE","sales":114,"average_price":1600253}
{"town":"LONDON","district":"CAMDEN","sales":3200,"average_price":1331681}
{"town":"LEATHERHEAD","district":"ELMBRIDGE","sales":111,"average_price":1273186}
{"town":"BARNET","district":"ENFIELD","sales":116,"average_price":1235428}
{"town":"COBHAM","district":"ELMBRIDGE","sales":330,"average_price":1222086}
{"town":"RADLETT","district":"HERTSMERE","sales":228,"average_price":1219858}しかし、元のテーブルにはアクセスできません:
./clickhouse client -mn --user restricted_api_user --query "SELECT * FROM uk_price_paid"Received exception from server (version 26.9.1):
Code: 497. DB::Exception: Received from localhost:9000. DB::Exception: restricted_api_user: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON default.uk_price_paid. (ACCESS_DENIED)
(query: SELECT * FROM default.uk_price_paid LIMIT 1)この方法でユーザーが制限されるのは、ハンドラーの URL だけではなく、ビューが公開するデータの範囲です。restricted_api_user は expensive_areas_api ビューを直接クエリできますが、元の不動産取引データには依然としてアクセスできません。
まとめ
この記事では、ClickHouse 26.8 で導入された機能を使い、英国不動産価格データセットを題材に HTTP API を構築しました。名前付きエンドポイントを作成し、型付きのパラメーターをクエリ文字列と URL パスの両方で受け取りました。元のクエリを変えずに結果を加工し、テーブルを URL 経由で直接公開し、クエリのデータと進捗を同じレスポンスでストリーミングしました。
また、クエリログで API リクエストを観測し、SQL SECURITY DEFINER ビューを使って、元の取引データへのアクセス権を付与せずに集計結果を公開しました。API が内容を制御した ClickHouse クエリを公開するだけでよいなら、これらの機能により別途パススルーサービスを用意する必要はなくなります。ただし、ビジネスロジック、オーケストレーション、アプリケーション固有のバリデーションが必要な場合は、アプリケーション層は引き続き必要です。



