使用 GitHub 数据编写查询
这个数据集包含了 ClickHouse 存储库的所有提交和更改。可以使用 ClickHouse 自带的 git-import
工具生成。
生成的数据为以下每个表提供一个 tsv
文件:
commits
- 带有统计信息的提交。file_changes
- 每个提交中更改的文件及其更改的信息和统计数据。line_changes
- 每个提交中每个更改文件的每一行的更改,包含该行的完整信息及其之前更改的信息。
截至 2022 年 11 月 8 日,每个 TSV 文件的大小和行数大约如下:
commits
- 7.8M - 266,051 行file_changes
- 53M - 266,051 行line_changes
- 2.7G - 7,535,157 行
生成数据
这是可选的。我们免费分发数据 - 请参阅下载和插入数据。
这将大约需要 3 分钟(截至 2022 年 11 月 8 日,在 MacBook Pro 2021 上)来完成 ClickHouse 存储库的操作。
可以从工具的本地帮助中获得可用选项的完整列表。
该帮助还提供了每个上述表的 DDL,例如:
这些查询应该在任何存储库中工作。欢迎探索并报告您的发现 关于运行时间的一些指南(截至 2022 年 11 月):
- Linux -
~/clickhouse git-import
- 160 分钟
下载和插入数据
以下数据可用于重现工作环境。或者,这个数据集在 play.clickhouse.com 上可用 - 详见查询。
以下存储库的生成文件可以在下面找到:
- ClickHouse(2022年11月8日)
- https://datasets-documentation.s3.amazonaws.com/github/commits/clickhouse/commits.tsv.xz - 2.5 MB
- https://datasets-documentation.s3.amazonaws.com/github/commits/clickhouse/file_changes.tsv.xz - 4.5MB
- https://datasets-documentation.s3.amazonaws.com/github/commits/clickhouse/line_changes.tsv.xz - 127.4 MB
- Linux(2022年11月8日)
要插入这些数据,请通过执行以下查询准备数据库:
使用 INSERT INTO SELECT
和 s3 函数 插入数据。例如,下面我们将 ClickHouse 文件插入到各自的表中:
commits
file_changes
line_changes
查询
该工具通过其帮助输出建议了几条查询。我们对这些查询进行了回答,并增加了一些额外的补充问题。这些查询的复杂性大致是逐渐增加的,与工具的任意顺序相对应。
该数据集可在 play.clickhouse.com 的 git_clickhouse
数据库中找到。我们为所有查询提供了此环境的链接,并根据需要调整数据库名称。请注意,由于采集数据时间的不同,播放结果可能与此处所示有所不同。
单个文件的历史
最简单的查询。我们查看 StorageReplicatedMergeTree.cpp
的所有提交信息。由于这些信息可能更有趣,我们按最近的信息排序。
我们还可以查看行更改,排除重命名的情况,即不会显示重命名事件之前的更改,当文件以不同名称存在时:
请注意,这个查询还有一个更复杂的变体,其中我们查找 逐行提交历史,考虑重命名。
查找当前活跃文件
这对于后续分析很重要,当我们只想考虑存储库中的当前文件。我们估算这组文件为那些未被重命名或删除(然后重新添加/重命名)的文件。
请注意,涉及 dbms
、libs
、tests/testflows/
目录下文件的重命名在提交历史中似乎存在断裂。因此我们也排除这些。
请注意,这允许文件被重命名然后再次重命名为其原始值。首先,我们将因重命名而删除的文件的 old_path
聚合。然后,我们将每个 path
的最后操作与之联接。最后,我们将此列表筛选为最终事件不是 Delete
的文件。
注意,我们在导入时跳过了几个目录,例如:
--skip-paths 'generated\.cpp|^(contrib|docs?|website|libs/(libcityhash|liblz4|libdivide|libvectorclass|libdouble-conversion|libcpuid|libzstd|libfarmhash|libmetrohash|libpoco|libwidechar_width))/'
将此模式应用于 git list-files
,报告 18155。
因此,我们当前的解决方案是对当前文件的估算
这里的差异归因于几个因素:
- 重命名可以与文件的其他修改同时发生。这些会在 file_changes 中作为单独的事件列出,但时间相同。
argMax
函数无法区分这些 - 它选取第一个值。插入的自然排序(唯一知道正确顺序的手段)在联接跨越时不被保持,因此可能选择修改事件。例如,下面的src/Functions/geometryFromColumn.h
文件在重命名为src/Functions/geometryConverters.h
之前有几次修改。我们目前的解决方案可能会将 Modify 事件选为最新更改,从而造成src/Functions/geometryFromColumn.h
被保留。
- 提交历史断裂 - 缺失删除事件。源和原因待定。
这些差异不应对我们的分析产生实质性影响。我们欢迎此查询的改进版本。
列出更改最多的文件
限制为当前文件,我们将更改的次数视为删除和添加的总和。
提交通常发生在哪一天?
这在周五有一些生产力下降是有道理的。很高兴看到人们在周末提交代码!非常感谢我们的贡献者!
子目录/文件的历史 - 随时间推移的行数、提交和贡献者
这将产生一个巨大的查询结果,如果没有过滤则不切实际地展示或可视化。因此,在以下示例中,我们允许对文件或子目录进行过滤。在这里,我们使用 toStartOfWeek
函数按周分组 - 根据需要进行调整。
这些数据可视化效果很好。下面我们使用 Superset。
对于添加和删除的行:

对于提交和作者:

最大作者数量的文件列表
仅限于当前文件。
存储库中最古老的代码行
仅限于当前文件。
历史最长的文件
仅限于当前文件。
我们的核心数据结构 Merge Tree 显然在不断演变,并且有着悠久的编辑历史!
月度文档和代码贡献者分布
在数据采集期间,由于 docs/
文件夹的非常混乱的提交历史,相关更改已被过滤掉。因此该查询的结果不准确。
我们是否在每月的某些时间(例如,在发布日期附近)写更多的文档?我们可以使用 countIf
函数计算简单的比率,使用 bar
函数可视化结果。
也许在月末时略多,但总体上我们保持良好的均匀分布。由于在数据插入时过滤了文档过滤,这又不可靠。
影响最广泛的作者
我们在这里将“多样性”视为作者贡献的独特文件数量。
让我们看看谁在最近的工作中贡献最多样化。我们不限制日期,而是限制某位作者的最近 N 次提交(在这种情况下,我们使用 3,欢迎您进行修改):
作者的最爱文件
在这里我们选择我们的创始人 Alexey Milovidov,将我们的分析限制在当前文件上。
这很合理,因为 Alexey 一直负责维护变更日志。但是,如果我们使用文件的基本名称来识别他受欢迎的文件 - 这允许重命名,并且应该侧重于代码贡献。
这或许更能反映他的兴趣领域。
最大文件和最少作者的关系
为此,我们首先需要识别最大文件。通过从提交历史记录重建每个文件的完整文件,来进行估算将是非常昂贵的!
为了进行估算,假设我们限制为当前文件,我们对行添加进行求和并减去删除。然后我们可以计算长度与作者数量之间的比例。
文本字典可能并不现实,因此让我们通过文件扩展名过滤仅限于代码!
这里存在一些近期偏见 - 较新的文件有更少的提交机会。如果我们限制到至少 1 年的文件会怎样?
提交和代码行随时间的分布; 按星期几,按作者; 针对特定子目录
我们将其解释为按星期几添加和删除的行数。在这种情况下,我们关注 Functions 目录
以及每日的时间,
这种分布是有道理的,因为我们的大多数开发团队都在阿姆斯特丹。bar
函数帮助我们可视化这些分布:
作者矩阵显示哪些作者倾向于重写其他作者的代码
sign = -1
表示代码删除。我们排除标点符号和空行的插入。
Sankey 图(SuperSet)可以很好地可视化这个。请注意,我们增加了 LIMIT BY
到 3,以获取每个作者的前 3 个代码删除者,从而改善视觉效果。

Alexey 显然喜欢删除别人的代码。让我们排除他,以便更均衡地查看代码删除。

哪位作者在每个星期几的贡献比例最高?
如果仅考虑提交数量:
好的,这里有可能的优势是最长的贡献者 - 我们的创始人 Alexey。让我们将分析限制在过去一年。
这还是有点简单,不能反映人们的工作。
一个更好的度量标准可能是在过去一年中,每一天的顶级贡献者占总工作量的比例。请注意,我们将删除和添加代码视为相等。
存储库中的代码年龄分布
我们将分析限制为当前文件。为了简洁起见,我们将结果深度限制为 2,并按根文件夹限制为 5 个文件。根据需要进行调整。
某个作者的代码有多少百分比被其他作者删除了?
对于这个问题,我们需要某个作者编写的行数,与他们被另一位贡献者删除的总行数的比率。
列出被重写次数最多的文件?
这个问题的最简单方法可能是简单地按照路径计算行更改次数(限制为当前文件),例如:
但是这并没有捕捉到“重写”的概念,即在任何一次提交中文件的大部分发生了变化。这需要更复杂的查询。如果我们认为重写是当文件超过 50% 被删除,且 50% 被添加时。您可以根据自己对重写的定义调整查询。
该查询仅限于当前文件。我们通过按 path
和 commit_hash
分组列出所有文件更改,返回添加和删除的行数。使用窗口函数,我们估算文件在任何时刻的总大小,通过执行累积求和并估算任何变更对文件大小的影响为 lines added - lines removed
。使用这个统计数据,我们可以计算每次更改添加或删除的文件百分比。最后,我们计算每个文件构成重写的文件更改次数,即 (percent_add >= 0.5) AND (percent_delete >= 0.5) AND current_size > 50
。注意我们要求文件行数超过 50,以避免记录较早对文件的贡献也被视为重写。这也避免了对非常小文件的偏见,因为它们更可能被重写。
哪一天的代码在存储库中保留的机会最大?
为此,我们需要唯一标识代码行。我们通过路径和行内容进行估算(因为同一行可能在文件中出现多次)。
我们查询添加的行,将此与删除的行联接 - 过滤掉后者比前者发生得更晚的情况。这样,我们就得到了被删除的行,从而可以计算这两个事件之间的时间。
最后,我们汇总该数据集,计算按星期几行在存储库中保留的平均天数。
按平均代码年龄排序的文件
该查询使用与 哪一天的代码在存储库中保留的机会最大 相同的原理 - 通过使用路径和行内容唯一标识一行代码。 这使我们能够识别行被添加和删除之间的时间。我们筛选为当前文件和代码,并计算每个文件中每行的平均时间。
谁倾向于写更多的测试 / CPP 代码 / 注释?
我们可以通过几种方法来解决这个问题。关注代码与测试的比例,这个查询相对简单 - 计算对包含 tests
的文件夹的贡献数量,并计算与总贡献的比率。
请注意,我们限制为具有 20 次以上更改的用户,以关注常规提交者,避免对一次性贡献的偏见。
我们可以将这个分布绘制为直方图。
大多数贡献者写的代码比测试多,这是可以预见的。
那么谁在贡献代码时添加的注释最多?
请注意,我们按代码贡献进行排序。所有最大贡献者的百分比令人惊讶,部分原因使我们的代码如此可读。
作者的提交比例随时间变化吗?
按作者计算这个问题是微不足道的,
但理想情况下,我们希望了解这一比例在所有作者的整体变化,从他们开始提交的第一天起。他们是否慢慢减少写的注释数量?
为此,我们首先计算每位作者的注释比率随时间的变化 - 类似于 谁倾向于写更多的测试 / CPP 代码 / 注释?。这与每位作者的开始日期联接,使我们能够根据周偏移量计算注释比率。
在所有作者中计算每周的平均值后,我们通过选择每第 10 周来对这些结果进行抽样。
令人鼓舞的是,我们的注释百分比相对稳定,随着作者的贡献时间并未下降。
代码被重写的平均时间和中位数(代码衰退的半衰期)是什么?
我们可以使用相同的原理 列出被重写次数最多的文件 来识别重写,但考虑所有文件。使用窗口函数计算每个文件的重写之间的时间。由此,我们可以计算所有文件的平均值和中位数。
写代码的最佳时机是在代码被重写的几率最大的情况下?
类似于 代码重写的平均时间和中位数(代码衰退的半衰期)是什么? 和 列出被重写次数最多的文件,除了我们按星期几进行汇总。根据需要进行调整,例如年中的月份。
哪位作者的代码最"粘"?
我们将 "粘" 定义为作者的代码在重写之前保持的时间。与之前的问题相似 代码重写的平均时间和中位数(代码衰退的半衰期)是什么? - 使用相同的重写指标,即对文件进行 50% 添加和 50% 删除。我们计算每位作者的平均重写时间,仅考虑贡献超过两个文件的贡献者。
作者连续提交天数最多
此查询首先需要计算作者提交的日期。使用窗口函数,按作者进行分区,我们可以计算他们提交之间的天数。对于每次提交,如果从上次提交到现在的时间为1天,我们将其标记为连续(1),否则标记为0 - 将此结果存储在 consecutive_day
中。
我们稍后的数组函数计算每位作者最长的连续 1 的序列。首先,使用 groupArray
函数收集作者的所有 consecutive_day
值。这个由 1 和 0 组成的数组,然后在 0 值处分割为子数组。最后,我们可以计算出最长的子数组。
文件的逐行提交历史
文件可能会被重命名。当这种情况发生时,我们会收到一个重命名事件,其中 path
列设置为文件的新路径,old_path
列表示之前的位置,例如:
这使得查看文件的完整历史记录变得具有挑战性,因为我们没有单一值能够连接所有行或文件更改。
为了解决这个问题,我们可以使用用户自定义函数(UDFs)。目前无法递归,所以要识别文件的历史记录,我们必须定义一系列 UDFs,并明确使它们相互调用。
这意味着我们只能追踪重命名到最大深度 - 下面的示例深度为 5。文件被重命名的次数不太可能超出这个深度,因此现在这已经足够。
通过调用 file_path_history('src/Storages/StorageReplicatedMergeTree.cpp')
,我们递归遍历重命名历史记录,每个函数使用 old_path
调用下一层。结果使用 arrayConcat
组合。
例如,
我们可以利用这个能力,现在组装文件的整个历史提交。在这个示例中,我们显示每个 path
值的每个提交。
未解决的问题
Git blame
由于当前无法在数组函数中保持状态,因此很难获得精确结果。这在使用 arrayFold
或 arrayReduce
时是可能的,这允许在每次迭代中保持状态。
一个近似的解决方案,足以进行高层分析,可能是这样的:
我们欢迎更精确和改进的解决方案。