百亿美金的设计,深度剖析 GitLab 的 Postgres 数据库 schema

原文链接

这篇文章写于 2022 年,前一年 GitLab 刚好完成 IPO。目前 GitLab 市值超过 100 亿美金,它的所有收入都来源于同名产品 GitLab,而这篇文章就是全面分析 GitLab 这个产品的数据库 schema。

我花了一些时间研究 GitLab 的 Postgres schema。GitLab 是 Github 的一个替代品。你可以自部署 GitLab,因为它是一个开源的 DevOps 平台。

我之所以要了解 Gitlab 这样的大项目的 schema,是为了与我正在设计的 schema 进行比较,并从他们的 schema 定义中学到一些最佳实践。我确实从中受益良多。

我清楚的知道,最佳实践取决于具体情况,不能盲目应用。

GitLab 的数据库 schema 文件 structure.sql [1] 包含超过 34000 行代码。GitLab 本质上是一个集成式的 Ruby on Rails 应用。虽然通常我们会用 schema.rb 文件来管理数据库的版本迁移,但 GitLab 团队在他们的问题追踪系统中的一个讨论 [2] 说明了他们选择 structure.sql 的原因。

原因在于 schema.rb 只能包含标准的迁移操作(使用 Rails DSL),这样做是为了使数据库 schema 文件对数据库系统保持中立,抽象化特定的 SQL 操作。这导致我们无法利用 PostgreSQL 的一些高级特性,如触发器、分区、物化视图等。

为了充分利用这些高级特性,我们应该考虑使用纯 SQL 格式的schema 文件 structure.sql,而不是 Ruby/Rails 的标准架构文件 schema.rb。

这意味着我们需要修改配置 config.active_record.schema_format = :sql,并重新以 SQL 格式生成数据库 schema。这可能还需要调整一些构建流程。 现在,让我们回顾一下我从 GitLab Postgres schema 中学到的东西。

为表使用正确的主键类型

在我的工作中,我犯了主键类型标准化的错误。这意味着将 bigint 或 uuid 标准化,这样所有表无论其结构、访问模式和增长速度如何,都将具有相同的类型。

当数据库规模较小时,这不会产生任何明显的影响,但当数据库规模扩大时,主键就会对存储空间、写入速度和读取速度产生明显的影响。因此,我们在为表选择正确的主键类型时应进行适当的思考。

正如我在之前的一篇文章 [3] 中所讨论的,当你使用 Postgres 本地 UUID v4 类型而不是 bigserial 类型时,表的大小会增加 25%,插入率则会下降到 bigserial 类型的 25%。这是一个很大的差别。我还与 ULID 进行了比较,但它的性能也很差。其中一个原因可能是 ULID 的实现。

在这种情况下,我很想了解 GitLab 是如何选择主键类型的。

在 573 个表中,380 个表使用 bigserial 主键类型,170 个表使用 serial4 主键类型,其余 23 个表使用复合主键。他们没有使用 uuid v4 主键或其他类似 ULID 的深奥键类型的表。

file

1 quintillion = 10 亿 billion

选择 serial 还是 bigserial 取决于表中记录的数量。

application_settings、badges、chat_teams、notification_settings、project_settings 等表使用串行类型。对于一些表,如 issues、web_hooks、merge_requests 和 projects,我很惊讶地发现它们使用了 serial 类型。

这个 serial 类型可能适用于自部署的社区或企业版本,但对于 GitLab.com SaaS 服务,这可能会造成问题。例如,GitHub 在 2020 年拥有 1.28 亿个公共仓库 [4]。即使每个仓库有 20 个问题,也会超过序列范围。此外,更改表格类型的成本也很高。表需要被重写,意味着你需要耐心等待。如果要对表进行分片,这同样会成为一个问题。

我做了一个快速实验,结果表明,对于我这个有两列和 1000 万条记录的表,将数据类型从 integer 改为 bigint 需要 11 秒。

create table exp_bs(id serial primary key, n bigint not null)

插入 1000 万条记录

insert into exp_bs(n) select g.n from generate_series(1,10000000) as g(n)

变更数据类型

alter table exp_bs alter column id TYPE bigint;

ALTER TABLE
Time: 10845.062 ms (00:10.845)

您还必须更改序列以改变其类型。这种操作很快。

alter sequence exp_bs_id_seq as bigint;

ALTER SEQUENCE
Time: 4.505 ms

所有 bigserial 序列都从 1 开始,直到 bigint 的最大值。

CREATE SEQUENCE audit_events_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

使用内部和外部 ID

通常,不向外部世界公开主键是一个好习惯。这在你使用 integer 或 bigint 型的顺序自增标识符时尤为重要,因为它们是可预测的。

因此,我很想知道在创建 GitLab 问题时会发生什么。是向外部用户公开主键 id,还是使用其他 id?如果公开 issues 表的主键 id,那么在项目中创建 issue 时,它就不会以 1 开头,你可以很容易猜出 GitLab 中存在多少个问题。这既不安全,用户体验也很差。

为了避免将主键暴露给终端用户,常见的解决方案是使用两个 ID。第一个是你的主键 id,它保持在系统内部,从不暴露于任何公共环境。第二个 id 是我们与外部世界共享的。根据我过去的经验,我使用 UUID v4 作为外部 id。正如我们在前一点中讨论的,使用 UUID 有存储成本。

GitLab 也在需要与外部世界共享 id 的表中使用内部和外部 id。像 issues、ci_pipelines、deployments、epics 以及其他一些表有两个 id - id 和 iid。下面是 issue schema 的一部分。如下所示,iid 的数据类型为 integer。

CREATE TABLE issues (
    id integer NOT NULL,
    title character varying,
    project_id integer,
    iid integer,
    // ……)

正如你所看到的,有 id 和 iid 两列。iid 列的值与最终用户共享。一个 issue 使用 project_id 和 iid 进行唯一标识。这是因为可能有多个 issue 具有相同的 iid。为了更清楚地说明这一点,如果您创建了两个项目,并在每个版本库中创建了一个 issue,那么这两个项目的可见 ID 都必须是 1,如下图所示。sg 和 sg2 项目都以 issue ID 1 开始。

https://gitlab.com/shekhargulati123/sg/-/issues/1
https://gitlab.com/shekhargulati123/sg2/-/issues/1

它们在 project_id 和 iid 上都有一个唯一索引,以便快速有效地获取 issue。

CREATE UNIQUE INDEX index_issues_on_project_id_and_iid ON public.issues USING btree (project_id, iid);

使用带有检查约束的 text 字符类型

Postgres 文档 [5] 中描述了三种字符类型。

file

我主要使用 character varying(n) 或 varchar(n) 来存储字符串值。GitLab schema 既使用character varying(n),也使用 text,但更常用的是 text 类型。下面是一个这样的示例表。

CREATE TABLE audit_events (
    id bigint NOT NULL,
    author_id integer NOT NULL,
    entity_id integer NOT NULL,
    entity_type character varying NOT NULL,
    details text,
    ip_address inet,
    author_name text,
    entity_path text,
    target_details text,
    created_at timestamp without time zone NOT NULL,
    target_type text,
    target_id bigint,
    CONSTRAINT check_492aaa021d CHECK ((char_length(entity_path) <= 5500)),
    CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255)),
    CONSTRAINT check_97a8c868e7 CHECK ((char_length(target_type) <= 255)),
    CONSTRAINT check_d493ec90b5 CHECK ((char_length(target_details) <= 5500))
)
PARTITION BY RANGE (created_at);

可以看到,除了 entity_type 之外,所有其他列都是 text 类型。他们使用 CHECK 来定义长度约束。

正如网上多篇文章 [6,7] 所述,这两种类型的性能差别不大。它们底层都采用了 varlena 类型来存储。

varchar(n) 的问题在于,如果 n 的限制变多,就需要独占锁。这可能会导致性能问题,具体取决于表的大小。

而使用 CHECK 约束的 text 列则没有这个问题。但在写入过程中会有一点代价。 让我们做一个简单的实验来证明这一点。我们先创建一个简单的表

create table cv_exp (id bigint primary key, s varchar(200) default gen_random_uuid() not null);
create index sidx on cv_exp (s);

插入 1000 万条记录

insert into cv_exp(id) select g.n from generate_series(1,10000000) as g(n);

如果我们将 s 的长度从 200 增加到 300,那么它很快执行完。

alter table cv_exp alter column s type varchar(300);

ALTER TABLE
Time: 37.460 ms

但是,如果我们将 s 的长度从 300 减少到 100,就会花费相当多的时间。

alter table cv_exp alter column s type varchar(100);

ALTER TABLE
Time: 35886.638 ms (00:35.887)

让我们对 text 列做同样的处理。

create table text_exp (id bigint primary key, s text default gen_random_uuid() not null,CONSTRAINT check_15e644d856 CHECK ((char_length(s) <= 200)));

插入 1000 万条记录。

insert into text_exp(id) select g.n from generate_series(1,10000000) as g(n);

在 Postgres 中没有 alter 约束。您必须删除约束,然后添加新约束。

alter table text_exp drop constraint check_15e644d856;

重新添加。

alter table text_exp add constraint check_15e644d856 CHECK ((char_length(s) <= 100));

ALTER TABLE
Time: 1870.250 ms (00:01.870)

正如您所看到的,与具有长度检查的 character varying 或 varchar(n) 相比,具有 CHECK 约束的 text 类型可以让你轻松改进 schema。

我还注意到,他们在不需要长度检查的地方使用了 character varying ,如下图所示。

CREATE TABLE project_custom_attributes (
    id integer NOT NULL,
    created_at timestamp with time zone NOT NULL,
    updated_at timestamp with time zone NOT NULL,
    project_id integer NOT NULL,
    key character varying NOT NULL,
    value character varying NOT NULL
);

命名规范

命名遵循以下规则。

  • 所有表都采用复数形式,如 issues、projects、audit_events、abuse_reports、approvers 等。
  • 表使用模块名称作为前缀,以提供命名空间。例如,所有与合并请求功能相关的表都以 merge_request 前缀开头,如下所示:
    • merge_request_assignees
    • merge_request_blocks
    • merge_request_cleanup_schedules
    • merge_request_context_commit_diff_files
    • merge_request_context_commits
    • 等等...
  • 表和列的命名遵循蛇形命名法(snake_case),即使用下划线将两个或更多单词连接起来,例如 title、created_at、is_active。
  • 表达布尔值的列根据其用途遵循以下三种命名规则之一:
    • 功能开关,如 create_issue、send_email、packages_enabled、merge_requests_rebase_enabled 等。
    • 实体状态,如 deployed、onboarding_complete、archived、hidden等。
    • 限定词,以 is_xxx 或 has_xxx 开头,如 is_active、is_sample、has_confluence 等。我认为这些可以通过上述两种方式来表达。
  • 索引的命名遵循 index_#{table_name}on#{column_1}and#{column_2}_#{condition} ,例如 index_services_on_type_and_id_and_template_when_active、index_projects_on_id_service_desk_enabled。

带时区和不带时区的时间戳

GitLab 同时使用了 带时区的时间戳 和 不带时区的时间戳。 根据我的理解,系统执行动作时采用的是 不带时区的时间戳,而涉及用户操作时则使用 带时区的时间戳。例如,以下 SQL 示例中的 created_at 和 updated_at 字段就采用了不带时区的时间戳,而 closed_at 字段则使用了带时区的时间戳。

CREATE TABLE issues (
    id integer NOT NULL,
    title character varying,
    created_at timestamp without time zone,
    updated_at timestamp without time zone,
    closed_at timestamp with time zone,
    closed_by_id integer,
);

另一个例子是 merge_request_metrics 表,其中 latest_closed_at、first_comment_at、first_commit_at 和 last_commit_at 字段使用了带时区的时间戳,而 latest_build_started_at、latest_build_finished_at 和 merge_at 则采用了不带时区的时间戳。你可能会对 merge_at 不使用时区感到疑惑。我认为这是因为系统可以基于特定的条件或检查来执行合并操作。

CREATE TABLE merge_request_metrics (
    id integer NOT NULL,
    latest_build_started_at timestamp without time zone,
    latest_build_finished_at timestamp without time zone,
    merged_at timestamp without time zone,
    created_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone NOT NULL,

    latest_closed_at timestamp with time zone,
    first_comment_at timestamp with time zone,
    first_commit_at timestamp with time zone,
    last_commit_at timestamp with time zone,
    first_approved_at timestamp with time zone,
    first_reassigned_at timestamp with time zone
);

外键约束

外键约束是两个表之间行的逻辑关联。通常在查询中使用外键来连接表。

外键约束是一种数据库机制,用于强制维护外键关系的完整性(参考完整性),确保子表只有在父表中存在对应行时才能进行引用。此约束还通过多种方式防止不同表中出现 “孤儿行 (orphaned row)”。

过去几年里,我参与的多个项目中,团队/架构师决定不采用外键约束,主要原因是担心性能影响。

外键创建时可能会导致性能下降的一个情况是,当它与 ON DELETE CASCADE 操作一起使用时。这种操作的机制是,如果父表中的一行被删除,子表中所有引用该行的记录也会在同一事务中被删除。本来可能预期删除一行,结果却可能导致删除数百、数千乃至更多的子表行。但这种情况只有在一个父行与大量子表行关联时才会出现问题。

团队不采用外键约束的另外两个原因是:

  • 在线 DDL schema 迁移操作中,特别是在 MySQL 中,外键约束的兼容性不好。
  • 一旦将数据分片至多个数据库服务器,维护外键约束变得困难。

类似 PlanetScale(基于开源 Vitess 数据库)的兼容 MySQL 的 serverless 数据库不支持外键。

因此,我很想知道 GitLab 是否使用外键约束。

除了 audit_events 、abuse_reports、web_hooks_logs 和 spam_logs 等少数几个表之外,GitLab 在大多数表中都使用了外键约束。我认为不在这些表中使用外键约束有两个主要原因。这两个原因是:

  • 这些表在本质上是不可变的。一旦条目被写入,你就不想再更改它们了
  • 这些表的行数可达数百万(或更多),因此即使是很小的性能损失也会造成很大影响

GitLab 使用外键的其他表都使用了 ON DELETE CASCADE、ON DELETE RESTRICT 和 ON DELETE SET NULL 操作。下面分别举例说明。

ALTER TABLE ONLY todos
    ADD CONSTRAINT fk_rails_a27c483435 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY projects
    ADD CONSTRAINT fk_projects_namespace_id FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE RESTRICT;
ALTER TABLE ONLY authentication_events
    ADD CONSTRAINT fk_rails_b204656a54 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
  • ON DELETE SET NULL 会将匹配记录的子表引用列设置为空。这会导致记录成为孤儿,但由于 NULL 的存在,你可以很容易地识别它们。在此操作中,删除一条记录也会导致子表中更新多条记录。这可能会导致大型事务、过度锁定和复制滞后。
  • ON DELETE RESTRICT 可以防止删除引用的子表记录。这不会导致子表成为孤儿,因为如果有子表引用父表记录,就无法删除父表记录。你会得到如下所示的异常。
ERROR:  update or delete on table "a" violates foreign key constraint "fk_a_id" on table "b"
DETAIL:  Key (id)=(1) is still referenced from table "b".

大表分区

GitLab 通过分区技术来处理可能增长至庞大规模的表格,目的是为了提升查询性能。

  • 按范围分区(PARTITION BY RANGE):这种分区方法根据选定的范围来分区表格数据。这种策略通常适用于需要对时间序列数据进行分区的场景。例如,audit_events(审计事件)和 web_hook_logs(Web 钩子日志)表就采用了这种分区策略。
  • 按列表分区(PARTITION BY LIST):这种分区方法根据某一列的离散值来对表格数据进行分区。loose_foreign_keys_deleted_records(松散外键删除记录)表就是采用这种分区策略的例子。
  • 按哈希分区(PARTITION BY HASH):这种分区方法通过为每个分区指定一个模数和余数来进行。每个分区将包含那些其分区键的哈希值除以指定模数后得到指定余数的行。product_analytics_events_experimental(产品分析事件实验)表采用了这种分区策略。 有关 Postgres 分区的更多信息,请参阅 Postgres 文档。

使用 Trigrams 和 gin_trgm_ops 支持 LIKE 搜索用例

GitLab 使用 GIN(通用反向索引)索引来执行高效搜索。

GIN 索引类型是为处理可细分的数据类型而设计的,您希望搜索单个组件值(数组元素、文本文档中的词条等)"。- 汤姆-莱恩

由于 LIKE 操作支持任意通配符表达式,因此从根本上说很难编制索引。其中一个例子是问题表,您可能想对标题和描述字段进行搜索。因此,我们使用 pg_trgm 扩展来创建一个可用于 Trigrams 的索引。

CREATE INDEX index_issues_on_title_trigram ON issues USING gin (title gin_trgm_ops);
CREATE INDEX index_issues_on_description_trigram ON issues USING gin (description gin_trgm_ops);

GIN 索引使搜索变得高效。让我们来看看实际操作。 我们将创建一个简单的表,如下所示。

create table words(id serial primary key, word text not null);

让我们插入一些数据。我从这个链接中提取了 CSV 格式的英语单词列表。

\copy words(word) from '/Users/xxx/Aword.csv' CSV;
select count(*) from words;

count
-------
11616
(1 row)

我们将在 word 列上创建一个 btree 索引,稍后我们将使用 gin 索引来显示其效率。

create index id1 on words using btree (word);

让我们运行解释计划查询。

EXPLAIN select * from words where word like '%bul%';

QUERY PLAN
-----------------------------------------------------------
Seq Scan on words  (cost=0.00..211.20 rows=1 width=14)
Filter: (word ~~ '%bul%'::text)
(2 rows)

现在,让我们删除 btree 索引。

drop index id1;

安装 pg_trm 扩展。

CREATE EXTENSION pg_trgm;

创建索引。

create index index_words_on_word_trigram ON words USING gin (word gin_trgm_ops);

现在,运行 Explain。

EXPLAIN select count(*) from words where word like '%bul%';

QUERY PLAN

----------------------------------------------------------------------------------------------------
  Aggregate  (cost=16.02..16.03 rows=1 width=8)
   ->  Bitmap Heap Scan on words  (cost=12.01..16.02 rows=1 width=0)
         Recheck Cond: (word ~~ '%bul%'::text)
          ->  Bitmap Index Scan on index_words_on_word_trigram  (cost=0.00..12.01 rows=1 width=0)
            Index Cond: (word ~~ '%bul%'::text)
(5 rows)

GitLab 还使用 tsvector 支持完整的全文搜索。

在主数据存储中进行文本搜索的优势在于:

  • 实时索引。创建索引无延迟
  • 访问完整数据
  • 降低架构的复杂性

使用 jsonb

正如我在前一篇文章中所讨论的,我在 schema 设计中将 json 数据类型用于以下用例:

  • 转储稍后处理的请求数据
  • 支持附加字段
  • 一对多关系,其中多方都没有自己的标识
  • Key Value
  • 更简单的 EAV (Entity-Attribute-Value) 设计

GitLab schema 设计也在多个表中使用 jsonb 数据类型。他们主要将其用于上述列表中的 1 和 2 用例。与纯文本存储相比,使用 jsonb 的优势在于 Postgres 支持对 jsonb 数据类型进行高效查询。 例如,表 error_tracking_error_events 以 jsonb 数据类型存储有效负载。这是一个转储请求数据的示例,将在后面的用例中进行处理。

CREATE TABLE error_tracking_error_events (
    id bigint NOT NULL,
    payload jsonb DEFAULT '{}'::jsonb NOT NULL,
    // ...
);

你可以使用 JSON schema 来验证 JSON 文档的结构。

另一个例子是下图中的 operations_strategies 表。您不知道可能会收到多少个参数,因此需要像 jsonb 这样灵活的数据类型。

CREATE TABLE operations_strategies (
    id bigint NOT NULL,
    feature_flag_id bigint NOT NULL,
    name character varying(255) NOT NULL,
    parameters jsonb DEFAULT '{}'::jsonb NOT NULL
);

支持附加字段用例如下所示。

CREATE TABLE packages_debian_file_metadata (    created_at timestamp with time zone NOT NULL,    updated_at timestamp with time zone NOT NULL,    package_file_id bigint NOT NULL,    file_type smallint NOT NULL,    component text,    architecture text,fields jsonb,);

他们还使用 jsonb 来存储已经是 JSON 格式的数据。例如,在表 vulnerability_finding_evidences 中,报告数据已经是 JSON 格式,因此他们将其保存为 jsonb 数据类型。

CREATE TABLE vulnerability_finding_evidences (
    id bigint NOT NULL,
    created_at timestamp with time zone NOT NULL,
    updated_at timestamp with time zone NOT NULL,
    vulnerability_occurrence_id bigint NOT NULL,
    data jsonb DEFAULT '{}'::jsonb NOT NULL
);

其他花絮

  • 审计字段(如 updated_at)只用于可修改记录的表中。例如,issues 就有一个 updated_at 列。对于只可附加的不可变日志表(如 audit_events),则没有 updated_at 列,如下代码片段所示。有 updated_at 列的 issues 表。
CREATE TABLE issues (
    id integer NOT NULL,
    title character varying,
    author_id integer,
    project_id integer,
    created_at timestamp without time zone,
    updated_at timestamp without time zone,
    // removed remaining columns and constraints
);

没有 updated_at 列的 audit_events 表。

CREATE TABLE audit_events (
    id bigint NOT NULL,
    author_id integer NOT NULL,
    entity_id integer NOT NULL,
    created_at timestamp without time zone NOT NULL,
    // removed remaining columns and constraints
);
    • 枚举值存储为 smallint 而非 character varying,以节省空间。唯一的问题是不能更改枚举值的顺序。在下面的示例中,reason 和 severity_level 是枚举值。
CREATE TABLE merge_requests_compliance_violations (
    id bigint NOT NULL,
    violating_user_id bigint NOT NULL,
    merge_request_id bigint NOT NULL,
    reason smallint NOT NULL,
    severity_level smallint DEFAULT 0 NOT NULL
);
  • 在少数几个表(如 issues 和 ci_builds)中使用乐观锁定机制,以防止多方同时编辑所导致的冲突。乐观锁定假设数据冲突的情况极少,如果发生冲突,应用会抛出异常并忽略更新。如果表中存在 lock_version 字段,则 Active Record 会支持乐观锁定,每次更新都会增加 lock_version 列的值。锁定机制可确保两次实例化的记录在第一次也被更新的情况下,最后保存的记录会引发 StaleObjectError 异常。下面显示的 ci_builds 表使用了 lock_version 列。
CREATE TABLE ci_builds (
    status character varying,
    finished_at timestamp without time zone,
    trace text,

      lock_version integer DEFAULT 0,

      // removed columns
    CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL))
);
  • 使用 inet 来存储 IP 地址。我不知道有 inet 类型。他们在 audit_events 和 authentication_events 表中使用了 inet。
CREATE TABLE audit_events (
    id bigint NOT NULL,
    ip_address inet,
    // ……
);

GitLab 并未在所有存储 ip_address 的表中使用 inet。例如,在 ci_runners 和 user_agent_details 表中,他们将其存储为 character varying。我不清楚为什么他们没有在所有存储 ip 地址的表中使用相同的类型。

与将 ip 地址存储为纯文本类型相比,您应该选择 inet 类型,因为这些类型提供了输入错误处理和专门功能。

让我们快速了解一下它的操作。首先,我们将创建一个包含两个字段(id 和 ip_addr)的表。

create table e (id serial primary key, ip_addr inet not null);

我们可以插入一条有效记录,如下所示。

insert into e(ip_addr) values ('192.168.1.255');

我们还可以插入带掩码的记录,如下所示。

insert into e(ip_addr) values ('192.168.1.5/24');

这两条记录都将被插入。

select id, abbrev(ip_addr) from e;

id |     abbrev
----+----------------
1 | 192.168.1.255
2 | 8.8.8.8
3 | 192.168.1.5/24
(3 rows)

如果我们试图保存无效数据,则插入将失败。

insert into e(ip_addr) values ('192.168.1');

ERROR:  invalid input syntax for type inet: "192.168.1"
LINE 1: insert into e(ip_addr) values ('192.168.1');

你可以使用 Postgres 支持的 inet 操作符检查子网是否包含 IP 地址,如下所示。

select * from e where ip_addr << inet '192.168.1.1/24';

id |    ip_addr
----+---------------
1 | 192.168.1.255
(1 row)

如果要检查子网是否包含或相等,我们可以执行以下操作。

select * from e where ip_addr <<= inet '192.168.1.1/24';
id |    ip_addr
----+----------------
1 | 192.168.1.255
3 | 192.168.1.5/24
(2 rows)

Postgres 还支持许多其他操作符和函数。你可以在 Postgres 文档中阅读它们。

  • Postgres 的 bytea 数据类型用于存储SHA、加密令牌、加密密钥、加密密码、指纹等。
  • Postgres 的数组类型用于存储具有多个值的列。

数组应在完全确定不需要在数组中的项目与其他表之间创建任何关系的情况下使用。它应该用于紧密耦合的一对多关系。

例如,在下图所示的表中,我们将 *_ids 存储为数组,而不是以平面方式存储并定义与其他表的关系。我们不知道有多少用户和项目会被提及,因此创建诸如 mentioned_user_id1 , mentioned_user_id2, mentioned_user_id3 等列会造成浪费。

CREATE TABLE alert_management_alert_user_mentions (    id bigint NOT NULL,    alert_management_alert_id bigint NOT NULL,    note_id bigint,    mentioned_users_ids integer[],    mentioned_projects_ids integer[],    mentioned_groups_ids integer[]);

Postgres 数组的另一个常见用例是存储 hosts、tags 和 urls 等字段。

CREATE TABLE dast_site_profiles (
    id bigint NOT NULL,
    excluded_urls text[] DEFAULT '{}'::text[] NOT NULL,
);

CREATE TABLE alert_management_alerts (
    id bigint NOT NULL,
    hosts text[] DEFAULT '{}'::text[] NOT NULL,
);

CREATE TABLE ci_pending_builds (
    id bigint NOT NULL,
    tag_ids integer[] DEFAULT '{}'::integer[],
);

总结

通过 GitLab 的数据库 schema 设计,我学到了很多。他们并不是盲目地将相同的实践应用于所有表设计。每个表都根据其目的、存储的数据类型以及增长速度做出最佳决策。

如果你需要像 GitLab 一样约束表结构设计的话,可以使用 Bytebase 的 SQL 审核功能,针对包括 Postgres 的各种数据库,它提供了上百条 SQL 审核规则。

参考资料

  • Gitlab schema structure.sql – https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/structure.sql
  • Issue 29465: Use structure.sql instead of schema.rb – https://gitlab.com/gitlab-org/gitlab/-/issues/29465
  • Choosing Primary Key Type in Postgres – https://shekhargulati.com/2022/06/23/choosing-a-primary-key-type-in-postgres/
  • Github’s Path to 128M public repositories – https://towardsdatascience.com/githubs-path-to-128m-public-repositories-f6f656ab56b1#:~:text=There%20are%20over%20128%20million%20public%20repositories%20on%20GitHub.
  • Postgres Character Types Documentation – https://www.postgresql.org/docs/current/datatype-character.html
  • Difference between text and varchar (character varying) – https://stackoverflow.com/questions/4848964/difference-between-text-and-varchar-character-varying
  • CHAR(x) vs. VARCHAR(x) vs. VARCHAR vs. TEXT – https://www.depesz.com/2010/03/02/charx-vs-varcharx-vs-varchar-vs-text/

💡 更多资讯,请关注 Bytebase 公号:Bytebase

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/403262.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ArcGIS Pro二次开发】(82):玩个花活_控规指标块生成

一、要实现的效果 废话不多说&#xff0c;这次要实现的是类似控规指标块的标注&#xff1a; 这里只是示例&#xff0c;用了5个格子&#xff0c;做成9个格子也是可以的。 实现这个效果最关键的是要用到Pro中的复合标注。 关于复合标注的用法可以搜一下帮助里的【使用复合注释…

网站常见的攻击类型有什么,如何针对性防护

在互联网时代&#xff0c;几乎每个网站都存在着潜在的安全威胁。这些威胁可能来自人为失误&#xff0c;也可能源自网络犯罪团伙所发起的复杂攻击。无论攻击的本质如何&#xff0c;网络攻击者的主要动机通常是谋求经济利益。这意味着不管是什么网站类型潜在的威胁一直都存在。 在…

关于2025年的AMC8竞赛,你可能感兴趣的一些问题和信息

最近几天&#xff0c;我分享了一些历年的AMC8数学竞赛真题和解析&#xff0c;有一些家长和孩子第一次接触&#xff0c;产生了浓厚的兴趣&#xff0c;并且问了许多关于AMC8的问题。为了帮助更多家长和孩子了解这个比赛&#xff0c;我把常见的问题&#xff0c;以及大家可能感兴趣…

[java基础揉碎]封装

封装介绍 封装(encapsulation)就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部&#xff0c;程序的其它部分只有通过被授权的操作[方法],才能对数据进行操作。 比如说我们用遥控器对电视的操作, 我们按电视机的开关键, 其实开关背后是一个复杂的过程,…

Codeforces Round 927 (Div. 3)

F. Feed Cats 题目大意 给一长度为的数轴&#xff0c;个区间在数轴上选取一些点作为特殊点在满足个区间中&#xff0c;每个区间内只能有一个特殊点问最多能选多少个特殊点 解题思路 对于每个点有放或不放两种状态考虑表示位置可能放或不放的最优结果若不放&#xff0c;若放…

unity hub初学配置

1、安装Unity Hub 2、设置中文 3、安装编辑器 4、新建项目 5、新建完成后进入编辑器 6、 编辑器设置中文 editPreferencesLanguages选择中文

数据安全治理实践路线(上)

基于以上数据安全治理实践理念&#xff0c;可以按照自顶向下和自底向上相结合的思路推进实践过程。一方面&#xff0c;组织自顶向下,以数据安全战略规划为指导,以规划、建设、运营、优化为主线&#xff0c;围绕构建数据安全治理体系这一核心&#xff0c;从组织架构、制度流程、…

SQL注入:使用预编译防御SQL注入时产生的问题

目录 前言 模拟预编译 真正的预编译 预编译中存在的SQL注入 宽字节 没有进行参数绑定 无法预编译的位置 前言 相信学习过SQL注入的小伙伴都知道防御SQL注入最好的方法&#xff0c;就是使用预编译也就是PDO是可以非常好的防御SQL注入的&#xff0c;但是如果错误的设置了…

【C语言】socket 层到网络接口的驱动程序之间的函数调用过程

一、socket 层到网络接口的驱动程序之间的函数调用过程概述 在 Linux 操作系统中&#xff0c;socket 层到网络接口的驱动程序之间的函数调用过程相对复杂&#xff0c;涉及多个层次的交互。以下是一个简化的概述&#xff0c;描述数据从 socket 传递到硬件驱动&#xff0c;再到硬…

新书推荐:《分布式商业生态战略:未来数字商业新逻辑与企业数字化转型新策略》

近两年&#xff0c;商业经济环境的不确定性越来越明显&#xff0c;市场经济受到疫情、技术、政策等多方因素影响越来越难以预测&#xff0c;黑天鹅事件时有发生。在国内外经济方面&#xff0c;国际的地缘政治对商业经济产生着重大的影响&#xff0c;例如供应链中断&#xff0c;…

PostgreSQL 实体化视图的使用

上周的教程中&#xff0c;通过 DVD Rental Database 示例&#xff0c;让我们了解了在 PostgreSQL 中创建实体化视图的过程。正如我们所了解的&#xff0c;PostgreSQL 实体化视图提供了一种强大的机制&#xff0c;通过预计算和存储查询结果集为物理表来提高查询性能。接下来的内…

C#_扩展方法

简述&#xff1a; 扩展方法所属类必需是静态类&#xff08;类名依据规范通常为XXXExtension&#xff0c;XXX为被扩展类&#xff09;扩展方法必需是公有的静态方法扩展方法的首个参数由this修饰&#xff0c;参数类型为被扩展类型 示例&#xff1a; static class DoubleExtens…

vue实现拖拽(vuedraggable)

实现效果: 左侧往右侧拖动&#xff0c;右侧列表可以进行拖拽排序。 安装引用&#xff1a; npm install vuedraggable import draggable from vuedraggable 使用&#xff1a; data数据&#xff1a; componentList: [{groupName: 考试题型,children: [{componentType: danxua…

【基础】提高前端的增益

低噪声&#xff0c;低偏移电压&#xff0c;低漂移-当你把信号链前端的增益提高后&#xff0c;所有的这些精密小信号处理的目标变得很简单。 这是一个很简单的概念。如图1所示&#xff0c;第二级的误差将除以第一级的增益。比如&#xff0c;第一级增益适度&#xff0c;值为10&a…

制造业客户数据安全解决方案(终端安全/文件加密/介质管理等)

针对前文制造业客户数据安全解决方案&#xff08;数据防泄密需求分析&#xff09;提到的泄密风险&#xff0c;本文详细介绍一套完整、合理的解决方案&#xff0c;通过该方案构建公司数据安全防护边界&#xff0c;自动加密、全方位保护数据安全。 PC端&#xff1a;https://isite…

Qt开发:MAC安装qt、qtcreate(配置桌面应用开发环境)

安装qt-creator brew install qt-creator安装qt brew install qt查看qt安装路径 brew info qtzhbbindembp ~ % brew info qt > qt: stable 6.6.1 (bottled), HEAD Cross-platform application and UI framework https://www.qt.io/ /opt/homebrew/Cellar/qt/6…

创建一个基于Node.js的实时聊天应用

在当今数字化社会&#xff0c;实时通讯已成为人们生活中不可或缺的一部分。无论是在社交媒体平台上与朋友交流&#xff0c;还是在工作场合中与同事协作&#xff0c;实时聊天应用都扮演着重要角色。与此同时&#xff0c;Node.js作为一种流行的后端技术&#xff0c;为开发者提供了…

10大数据恢复软件可以帮助您恢复电脑数据

您可能会非常紧张&#xff0c;因为知道有人意外地从您的硬盘驱动器中删除了您的宝贵数据&#xff08;甚至使用 ShiftDelete 从回收站中删除&#xff09;&#xff0c;并且您确实需要这些数据&#xff0c;并且没有其他备份源可以在其中找到这些数据。不要担心&#xff0c;保持冷静…

B² NETWORK空投

空投要点 众多大机构支持&#xff0c;是为数不多的有 Bitcoin 主网验证 Rollup 解决方案的 BTC Layer2&#xff0c;提前埋伏其实是普通人抢早期筹码最好的方式&#xff0c;参加 B Buzz 就是手握金铲子&#xff0c;对标eth二层网络的繁荣程度你就能想象这个前景明牌空投5%给早期…

Kotlin 进阶 学习 委托

1.接口委托 package com.jmj.jetpackcomposecompositionlocal.byStudy/*** 接口委托*/ interface HomeDao{fun getAllData():List<String> }interface ADao{fun getById(id:Int):String }class HomeDaoImpl:HomeDao{override fun getAllData(): List<String> {ret…