ETL β’ Denormalization β’ Query-Driven Design
A complete migration workflow demonstrating how a normalized relational movie database (MySQL) is transformed into a denormalized document database (MongoDB) β covering data modeling, ETL, and performance optimization on a MovieLens-style dataset.
Key Features:
- Complete ETL Pipeline β extract from MySQL, transform to document model, load into MongoDB
- Data Denormalization β embedded documents and arrays for optimized query performance
- Schema Design β document structure balancing read/write patterns
- Index Optimization β strategic indexing for common query patterns
- Data Validation β migration verification with count comparisons and integrity checks
| Table | Columns |
|---|---|
movies |
movieId (PK), title, genres |
links |
movieId (FK), imdbId, tmdbId |
ratings |
userId, movieId (FK), rating, rating_time |
movie_tags |
userId, movieId (FK), tag, tag_time |
movies
{
"_id": ObjectId,
"movieId": 1,
"title": "Toy Story",
"genres": ["Adventure", "Animation", "Children", "Comedy", "Fantasy"],
"release_year": "1995",
"links": { "imdbId": "0114709", "tmdbId": 862 },
"averageRating": 3.92,
"ratingCount": 215,
"tags": [
{"tag": "fun", "tagCount": 1},
{"tag": "pixar", "tagCount": 3}
]
}ratings
{
"_id": ObjectId,
"userId": 1,
"movieId": 1,
"rating": 4.0,
"rating_time": ISODate("2000-07-31T00:15:03Z")
}users
{
"_id": ObjectId,
"userId": 1,
"stats": {
"totalRatings": 232,
"averageRating": 4.23,
"totalTags": 13
},
"recentRatings": [
{"movieId": 150, "rating": 5.0, "rating_time": ISODate}
],
"recentTags": [
{"movieId": 260, "tag": "classic", "timestamp": ISODate}
]
}Connect to MySQL and pull from all four tables:
conn = pymysql.connect(host=host, user=user, password=password, database=database)
movies_df = pd.read_sql("SELECT m.*, l.imdbId, l.tmdbId FROM movies m LEFT JOIN links l ON m.movieId = l.movieId", conn)Movie documents:
- Extract release year from title via regex
- Split pipe-delimited genres into arrays
- Embed link info as nested objects
- Calculate and embed rating statistics
- Aggregate and embed tag information
User documents:
- Aggregate all ratings and tags per user
- Calculate stats (total ratings, average rating, total tags)
- Maintain recent activity (top 10 ratings and tags by timestamp)
db.movies.insert_many(movies_df.to_dict('records'))
db.ratings.insert_many(ratings_df.to_dict('records'))
db.users.insert_many(users_list)# Movies collection
db.movies.create_index('movieId', unique=True)
db.movies.create_index('genres')
db.movies.create_index([('title', 'text')])
# Ratings collection
db.ratings.create_index([('userId', 1), ('rating_time', -1)])
db.ratings.create_index('movieId')
# Users collection
db.users.create_index('userId', unique=True)| Entity | MySQL Count | MongoDB Count | Status |
|---|---|---|---|
| Movies | 9,742 | 9,742 | β Complete |
| Ratings | 100,836 | 100,836 | β Complete |
| Users | 610 | 610 | β Complete |
βββ migration_script.ipynb # Main migration script
βββ .env # Environment variables (not tracked)
βββ requirements.txt # Python dependencies
β
βββ data_models/ # Database schemas & datasets
β βββ MongoDB_MoviesDB_DataModel # MongoDB data model
β βββ MySql_MovieDB_DataModel # MySQL data model
β
βββ datasets/
| βββ README.txt # Dataset references
β βββ movies.csv
β βββ ratings.csv
β βββ tags.csv
β βββ links.csv
βββ SQL Codes/ # SQL Codes
β βββ links_sql.sql
β βββ movies_sql.sql
β βββ tags_sql.sql
β
βββ visualizations/ # Migration result charts
β βββ movies_migration.png
β βββ ratings_migration.png
β βββ users_migration.png
β
βββ README.md # Project documentation
- Python 3.7+
- MySQL Server
- MongoDB Server
- Access credentials for both databases
1. Clone the repository
git clone https://github.com/yourusername/mysql-mongodb-migration.git
cd mysql-mongodb-migration2. Install dependencies
pip install -r requirements.txt3. Configure environment variables in .env
# MySQL Configuration
host=localhost
user=your_mysql_user
password=your_mysql_password
database=moviesdb
# MongoDB Configuration
MONGO_URI=mongodb://localhost:27017/4. Run the migration
python migration_script.py- Embedding vs. Referencing β link info and tags embedded in
moviesfor read optimization; ratings kept separate due to high volume and update frequency - Denormalization Trade-offs β pre-calculated aggregates (
averageRating,ratingCount) reduce query complexity at the cost of update complexity - Index Strategy β compound index on
(userId, rating_time)supports user activity queries; text index ontitleenables search - Array Fields β genres stored as arrays enable multi-value queries via MongoDB's array operators
- Read Optimization β embedded documents eliminate JOINs for common queries
- Write Trade-offs β updates to ratings require updating movie aggregates
- Index Overhead β strategic indexing balances query performance with storage cost
- Document Size β kept under MongoDB's 16MB limit via recent-activity limits
Find all Action movies with high ratings:
db.movies.find({
genres: "Action",
averageRating: { $gte: 4.0 }
})Get a user's recent activity:
db.users.findOne(
{ userId: 1 },
{ recentRatings: 1, recentTags: 1 }
)Text search for movies:
db.movies.find({
$text: { $search: "toy story" }
})- Implement incremental migration for ongoing data sync
- Add data validation and error handling
- Create MongoDB aggregation pipeline examples
- Implement rollback mechanism
- Add unit tests for transformation logic
- Performance benchmarking between MySQL and MongoDB queries
Contributions, issues, and feature requests are welcome!
Karthick S
- MovieLens dataset for sample data structure
- MongoDB documentation for best practices