LogoSkills

clickhouse-backend-rules

ClickHouse backend type-safety and query rules (scalar casting, etc.). Use when writing ClickHouse queries or fixing scalar type-casting and query errors.

ClickHouse Backend Rules#

ํ•œ๋งˆ๋””๋กœ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์ด ๋ฌธ์„œ๋Š” ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ๋ถ„์„ ์ฐฝ๊ณ ์ธ ClickHouse๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์ž์ฃผ ์ƒ๊ธฐ๋Š” ์‹ค์ˆ˜๋ฅผ ๋ง‰์•„ ์ฃผ๋Š” ์•ˆ์ „ ์ˆ˜์น™ ๋ชจ์Œ์ž…๋‹ˆ๋‹ค. ๋งˆ์น˜ ํฐ ๋ฌผ๋ฅ˜ ์ฐฝ๊ณ ์—์„œ ๋ฌผ๊ฑด์„ ๋„ฃ์„ ๋•Œ์™€ ๊บผ๋‚ผ ๋•Œ ๊ทœ์น™์„ ์ •ํ•ด ๋‘์–ด ๋ฌผ๊ฑด์ด ์‚ฌ๋ผ์ง€๊ฑฐ๋‚˜ ์—‰๋šฑํ•œ ์นธ์— ๋“ค์–ด๊ฐ€๋Š” ์ผ์„ ๋ง‰๋Š” ๊ฒƒ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž(๋˜๋Š” AI)๊ฐ€ ๋ถ„์„ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ๋•Œ ์ด ์ˆ˜์น™์„ ๋”ฐ๋ฅด๋ฉด ์ˆซ์ž๊ฐ€ ์–ด๊ธ‹๋‚˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ ๋ณด์ด๋Š” ํ”ํ•œ ๋ฒ„๊ทธ๋ฅผ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฌด์—‡์„ยท์–ธ์ œ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

  • ๋ฌด์—‡์„ ํ•ด ์ฃผ๋‚˜์š”: ์ˆซ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์ž˜๋ชป ์ฝ์–ด ์ƒ๊ธฐ๋Š” ์˜ค๋ฅ˜, ์ €์žฅํ•œ ๊ณณ๊ณผ ์ฐพ๋Š” ๊ณณ์ด ๋‹ฌ๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์•ˆ ๋ณด์ด๋Š” ๋ฌธ์ œ, ๋นˆ ๊ฐ’(NULL)์ด๋‚˜ ์‹œ๊ฐ„๋Œ€(ํ•œ๊ตญ ์‹œ๊ฐ„) ์ฒ˜๋ฆฌ ์‹ค์ˆ˜๋ฅผ ๋ง‰์•„ ์ค๋‹ˆ๋‹ค.
  • ์–ธ์ œ ์ž‘๋™ํ•˜๋‚˜์š”: ํ†ต๊ณ„๋‚˜ ๋ถ„์„ ๊ฐ™์€ ์ƒˆ ๊ธฐ๋Šฅ์„ ClickHouse ๋ฐ์ดํ„ฐ๋กœ ๋งŒ๋“ค ๋•Œ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด "ํ‰๊ท  ์ฝ์€ ์‹œ๊ฐ„", "์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ ์ˆ˜" ๊ฐ™์€ ์ง‘๊ณ„ ํ™”๋ฉด์„ ๋งŒ๋“ค ๋•Œ ์“ฐ์ž…๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ์ ์œผ๋กœ: ๋ถ„์„ ํ™”๋ฉด์˜ ์ˆซ์ž๊ฐ€ ์ •ํ™•ํ•ด์ง€๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ๋น ์ง€๊ฑฐ๋‚˜ ์‹œ๊ฐ„์ด ์–ด๊ธ‹๋‚˜๋Š” ์ผ์ด ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์šฉ์–ด (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์šฉ์–ด์‰ฌ์šด ์„ค๋ช…
ClickHouse์•„์ฃผ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ชจ์•„ ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ์ €์žฅ ์ฐฝ๊ณ (๋ถ„์„ ์ „์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค).
CDC "๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋”ฐ๋ผ ์˜ฎ๊ธฐ๊ธฐ". ํ•œ์ชฝ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(PostgreSQL)์— ์ €์žฅํ•˜๋ฉด ๊ทธ ๋ณ€ํ™”๋ฅผ ์ž๋™์œผ๋กœ ๋ถ„์„ ์ฐฝ๊ณ (ClickHouse)๋กœ ๋ณต์‚ฌํ•ด ์ฃผ๋Š” ๋ฐฉ์‹.
PostgreSQL๊ฑฐ๋ž˜ ๊ธฐ๋ก์ฒ˜๋Ÿผ ์ •ํ™•ํ•จ์ด ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜๋Š” ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค.
NULL"๊ฐ’์ด ๋น„์–ด ์žˆ์Œ"์„ ๋œปํ•˜๋Š” ์ƒํƒœ. ๊ทธ๋ƒฅ ๋‘๋ฉด 0์ฒ˜๋Ÿผ ์ž˜๋ชป ๊ณ„์‚ฐ๋  ์ˆ˜ ์žˆ์–ด ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•ด ์ค˜์•ผ ํ•จ.
ํƒ€์ž…(type) ์บ์ŠคํŒ…๋ฐ์ดํ„ฐ์˜ ํ˜•ํƒœ(์ •์ˆ˜/์†Œ์ˆ˜ ๋“ฑ)๋ฅผ ๋‹ค๋ฅธ ํ˜•ํƒœ๋กœ ๋ฐ”๊พธ๋Š” ์ผ. ์ž˜๋ชป ๋ฐ”๊พธ๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋‚จ.
ํŠธ๋žœ์žญ์…˜์—ฌ๋Ÿฌ ์ž‘์—…์„ "์ „๋ถ€ ์„ฑ๊ณต ์•„๋‹ˆ๋ฉด ์ „๋ถ€ ์ทจ์†Œ"๋กœ ๋ฌถ์–ด ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด์ค‘๊ฐ„ํ•˜๊ฒŒ ๋‚จ์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ์•ˆ์ „์žฅ์น˜.
UTC / KSTUTC๋Š” ์„ธ๊ณ„ ํ‘œ์ค€ ์‹œ๊ฐ„, KST๋Š” ํ•œ๊ตญ ์‹œ๊ฐ„. ์ €์žฅ์€ UTC๋กœ ํ•˜๊ณ  ํ™”๋ฉด์—๋Š” ํ•œ๊ตญ ์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๊ฟ” ๋ณด์—ฌ ์คŒ.

ClickHouse ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ๋ฒ„๊ทธ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๊ทœ์น™์ž…๋‹ˆ๋‹ค.

ํƒ€์ž… ์•ˆ์ „์„ฑ ๊ทœ์น™#

scalar() ๋ฉ”์„œ๋“œ ํƒ€์ž… ์บ์ŠคํŒ…#

๋ฌธ์ œ: ClickHouse scalar<double>() ๋ฉ”์„œ๋“œ๋Š” ๊ฐ’์ด 0์ผ ๋•Œ int๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

// โŒ WRONG: ๊ฐ’์ด 0์ผ ๋•Œ int ๋ฐ˜ํ™˜ โ†’ ํƒ€์ž… ์—๋Ÿฌ
final avgTime = await client.scalar<double>(
  'SELECT avg(reading_time) FROM ...',
);  // ๊ฒฐ๊ณผ๊ฐ€ 0์ด๋ฉด int(0) ๋ฐ˜ํ™˜ โ†’ double ์บ์ŠคํŒ… ์‹คํŒจ!

// โœ… CORRECT: num์œผ๋กœ ๋ฐ›์•„์„œ toDouble() ๋ณ€ํ™˜
final avgTimeRaw = await client.scalar<num>(
  'SELECT avg(reading_time) FROM ...',
);
final avgTime = avgTimeRaw?.toDouble() ?? 0.0;

ํ—ฌํผ ํ•จ์ˆ˜ ํŒจํ„ด (๊ถŒ์žฅ)#

// analytics_service.dart ์ฐธ์กฐ
double _toDouble(Object? value) {
  if (value == null) return 0;
  if (value is double) return value;
  if (value is int) return value.toDouble();
  if (value is num) return value.toDouble();
  return 0;
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
final result = await client.scalar<num>('SELECT sum(amount) FROM ...');
final amount = _toDouble(result);

Row ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ#

// โŒ WRONG: ์ง์ ‘ ์บ์ŠคํŒ…
final rows = await client.select('SELECT value FROM ...');
final value = rows.first['value'] as double;  // int์ผ ์ˆ˜ ์žˆ์Œ!

// โœ… CORRECT: ํ—ฌํผ ํ•จ์ˆ˜ ์‚ฌ์šฉ
final rows = await client.select('SELECT value FROM ...');
final value = _toDouble(rows.first['value']);

CDC ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ทœ์น™#

๋ฐ์ดํ„ฐ ์ €์žฅ๊ณผ ์กฐํšŒ ํ…Œ์ด๋ธ” ์ผ์น˜ ํ™•์ธ#

๋ฌธ์ œ: ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ํ…Œ์ด๋ธ”๊ณผ ์กฐํšŒํ•˜๋Š” ํ…Œ์ด๋ธ”์ด ๋‹ค๋ฅด๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// โŒ WRONG: ์ €์žฅ๊ณผ ์กฐํšŒ ํ…Œ์ด๋ธ” ๋ถˆ์ผ์น˜
// EventTrackingService โ†’ ClickHouse `events` ํ…Œ์ด๋ธ”์— ์ €์žฅ
await eventService.track('screen_view', ...);

// AnalyticsService โ†’ ClickHouse `user_event` ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ
final result = await client.select('SELECT * FROM user_event');
// ๊ฒฐ๊ณผ: ๋นˆ ๋ฐ์ดํ„ฐ! (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”)

// โœ… CORRECT: CDC ํ๋ฆ„ ์‚ฌ์šฉ
// PostgreSQL `user_event` โ†’ CDC โ†’ ClickHouse `user_event`
await UserEvent.db.insertRow(session, event);
final result = await client.select('SELECT * FROM user_event');

CDC vs ์ง์ ‘ ์ €์žฅ ์„ ํƒ ๊ธฐ์ค€#

๋ฐฉ์‹์‚ฌ์šฉ ์‹œ์ ์žฅ์ ๋‹จ์ 
PostgreSQL CDC ํŠธ๋žœ์žญ์…˜ ์ผ๊ด€์„ฑ ํ•„์š”, ๊ฐ์‚ฌ ์ถ”์  ํ•„์š” ACID, ๊ฐ์‚ฌ ๋กœ๊ทธ, ๋ฐฑ์—… ์ง€์—ฐ ์‹œ๊ฐ„ (1-5์ดˆ)
์ง์ ‘ ClickHouse๊ณ ๋นˆ๋„ ์ด๋ฒคํŠธ, ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์ „์šฉ๋น ๋ฅธ ์ €์žฅํŠธ๋žœ์žญ์…˜ ์—†์Œ
// CDC ๋ฐฉ์‹ (๊ถŒ์žฅ - ๋Œ€๋ถ€๋ถ„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ์ด๋ฒคํŠธ)
final event = UserEvent(
  userId: userId,
  eventName: 'screen_view',
  properties: jsonEncode(properties),
  timestamp: DateTime.now(),
);
await UserEvent.db.insertRow(session, event);

// ์ง์ ‘ ์ €์žฅ ๋ฐฉ์‹ (๊ณ ๋นˆ๋„ ์ด๋ฒคํŠธ ์ „์šฉ)
await eventService.track('mouse_move', properties: {...});

ClickHouse ์ฟผ๋ฆฌ ๊ทœ์น™#

NULL ์ฒ˜๋ฆฌ#

// ClickHouse์—์„œ NULL์€ ๊ธฐ๋ณธ์ ์œผ๋กœ 0 ๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ๋จ
// ๋ช…์‹œ์  NULL ์ฒ˜๋ฆฌ ๊ถŒ์žฅ

// โŒ WRONG: NULL ๋ฏธ์ฒ˜๋ฆฌ
SELECT avg(value) FROM table  // NULL ํฌํ•จ ์‹œ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ

// โœ… CORRECT: NULL ๋ช…์‹œ์  ์ฒ˜๋ฆฌ
SELECT avgIf(value, value IS NOT NULL) FROM table
SELECT coalesce(avg(value), 0) FROM table

๋‚ ์งœ/์‹œ๊ฐ„ ์ฒ˜๋ฆฌ#

// ClickHouse๋Š” UTC ๊ธฐ์ค€์œผ๋กœ ์ €์žฅ
// ํ•œ๊ตญ ์‹œ๊ฐ„(KST) ๋ณ€ํ™˜ ํ•„์š”

// โœ… ์‹œ๊ฐ„๋Œ€ ๋ณ€ํ™˜
SELECT toDateTime(timestamp, 'Asia/Seoul') as local_time FROM ...

// โœ… ๋‚ ์งœ ๊ทธ๋ฃนํ•‘ (ํ•œ๊ตญ ์‹œ๊ฐ„ ๊ธฐ์ค€)
SELECT toDate(timestamp, 'Asia/Seoul') as date, count() FROM ...
GROUP BY date

์ฒดํฌ๋ฆฌ์ŠคํŠธ#

์ƒˆ๋กœ์šด ๋ถ„์„ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์‹œ:

  • scalar<num>() ์‚ฌ์šฉ + _toDouble() ํ—ฌํผ ์ ์šฉ
  • ์ €์žฅ ํ…Œ์ด๋ธ”๊ณผ ์กฐํšŒ ํ…Œ์ด๋ธ” ์ผ์น˜ ํ™•์ธ
  • CDC ํ๋ฆ„ vs ์ง์ ‘ ์ €์žฅ ๊ฒฐ์ •
  • NULL ์ฒ˜๋ฆฌ ๋ช…์‹œ
  • ์‹œ๊ฐ„๋Œ€(KST) ๋ณ€ํ™˜ ์ ์šฉ

๊ด€๋ จ ํŒŒ์ผ#

  • backend/kobic_server/lib/src/feature/analytics/service/analytics_service.dart - _toDouble() ํ—ฌํผ ํŒจํ„ด
  • backend/kobic_server/lib/src/feature/analytics/endpoint/screen_view_endpoint.dart - CDC ๋ฐฉ์‹ ์˜ˆ์‹œ
  • .claude/skills/clickhouse/SKILL.md - CDC ํ…Œ์ด๋ธ” ๋ชฉ๋ก