One dataset.
Three databases.

DataSphere stores an e-commerce platform's data where it fits best: orders in MySQL, flexible product and review documents in MongoDB, and the customer–product–friend network in Neo4j. Then it measures what each choice costs.

GitHub repo Open in Colab Download .ipynb Streamlit app

MySQLstructured, transactional
MongoDBsemi-structured, flexible
Neo4jhighly connected

One network, six views: drag a node, click to inspect

(a) Neighbourhoods. Each shaded region holds the customers closest to one top hub. Drag a node to reshape it.
(b) Influence rings. Distance from the centre shows PageRank tier; node size shows number of links.
(c) Arc diagram. Customers in a line, grouped by hub. Arcs above join customers in the same group; arcs below cross groups.
(d) Adjacency matrix. Rows and columns are customers; a filled square is a link. Dense blocks on the diagonal are tightly knit groups.
(e) Chord ring. Customers around a ring, grouped by hub. Coloured links stay inside a group; grey links cross between groups.
(f) Bubble clusters. One bubble per customer, sized by PageRank and packed inside its group. Select one to see its links.

from the selected node (or a random one) to a random customer

The same 60-customer network in six views. Click a node in any panel to highlight it and its neighbours in all of them.

Choosing a database per workload

The question isn't which database is best. It's which one matches the data's shape, how it is read, and how strict consistency has to be.

Workload fit

Score 1–5 from the project's access-pattern analysis.

Decision matrix

What went where, and why.

DataStoreReason
Orders, payments, inventoryMySQLACID, joins, constraints
Product catalogue, reviewsMongoDBVariable attributes, nested data
Follows, co-purchasesNeo4jMulti-hop traversal
DashboardsMySQLWindow functions, views

Integration flow

MySQL is the system of record; the other two are fed from it.

Client / API Integration layer MySQL MongoDB Neo4j Benchmarks

Where the data goes

Rows in MySQL become documents in MongoDB and nodes and relationships in Neo4j. Band width scales with the square root of the count.

Methodology

How the project went from a blank page to comparable numbers: choose each database for a reason, load the same data into all three, and measure under rules that keep the comparison fair.

1

Model the domain

List entities, relationships and the questions the system must answer.

2

Pick a paradigm

Match each part of the data to a store by shape, access pattern and consistency need.

3

Load one dataset

Generate it once from a fixed seed, then load the same logical data into every store.

4

Define workloads

Write the same questions as SQL, aggregation pipelines and Cypher.

5

Measure

Warm up, repeat, record median and tail latency, throughput and size.

6

Verify and analyse

Confirm each query plan, check variance, then compare.

How a paradigm was chosen

Four questions asked of each dataset.

QuestionIf yes, lean toward
Are rows tightly related and must stay consistent?MySQL
Does each record vary in shape and get read whole?MongoDB
Is the question about paths or neighbours?Neo4j
Is it heavy ad-hoc aggregation?MySQL

The dataset

One logical dataset, three physical forms.

  • Generated from a fixed seed, so every run sees identical data.
  • Skewed on purpose: a few popular products and heavy buyers, like real shops.
  • Several sizes from 1M to 100M rows to test scalability.
  • Same facts everywhere: orders as tables, products as documents, follows and purchases as edges.

Benchmark protocol

Rules applied to every measurement.

  • Discard the first runs (warm-up), then repeat each query many times.
  • Report median, p95 and p99, never the average alone.
  • Measure cold-cache and warm-cache separately.
  • Change one variable at a time: index, size or isolation level.
  • Declare indexes before timing and confirm the plan with EXPLAIN, .explain() or PROFILE.
  • Use the same machine, same client and same dataset for all three.

Why warm-up runs are discarded

Illustrative: the first runs pay for cold caches. The pink points are excluded.

Mean vs percentiles

Illustrative: one cold run drags the average up; median and tails describe the steady state.

Cold-cache vs warm-cache

Illustrative: first touch pays for disk; repeats run from memory (ms, log scale).

What was measured, and how

One row per metric.

MetricMethod
Query latencyClient-side timer, cross-checked with EXPLAIN ANALYZE
ThroughputOperations per second with N concurrent clients
Index performanceSame query before and after; rows examined
Storage overheadOn-disk size of identical data in each store
Transaction behaviourIsolation-level runs; commits, aborts, deadlocks
Traversal efficiencyTime versus hop depth, joins versus Cypher
ScalabilityRepeat at growing dataset sizes

Test environment

Fill these in with your own setup before publishing.

CPU / cores[fill in]
Memory[fill in]
Storage[fill in]
OS[fill in]
MySQL version[fill in]
MongoDB version[fill in]
Neo4j version[fill in]
Dataset scale[fill in]
Repetitions per query[fill in]

Find them with (macOS; Linux equivalents in comments):

# CPU / cores / memory (macOS)
sysctl -n machdep.cpu.brand_string; sysctl -n hw.ncpu
system_profiler SPHardwareDataType | grep -E "Memory|Chip"
# Linux: lscpu | grep -E "Model name|^CPU\(s\)"; free -g; lsblk
# database versions
mysql --version; mongod --version; cypher-shell --version

Limits of the comparison

What the numbers can and cannot say.

CAN say
  • Relative strengths per workload: joins → MySQL, traversal → Neo4j, flexible docs → MongoDB.
  • Where indexing and rewrites pay off (every claim measured before and after).
  • Rough scaling shape from 1M to 100M rows.
  • How latency grows with size and hop depth (measured curves for scans, B-tree lookups and traversals).
  • What stronger isolation costs in throughput (measured per level at 32 clients).
CANNOT say
  • Absolute production speeds: synthetic data, single node, one fixed seed.
  • Sharding, replication lag or network cost: not measured.
  • Behaviour past the tested limits: 128 clients, 100M rows, 5-hop traversals.
  • Safety against attack types beyond the five tested families.

Step 1 · Train

Predictors + calibration, SHAP, distributions.

python train_models.py --fast

Step 2 · Distill

In-page calculator weights (≈3 KB).

python export_js.py

Step 3 · Launch

Predictor workbench, seven tabs.

streamlit run streamlit_app/ml_app.py

Step 4 · Open

Calculators work offline; the frame embeds the live app.

open index.html

Step 5 · Verify

Calculators answer; charts filled; app boots clean.

checklist in §Methodology

Step 6 · Publish

Fill test-env rows; swap sample numbers; deploy + paste app URL.

git commit -am "publish" && git push

MySQL: relational layer

ER model → functional dependencies → 3NF/BCNF, then analytics with CTEs, recursive CTEs and window functions.

Schema (BCNF)

Keys and relationships.

customersPK idname, emailcity ordersPK idFK customer_idplaced_at, status order_itemsPK (order,prod)qty, unit_price paymentsPK id, FK orderamount, method productsPK id, skuFK category_idprice categoriesPK idFK parent_id (self)

Revenue by category

Output of the window-function report (sample data, £k).

Query gallery

Index strategy: latency

Same query, before and after indexing (ms, log scale).

Rows examined (EXPLAIN ANALYZE)

Query rewrite + composite/covering index.

Query optimization, step by step

Normalise first, then let the optimizer work: B+-tree indexes, accurate cardinality estimates, a good join order, and materialized views for repeated reports.

Normalization to BCNF

One wide table, decomposed along its functional dependencies until every determinant is a key.

-- Before: orders_wide
(order_id, cust_id, cust_city,
 prod_id, prod_name, qty)
-- Functional dependencies
order_id → cust_id
cust_id → cust_city
  -- transitive: violates 3NF
prod_id → prod_name
  -- partial: violates 2NF
(order_id, prod_id) → qty
  -- full dependence on the key
-- After: BCNF decomposition
customers(cust_id, cust_city)
products(prod_id, prod_name)
orders(order_id, cust_id)
order_items(order_id,
 prod_id, qty)

B+-tree lookup

Point lookup for key 41 traverses root to leaf; linked leaves keep range scans sequential.

30 | 605 12 2130 41 5260 75 90Three page reads index 100M rows at fan-out ≈ 5001K1M100M1234

Height vs table size at fan-out ≈ 500: level count barely moves as rows explode. Doubling the table rarely adds a level, and linked leaves turn ranges into a sideways walk.

EXPLAIN ANALYZE plan

Cost-selected plan for customer 42's orders; estimates track actuals.

Nested-loop joincost 214 · 18 rows Index range scan: ordersidx_cust_date · est 20 / actual 18 Single-row lookup: customersPRIMARY · est 1 / actual 1123

Numbered in execution order: ① scan, ② lookup per row, ③ join. Row estimates feed the choice above them — a wide est/actual gap means stale statistics, so run ANALYZE TABLE and re-check.

Cardinality: estimated vs actual

Fresh histograms keep the optimizer honest (rows per plan node).

Materialized view vs live query

Dashboard query time (ms). MySQL has no native MV, so a summary table is refreshed by an event.

Optimizations applied

Rewrites that changed the plan.

-- push filters before joins
SELECT ... FROM (
  SELECT * FROM orders
  WHERE placed_at>='2024-01-01'
) o JOIN ...
-- subquery → join
-- IN → EXISTS
-- covering index skips
-- table lookups entirely
CREATE INDEX idx_cov
ON orders (
  customer_id,
  placed_at, status);

Join algorithms by table size

Time to join two equal-size tables (ms, log scale).

Storage by normal form

Same data, fewer duplicated values (MB).

Live SQL lab: run it yourself

This builds a 200,000-order database in your browser and runs real queries on it. Timings below are measured on your machine, not sample data.

Loads a SQL engine (SQLite compiled to WebAssembly, so syntax follows SQLite, not MySQL).

MongoDB: documents

Embed what is read together, reference what grows without bound, and validate with a JSON schema.

Product document

Embedded specs and top reviews.

{ "_id": "p_1042",
  "name": "Trail Runner X",
  "category": [
    "Shoes", "Running"],
  "specs": {
    "weight_g": 280,
    "drop_mm": 6 },
  "reviews": [
    { "user": 88,
      "stars": 5 } ],
  "price": 89.0 }

Aggregation pipeline

Top-rated products with 50+ reviews.

db.products.aggregate([
  { $unwind:
    "$reviews" },
  { $group: {
      _id: "$_id",
      avg: { $avg:
        "$reviews.stars" },
      n: { $sum: 1 } } },
  { $match: {
      n: { $gte: 50 } } },
  { $sort: { avg: -1 } },
  { $limit: 10 } ])

Documents surviving each stage

Sample run over 120k products.

Index impact on query time

Collection scan vs index (ms).

Reviews per product

Most products have few reviews; a few have many (sample).

Time per pipeline stage

Where the aggregation spends its time, ms (sample).

Schema validation

Flexible, but not lawless.

db.createCollection(
 "products", {
  validator: {
   $jsonSchema: {
    bsonType: "object",
    required: [
     "name", "price"],
    properties: {
     price: {
      bsonType: "double",
      minimum: 0 },
     reviews: {
      bsonType: "array",
      maxItems: 100 }}}}})

Embed or reference?

The rule used for each relationship.

RelationshipChoiceWhy
Product → specsEmbedRead together, 1:1
Product → top reviewsEmbedBounded (≤100)
Product → all ordersReferenceUnbounded growth

Compound and text indexes

ESR rule: Equality, Sort, Range.

db.products.createIndex(
 { category: 1,
   price: -1,
   rating: 1 })
db.products.createIndex(
 { name: "text",
   description: "text" })
db.products.find(
 { $text: {
   $search:
   "waterproof" } })
 .projection(
  { name: 1, price: 1 })

Embedded vs referenced reads

Read latency as the parent document grows (ms).

Storage by collection

Share of the 7.6 GB MongoDB footprint.

Run it: aggregation playground

20,000 generated product documents and a small in-browser aggregation engine. It supports $match, $unwind, $group, $project, $sort and $limit.

Neo4j: relationships first

Friends-of-friends and "customers who bought this also bought" are path queries. Graphs answer them without join explosions.

Cypher

Recommendations within 3 hops.

MATCH (me:Customer
 {id:$id})-[:FOLLOWS*1..3]
 ->(f)-[:BOUGHT]->(p)
WHERE NOT (me)
 -[:BOUGHT]->(p)
RETURN p.name,
 count(*) AS score
ORDER BY score
DESC LIMIT 10

Top customers by PageRank

Computed live from the hero graph.

Degree distribution

A few hubs, many leaves.

Traversal depth: SQL joins vs Cypher

Time to find friends N hops away (ms, log scale).

Network reach within N hops

Share of the 60 customers reached (sample).

Recommendation hit-rate

Hits in top-10 by method (sample).

Shortest path and centrality

Pattern matching and graph algorithms.

MATCH p = shortestPath(
  (a:Customer {id:1})-[:FOLLOWS*..6]-(b:Customer {id:42}))
RETURN length(p), [n IN nodes(p) | n.id];

MATCH (c:Customer)<-[:FOLLOWS]-(f)
RETURN c.id, count(f) AS inDegree
ORDER BY inDegree DESC LIMIT 10

Graph model

Nodes, relationships, properties.

CustomerCustomerProductFOLLOWSBOUGHT{id,name,city}{sku,price}

Degree vs PageRank

Each dot is a customer, computed live. Rank rewards who you are connected to, not just how many.

Customers per hub neighbourhood

Each customer is grouped with its nearest top-5 hub.

Degree distribution, log–log

A heavy tail is the signature of hub-dominated networks. Computed live.

Transactions and concurrency

ACID workflows in MySQL, plus experiments with isolation, locking and deadlocks.

Isolation levels and anomalies

What each level permits in InnoDB.

LevelDirtyNon-rep.Phantom
READ UNCOMMITTEDpossiblepossiblepossible
READ COMMITTEDnopossiblepossible
REPEATABLE READnonono*
SERIALIZABLEnonono

*InnoDB's gap locks and MVCC snapshot prevent most phantoms at this level.

Throughput by isolation level

Transactions per second, 32 clients (sample).

Deadlock: two transactions, opposite lock order

InnoDB detects the cycle and rolls back the cheaper transaction.

T1T2 lock acct A wait acct B… lock acct B wait acct A… ERROR 1213: rollback T2

Two-phase locking

Grow, then shrink. Never lock after the first unlock; that guarantees conflict-serializable schedules.

T1: lock-X(A) read(A)
write(A) lock-X(B)
read(B) write(B)
COMMIT unlock(A,B)
-- grow: lock, read,
-- write; shrink: unlock
-- strict 2PL holds
-- X-locks until commit

MVCC read view

Readers see a snapshot and never block writers.

-- Session 1
-- (REPEATABLE READ)
START TRANSACTION;
SELECT bal
FROM acct
WHERE id=1;
-- reads 500
-- Session 2 commits:
-- UPDATE sets bal=400
SELECT bal
FROM acct
WHERE id=1;
-- still reads 500
COMMIT;
-- next read: 400

Deadlocks per 1,000 transactions

Rises with contention (sample).

Throughput vs concurrency

Transactions per second as clients increase (sample).

Abort rate heatmap

Percent of transactions rolled back, by isolation level and clients (sample).

Security

Least privilege through roles, and queries that never concatenate user input.

Role-based access

Privileges per role.

SELECTINSERTUPDATEDELETEDDL
analyst✓––––
app_user✓✓✓––
admin✓✓✓✓✓
CREATE ROLE analyst;
GRANT SELECT ON shop.* TO analyst;

Injection: try it

Type a name. The unsafe query is built by string concatenation; the safe one is parameterised.

Unsafe

Safe (parameterised)

Injection attacks that succeed

Concatenated SQL vs parameterised queries (% of test payloads).

Cross-database benchmarks

Same dataset, same questions, three engines. The point is where each one wins, not which one wins overall.

Query latency by workload

Median ms, log scale. Lower is better.

Overall profile

Normalised 0–100. Higher is better.

Scalability

Point-lookup throughput as data grows (ops/s ×1000).

Storage overhead

On-disk size for the same data (GB).

Throughput per GB

Point-lookup kops per GB stored, derived (sample).

Workloads won

Times fastest of the six benchmarked workloads (sample).

Latency percentiles

Tail latency tells a different story than the median (ms).

Where the time goes

Per-query breakdown (ms).

Slowdown vs the best engine

How many times slower each database is than the fastest one for the same workload.

bestdeeper orange = further from best

Latency distribution

Share of queries finished within a given time (empirical CDF, 400 queries each).

medianp95slow tail →

Reading guide: steep middle, flat slow tail. Median and p95 mark where half and 95% of queries have finished.

When an index stops helping

Time to fetch a growing share of a 1M-row table. Cost model: random index reads cost about 10x a sequential row.

full scanindex lookup≈10%small sharewhole table

The model behind the chart above, zoomed out: past about a 10% share the index loses and the scan wins.

Tail penalty: p99 ÷ p50

How much worse the tail is than the median, derived (sample).

Worst mismatch per database

Max slowdown vs the best engine, log scale, derived (sample).

Tail gap in ms

p99 minus p50 per database, derived (sample).

Scale explorer

Drag to change the row count. Latency from textbook cost models: B-tree O(log n), full scan O(n), traversal independent of total size.

Each figure is tagged by provenance: green tags are computed or measured in your browser; amber tags are sample data. Replace the amber figures' numbers in the DATA object in the script at the bottom of this file with your measured results.

ML predictions, live

Three predictors trained across all three stores — high-value customers, next order amounts, review ratings — plus calibration, SHAP explanations and data-quality plots. It runs below, embedded from Streamlit.

Calculator: high-value customer

Distilled linear model, test AUC . Defaults are population medians.

Calculator: order amount

Distilled ridge model, test R² , RMSE £.

Calculator: review rating

Distilled ridge model, test accuracy . Output rounded to 1–5 ★.

Try the models

Customer value, order amount and rating predictors in one app. Prefer notebooks? Open in Colab or download the .ipynb.

Code: Harshini-Suresha/datasphere-ml · Live: Streamlit app

What the project showed

Choose MySQL when

Correctness matters most: money, inventory, multi-row invariants, ad-hoc analytics over normalised data.

Choose MongoDB when

Records vary in shape and are read whole: catalogues, content, events. Scale-out comes built in.

Choose Neo4j when

The question is about relationships: paths, recommendations, influence. Cost follows hops, not table size.

The common thread: index for the access pattern you actually have, measure with EXPLAIN or .explain(), and keep one system of record.