Skip to content

Instantly share code, notes, and snippets.

@shekhargulati
Last active January 13, 2025 20:21
Show Gist options
  • Save shekhargulati/7e261b3bfa23cd310a8fb7b084d49f98 to your computer and use it in GitHub Desktop.
Save shekhargulati/7e261b3bfa23cd310a8fb7b084d49f98 to your computer and use it in GitHub Desktop.

HIGH-LEVEL ANALYSIS OF THE SCHEMA

GitLab’s Postgres schema is large and carefully structured to support an extremely feature-rich software development platform. It involves a variety of relational best practices (e.g., remembering who created a row and when), introduces sophisticated partitioning strategies for very large or fast-growing tables, uses JSON fields where additional flexibility is helpful, and relies on consistent naming conventions and check constraints to maintain consistency and performance.

Below is a closer look at some of these key design decisions and general patterns in GitLab’s schema, along with lessons one can learn for real-world Postgres database design.

────────────────────────────────────────────────────────────────

  1. PARTITIONING STRATEGIES ────────────────────────────────────────────────────────────────

GitLab uses partitioning extensively to handle very large tables (e.g., logs, events, job artifacts). Partitioning can reduce table bloat and improve performance for queries, inserts, and maintenance. Various partition strategies appear:

1A. RANGE PARTITIONING ────────────────────── Many tables dealing with chronological data (e.g., web_hook_logs, groups_visits, instance_audit_events, user_audit_events) use:

PARTITION BY RANGE (created_at);

They rely on a date or timestamp column (created_at, visited_at, “timestamp”) and partition by time windows (daily, weekly, monthly). For example:

CREATE TABLE web_hook_logs ( … created_at timestamp without time zone NOT NULL ) PARTITION BY RANGE (created_at);

LESSON • Range partitioning is a good choice for time-based data such as logs, metrics, or events.
• Helps archival or cleaning processes: older partitions are easily detached or dropped.
• Improves large-table performance by pruning partitions when queries target only recent ranges.

1B. LIST PARTITIONING ───────────────────── List partitioning is used where finite sets of partition “keys” exist. For example, many “p_ci_*” tables are partitioned by:

CREATE TABLE p_ci_builds ( … partition_id bigint NOT NULL, … ) PARTITION BY LIST (partition_id);

The partition_id column allows GitLab to place different sets of build records into separate partitions. This is sometimes done on numeric “shards” or as a more complex partition key that GitLab’s application code knows how to assign.

LESSON • List partitioning is well suited if you have discrete categories (e.g., partition_id = 1, 2, 3 ...).
• Each partition is physically separate, while logically they remain one table.
• The application must route new rows to correct partitions.

1C. HASH PARTITIONING ───────────────────── At times GitLab also uses PARTITION BY HASH. For example:

CREATE TABLE analytics_cycle_analytics_issue_stage_events ( … ) PARTITION BY HASH (stage_event_hash_id);

LESSON • Hash partitioning evenly distributes table rows among multiple partitions (rather than time or list-based).
• Helps spread out large volumes of data uniformly if no natural time-based or discrete list partitioning is desired.

1D. PARTITION-NAMING CONVENTIONS ──────────────────────────────── Notice the “p_ci_*” prefix in many build-related tables, plus separate “gitlab_partitions_static” or “gitlab_partitions_dynamic” schemas:

CREATE TABLE p_ci_builds ( … ) PARTITION BY LIST (partition_id);

LESSON • Use consistent naming to clarify how tables are partitioned and differentiate them from “main” tables.
• Helps DB admins and developers quickly identify partitioned data and the “parent” vs. “child” partition structure.

────────────────────────────────────────────────────────────────

  1. CHECK CONSTRAINTS FOR DATA VALIDATION ────────────────────────────────────────────────────────────────

GitLab uses many CHECK constraints for enforcing length limits, enforcing “non-null” or “positive integer” semantics, toggling booleans, and so on. For example:

CHECK ((char_length(static_object_token_encrypted) <= 255))

or

CHECK ((max_value >= min_value))

These appear as lines like:

CONSTRAINT check_7bde697e8e CHECK ((char_length(static_object_token_encrypted) <= 255)), CONSTRAINT check_9aa9432137 CHECK ((project_id IS NOT NULL)),

LESSON • CHECK constraints allow domain-level validation in the database (e.g., string length).
• This prevents bad data from entering the system and reduces reliance on application-only checks.
• Many length checks (e.g., <= 255) match data patterns that must not exceed typical string widths.

────────────────────────────────────────────────────────────────

  1. USAGE OF BIGINT PRIMARY KEYS ────────────────────────────────────────────────────────────────

A consistent pattern: almost every table has:

id bigint NOT NULL,

Even smaller or “join” tables use a bigint PK. For instance:

CREATE TABLE project_aliases ( id bigint NOT NULL, project_id bigint NOT NULL, … );

LESSON • Using bigint from the start avoids “integer overflow” on large installations.
• Consistent “id” naming helps standardize references at the application layer.
• Paired with indexes, it works well for large-scale, high-write environments.

────────────────────────────────────────────────────────────────

  1. JSON / JSONB FOR FLEXIBLE DATA ────────────────────────────────────────────────────────────────

Many columns store JSONB for dynamic or nested data. Examples:

payload jsonb, config_options jsonb, pipeline_schedules.cadence jsonb, packages_rubygems_metadata.metadata jsonb,

…plus many more. For instance:

CREATE TABLE ai_conversation_messages ( … extras jsonb DEFAULT '{}'::jsonb NOT NULL, error_details jsonb DEFAULT '{}'::jsonb NOT NULL, … );

LESSON • JSONB allows storing flexible/unstructured data that changes over time.
• Great for logs, analytics, or advanced features.
• Must plan careful indexing or usage patterns since JSON can become large or unwieldy if used everywhere.

────────────────────────────────────────────────────────────────

  1. TIMESTAMPS & HISTORY TRACKING ────────────────────────────────────────────────────────────────

Most tables feature created_at and updated_at columns. Many also have an optional “deleted_at” or “archived_at” to implement “soft deletes” or record historical states. For example:

created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
…
archived boolean DEFAULT false NOT NULL,
archived_at timestamp with time zone,

LESSON • Tracking creation and update times is crucial for audit logs or timeline features (commonly needed in collaborative systems). • Recording “deleted_at” instead of truly discarding data allows for possible restoration or references to historical events.

────────────────────────────────────────────────────────────────

  1. OPTIMISTIC CONCURRENCY USING LOCK_VERSION ────────────────────────────────────────────────────────────────

Some tables include lock_version or a similar integer column. For example:

lock_version integer DEFAULT 0

This suggests GitLab uses Optimistic Locking for concurrency control at the application level.

LESSON • If multiple processes or threads are editing the same row, a lock_version approach can detect if someone else changed it before you.
• This can prevent “last write wins” collisions in high-concurrency scenarios.

────────────────────────────────────────────────────────────────

  1. MULTI-TENANCY USING NAMESPACE/PARENT REFERENCES ────────────────────────────────────────────────────────────────

GitLab organizes data under “namespaces,” “groups,” “projects,” and sometimes “organizations.” Many tables link to them:

CREATE TABLE namespaces ( id bigint NOT NULL, … );

CREATE TABLE projects ( id bigint NOT NULL, namespace_id bigint, … );

Similarly, “group_id,” “organization_id,” or “project_id” references in many tables let GitLab separate or scope data by owning entity.

LESSON • For multi-tenant software, each record typically associates with a “tenant” or “namespace.”
• Hierarchical references (group -> project -> user) help with scoping, security, and resource usage.
• The schema must carefully handle foreign keys or references to unify data across multiple ownership contexts.

────────────────────────────────────────────────────────────────

  1. “CHECKOUT”-STYLE TABLES & MIGRATION TABLES ────────────────────────────────────────────────────────────────

We see function comments, “table_sync_function_X,” or “batched_background_migrations,” “bulk_import_*” tables, etc. For example:

CREATE TABLE batched_background_migrations ( … );

These support GitLab’s approach to schema changes at scale (background migrations, advanced import flows, etc.).

LESSON • Large installations need “background migration” or “async migration” infrastructure.
• Keep track of migrations in separate “jobs” or “tracker” tables.
• For slow transformations, chunk data or do it in small batched processes.

────────────────────────────────────────────────────────────────

  1. MANY-TO-MANY AND ASSOCIATION TABLES ────────────────────────────────────────────────────────────────

We see an ephemeral pattern of “join tables” ending with “_links,” “_assignees,” “_groups,” or “_projects.” For instance:

CREATE TABLE issue_assignees ( user_id bigint NOT NULL, issue_id bigint NOT NULL );

That is a classic many-to-many table.

LESSON • Keep association or linking logic in a cross-reference table.
• The columns are typically foreign keys to each side (issue_id, user_id) without additional data.
• Approach is standard for many-to-many relationships.

────────────────────────────────────────────────────────────────

  1. SUGGESTIONS FOR IMPROVEMENTS OR ALTERNATIVES ────────────────────────────────────────────────────────────────

  2. Foreign-Key Constraints vs. Application-Level Enforcement
    • Many columns (e.g., user_id, project_id) appear without explicit FOREIGN KEY lines in the schema snippet. Some may rely on the application for referential integrity.
    • In a smaller, simpler environment, an explicit foreign key would be safer. But GitLab often omits them for performance or for advanced re-architecting ease.
    • Potential improvement: selectively add foreign keys where read performance is not critical and references are stable.

  3. JSON Growth Management
    • JSONB columns are extremely powerful but can grow large. Overuse can lead to unwieldy queries or large row sizes.
    • Consider more partial normalization for complex JSON if certain fields need indexing or if nested data are commonly filtered.

  4. Further Partition Pruning Config
    • Large partitioned tables can benefit from explicit partitioning strategies or hashed partitions if “list” or “range” is still big.
    • Evaluate partial indexes or local indexes on partitions to speed up queries or maintenance.

  5. Strict Enforcement of Data Types
    • Many columns with type “character varying” but no length might risk extremely large data. Possibly add length limits for performance or clarity.
    • Where feasible, use numeric or boolean columns instead of text to store flags or enumerations.

  6. More Comprehensive Soft-Delete Patterns
    • Some tables have “deleted_at” or “archived_at,” others do not. A consistent structure might help systematically manage user content or logs.
    • Ensure consistent naming or approach (deleted_at vs. removed_at vs. archived_at) to reduce confusion.

────────────────────────────────────────────────────────────────

CONCLUSION

Taken as a whole, GitLab’s Postgres schema showcases enterprise-level design decisions optimized for scaling writes, queries, and data retention for hundreds of thousands (or more) of concurrent users. Key takeaways:

• Partition thoroughly where data grows rapidly (logs, events, ephemeral data).
• Validate data integrity with check constraints and bigint PK columns.
• Distinguish flexible fields with JSON if storing schema-less data.
• Keep track of concurrency (lock_version) and tenancy (namespace_id, etc.).
• Consider partial or no foreign keys for performance in high-scale, frequently migrating systems—but weigh the trade-offs carefully.
• Introduce consistent naming, time fields (created_at, updated_at, etc.), and possibly “deleted_at” for soft deletes.

All of these patterns—and the fact that the schema is still evolving—demonstrate how scaling application requirements shape a robust, partitioned, check-constrained schema design that can handle both OLTP usage and large-scale data ingestion in a single Postgres database.

@shekhargulati
Copy link
Author

  • Prompt Tokens: 96894
  • Completion Tokens: 2483
  • Total Tokens: 99377
  • Reasoning Tokens: 448
  • Input Cost: $1.4534
  • Output Cost: $0.1490
  • Total Cost: $1.6024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment