MongoDB to SQL Converter
Paste a MongoDB query and read it as SQL. A find with its cursor chain, an aggregation pipeline, or a write. Driver syntax and shell syntax both read. Nothing leaves your browser.
MongoDB and SQL, and the places they part
Most of a query has a direct SQL form. The rest is worth knowing about before you trust the reading.
What reads back cleanly
A find with its cursor chain becomes a SELECT. A pipeline of $match, $group, $sort and $lookup becomes WHERE, GROUP BY, ORDER BY and JOIN. The writes read back as INSERT, UPDATE and DELETE.
What has no SQL form
$elemMatch, $exists, $expr and the array operators work on shapes a SQL column does not have. A pipeline whose stages run in an order SQL cannot express, such as a $match after a $limit, is refused rather than quietly reordered.
Driver code or shell code
Paste either. The one place they collide is the second argument to find, a projection in the shell and an options bag in the driver. This reads it the way the driver does, and says so when both readings are possible.
MongoDB to SQL, operator by operator
The whole table the converter works from. For what each pipeline stage does on its own, the aggregation cheat sheet covers every one. To go the other way, use the SQL to MongoDB converter.
| MongoDB | SQL |
|---|---|
| .find({}) | SELECT * FROM … |
| .project({ a: 1, _id: 0 }) | SELECT a |
| .project({ x: '$a' }) | SELECT a AS x |
| { a: 1 } | WHERE a = 1 |
| { a: { $gt: 1 } } | WHERE a > 1 |
| { a: { $in: [1, 2] } } | a IN (1, 2) |
| { a: { $gte: 1, $lte: 9 } } | a BETWEEN 1 AND 9 |
| { a: /^ab/ } | a LIKE 'ab%' |
| { a: null } | a IS NULL |
| { a: { $ne: null } } | a IS NOT NULL |
| { $or: [ … ] } | OR |
| .sort({ a: -1 }) | ORDER BY a DESC |
| .skip(20).limit(10) | LIMIT 10 OFFSET 20 |
| .countDocuments(filter) | SELECT COUNT(*) |
| .distinct('a') | SELECT DISTINCT a |
| { $group: { _id: '$a' } } | GROUP BY a |
| { $sum: '$a' }, $avg, $min, $max | SUM(a), AVG(a), MIN(a), MAX(a) |
| { $sum: 1 } | COUNT(*) |
| a second $match after $group | HAVING |
| $lookup then $unwind | INNER JOIN |
| $unwind with preserveNullAndEmptyArrays | LEFT JOIN |
| { $count: 'n' } | SELECT COUNT(*) AS n |
| .insertOne(…) / .insertMany([…]) | INSERT INTO … VALUES |
| .updateMany(filter, { $set: … }) | UPDATE … SET |
| .updateMany(filter, { $inc: { n: 1 } }) | UPDATE … SET n = n + 1 |
| .deleteMany(filter) | DELETE FROM … WHERE |
| .createIndex(keys, { unique: true }) | CREATE UNIQUE INDEX |
MongoDB to SQL, worked through
Every example below is produced by the converter on this page when the build runs, so what you read here is what the tool writes.
A cursor chain becomes a SELECT
db.collection('users') .find({ age: { $gt: 21 }, country: 'DE' }) .project({ name: 1, email: 1, _id: 0 }) .sort({ name: 1 }) .limit(20)
SELECT name, email FROM users WHERE age > 21 AND country = 'DE' ORDER BY name ASC LIMIT 20
$group and $match become GROUP BY and HAVING
db.collection('orders').aggregate([ { $match: { paid: true } }, { $group: { _id: '$status', revenue: { $sum: '$amount' } } }, { $sort: { revenue: -1 } }, { $limit: 10 } ])
SELECT status, SUM(amount) AS revenue FROM orders WHERE paid = TRUE GROUP BY status ORDER BY revenue DESC LIMIT 10
$lookup becomes a JOIN, and the alias becomes the table name
db.collection('users').aggregate([ { $lookup: { from: 'orders', localField: '_id', foreignField: 'user_id', as: 'o' } }, { $unwind: '$o' }, { $match: { 'o.total': { $gt: 100 } } } ])
SELECT * FROM users INNER JOIN orders ON users._id = orders.user_id WHERE orders.total > 100
The two argument projection, which Monghoul refuses to run
db.users.find({ status: 'paid' }, { total: 1, _id: 0 })
SELECT total FROM users WHERE status = 'paid'
- The second argument reads as a shell projection. Monghoul runs the Node driver, where that argument is an options bag, so this exact code throws there. Write .project({ … }) instead.
updateMany becomes UPDATE, and $inc becomes an addition
db.collection('users').updateMany( { last_seen: { $lt: '2023-01-01' } }, { $set: { status: 'archived' }, $inc: { logins: 1 } } )
UPDATE users SET status = 'archived', logins = logins + 1 WHERE last_seen < '2023-01-01'
Frequently asked questions
Does my query leave the browser?
No. The parser and the code generator are JavaScript that runs on this page. There is no server call, no account, and no upload.
Can I paste shell syntax?
Yes. db.users.find({}, { name: 1 }) and db.collection("users").find({}).project({ name: 1 }) both read. The second argument to find is the one ambiguous spot, because it means a projection in the shell and an options bag in the driver. It is read the driver way, and a note says so when both readings hold.
How does a $lookup become a JOIN?
A $lookup followed by an $unwind becomes an INNER JOIN, because $unwind drops a document with no match. When the $unwind sets preserveNullAndEmptyArrays it becomes a LEFT JOIN. A $lookup with no $unwind is refused: it embeds an array on each document, and a JOIN makes one row per match, so the two return different shapes.
Why does it refuse some pipelines?
SQL runs its clauses in a fixed order and a pipeline does not. A $match after a $limit filters inside the limited set, and no SQL statement means that. Sorting the stages into clauses anyway would produce SQL that looks right and returns different rows, so it refuses and says which stage is the problem.
What happens to an ObjectId?
SQL has no ObjectId type, so the 24 character hex string is written as text and a note points it out. ISODate and new Date become their literal, and NumberLong, NumberInt and NumberDecimal become the number.
Will the SQL run against my database?
Treat it as a reading of the query rather than a statement to execute. MongoDB documents have no fixed columns, so a nested field becomes a dotted column name and a missing field has no exact SQL counterpart. The value here is understanding what a pipeline does, and every place the two languages disagree carries a note.
Reading someone else's pipeline?
Monghoul runs the query and shows you the plan: every stage named, the scan efficiency worked out, and the index that would remove a collection scan. There is a visual pipeline builder for the stages a JOIN turns into. Free tier included.
Download Monghoul