I spent an embarrassing amount of time building comparison tables before admitting the data model was wrong. Not the UI — the schema underneath. Every "Tool A vs Tool B" page starts from an assumption that quietly poisons everything downstream: that both tools have the same features, and you just need to check boxes.
They don't. That is usually the entire reason one exists.
The false equivalence problem is a schema problem
The default schema is a matrix. Rows are features, columns are tools, cells are booleans.
-- the version that ruins your dataset
CREATE TABLE comparison (
feature_id TEXT,
tool_id TEXT,
supported BOOLEAN
);
The failure is that supported = false is doing at least five distinct jobs: the tool genuinely lacks this; the tool solves the same problem a completely different way so the feature name doesn't apply; it exists but only on a higher tier; it exists via a plugin rather than natively; or nobody checked.
Rendering all five as a red ✗ is not a display bug you can fix in the template — the information was destroyed at write time. And the second case is where comparison sites do the most damage. Scoring a git-backed CLI tool as "✗ real-time collaborative editing" is technically true and completely misleading; the tool's answer to collaboration is branches and review, which is a different shape, not an absence.
So the first schema change is: the cell is not a boolean.
CREATE TABLE tool_capability (
tool_id TEXT NOT NULL,
capability_id TEXT NOT NULL,
status TEXT NOT NULL, -- see enum below
tier_required TEXT, -- 'free' | 'pro' | 'enterprise' | NULL
via TEXT, -- 'native' | 'plugin' | 'integration' | 'api'
note TEXT, -- required when status = 'different_model'
evidence_url TEXT NOT NULL,
verified_at DATE NOT NULL,
PRIMARY KEY (tool_id, capability_id)
);
with
status ∈ {
full, -- does the thing, no meaningful caveat
partial, -- does some of it; note explains the boundary
different_model, -- solves the problem, different concept; note REQUIRED
not_applicable, -- the concept does not exist in this tool's world
absent, -- genuine gap
unknown -- not verified; render as "—", never as ✗
}
different_model and not_applicable are the two that make the dataset honest. unknown is the one that keeps you honest — most comparison content is quietly full of unverified guesses rendered with the same confidence as tested facts. If your schema has no way to say "I don't know," your writers will say "no."
Capabilities are not features
The second fix is renaming the row axis, which sounds cosmetic and isn't.
Features are vendor vocabulary. One tool calls it Workspaces, another Projects, a third Teams, and a fourth has no such object because permissions attach directly to resources. If your rows are feature names, you have adopted one vendor's ontology as the neutral standard — usually the market leader's, which structurally flatters it and makes everyone else look incomplete.
Model rows as user jobs instead:
capability: "restrict access to a subset of content per group of people"
tool_a: full (Workspaces + roles)
tool_b: different_model (per-resource ACLs, no container object)
tool_c: partial (all-or-nothing sharing, no groups)
tool_d: full tier_required = 'enterprise'
Now different_model is expressible without being a demerit, and the note carries the actual decision-relevant information: tool_b can do this but you will configure it per resource, which is fine for 20 documents and miserable for 2,000.
Give capabilities their own metadata too, including an objectivity field — verifiable | contextual | taste. That column stops you from summing incomparable things. "Exports to CSV" is verifiable. "Feels fast" is taste. A single composite score that averages both is a number with no meaning, and it is the thing readers trust most.
Pricing is a model, not a number
Pricing deserves its own table because "$20/mo" is almost never comparable across two tools. The dimensions that actually differ:
- Unit of charge: seat, workspace, usage, flat, hybrid
- Free tier shape: perpetual-limited vs. time-limited trial vs. none
- The cliff: which specific feature forces the upgrade (SSO, audit logs, API access — this is where the real cost hides)
- Scaling behavior: cost at 1, 5, 20, 100 seats is a curve, not a point
So store unit, base_cents, per_unit_cents, included_units, and captured_at — the model, not the headline number. Then you can render honest scenario costs ("at 12 seats") instead of a headline price that only applies to a solo user on annual billing. Every pricing row needs captured_at because pricing rots faster than anything else in the dataset, and a stale price presented confidently is the fastest way to lose a reader permanently.
Provenance, or you are just writing opinions
Two constraints I would now enforce at the database level rather than in review:
Every claim carries evidence and a date. evidence_url and verified_at are NOT NULL. A claim older than your staleness threshold degrades to unknown automatically rather than sitting there looking authoritative. This is the difference between a dataset and a blog post.
Comparisons are derived, never authored. A "A vs B" page is a query over tool_capability, not a hand-written row. Hand-written comparison rows drift out of sync with the tool pages within weeks and you will never notice. Making it derived means one write updates every page that references it, and it makes an entire class of contradiction impossible.
Working through this on stackalt — where each entry has to say something real about pricing model, migration friction, and where a tool simply has no equivalent concept — the different_model status ended up being the most-used non-full value by a wide margin. Which is the finding, really: most tools that survive in the same category are not worse versions of each other. They disagree about what the problem is, and a boolean matrix has no vocabulary for disagreement.
Top comments (0)