跳到主要内容
跳到主要内容

使用 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 上可用 - 详见查询

以下存储库的生成文件可以在下面找到:

要插入这些数据,请通过执行以下查询准备数据库:

使用 INSERT INTO SELECTs3 函数 插入数据。例如,下面我们将 ClickHouse 文件插入到各自的表中:

commits

file_changes

line_changes

查询

该工具通过其帮助输出建议了几条查询。我们对这些查询进行了回答,并增加了一些额外的补充问题。这些查询的复杂性大致是逐渐增加的,与工具的任意顺序相对应。

该数据集可在 play.clickhouse.comgit_clickhouse 数据库中找到。我们为所有查询提供了此环境的链接,并根据需要调整数据库名称。请注意,由于采集数据时间的不同,播放结果可能与此处所示有所不同。

单个文件的历史

最简单的查询。我们查看 StorageReplicatedMergeTree.cpp 的所有提交信息。由于这些信息可能更有趣,我们按最近的信息排序。

play

我们还可以查看行更改,排除重命名的情况,即不会显示重命名事件之前的更改,当文件以不同名称存在时:

play

请注意,这个查询还有一个更复杂的变体,其中我们查找 逐行提交历史,考虑重命名。

查找当前活跃文件

这对于后续分析很重要,当我们只想考虑存储库中的当前文件。我们估算这组文件为那些未被重命名或删除(然后重新添加/重命名)的文件。

请注意,涉及 dbmslibstests/testflows/ 目录下文件的重命名在提交历史中似乎存在断裂。因此我们也排除这些。

play

请注意,这允许文件被重命名然后再次重命名为其原始值。首先,我们将因重命名而删除的文件的 old_path 聚合。然后,我们将每个 path 的最后操作与之联接。最后,我们将此列表筛选为最终事件不是 Delete 的文件。

play

注意,我们在导入时跳过了几个目录,例如:

--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 被保留。

play

  • 提交历史断裂 - 缺失删除事件。源和原因待定。

这些差异不应对我们的分析产生实质性影响。我们欢迎此查询的改进版本

列出更改最多的文件

限制为当前文件,我们将更改的次数视为删除和添加的总和。

play

提交通常发生在哪一天?

play

这在周五有一些生产力下降是有道理的。很高兴看到人们在周末提交代码!非常感谢我们的贡献者!

子目录/文件的历史 - 随时间推移的行数、提交和贡献者

这将产生一个巨大的查询结果,如果没有过滤则不切实际地展示或可视化。因此,在以下示例中,我们允许对文件或子目录进行过滤。在这里,我们使用 toStartOfWeek 函数按周分组 - 根据需要进行调整。

play

这些数据可视化效果很好。下面我们使用 Superset。

对于添加和删除的行:

对于提交和作者:

最大作者数量的文件列表

仅限于当前文件。

play

存储库中最古老的代码行

仅限于当前文件。

play

历史最长的文件

仅限于当前文件。

play

我们的核心数据结构 Merge Tree 显然在不断演变,并且有着悠久的编辑历史!

月度文档和代码贡献者分布

在数据采集期间,由于 docs/ 文件夹的非常混乱的提交历史,相关更改已被过滤掉。因此该查询的结果不准确。

我们是否在每月的某些时间(例如,在发布日期附近)写更多的文档?我们可以使用 countIf 函数计算简单的比率,使用 bar 函数可视化结果。

play

也许在月末时略多,但总体上我们保持良好的均匀分布。由于在数据插入时过滤了文档过滤,这又不可靠。

影响最广泛的作者

我们在这里将“多样性”视为作者贡献的独特文件数量。

play

让我们看看谁在最近的工作中贡献最多样化。我们不限制日期,而是限制某位作者的最近 N 次提交(在这种情况下,我们使用 3,欢迎您进行修改):

play

作者的最爱文件

在这里我们选择我们的创始人 Alexey Milovidov,将我们的分析限制在当前文件上。

play

这很合理,因为 Alexey 一直负责维护变更日志。但是,如果我们使用文件的基本名称来识别他受欢迎的文件 - 这允许重命名,并且应该侧重于代码贡献。

play

这或许更能反映他的兴趣领域。

最大文件和最少作者的关系

为此,我们首先需要识别最大文件。通过从提交历史记录重建每个文件的完整文件,来进行估算将是非常昂贵的!

为了进行估算,假设我们限制为当前文件,我们对行添加进行求和并减去删除。然后我们可以计算长度与作者数量之间的比例。

play

文本字典可能并不现实,因此让我们通过文件扩展名过滤仅限于代码!

play

这里存在一些近期偏见 - 较新的文件有更少的提交机会。如果我们限制到至少 1 年的文件会怎样?

play

提交和代码行随时间的分布; 按星期几,按作者; 针对特定子目录

我们将其解释为按星期几添加和删除的行数。在这种情况下,我们关注 Functions 目录

play

以及每日的时间,

play

这种分布是有道理的,因为我们的大多数开发团队都在阿姆斯特丹。bar 函数帮助我们可视化这些分布:

play

作者矩阵显示哪些作者倾向于重写其他作者的代码

sign = -1 表示代码删除。我们排除标点符号和空行的插入。

play

Sankey 图(SuperSet)可以很好地可视化这个。请注意,我们增加了 LIMIT BY 到 3,以获取每个作者的前 3 个代码删除者,从而改善视觉效果。

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

哪位作者在每个星期几的贡献比例最高?

如果仅考虑提交数量:

play

好的,这里有可能的优势是最长的贡献者 - 我们的创始人 Alexey。让我们将分析限制在过去一年。

play

这还是有点简单,不能反映人们的工作。

一个更好的度量标准可能是在过去一年中,每一天的顶级贡献者占总工作量的比例。请注意,我们将删除和添加代码视为相等。

play

存储库中的代码年龄分布

我们将分析限制为当前文件。为了简洁起见,我们将结果深度限制为 2,并按根文件夹限制为 5 个文件。根据需要进行调整。

play

某个作者的代码有多少百分比被其他作者删除了?

对于这个问题,我们需要某个作者编写的行数,与他们被另一位贡献者删除的总行数的比率。

play

列出被重写次数最多的文件?

这个问题的最简单方法可能是简单地按照路径计算行更改次数(限制为当前文件),例如:

但是这并没有捕捉到“重写”的概念,即在任何一次提交中文件的大部分发生了变化。这需要更复杂的查询。如果我们认为重写是当文件超过 50% 被删除,且 50% 被添加时。您可以根据自己对重写的定义调整查询。

该查询仅限于当前文件。我们通过按 pathcommit_hash 分组列出所有文件更改,返回添加和删除的行数。使用窗口函数,我们估算文件在任何时刻的总大小,通过执行累积求和并估算任何变更对文件大小的影响为 lines added - lines removed。使用这个统计数据,我们可以计算每次更改添加或删除的文件百分比。最后,我们计算每个文件构成重写的文件更改次数,即 (percent_add >= 0.5) AND (percent_delete >= 0.5) AND current_size > 50。注意我们要求文件行数超过 50,以避免记录较早对文件的贡献也被视为重写。这也避免了对非常小文件的偏见,因为它们更可能被重写。

play

哪一天的代码在存储库中保留的机会最大?

为此,我们需要唯一标识代码行。我们通过路径和行内容进行估算(因为同一行可能在文件中出现多次)。

我们查询添加的行,将此与删除的行联接 - 过滤掉后者比前者发生得更晚的情况。这样,我们就得到了被删除的行,从而可以计算这两个事件之间的时间。

最后,我们汇总该数据集,计算按星期几行在存储库中保留的平均天数。

play

按平均代码年龄排序的文件

该查询使用与 哪一天的代码在存储库中保留的机会最大 相同的原理 - 通过使用路径和行内容唯一标识一行代码。 这使我们能够识别行被添加和删除之间的时间。我们筛选为当前文件和代码,并计算每个文件中每行的平均时间。

play

谁倾向于写更多的测试 / CPP 代码 / 注释?

我们可以通过几种方法来解决这个问题。关注代码与测试的比例,这个查询相对简单 - 计算对包含 tests 的文件夹的贡献数量,并计算与总贡献的比率。

请注意,我们限制为具有 20 次以上更改的用户,以关注常规提交者,避免对一次性贡献的偏见。

play

我们可以将这个分布绘制为直方图。

play

大多数贡献者写的代码比测试多,这是可以预见的。

那么谁在贡献代码时添加的注释最多?

play

请注意,我们按代码贡献进行排序。所有最大贡献者的百分比令人惊讶,部分原因使我们的代码如此可读。

作者的提交比例随时间变化吗?

按作者计算这个问题是微不足道的,

但理想情况下,我们希望了解这一比例在所有作者的整体变化,从他们开始提交的第一天起。他们是否慢慢减少写的注释数量?

为此,我们首先计算每位作者的注释比率随时间的变化 - 类似于 谁倾向于写更多的测试 / CPP 代码 / 注释?。这与每位作者的开始日期联接,使我们能够根据周偏移量计算注释比率。

在所有作者中计算每周的平均值后,我们通过选择每第 10 周来对这些结果进行抽样。

play

令人鼓舞的是,我们的注释百分比相对稳定,随着作者的贡献时间并未下降。

代码被重写的平均时间和中位数(代码衰退的半衰期)是什么?

我们可以使用相同的原理 列出被重写次数最多的文件 来识别重写,但考虑所有文件。使用窗口函数计算每个文件的重写之间的时间。由此,我们可以计算所有文件的平均值和中位数。

play

写代码的最佳时机是在代码被重写的几率最大的情况下?

类似于 代码重写的平均时间和中位数(代码衰退的半衰期)是什么?列出被重写次数最多的文件,除了我们按星期几进行汇总。根据需要进行调整,例如年中的月份。

play

哪位作者的代码最"粘"?

我们将 "粘" 定义为作者的代码在重写之前保持的时间。与之前的问题相似 代码重写的平均时间和中位数(代码衰退的半衰期)是什么? - 使用相同的重写指标,即对文件进行 50% 添加和 50% 删除。我们计算每位作者的平均重写时间,仅考虑贡献超过两个文件的贡献者。

play

作者连续提交天数最多

此查询首先需要计算作者提交的日期。使用窗口函数,按作者进行分区,我们可以计算他们提交之间的天数。对于每次提交,如果从上次提交到现在的时间为1天,我们将其标记为连续(1),否则标记为0 - 将此结果存储在 consecutive_day 中。

我们稍后的数组函数计算每位作者最长的连续 1 的序列。首先,使用 groupArray 函数收集作者的所有 consecutive_day 值。这个由 1 和 0 组成的数组,然后在 0 值处分割为子数组。最后,我们可以计算出最长的子数组。

play

文件的逐行提交历史

文件可能会被重命名。当这种情况发生时,我们会收到一个重命名事件,其中 path 列设置为文件的新路径,old_path 列表示之前的位置,例如:

play

这使得查看文件的完整历史记录变得具有挑战性,因为我们没有单一值能够连接所有行或文件更改。

为了解决这个问题,我们可以使用用户自定义函数(UDFs)。目前无法递归,所以要识别文件的历史记录,我们必须定义一系列 UDFs,并明确使它们相互调用。

这意味着我们只能追踪重命名到最大深度 - 下面的示例深度为 5。文件被重命名的次数不太可能超出这个深度,因此现在这已经足够。

通过调用 file_path_history('src/Storages/StorageReplicatedMergeTree.cpp'),我们递归遍历重命名历史记录,每个函数使用 old_path 调用下一层。结果使用 arrayConcat 组合。

例如,

我们可以利用这个能力,现在组装文件的整个历史提交。在这个示例中,我们显示每个 path 值的每个提交。

未解决的问题

Git blame

由于当前无法在数组函数中保持状态,因此很难获得精确结果。这在使用 arrayFoldarrayReduce 时是可能的,这允许在每次迭代中保持状态。

一个近似的解决方案,足以进行高层分析,可能是这样的:

我们欢迎更精确和改进的解决方案。