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
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.
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.
Score 1–5 from the project's access-pattern analysis.
What went where, and why.
| Data | Store | Reason |
|---|---|---|
| Orders, payments, inventory | MySQL | ACID, joins, constraints |
| Product catalogue, reviews | MongoDB | Variable attributes, nested data |
| Follows, co-purchases | Neo4j | Multi-hop traversal |
| Dashboards | MySQL | Window functions, views |
MySQL is the system of record; the other two are fed from it.
Rows in MySQL become documents in MongoDB and nodes and relationships in Neo4j. Band width scales with the square root of the count.
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.
List entities, relationships and the questions the system must answer.
Match each part of the data to a store by shape, access pattern and consistency need.
Generate it once from a fixed seed, then load the same logical data into every store.
Write the same questions as SQL, aggregation pipelines and Cypher.
Warm up, repeat, record median and tail latency, throughput and size.
Confirm each query plan, check variance, then compare.
Four questions asked of each dataset.
| Question | If 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 |
One logical dataset, three physical forms.
Rules applied to every measurement.
EXPLAIN, .explain() or PROFILE.Illustrative: the first runs pay for cold caches. The pink points are excluded.
Illustrative: one cold run drags the average up; median and tails describe the steady state.
Illustrative: first touch pays for disk; repeats run from memory (ms, log scale).
One row per metric.
| Metric | Method |
|---|---|
| Query latency | Client-side timer, cross-checked with EXPLAIN ANALYZE |
| Throughput | Operations per second with N concurrent clients |
| Index performance | Same query before and after; rows examined |
| Storage overhead | On-disk size of identical data in each store |
| Transaction behaviour | Isolation-level runs; commits, aborts, deadlocks |
| Traversal efficiency | Time versus hop depth, joins versus Cypher |
| Scalability | Repeat at growing dataset sizes |
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
What the numbers can and cannot say.
Predictors + calibration, SHAP, distributions.
python train_models.py --fast
In-page calculator weights (≈3 KB).
python export_js.py
Predictor workbench, seven tabs.
streamlit run streamlit_app/ml_app.py
Calculators work offline; the frame embeds the live app.
open index.html
Calculators answer; charts filled; app boots clean.
checklist in §Methodology
Fill test-env rows; swap sample numbers; deploy + paste app URL.
git commit -am "publish" && git push
ER model → functional dependencies → 3NF/BCNF, then analytics with CTEs, recursive CTEs and window functions.
Keys and relationships.
Output of the window-function report (sample data, £k).
Same query, before and after indexing (ms, log scale).
Query rewrite + composite/covering index.
Normalise first, then let the optimizer work: B+-tree indexes, accurate cardinality estimates, a good join order, and materialized views for repeated reports.
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)
Point lookup for key 41 traverses root to leaf; linked leaves keep range scans sequential.
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.
Cost-selected plan for customer 42's orders; estimates track actuals.
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.
Fresh histograms keep the optimizer honest (rows per plan node).
Dashboard query time (ms). MySQL has no native MV, so a summary table is refreshed by an event.
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);
Time to join two equal-size tables (ms, log scale).
Same data, fewer duplicated values (MB).
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.
Average time for 300 customer lookups on 200k rows, measured live (ms per query).
Things worth experimenting with.
-- 1. Run the plan with and without an index: CREATE INDEX i1 ON orders(customer_id); -- 2. Compare SCAN vs SEARCH in the plan. -- 3. Rewrite the window query as a self-join -- and compare the time.
Embed what is read together, reference what grows without bound, and validate with a JSON schema.
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 }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 } ])Sample run over 120k products.
Collection scan vs index (ms).
Most products have few reviews; a few have many (sample).
Where the aggregation spends its time, ms (sample).
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 }}}}})
The rule used for each relationship.
| Relationship | Choice | Why |
|---|---|---|
| Product → specs | Embed | Read together, 1:1 |
| Product → top reviews | Embed | Bounded (≤100) |
| Product → all orders | Reference | Unbounded growth |
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 })Read latency as the parent document grows (ms).
Share of the 7.6 GB MongoDB footprint.
20,000 generated product documents and a small in-browser aggregation engine. It supports $match, $unwind, $group, $project, $sort and $limit.
Friends-of-friends and "customers who bought this also bought" are path queries. Graphs answer them without join explosions.
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
Computed live from the hero graph.
A few hubs, many leaves.
Time to find friends N hops away (ms, log scale).
Share of the 60 customers reached (sample).
Hits in top-10 by method (sample).
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
Nodes, relationships, properties.
Each dot is a customer, computed live. Rank rewards who you are connected to, not just how many.
Each customer is grouped with its nearest top-5 hub.
A heavy tail is the signature of hub-dominated networks. Computed live.
ACID workflows in MySQL, plus experiments with isolation, locking and deadlocks.
What each level permits in InnoDB.
| Level | Dirty | Non-rep. | Phantom |
|---|---|---|---|
| READ UNCOMMITTED | possible | possible | possible |
| READ COMMITTED | no | possible | possible |
| REPEATABLE READ | no | no | no* |
| SERIALIZABLE | no | no | no |
*InnoDB's gap locks and MVCC snapshot prevent most phantoms at this level.
Transactions per second, 32 clients (sample).
InnoDB detects the cycle and rolls back the cheaper transaction.
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
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
Rises with contention (sample).
Transactions per second as clients increase (sample).
Percent of transactions rolled back, by isolation level and clients (sample).
Least privilege through roles, and queries that never concatenate user input.
Privileges per role.
| SELECT | INSERT | UPDATE | DELETE | DDL | |
|---|---|---|---|---|---|
| analyst | ✓ | – | – | – | – |
| app_user | ✓ | ✓ | ✓ | – | – |
| admin | ✓ | ✓ | ✓ | ✓ | ✓ |
CREATE ROLE analyst; GRANT SELECT ON shop.* TO analyst;
Type a name. The unsafe query is built by string concatenation; the safe one is parameterised.
Unsafe
Safe (parameterised)
Concatenated SQL vs parameterised queries (% of test payloads).
Same dataset, same questions, three engines. The point is where each one wins, not which one wins overall.
Median ms, log scale. Lower is better.
Normalised 0–100. Higher is better.
Point-lookup throughput as data grows (ops/s ×1000).
On-disk size for the same data (GB).
Point-lookup kops per GB stored, derived (sample).
Times fastest of the six benchmarked workloads (sample).
Tail latency tells a different story than the median (ms).
Per-query breakdown (ms).
How many times slower each database is than the fastest one for the same workload.
bestdeeper orange = further from best
Share of queries finished within a given time (empirical CDF, 400 queries each).
Reading guide: steep middle, flat slow tail. Median and p95 mark where half and 95% of queries have finished.
Time to fetch a growing share of a 1M-row table. Cost model: random index reads cost about 10x a sequential row.
The model behind the chart above, zoomed out: past about a 10% share the index loses and the scan wins.
How much worse the tail is than the median, derived (sample).
Max slowdown vs the best engine, log scale, derived (sample).
p99 minus p50 per database, derived (sample).
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.
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.
Distilled linear model, test AUC . Defaults are population medians.
Distilled ridge model, test R² , RMSE £.
Distilled ridge model, test accuracy . Output rounded to 1–5 ★.
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
Correctness matters most: money, inventory, multi-row invariants, ad-hoc analytics over normalised data.
Records vary in shape and are read whole: catalogues, content, events. Scale-out comes built in.
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.