ClickHouse HOLs
GitHub

JSON Type Stress Test


A stress test of the JSON data type's path limits: what happens as the number of distinct paths per row grows past max_dynamic_paths, where the overflow goes, and what reading from that overflow costs.

📋 Overview

Every JSON column has a budget of dynamic paths — paths stored as real subcolumns. Paths beyond max_dynamic_paths are not rejected; they spill into shared data, which still queries correctly but far more expensively. This lab pushes a table to 50,000 fields per row, measures the dynamic/shared split at each setting, and times reads on both sides of the boundary.

Headline result: reading a shared path was 5.3× slower and used 3.4× more memory than reading a dynamic path on the same table.

🎯 Key Features

  1. max_dynamic_paths sweep — 100 / 1,000 / 10,000 and an extreme 50,000-field run
  2. Dynamic vs shared path distribution — JSONDynamicPaths() / JSONSharedDataPaths()
  3. Worst-case field naming — a distinct field name on every row
  4. Merge behaviour — how the path budget is resolved when parts merge
  5. max_dynamic_types — the type-count analogue of the path budget
  6. Read performance across the boundary — timing, bytes and memory

🚀 Quick Start

Prerequisites

Setup and Run

cd workload/json-stress-test

clickhouse-client --queries-file 01_setup.sql
clickhouse-client --queries-file 02_unique_merge_test.sql

03_performance_test.sql is a recorded result set, not a runnable script: it holds the measurements from the performance phase as comments. Read it rather than executing it.

📚 Test Details

Phase 1 — Path Budget (01_setup.sql)

Test Setting Fields per row Dynamic Shared
test_paths_100 max_dynamic_paths=100 150 100 50
test_paths_1000 max_dynamic_paths=1000 2,000 1,000 1,000
test_paths_10000 max_dynamic_paths=10000 15,000 10,000 5,000
test_paths_extreme max_dynamic_paths=10000 50,000 10,000 40,000

Nothing fails. The excess simply moves to shared data — which is exactly why this limit is easy to cross without noticing.

Phase 2 — Unique Fields and Merges (02_unique_merge_test.sql)

Phase 3 — Read Performance (03_performance_test.sql)

Measured on perf_test_large, JSON(max_dynamic_paths=5000), 6,100 rows × 5,000 fields, 2.10 MiB on disk:

Metric Dynamic path Shared path Difference
Query time 32 ms 170 ms 5.3× slower
Memory 13.36 MiB 45.57 MiB 3.4× more
Data read 405.08 KiB 248.83 KiB —

Note that the shared-path query read fewer bytes and was still far slower and heavier: the cost is in reconstructing values from shared data, not in I/O volume.

🔑 Key Learning Points

📂 File Structure

json-stress-test/
├── README.md                   # This document
├── 01_setup.sql                # Phase 1: max_dynamic_paths sweep
├── 02_unique_merge_test.sql    # Phase 2: unique fields, merges, max_dynamic_types
├── 03_performance_test.sql     # Phase 3: recorded performance results (read-only)
├── 01_previous_tests.sql       # Original combined script, superseded by 01–03
└── progress_log.md             # Run log with intermediate measurements

🔍 Related Labs

📝 Notes


JSON 데이터 타입의 경로 한계를 밀어붙이는 스트레스 테스트입니다. 행당 고유 경로 수가 max_dynamic_paths를 넘어서면 무슨 일이 일어나는지, 초과분은 어디로 가는지, 그리고 그곳에서 읽는 비용이 얼마인지 측정합니다.

📋 개요

모든 JSON 컬럼에는 dynamic path 예산이 있습니다 — 실제 서브컬럼으로 저장되는 경로입니다. max_dynamic_paths를 넘는 경로는 거부되지 않고 shared data로 넘어가며, 조회는 정상적으로 되지만 비용이 훨씬 큽니다. 이 실습은 행당 5만 필드까지 밀어 올리며 설정별 dynamic/shared 분포를 확인하고, 경계 양쪽의 읽기 성능을 측정합니다.

핵심 결과: 같은 테이블에서 shared path 읽기는 dynamic path 읽기보다 5.3배 느리고 메모리를 3.4배 더 썼습니다.

🎯 주요 기능

  1. max_dynamic_paths 스윕 — 100 / 1,000 / 10,000 및 5만 필드 극한 테스트
  2. dynamic vs shared 경로 분포 — JSONDynamicPaths() / JSONSharedDataPaths()
  3. 최악의 필드명 패턴 — 행마다 완전히 다른 필드명
  4. 머지 동작 — 파트 머지 시 경로 예산이 어떻게 결정되는가
  5. max_dynamic_types — 경로 예산의 타입 개수 버전
  6. 경계 양쪽의 읽기 성능 — 시간, 바이트, 메모리

🚀 빠른 시작

cd workload/json-stress-test

clickhouse-client --queries-file 01_setup.sql
clickhouse-client --queries-file 02_unique_merge_test.sql

03_performance_test.sql은 실행 스크립트가 아니라 측정 결과 기록입니다(주석 형태). 실행하지 말고 읽어보세요.

📚 테스트 상세

Phase 1 — 경로 예산 (01_setup.sql)

테스트 설정 행당 필드 Dynamic Shared
test_paths_100 max_dynamic_paths=100 150 100 50
test_paths_1000 max_dynamic_paths=1000 2,000 1,000 1,000
test_paths_10000 max_dynamic_paths=10000 15,000 10,000 5,000
test_paths_extreme max_dynamic_paths=10000 50,000 10,000 40,000

어느 것도 실패하지 않습니다. 초과분은 그냥 shared data로 이동합니다 — 그래서 이 한계를 모르고 넘기기 쉽습니다.

Phase 2 — 고유 필드와 머지 (02_unique_merge_test.sql)

Phase 3 — 읽기 성능 (03_performance_test.sql)

perf_test_large, JSON(max_dynamic_paths=5000), 6,100행 × 5,000필드, 디스크 2.10 MiB 기준:

항목 Dynamic path Shared path 차이
쿼리 시간 32 ms 170 ms 5.3배 느림
메모리 13.36 MiB 45.57 MiB 3.4배 많음
읽은 데이터 405.08 KiB 248.83 KiB —

shared path 쿼리가 바이트를 더 적게 읽고도 훨씬 느리고 무거웠다는 점에 주목하세요. 비용은 I/O 양이 아니라 shared data에서 값을 재구성하는 데 있습니다.

🔑 핵심 학습 포인트

📂 파일 구조

json-stress-test/
├── README.md                   # 이 문서
├── 01_setup.sql                # Phase 1: max_dynamic_paths 스윕
├── 02_unique_merge_test.sql    # Phase 2: 고유 필드, 머지, max_dynamic_types
├── 03_performance_test.sql     # Phase 3: 성능 측정 결과 기록 (읽기 전용)
├── 01_previous_tests.sql       # 01–03으로 분리되기 전의 원본 통합 스크립트
└── progress_log.md             # 중간 측정치가 담긴 진행 로그

🔍 관련 실습

📝 참고사항


Happy Learning! 🚀

License

MIT — same as the rest of the repository.

라이선스

MIT — 저장소 전체와 동일합니다.

Open this lab on GitHub →GitHub에서 이 실습 열기 →