> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-8c05c8a2.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> ClickHouse로 Stack Overflow 데이터 분석

# ClickHouse로 Stack Overflow 데이터 분석

export const Image = ({img, alt, size}) => {
  return <Frame>
      <img src={img} alt={alt} />
    </Frame>;
};

이 데이터셋에는 Stack Overflow의 모든 `Posts`, `Users`, `Votes`, `Comments`, `Badges`, `PostHistory`, `PostLinks` 데이터가 포함되어 있습니다.

2024년 4월까지의 모든 게시물이 포함된 미리 준비된 Parquet 버전의 데이터를 다운로드하거나, 최신 XML 포맷 데이터를 다운로드해 적재할 수 있습니다. Stack Overflow는 이 데이터를 주기적으로 업데이트하며, 지금까지는 3개월마다 업데이트해 왔습니다.

다음 다이어그램은 Parquet 포맷을 기준으로 사용 가능한 테이블의 스키마를 보여줍니다.

<Image img="https://mintcdn.com/private-7c7dfe99-mintlify-8c05c8a2/51i4N09qYnRiav8Z/images/getting-started/example-datasets/stackoverflow.png?fit=max&auto=format&n=51i4N09qYnRiav8Z&q=85&s=13459e34cd4e0272c83b7d86aeb281fe" alt="Stack Overflow 스키마" size="md" width="1268" height="856" data-path="images/getting-started/example-datasets/stackoverflow.png" />

이 데이터의 스키마 설명은 [여기](https://meta.stackexchange.com/questions/2677/database-schema-documentation-for-the-public-data-dump-and-sede)에서 확인할 수 있습니다.

<div id="pre-prepared-data">
  ## 미리 준비된 데이터
</div>

이 데이터의 사본을 Parquet 포맷으로 제공하며, 2024년 4월 기준 최신 상태입니다. 이 데이터셋은 행 수(Posts 6천만 개)만 보면 ClickHouse 기준으로는 작은 편이지만, 상당한 양의 텍스트와 큰 String 컬럼을 포함하고 있습니다.

```sql theme={null}
CREATE DATABASE stackoverflow
```

다음 측정 시간은 `eu-west-2`에 위치한 96 GiB, 24 vCPU ClickHouse Cloud 클러스터를 기준으로 합니다. 데이터셋은 `eu-west-3`에 위치해 있습니다.

<div id="posts">
  ### Posts
</div>

```sql theme={null}
CREATE TABLE stackoverflow.posts
(
    `Id` Int32 CODEC(Delta(4), ZSTD(1)),
    `PostTypeId` Enum8('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
    `AcceptedAnswerId` UInt32,
    `CreationDate` DateTime64(3, 'UTC'),
    `Score` Int32,
    `ViewCount` UInt32 CODEC(Delta(4), ZSTD(1)),
    `Body` String,
    `OwnerUserId` Int32,
    `OwnerDisplayName` String,
    `LastEditorUserId` Int32,
    `LastEditorDisplayName` String,
    `LastEditDate` DateTime64(3, 'UTC') CODEC(Delta(8), ZSTD(1)),
    `LastActivityDate` DateTime64(3, 'UTC'),
    `Title` String,
    `Tags` String,
    `AnswerCount` UInt16 CODEC(Delta(2), ZSTD(1)),
    `CommentCount` UInt8,
    `FavoriteCount` UInt8,
    `ContentLicense` LowCardinality(String),
    `ParentId` String,
    `CommunityOwnedDate` DateTime64(3, 'UTC'),
    `ClosedDate` DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYear(CreationDate)
ORDER BY (PostTypeId, toDate(CreationDate), CreationDate)

INSERT INTO stackoverflow.posts SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 265.466 sec. Processed 59.82 million rows, 38.07 GB (225.34 thousand rows/s., 143.42 MB/s.)
```

Posts는 연도별로도 사용할 수 있습니다. 예: [https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/2020.parquet](https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/2020.parquet)

<div id="votes">
  ### Votes
</div>

```sql theme={null}
CREATE TABLE stackoverflow.votes
(
    `Id` UInt32,
    `PostId` Int32,
    `VoteTypeId` UInt8,
    `CreationDate` DateTime64(3, 'UTC'),
    `UserId` Int32,
    `BountyAmount` UInt8
)
ENGINE = MergeTree
ORDER BY (VoteTypeId, CreationDate, PostId, UserId)

INSERT INTO stackoverflow.votes SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/votes/*.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 21.605 sec. Processed 238.98 million rows, 2.13 GB (11.06 million rows/s., 98.46 MB/s.)
```

Votes는 연도별로도 사용할 수 있습니다. 예: [https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/votes/2020.parquet](https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/votes/2020.parquet)

<div id="comments">
  ### Comments
</div>

```sql theme={null}
CREATE TABLE stackoverflow.comments
(
    `Id` UInt32,
    `PostId` UInt32,
    `Score` UInt16,
    `Text` String,
    `CreationDate` DateTime64(3, 'UTC'),
    `UserId` Int32,
    `UserDisplayName` LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY CreationDate

INSERT INTO stackoverflow.comments SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/comments/*.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 56.593 sec. Processed 90.38 million rows, 11.14 GB (1.60 million rows/s., 196.78 MB/s.)
```

Comments는 연도별로도 사용할 수 있습니다. 예: [https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/comments/2020.parquet](https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/comments/2020.parquet)

<div id="users">
  ### Users
</div>

```sql theme={null}
CREATE TABLE stackoverflow.users
(
    `Id` Int32,
    `Reputation` LowCardinality(String),
    `CreationDate` DateTime64(3, 'UTC') CODEC(Delta(8), ZSTD(1)),
    `DisplayName` String,
    `LastAccessDate` DateTime64(3, 'UTC'),
    `AboutMe` String,
    `Views` UInt32,
    `UpVotes` UInt32,
    `DownVotes` UInt32,
    `WebsiteUrl` String,
    `Location` LowCardinality(String),
    `AccountId` Int32
)
ENGINE = MergeTree
ORDER BY (Id, CreationDate)

INSERT INTO stackoverflow.users SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/users.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 10.988 sec. Processed 22.48 million rows, 1.36 GB (2.05 million rows/s., 124.10 MB/s.)
```

<div id="badges">
  ### Badges
</div>

```sql theme={null}
CREATE TABLE stackoverflow.badges
(
    `Id` UInt32,
    `UserId` Int32,
    `Name` LowCardinality(String),
    `Date` DateTime64(3, 'UTC'),
    `Class` Enum8('Gold' = 1, 'Silver' = 2, 'Bronze' = 3),
    `TagBased` Bool
)
ENGINE = MergeTree
ORDER BY UserId

INSERT INTO stackoverflow.badges SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/badges.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 6.635 sec. Processed 51.29 million rows, 797.05 MB (7.73 million rows/s., 120.13 MB/s.)
```

<div id="postlinks">
  ### PostLinks
</div>

```sql theme={null}
CREATE TABLE stackoverflow.postlinks
(
    `Id` UInt64,
    `CreationDate` DateTime64(3, 'UTC'),
    `PostId` Int32,
    `RelatedPostId` Int32,
    `LinkTypeId` Enum8('Linked' = 1, 'Duplicate' = 3)
)
ENGINE = MergeTree
ORDER BY (PostId, RelatedPostId)

INSERT INTO stackoverflow.postlinks SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/postlinks.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 1.534 sec. Processed 6.55 million rows, 129.70 MB (4.27 million rows/s., 84.57 MB/s.)
```

<div id="posthistory">
  ### PostHistory
</div>

```sql theme={null}
CREATE TABLE stackoverflow.posthistory
(
    `Id` UInt64,
    `PostHistoryTypeId` UInt8,
    `PostId` Int32,
    `RevisionGUID` String,
    `CreationDate` DateTime64(3, 'UTC'),
    `UserId` Int32,
    `Text` String,
    `ContentLicense` LowCardinality(String),
    `Comment` String,
    `UserDisplayName` String
)
ENGINE = MergeTree
ORDER BY (CreationDate, PostId)

INSERT INTO stackoverflow.posthistory SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posthistory/*.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 422.795 sec. Processed 160.79 million rows, 67.08 GB (380.30 thousand rows/s., 158.67 MB/s.)
```

<div id="original-dataset">
  ## 원본 데이터셋
</div>

원본 데이터셋은 [https://archive.org/download/stackexchange](https://archive.org/download/stackexchange)에서 압축된(7zip) XML 포맷으로 제공되며, 파일 접두사는 `stackoverflow.com*`입니다.

<div id="download">
  ### 다운로드
</div>

```bash theme={null}
wget https://archive.org/download/stackexchange/stackoverflow.com-Badges.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-Comments.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-PostHistory.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-PostLinks.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-Users.7z
wget https://archive.org/download/stackexchange/stackoverflow.com-Votes.7z
```

이 파일들은 최대 35GB이며 인터넷 연결 상태에 따라 다운로드에 약 30분이 걸릴 수 있습니다 - 다운로드 서버는 초당 약 20MB로 속도가 제한됩니다.

<div id="convert-to-json">
  ### JSON으로 변환
</div>

이 문서를 작성하는 시점 기준으로 ClickHouse는 XML을 입력 형식으로 네이티브 지원하지 않습니다. 데이터를 ClickHouse에 로드하려면 먼저 NDJSON으로 변환해야 합니다.

XML을 JSON으로 변환할 때는 XML 문서를 위한 간단한 `jq` 래퍼인 Linux 도구 [`xq`](https://github.com/kislyuk/yq)를 사용하는 것을 권장합니다.

xq와 jq를 설치합니다:

```bash theme={null}
sudo apt install jq
pip install yq
```

다음 단계는 위의 모든 파일에 동일하게 적용됩니다. 여기서는 `stackoverflow.com-Posts.7z` 파일을 예시로 사용합니다. 필요에 따라 수정하십시오.

[p7zip](https://p7zip.sourceforge.net/)을 사용해 파일 압축을 해제합니다. 그러면 단일 XML 파일이 생성되며, 이 경우 `Posts.xml`입니다.

> 파일은 약 4.5배 압축되어 있습니다. 압축된 크기가 22GB인 경우, Posts 파일은 압축 해제 시 약 97GB가 필요합니다.

```bash theme={null}
p7zip -d stackoverflow.com-Posts.7z
```

다음은 xml 파일을 각각 10000행씩 포함하는 여러 파일로 분할합니다.

```bash theme={null}
mkdir posts
cd posts
# 다음 명령은 입력 xml 파일을 10000 행씩 하위 파일로 분할합니다
tail +3 ../Posts.xml | head -n -1 | split -l 10000 --filter='{ printf "<rows>\n"; cat - ; printf "</rows>\n"; } > $FILE' -
```

위 작업을 실행하면 각각 10000줄로 이루어진 파일 세트가 생성됩니다. 이렇게 하면 다음 명령에서 메모리 오버헤드가 과도하게 커지지 않습니다(xml을 JSON으로 변환하는 작업은 메모리에서 수행됩니다).

```bash theme={null}
find . -maxdepth 1 -type f -exec xq -c '.rows.row[]' {} \; | sed -e 's:"@:":g' > posts_v2.json
```

위 명령을 실행하면 `posts.json` 파일 하나가 생성됩니다.

다음 명령으로 ClickHouse에 로드하십시오. `posts.json` 파일의 schema가 지정되어 있다는 점에 유의하십시오. 대상 테이블에 맞도록 데이터 타입별로 이를 조정해야 합니다.

```bash theme={null}
clickhouse local --query "SELECT * FROM file('posts.json', JSONEachRow, 'Id Int32, PostTypeId UInt8, AcceptedAnswerId UInt32, CreationDate DateTime64(3, \'UTC\'), Score Int32, ViewCount UInt32, Body String, OwnerUserId Int32, OwnerDisplayName String, LastEditorUserId Int32, LastEditorDisplayName String, LastEditDate DateTime64(3, \'UTC\'), LastActivityDate DateTime64(3, \'UTC\'), Title String, Tags String, AnswerCount UInt16, CommentCount UInt8, FavoriteCount UInt8, ContentLicense String, ParentId String, CommunityOwnedDate DateTime64(3, \'UTC\'), ClosedDate DateTime64(3, \'UTC\')') FORMAT Native" | clickhouse client --host <host> --secure --password <password> --query "INSERT INTO stackoverflow.posts_v2 FORMAT Native"
```

<div id="example-queries">
  ## 예시 쿼리
</div>

시작하는 데 도움이 될 몇 가지 간단한 질문입니다.

<div id="most-popular-tags-on-stack-overflow">
  ### Stack Overflow에서 가장 많이 사용된 태그
</div>

```sql theme={null}
SELECT
    arrayJoin(arrayFilter(t -> (t != ''), splitByChar('|', Tags))) AS Tags,
    count() AS c
FROM stackoverflow.posts
GROUP BY Tags
ORDER BY c DESC
LIMIT 10
```

```response theme={null}
┌─Tags───────┬───────c─┐
│ javascript │ 2527130 │
│ python     │ 2189638 │
│ java       │ 1916156 │
│ c#         │ 1614236 │
│ php        │ 1463901 │
│ android    │ 1416442 │
│ html       │ 1186567 │
│ jquery     │ 1034621 │
│ c++        │  806202 │
│ css        │  803755 │
└────────────┴─────────┘

10 rows in set. Elapsed: 1.013 sec. Processed 59.82 million rows, 1.21 GB (59.07 million rows/s., 1.19 GB/s.)
Peak memory usage: 224.03 MiB.
```

<div id="user-with-the-most-answers-active-accounts">
  ### 답변을 가장 많이 한 사용자(활성 계정)
</div>

계정에는 `UserId`가 필요합니다.

```sql theme={null}
SELECT
    any(OwnerUserId) UserId,
    OwnerDisplayName,
    count() AS c
FROM stackoverflow.posts WHERE OwnerDisplayName != '' AND PostTypeId='Answer' AND OwnerUserId != 0
GROUP BY OwnerDisplayName
ORDER BY c DESC
LIMIT 5
```

```response theme={null}
┌─UserId─┬─OwnerDisplayName─┬────c─┐
│  22656 │ Jon Skeet        │ 2727 │
│  23354 │ Marc Gravell     │ 2150 │
│  12950 │ tvanfosson       │ 1530 │
│   3043 │ Joel Coehoorn    │ 1438 │
│  10661 │ S.Lott           │ 1087 │
└────────┴──────────────────┴──────┘

5 rows in set. Elapsed: 0.154 sec. Processed 35.83 million rows, 193.39 MB (232.33 million rows/s., 1.25 GB/s.)
Peak memory usage: 206.45 MiB.
```

<div id="clickhouse-related-posts-with-the-most-views">
  ### 조회 수가 가장 많은 ClickHouse 관련 게시물
</div>

```sql theme={null}
SELECT
    Id,
    Title,
    ViewCount,
    AnswerCount
FROM stackoverflow.posts
WHERE Title ILIKE '%ClickHouse%'
ORDER BY ViewCount DESC
LIMIT 10
```

```response theme={null}
┌───────Id─┬─Title────────────────────────────────────────────────────────────────────────────┬─ViewCount─┬─AnswerCount─┐
│ 52355143 │ Is it possible to delete old records from clickhouse table?                      │     41462 │           3 │
│ 37954203 │ Clickhouse Data Import                                                           │     38735 │           3 │
│ 37901642 │ Updating data in Clickhouse                                                      │     36236 │           6 │
│ 58422110 │ Pandas: How to insert dataframe into Clickhouse                                  │     29731 │           4 │
│ 63621318 │ DBeaver - Clickhouse - SQL Error [159] .. Read timed out                         │     27350 │           1 │
│ 47591813 │ How to filter clickhouse table by array column contents?                         │     27078 │           2 │
│ 58728436 │ How to search the string in query with case insensitive on Clickhouse database?  │     26567 │           3 │
│ 65316905 │ Clickhouse: DB::Exception: Memory limit (for query) exceeded                     │     24899 │           2 │
│ 49944865 │ How to add a column in clickhouse                                                │     24424 │           1 │
│ 59712399 │ How to cast date Strings to DateTime format with extended parsing in ClickHouse? │     22620 │           1 │
└──────────┴──────────────────────────────────────────────────────────────────────────────────┴───────────┴─────────────┘

10 rows in set. Elapsed: 0.472 sec. Processed 59.82 million rows, 1.91 GB (126.63 million rows/s., 4.03 GB/s.)
Peak memory usage: 240.01 MiB.
```

<div id="most-controversial-posts">
  ### 가장 논란이 된 게시물
</div>

```sql theme={null}
SELECT
    Id,
    Title,
    UpVotes,
    DownVotes,
    abs(UpVotes - DownVotes) AS Controversial_ratio
FROM stackoverflow.posts
INNER JOIN
(
    SELECT
        PostId,
        countIf(VoteTypeId = 2) AS UpVotes,
        countIf(VoteTypeId = 3) AS DownVotes
    FROM stackoverflow.votes
    GROUP BY PostId
    HAVING (UpVotes > 10) AND (DownVotes > 10)
) AS votes ON posts.Id = votes.PostId
WHERE Title != ''
ORDER BY Controversial_ratio ASC
LIMIT 3
```

```response theme={null}
┌───────Id─┬─Title─────────────────────────────────────────────┬─UpVotes─┬─DownVotes─┬─Controversial_ratio─┐
│   583177 │ VB.NET Infinite For Loop                          │      12 │        12 │                   0 │
│  9756797 │ Read console input as enumerable - one statement? │      16 │        16 │                   0 │
│ 13329132 │ What's the point of ARGV in Ruby?                 │      22 │        22 │                   0 │
└──────────┴───────────────────────────────────────────────────┴─────────┴───────────┴─────────────────────┘

3 rows in set. Elapsed: 4.779 sec. Processed 298.80 million rows, 3.16 GB (62.52 million rows/s., 661.05 MB/s.)
Peak memory usage: 6.05 GiB.
```

<div id="attribution">
  ## 출처 표기
</div>

`cc-by-sa 4.0` 라이선스에 따라 이 데이터를 제공한 Stack Overflow에 감사드리며, [https://archive.org/details/stackexchange](https://archive.org/details/stackexchange)에 있는 원본 데이터 출처와 Stack Overflow의 노고를 밝힙니다.
