Created
February 1, 2026 07:06
-
-
Save hernandohhoyos/4d8c9ed6a6e7ae019f271102197cba3a to your computer and use it in GitHub Desktop.
SQL: Schemeless Data Storage in PostgreSQL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Approach: Schemeless Data Storage in PostgreSQL | |
| * | |
| * This implementation utilizes the JSONB data type to achieve a schemeless (document-oriented) | |
| * architecture within a relational database. | |
| * | |
| * Advantages: | |
| * - Schema Flexibility: Allows storing diverse data structures without migrations. | |
| * - Development Speed: Faster iteration during early-stage development. | |
| * - Dynamic Attributes: Easily handle entities with unpredictable or sparse fields. | |
| * | |
| * Disadvantages: | |
| * - Validation Overhead: Requires explicit CHECK constraints or triggers to enforce data types. | |
| * - Query Complexity: Accessing nested fields requires specific JSON operators. | |
| * - Performance: Slightly higher CPU/storage cost compared to strictly typed columns. | |
| * - Tooling: Some ORMs and BI tools have limited support for deep JSON structures. | |
| */ | |
| CREATE TABLE IF NOT EXISTS domain_data ( | |
| id UUID PRIMARY KEY DEFAULT uuidv7(), | |
| is_active BOOLEAN DEFAULT TRUE, -- For logical deletion. | |
| created_at TIMESTAMPTZ DEFAULT now(), | |
| updated_at TIMESTAMPTZ DEFAULT now(), | |
| entity VARCHAR(25) NOT NULL, | |
| medadata JSONB NOT NULL | |
| ); | |
| -- Option 1: Add checks directly. | |
| ALTER TABLE domain_data ADD CONSTRAINT check_metadata_structure CHECK ( | |
| (entity = 'user' AND medadata ? 'email' AND medadata ? 'username') OR | |
| (entity = 'product' AND medadata ? 'sku' AND medadata ? 'price') OR | |
| (entity NOT IN ('user', 'product')) -- Allow others without validation | |
| ); | |
| ALTER TABLE domain_data ADD CONSTRAINT check_metadata_types CHECK ( | |
| CASE | |
| WHEN entity = 'product' THEN | |
| jsonb_typeof(medadata -> 'price') = 'number' AND | |
| jsonb_typeof(medadata -> 'sku') = 'string' | |
| ELSE TRUE | |
| END | |
| ); | |
| -- Option 2: Use a function. | |
| CREATE OR REPLACE FUNCTION validate_entity_metadata(ent VARCHAR, data JSONB) | |
| RETURNS BOOLEAN AS $$ | |
| BEGIN | |
| IF ent = 'user' THEN | |
| RETURN (data ? 'email') AND (data ->> 'email' ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'); | |
| ELSIF ent = 'product' THEN | |
| RETURN (data ? 'sku') AND (data -> 'price' != 'null'); | |
| END IF; | |
| RETURN TRUE; -- Unknown entities pass by default | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| ALTER TABLE domain_data ADD CONSTRAINT constraint_entity_validation | |
| CHECK (validate_entity_metadata(entity, medadata)); | |
| -- This index will only take up space for users, ignoring products. | |
| CREATE UNIQUE INDEX idx_unique_user_email | |
| ON domain_data ((medadata->>'email')) | |
| WHERE (entity = 'user'); | |
| -- Option 3: partitions. | |
| -- pg_partman extension. It automatically creates the "next month" partition so you never run out of space to insert. | |
| CREATE TABLE IF NOT EXISTS domain_data ( | |
| id UUID PRIMARY KEY DEFAULT uuidv7(), | |
| is_active BOOLEAN DEFAULT TRUE, -- For logical deletion. | |
| created_at TIMESTAMPTZ DEFAULT now(), | |
| updated_at TIMESTAMPTZ DEFAULT now(), | |
| entity VARCHAR(25) NOT NULL, | |
| medadata JSONB NOT NULL, | |
| -- The partition key must be part of the Primary Key in Postgres | |
| PRIMARY KEY (id, created_at) | |
| ) PARTITION BY RANGE (created_at); | |
| -- Partition for January 2026 | |
| CREATE TABLE domain_data_2026_01 PARTITION OF domain_data | |
| FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); | |
| -- Partition for February 2026 | |
| CREATE TABLE domain_data_2026_02 PARTITION OF domain_data | |
| FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'); | |
| -- 1. GIN index for general searches within any JSONB field | |
| CREATE INDEX idx_metadata_gin ON domain_data USING GIN (metadata); | |
| -- 2. B-Tree index for the entity_type column (useful for fast filters) | |
| CREATE INDEX idx_entity_type ON domain_data (entity_type); | |
| -- 3. Expression Index (Example: if searching frequently by product 'sku') | |
| CREATE INDEX idx_metadata_sku ON domain_data ((metadata->>'sku')) | |
| WHERE (entity_type = 'product'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment