Apache ORC 파일 구조
Apache ORC(Optimized Row Columnar) 파일은 빅데이터 환경에서 효율적인 컬럼 지향 분석 쿼리를 지원하기 위해 설계된 파일 포맷임.
주로 Apache Hive, Apache Spark, Presto, Trino 등 다양한 분산 SQL 엔진 및 Hadoop 에코시스템에서 사용되며, 대규모 데이터 집합을 더 빠르고 더 적은 자원을 사용하여 처리할 수 있도록 여러 최적화를 내장하고 있음.
전체 파일 구조
ORC 파일은 크게 다음과 같은 구성 요소로 이루어짐.
1. Postscript
2. Footer
3. Stripes (파일에서 실제 데이터가 저장되는 단위)
[스트라이프들] [Footer] [Postscript]
파일의 끝 부분에 Postscript와 Footer가 위치하고, 파일의 앞쪽에 데이터가 Stripe 단위로 배치됨.
이러한 구조적 특징 때문에 실제 데이터를 스캔하기 전에, Footer와 Postscript를 통해 메타데이터 및 인덱스 정보를 확인함으로써 빠른 쿼리 최적화가 가능함.
PostScript
위치: ORC 파일의 맨 마지막(파일 끝).
역할: 파일 전체에 대한 전반적인 정보를 담고 있으며, Footer의 크기와 위치, 사용된 압축 알고리즘, ORC 버전 정보 등이 기록됨.
대표적인 필드는 다음과 같음.
1. FooterLength: Footer(및 Metadata) 섹션의 길이(바이트).
2. Compression: 데이터와 Footer 섹션에 적용된 압축 방식(e.g., NONE, ZLIB, SNAPPY, LZ4, ZSTD 등).
3. CompressionBlockSize: 압축 블록 크기.
4. WriterVersion: ORC 파일을 쓴 writer 애플리케이션의 버전.
5. Magic: ORC 식별자(“ORC”)를 가리키는 매직 넘버.
Postscript에 기록된 정보를 기반으로 Footer를 효율적으로 찾을 수 있으며, 어떤 압축을 사용했는지 파악하여 Footer를 해제압축 한 뒤 해석할 수 있음.
Footer
위치: Postscript 바로 앞에 위치(파일 끝에서부터 역으로 파악).
역할: Stripe에 대한 상세한 메타데이터와 컬럼 스키마, 파일 전체 통계, Stripe 위치 정보 등을 담고 있음.
Footer에는 다음과 같은 정보가 포함됨.
1. Types: 각 컬럼(필드)의 ORC 내 데이터 타입 정보. (STRUCT, MAP, ARRAY, STRING, DECIMAL 등)
2. StripeInformationList: 각각의 Stripe가 시작하는 오프셋, 크기, Stripe 내의 행(Row) 수, 인덱스 데이터/푸터 구간 크기 등 Stripe에 대한 상세 메타데이터가 기록됨.
3. Statistics: 전체 파일 차원의 각 컬럼 통계를 저장함 (최댓값, 최솟값, NULL 카운트, 문자열 길이 등).
4. NumberOfRows: 전체 파일에 포함된 총 행(Row)의 수.
이 Footer 정보를 통해 읽고자 하는 Stripe를 빠르게 찾아서 접근할 수 있고, 컬럼별 통계를 확인함으로써 쿼리 프루닝(Query Pruning)을 수행할 수 있음.
Stripe 구조
ORC 파일의 실제 데이터는 Stripe 단위로 저장됨.
Stripe는 대개 수백만 행을 저장하도록 설정하며(일반적으로 수십 MB 수준), 이 Stripe 구조를 통해 병렬 처리를 극대화하고 메타데이터 및 인덱스 관리를 단순화함.
각 Stripe는 크게 다음과 같은 섹션을 가짐.
1. Index Data
2. Row Data
3. Stripe Footer
1. Index Data
각 컬럼별로 RowGroup 인덱스와 Column Statistics가 들어 있음.
기본적으로 ORC는 Stripe 내부를 일정한 RowGroup(미니 배치)으로 다시 나눔.
예를 들어, 한 Stripe에 10만 건의 데이터가 있다고 할 때, 이를 1만 건씩 10개의 RowGroup으로 쪼개는 식임.
RowGroup 단위로 컬럼별 최솟값, 최댓값, NULL의 개수, 전체 Count 등이 기록됨.
이 인덱스 정보 덕분에 쿼리가 특정 범위를 벗어나는 데이터를 즉시 스킵할 수 있어 I/O를 줄이고 쿼리 성능을 크게 향상시킴(Partition Pruning + Column Pruning과 유사한 효과).
추가적으로 Bloom Filter 같은 더 정교한 인덱스 옵션을 통해 특정 키값 존재 여부를 빠르게 판별할 수 있음.
2. Row Data
ORC가 컬럼 지향으로 물리 데이터 배치를 하는 핵심 섹션임.
컬럼별로 서로 다른 인코딩 방식을 사용할 수 있음.
예컨대, 정수형 컬럼은 Run Length Encoding(RLE), 문자열 컬럼은 Dictionary Encoding 또는 Direct Encoding을 사용하는 식으로, 데이터 분포와 유형에 따라 최적화된 인코딩을 적용함.
압축(Compression) 또한 컬럼 단위 혹은 Stripe 단위로 적용됨.
일반적으로 ZLIB, SNAPPY, LZ4, ZSTD 등이 사용될 수 있으며, 행별 반복이 많거나 문자열 도메인이 작은 컬럼에서는 높은 압축 효율을 기대할 수 있음.
각 컬럼은 별도 섹션으로 저장되므로, Column Pruning이 매우 효율적임(SELECT *가 아닌, SELECT 특정 컬럼에만 접근할 경우 필요한 컬럼 섹션만 읽으면 됨).
3. Stripe Footer
Stripe 내부 Footer로, 해당 Stripe 내 각 컬럼의 인코딩 정보(Dictionary 크기, RLE 파라미터 등)와 스트림 정보가 저장됨.
엔진이 Row Data 섹션을 해석하는 데 필요한 메타데이터를 포함하여, 실제 컬럼 데이터 블록을 어떻게 읽고 복원할지 안내함.
컬럼 인코딩
ORC는 컬럼 지향 구조에서 다음과 같은 인코딩 기법을 주로 활용함.
1. Run Length Encoding (RLE)
정수 컬럼 등에 적용되는 가장 흔한 방법으로, 연속되는 값이 많은 경우 효율적임.
ORC에서는 다양한 형태(RLE v1, RLE v2 등)로 개선된 RLE 알고리즘을 지원하며, 데이터 특성(주로 반복 정도, 범위 크기)에 따라 인코딩 방식을 자동 선택함.
2. Dictionary Encoding
문자열 컬럼과 같이 중복이 많은 컬럼에 효과적임.
우선 컬럼에서 등장하는 각 유니크 값에 대해 딕셔너리를 만들고, 실제 Row Data 영역에는 딕셔너리의 인덱스(index)만 저장함.
스트라이프 내 데이터에서 각 문자열의 출현 빈도수가 높거나 유니크 값의 개수가 적으면 압축 효율이 극대화됨.
3. Direct Encoding
데이터 분포가 상대적으로 균등하고 딕셔너리로 인코딩했을 때 이점이 적다면, 그대로 원본 문자열이나 숫자 데이터를 RLE, 비트 팩킹(bit-packing) 등으로만 관리함.
이처럼 컬럼별로 상황에 맞는 인코딩 방식을 적용하여 저장 공간과 I/O를 모두 절감함.
파일 및 컬럼 통계
ORC가 파일 차원과 Stripe 차원 그리고 RowGroup 차원에 걸쳐 모두 상세한 통계를 저장한다는 점은 성능 최적화의 핵심임.
통계 정보는 크게 다음과 같이 관리됨.
1. 파일 전체 통계 (Footer에 저장)
2. Stripe 통계 (Stripe Footer 또는 Footer에 기록)
3. RowGroup(인덱스) 통계 (Index Data에 기록)
이 통계에는 컬럼별 최솟값, 최댓값, 합계, 평균, NULL 카운트 등이 들어가며, 문자열의 경우 길이 정보, 날짜/타임스탬프의 경우 min/max 범위가 저장될 수 있음.
이런 통계 덕분에 쿼리 실행 엔진이 다음을 수행할 수 있음.
1. Predicate Pushdown: “WHERE 컬럼 between x and y” 같은 조건을 만났을 때, 각 Stripe/RowGroup 통계를 참조하여 값의 범위가 겹치지 않으면 해당 Stripe/RowGroup 전체를 건너뜀(Skip).
2. Partition Pruning / Column Pruning: 필요 없는 컬럼은 읽지 않고(컬럼 프루닝), 필요 없는 범위를 가진 Stripe는 전부 스킵함.
압축
ORC는 Stripe 단위 또는 컬럼 단위로 압축을 적용할 수 있음.
파일 작성 시점(Writer)에 설정된 압축 코덱(예: ZLIB, SNAPPY, LZ4, ZSTD 등)에 따라 데이터가 압축되어 저장됨.
압축 블록 크기도 함께 정의되므로, 압축 블록을 경계로 병렬 처리가 가능해짐.
1. ZLIB: 높은 압축률을 제공하지만 CPU 사용량이 상대적으로 큼.
2. SNAPPY: 압축률은 중간 정도지만 압축/해제압축 속도가 빠름.
3. LZ4, ZSTD: 최근에는 ZSTD가 빠른 속도와 좋은 압축률을 모두 제공하여 많이 사용됨.
압축된 데이터를 Stripe 단위로 분리하여 저장하기 때문에, 특정 Stripe를 읽을 때 필요한 Stripe만 해제압축하는 것이 가능해 효율적임.
고급 기능
1. Bloom Filter
ORC는 Bloom Filter를 옵션으로 지원함.
컬럼별로 Bloom Filter를 사용해 특정 키가 있는지/없는지를 확률적으로 빠르게 파악할 수 있음.
Bloom Filter는 False Positive는 발생해도 False Negative는 발생하지 않는 확률적 자료구조임.
쿼리에서 WHERE column = 'X' 같은 조건이 들어왔을 때, Bloom Filter 정보를 통해 “절대 존재할 수 없는 Stripe/RowGroup”을 빠르게 스킵할 수 있음.
2. Incremental Update (ACID 지원)
Hive ACID 테이블 등에 ORC를 사용하면, Update/Delete 작업을 지원하기 위해 ORC 내부에 base/Delta 파일 형태의 구조가 생길 수 있음.
단순 ORC 파일 자체 구조는 불변(immutable)이지만, 트랜잭션 로그/Delta 파일을 추가적으로 관리하여 확장 기능을 제공함.
3. Predicate Pushdown & Vectorized Reader
엔진 관점에서 ORC 파일을 읽을 때, ORC의 컬럼 지향 구조와 통계를 적극적으로 활용해 벡터화(Vectorized) 연산을 수행함.
이는 CPU 캐시 효율과 SIMD 명령어 사용 등을 극대화하여 매우 빠른 성능을 발휘함.
정리
1. 컬럼 지향으로 데이터가 저장되어, 필요한 컬럼만 부분 읽기가 가능(컬럼 프루닝).
2. Stripe 단위로 메타데이터, 인덱스, 통계가 풍부하게 저장되어 빠른 스킵(Pruning) 및 검색이 가능.
3. 다양한 인코딩 & 압축 알고리즘을 자동 선택/적용하여 높은 압축률 및 빠른 I/O 성능.
4. 파일/Stripe/RowGroup 단위 통계 정보를 통해 Predicate Pushdown, Bloom Filter 등을 활용한 고속 쿼리 가능.
5. 벡터화된 읽기(Vectorized Reader)를 통해 CPU 효율을 극대화해 분석 쿼리에 최적화.
이처럼 Apache ORC 파일은 빅데이터 환경에서 대량 데이터를 효율적으로 저장하고 고속으로 분석할 수 있도록 최적화된 컬럼 지향 파일 포맷임.
특히 풍부한 통계와 인덱스, 다양한 압축/인코딩 방식, 벡터화 연산 최적화를 통해 대규모 데이터 웨어하우스/데이터 레이크 시나리오에서 많이 사용되고 있음.
'Data Engineering > Spark' 카테고리의 다른 글
[Spark] Apache Spark 구조 (2) | 2025.03.15 |
---|---|
[Spark] Apache Iceberg (2) | 2025.03.14 |
[Spark] Apache Parquet 파일 구조 (0) | 2025.03.09 |
[Spark] Spark Application 실행 과정 (1) | 2025.03.08 |
[Spark] Adaptive Query Execution (0) | 2025.01.17 |
Apache ORC 파일 구조
Apache ORC(Optimized Row Columnar) 파일은 빅데이터 환경에서 효율적인 컬럼 지향 분석 쿼리를 지원하기 위해 설계된 파일 포맷임.
주로 Apache Hive, Apache Spark, Presto, Trino 등 다양한 분산 SQL 엔진 및 Hadoop 에코시스템에서 사용되며, 대규모 데이터 집합을 더 빠르고 더 적은 자원을 사용하여 처리할 수 있도록 여러 최적화를 내장하고 있음.
전체 파일 구조
ORC 파일은 크게 다음과 같은 구성 요소로 이루어짐.
1. Postscript
2. Footer
3. Stripes (파일에서 실제 데이터가 저장되는 단위)
[스트라이프들] [Footer] [Postscript]
파일의 끝 부분에 Postscript와 Footer가 위치하고, 파일의 앞쪽에 데이터가 Stripe 단위로 배치됨.
이러한 구조적 특징 때문에 실제 데이터를 스캔하기 전에, Footer와 Postscript를 통해 메타데이터 및 인덱스 정보를 확인함으로써 빠른 쿼리 최적화가 가능함.
PostScript
위치: ORC 파일의 맨 마지막(파일 끝).
역할: 파일 전체에 대한 전반적인 정보를 담고 있으며, Footer의 크기와 위치, 사용된 압축 알고리즘, ORC 버전 정보 등이 기록됨.
대표적인 필드는 다음과 같음.
1. FooterLength: Footer(및 Metadata) 섹션의 길이(바이트).
2. Compression: 데이터와 Footer 섹션에 적용된 압축 방식(e.g., NONE, ZLIB, SNAPPY, LZ4, ZSTD 등).
3. CompressionBlockSize: 압축 블록 크기.
4. WriterVersion: ORC 파일을 쓴 writer 애플리케이션의 버전.
5. Magic: ORC 식별자(“ORC”)를 가리키는 매직 넘버.
Postscript에 기록된 정보를 기반으로 Footer를 효율적으로 찾을 수 있으며, 어떤 압축을 사용했는지 파악하여 Footer를 해제압축 한 뒤 해석할 수 있음.
Footer
위치: Postscript 바로 앞에 위치(파일 끝에서부터 역으로 파악).
역할: Stripe에 대한 상세한 메타데이터와 컬럼 스키마, 파일 전체 통계, Stripe 위치 정보 등을 담고 있음.
Footer에는 다음과 같은 정보가 포함됨.
1. Types: 각 컬럼(필드)의 ORC 내 데이터 타입 정보. (STRUCT, MAP, ARRAY, STRING, DECIMAL 등)
2. StripeInformationList: 각각의 Stripe가 시작하는 오프셋, 크기, Stripe 내의 행(Row) 수, 인덱스 데이터/푸터 구간 크기 등 Stripe에 대한 상세 메타데이터가 기록됨.
3. Statistics: 전체 파일 차원의 각 컬럼 통계를 저장함 (최댓값, 최솟값, NULL 카운트, 문자열 길이 등).
4. NumberOfRows: 전체 파일에 포함된 총 행(Row)의 수.
이 Footer 정보를 통해 읽고자 하는 Stripe를 빠르게 찾아서 접근할 수 있고, 컬럼별 통계를 확인함으로써 쿼리 프루닝(Query Pruning)을 수행할 수 있음.
Stripe 구조
ORC 파일의 실제 데이터는 Stripe 단위로 저장됨.
Stripe는 대개 수백만 행을 저장하도록 설정하며(일반적으로 수십 MB 수준), 이 Stripe 구조를 통해 병렬 처리를 극대화하고 메타데이터 및 인덱스 관리를 단순화함.
각 Stripe는 크게 다음과 같은 섹션을 가짐.
1. Index Data
2. Row Data
3. Stripe Footer
1. Index Data
각 컬럼별로 RowGroup 인덱스와 Column Statistics가 들어 있음.
기본적으로 ORC는 Stripe 내부를 일정한 RowGroup(미니 배치)으로 다시 나눔.
예를 들어, 한 Stripe에 10만 건의 데이터가 있다고 할 때, 이를 1만 건씩 10개의 RowGroup으로 쪼개는 식임.
RowGroup 단위로 컬럼별 최솟값, 최댓값, NULL의 개수, 전체 Count 등이 기록됨.
이 인덱스 정보 덕분에 쿼리가 특정 범위를 벗어나는 데이터를 즉시 스킵할 수 있어 I/O를 줄이고 쿼리 성능을 크게 향상시킴(Partition Pruning + Column Pruning과 유사한 효과).
추가적으로 Bloom Filter 같은 더 정교한 인덱스 옵션을 통해 특정 키값 존재 여부를 빠르게 판별할 수 있음.
2. Row Data
ORC가 컬럼 지향으로 물리 데이터 배치를 하는 핵심 섹션임.
컬럼별로 서로 다른 인코딩 방식을 사용할 수 있음.
예컨대, 정수형 컬럼은 Run Length Encoding(RLE), 문자열 컬럼은 Dictionary Encoding 또는 Direct Encoding을 사용하는 식으로, 데이터 분포와 유형에 따라 최적화된 인코딩을 적용함.
압축(Compression) 또한 컬럼 단위 혹은 Stripe 단위로 적용됨.
일반적으로 ZLIB, SNAPPY, LZ4, ZSTD 등이 사용될 수 있으며, 행별 반복이 많거나 문자열 도메인이 작은 컬럼에서는 높은 압축 효율을 기대할 수 있음.
각 컬럼은 별도 섹션으로 저장되므로, Column Pruning이 매우 효율적임(SELECT *가 아닌, SELECT 특정 컬럼에만 접근할 경우 필요한 컬럼 섹션만 읽으면 됨).
3. Stripe Footer
Stripe 내부 Footer로, 해당 Stripe 내 각 컬럼의 인코딩 정보(Dictionary 크기, RLE 파라미터 등)와 스트림 정보가 저장됨.
엔진이 Row Data 섹션을 해석하는 데 필요한 메타데이터를 포함하여, 실제 컬럼 데이터 블록을 어떻게 읽고 복원할지 안내함.
컬럼 인코딩
ORC는 컬럼 지향 구조에서 다음과 같은 인코딩 기법을 주로 활용함.
1. Run Length Encoding (RLE)
정수 컬럼 등에 적용되는 가장 흔한 방법으로, 연속되는 값이 많은 경우 효율적임.
ORC에서는 다양한 형태(RLE v1, RLE v2 등)로 개선된 RLE 알고리즘을 지원하며, 데이터 특성(주로 반복 정도, 범위 크기)에 따라 인코딩 방식을 자동 선택함.
2. Dictionary Encoding
문자열 컬럼과 같이 중복이 많은 컬럼에 효과적임.
우선 컬럼에서 등장하는 각 유니크 값에 대해 딕셔너리를 만들고, 실제 Row Data 영역에는 딕셔너리의 인덱스(index)만 저장함.
스트라이프 내 데이터에서 각 문자열의 출현 빈도수가 높거나 유니크 값의 개수가 적으면 압축 효율이 극대화됨.
3. Direct Encoding
데이터 분포가 상대적으로 균등하고 딕셔너리로 인코딩했을 때 이점이 적다면, 그대로 원본 문자열이나 숫자 데이터를 RLE, 비트 팩킹(bit-packing) 등으로만 관리함.
이처럼 컬럼별로 상황에 맞는 인코딩 방식을 적용하여 저장 공간과 I/O를 모두 절감함.
파일 및 컬럼 통계
ORC가 파일 차원과 Stripe 차원 그리고 RowGroup 차원에 걸쳐 모두 상세한 통계를 저장한다는 점은 성능 최적화의 핵심임.
통계 정보는 크게 다음과 같이 관리됨.
1. 파일 전체 통계 (Footer에 저장)
2. Stripe 통계 (Stripe Footer 또는 Footer에 기록)
3. RowGroup(인덱스) 통계 (Index Data에 기록)
이 통계에는 컬럼별 최솟값, 최댓값, 합계, 평균, NULL 카운트 등이 들어가며, 문자열의 경우 길이 정보, 날짜/타임스탬프의 경우 min/max 범위가 저장될 수 있음.
이런 통계 덕분에 쿼리 실행 엔진이 다음을 수행할 수 있음.
1. Predicate Pushdown: “WHERE 컬럼 between x and y” 같은 조건을 만났을 때, 각 Stripe/RowGroup 통계를 참조하여 값의 범위가 겹치지 않으면 해당 Stripe/RowGroup 전체를 건너뜀(Skip).
2. Partition Pruning / Column Pruning: 필요 없는 컬럼은 읽지 않고(컬럼 프루닝), 필요 없는 범위를 가진 Stripe는 전부 스킵함.
압축
ORC는 Stripe 단위 또는 컬럼 단위로 압축을 적용할 수 있음.
파일 작성 시점(Writer)에 설정된 압축 코덱(예: ZLIB, SNAPPY, LZ4, ZSTD 등)에 따라 데이터가 압축되어 저장됨.
압축 블록 크기도 함께 정의되므로, 압축 블록을 경계로 병렬 처리가 가능해짐.
1. ZLIB: 높은 압축률을 제공하지만 CPU 사용량이 상대적으로 큼.
2. SNAPPY: 압축률은 중간 정도지만 압축/해제압축 속도가 빠름.
3. LZ4, ZSTD: 최근에는 ZSTD가 빠른 속도와 좋은 압축률을 모두 제공하여 많이 사용됨.
압축된 데이터를 Stripe 단위로 분리하여 저장하기 때문에, 특정 Stripe를 읽을 때 필요한 Stripe만 해제압축하는 것이 가능해 효율적임.
고급 기능
1. Bloom Filter
ORC는 Bloom Filter를 옵션으로 지원함.
컬럼별로 Bloom Filter를 사용해 특정 키가 있는지/없는지를 확률적으로 빠르게 파악할 수 있음.
Bloom Filter는 False Positive는 발생해도 False Negative는 발생하지 않는 확률적 자료구조임.
쿼리에서 WHERE column = 'X' 같은 조건이 들어왔을 때, Bloom Filter 정보를 통해 “절대 존재할 수 없는 Stripe/RowGroup”을 빠르게 스킵할 수 있음.
2. Incremental Update (ACID 지원)
Hive ACID 테이블 등에 ORC를 사용하면, Update/Delete 작업을 지원하기 위해 ORC 내부에 base/Delta 파일 형태의 구조가 생길 수 있음.
단순 ORC 파일 자체 구조는 불변(immutable)이지만, 트랜잭션 로그/Delta 파일을 추가적으로 관리하여 확장 기능을 제공함.
3. Predicate Pushdown & Vectorized Reader
엔진 관점에서 ORC 파일을 읽을 때, ORC의 컬럼 지향 구조와 통계를 적극적으로 활용해 벡터화(Vectorized) 연산을 수행함.
이는 CPU 캐시 효율과 SIMD 명령어 사용 등을 극대화하여 매우 빠른 성능을 발휘함.
정리
1. 컬럼 지향으로 데이터가 저장되어, 필요한 컬럼만 부분 읽기가 가능(컬럼 프루닝).
2. Stripe 단위로 메타데이터, 인덱스, 통계가 풍부하게 저장되어 빠른 스킵(Pruning) 및 검색이 가능.
3. 다양한 인코딩 & 압축 알고리즘을 자동 선택/적용하여 높은 압축률 및 빠른 I/O 성능.
4. 파일/Stripe/RowGroup 단위 통계 정보를 통해 Predicate Pushdown, Bloom Filter 등을 활용한 고속 쿼리 가능.
5. 벡터화된 읽기(Vectorized Reader)를 통해 CPU 효율을 극대화해 분석 쿼리에 최적화.
이처럼 Apache ORC 파일은 빅데이터 환경에서 대량 데이터를 효율적으로 저장하고 고속으로 분석할 수 있도록 최적화된 컬럼 지향 파일 포맷임.
특히 풍부한 통계와 인덱스, 다양한 압축/인코딩 방식, 벡터화 연산 최적화를 통해 대규모 데이터 웨어하우스/데이터 레이크 시나리오에서 많이 사용되고 있음.
'Data Engineering > Spark' 카테고리의 다른 글
[Spark] Apache Spark 구조 (2) | 2025.03.15 |
---|---|
[Spark] Apache Iceberg (2) | 2025.03.14 |
[Spark] Apache Parquet 파일 구조 (0) | 2025.03.09 |
[Spark] Spark Application 실행 과정 (1) | 2025.03.08 |
[Spark] Adaptive Query Execution (0) | 2025.01.17 |