Cloud Parts

EAV, JSON column, wide table or document?

DimensionEntity-attribute-value (EAV)Emergent schema (JSON columns)Wide tablesDocument databases
In one lineOne row per fact: entity, attribute, valueKnown fields as columns, the rest in one JSON columnOne column per attribute, mostly NULLEach record is a self-contained JSON-like document
Adding an attributeInsert a row into the attribute catalog; no DDLJust write the new key; no DDLALTER TABLE ADD COLUMN; in PostgreSQL a nullable column is a metadata change, with no table rewriteJust write the new field; no DDL
TypesTyped value columns or one value table per type, chosen from metadata; often everything ends up as textJSON’s string, number, boolean and null; dates are strings until you cast themAny SQL type, per columnBSON types in MongoDB, including dates, 64-bit integers and Decimal128
ConstraintsNone per attribute in the schema; enforced by triggers that read metadata, or by the applicationCHECK constraints on expressions; JSON Schema via MySQL’s JSON_SCHEMA_VALID or PostgreSQL extensionsOrdinary NOT NULL, CHECK and foreign keys on each columnOptional $jsonSchema validation per collection; no foreign keys
Indexing(attribute, value) indexes, one attribute at a time; multi-attribute filters become joinsGIN over the whole column, expression indexes per key, or generated columns; MySQL indexes only generated columns and arraysA B-tree per column you filter on; SQL Server adds filtered indexes on sparse columnsAny field, including nested fields and arrays (multikey indexes)
Query shapePivot with one CASE or join per attributeOperators and casts: props->>'plan', props @> '{...}', JSON pathPlain SELECT col WHERE col = ...Field paths in find and aggregation; $lookup for joins between collections
Planner statisticsPer column, so all attributes share one histogramNone on keys inside the column unless you add an expression index or CREATE STATISTICSPer column, as for any tableMongoDB races candidate index plans in a short trial; a cost-based ranker covers some queries since 8.3
Sparse data on diskOnly present facts, but each is a full row with IDs and a row header (about 23 bytes in PostgreSQL)Only present keys, but every row repeats its key namesA NULL costs one bit in PostgreSQL’s null bitmap; SQL Server sparse columns shrink NULLs at extra cost per non-NULL valueOnly present fields, but every document repeats its field names (BSON stores each name)
LimitsPractically unlimited attributesLarge documents work, but any update locks the whole row; keep them small1,600 columns per table in PostgreSQL; 1,024 in SQL Server, or 30,000 with sparse column sets16 MiB per document in MongoDB
Reporting and BIPivot first, into views, materialized views or exportsTools see one opaque column; extract keys into views or columnsWorks directlyUsually flattened into a warehouse first
Pick it whenAttributes are user-defined data with thousands of possibilities and a handful per entityA relational app has a varying tail of fields you don’t yet understandAttributes are known, at most a few hundred, and you report on themWhole records vary and are read and written as one unit
Seen inWordPress meta tables, Magento catalog, clinical data repositories, RDF triple storesPostgreSQL jsonb, MySQL JSON, SQLite JSON functions, Snowflake VARIANTSurvey and feature tables, SQL Server sparse columnsMongoDB, Firestore, Cosmos DB, DocumentDB

Rule of thumb

Start with columns for every field you know you’ll query, and put the unpredictable rest in a JSON column. Watch which keys get used and promote those to real or generated columns. Reach for EAV only when attributes are themselves data that users define, and you’re ready to build the metadata layer it needs; switch to a document database when whole records, not just a tail of fields, vary.

Sources