Skip to content
Monghoul
Free tool · Runs in your browser

SQL to MongoDB Converter

Write a SQL query and get MongoDB code that runs. The output is Node driver syntax, which is what a Monghoul query tab executes, so it can be pasted in and run unchanged. It reads a query back into SQL too, in either syntax. Nothing leaves your browser.

Write SQL
SELECT with joins and grouping, plus INSERT, UPDATE, DELETE, and CREATE INDEX.
Try a sample
Output
Node driver syntax, which is what a Monghoul query tab runs. Paste it in and it executes as it is.
Write a SQL statement above and the MongoDB query appears here.

SQL and MongoDB, and the places they part

Most of SQL has a direct MongoDB form. The rest is worth knowing about before you run the query.

The mapping

What converts cleanly

SELECT with WHERE, ORDER BY, LIMIT and OFFSET becomes a cursor chain. GROUP BY, HAVING and a join become an aggregation pipeline. INSERT, UPDATE, DELETE and CREATE INDEX convert as well.

The warnings

Where the two disagree

SQL leaves a NULL row out of a negative test and MongoDB does not. A date written as text stays text. Each difference gets a note under the output, with the fix named, rather than a silent guess.

The output

Driver code, not shell code

Monghoul runs the MongoDB Node driver, where find takes a filter and an options bag. The shell puts a projection in that second argument, and the driver rejects it. This writes .project() on the cursor instead, so the code runs unchanged.

SQL to MongoDB, clause by clause

The whole table the converter works from. For the pipeline stages behind the grouping and the joins, the aggregation cheat sheet carries a stage level view. To read a query the other way, use the MongoDB to SQL converter.

SQL MongoDB
SELECT a, b .project({ a: 1, b: 1, _id: 0 })
SELECT a AS x .project({ x: '$a', _id: 0 })
WHERE a = 1 { a: 1 }
WHERE a > 1 { a: { $gt: 1 } }
a IN (1, 2) { a: { $in: [1, 2] } }
a BETWEEN 1 AND 9 { a: { $gte: 1, $lte: 9 } }
a LIKE 'ab%' { a: /^ab/ }
a NOT LIKE 'ab%' { a: { $not: /^ab/ } }
a IS NULL { a: null }
AND one filter object, or $and when a key repeats
OR { $or: [ … ] }
ORDER BY a DESC .sort({ a: -1 })
LIMIT 10 OFFSET 20 .skip(20).limit(10)
SELECT DISTINCT a .distinct('a', filter)
SELECT COUNT(*) .countDocuments(filter)
GROUP BY a { $group: { _id: '$a' } }
SUM(a), AVG(a), MIN(a), MAX(a) { $sum: '$a' }, $avg, $min, $max
HAVING a second $match, after the $group
JOIN t ON x = y $lookup, then $unwind
LEFT JOIN $unwind with preserveNullAndEmptyArrays
INSERT INTO … VALUES .insertOne(…) or .insertMany([…])
UPDATE … SET a = 1 .updateMany(filter, { $set: { a: 1 } })
UPDATE … SET n = n + 1 .updateMany(filter, { $inc: { n: 1 } })
DELETE FROM … WHERE .deleteMany(filter)
CREATE UNIQUE INDEX .createIndex(keys, { unique: true })

SQL to MongoDB, 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.

WHERE, ORDER BY and LIMIT become a cursor chain

SQL
SELECT name, email
FROM users
WHERE age > 21 AND country = 'DE'
ORDER BY name
LIMIT 20
MongoDB
db.collection('users')
  .find({ age: { $gt: 21 }, country: 'DE' })
  .project({ name: 1, email: 1, _id: 0 })
  .sort({ name: 1 })
  .limit(20)

GROUP BY and HAVING become an aggregation pipeline

SQL
SELECT status, COUNT(*) AS orders, SUM(amount) AS revenue
FROM orders
WHERE paid = TRUE
GROUP BY status
HAVING COUNT(*) > 5
ORDER BY revenue DESC
LIMIT 10
MongoDB
db.collection('orders').aggregate([
  { $match: { paid: true } },
  {
    $group: {
      _id: '$status',
      orders: { $sum: 1 },
      revenue: { $sum: '$amount' }
    }
  },
  { $match: { orders: { $gt: 5 } } },
  { $sort: { revenue: -1 } },
  { $limit: 10 },
  { $project: { status: '$_id', orders: 1, revenue: 1, _id: 0 } }
])

JOIN becomes $lookup and $unwind

SQL
SELECT u.name AS customer, o.total
FROM users u
JOIN orders o ON u._id = o.user_id
WHERE o.total > 100
MongoDB
db.collection('users').aggregate([
  {
    $lookup: {
      from: 'orders',
      localField: '_id',
      foreignField: 'user_id',
      as: 'o'
    }
  },
  { $unwind: '$o' },
  { $match: { 'o.total': { $gt: 100 } } },
  { $project: { customer: '$name', total: '$o.total', _id: 0 } }
])
  • $unwind drops a document with no match, which is what INNER JOIN does. Set preserveNullAndEmptyArrays to keep it.

LIKE, IN, BETWEEN and IS NULL

SQL
SELECT *
FROM logs
WHERE level IN ('warn', 'error')
  AND message LIKE 'timeout%'
  AND created_at BETWEEN '2024-01-01' AND '2024-06-30'
  AND user_id IS NOT NULL
MongoDB
db.collection('logs').find({
  level: { $in: ['warn', 'error'] },
  message: /^timeout/,
  created_at: { $gte: '2024-01-01', $lte: '2024-06-30' },
  user_id: { $ne: null }
})
  • LIKE converts to a case sensitive regular expression. MySQL's default collation matches either case, so add the i flag when that is what the query relied on.
  • A date written as text stays text. MongoDB compares a string to a Date by BSON type, so a date field returns nothing. Wrap the value in ISODate('2024-01-01') when the field holds a real date.
  • MongoDB has no NULL. { a: null } matches a document where the field is null and one where it is absent, which is the closest form to IS NULL. Add $exists when you need to tell them apart.

A write statement becomes updateMany

SQL
UPDATE users
SET status = 'archived', logins = logins + 1
WHERE last_seen < '2023-01-01'
MongoDB
db.collection('users').updateMany(
  { last_seen: { $lt: '2023-01-01' } },
  { $set: { status: 'archived' }, $inc: { logins: 1 } }
)
  • A date written as text stays text. MongoDB compares a string to a Date by BSON type, so a date field returns nothing. Wrap the value in ISODate('2024-01-01') when the field holds a real date.
  • UPDATE changes every matching row, so this is updateMany. Change it to updateOne when you mean only the first match.

CREATE INDEX becomes createIndex

SQL
CREATE UNIQUE INDEX idx_email
ON users (email, created_at DESC)
MongoDB
db.collection('users').createIndex(
  { email: 1, created_at: -1 },
  { name: 'idx_email', unique: true }
)

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.

Why does the output say db.collection("users") instead of db.users?

The shorthand fails for a collection whose name the driver already uses, such as stats, admin or watch. In those cases db.stats is the driver method and not your collection. db.collection(name) always reaches the collection, so the Monghoul output uses it every time.

Why not shell syntax?

The shell writes a projection as the second argument to find. The Node driver reads that argument as an options bag, so Monghoul refuses a plain field list there and ignores a computed one. The output writes .project() on the cursor instead, which the driver and the shell both accept, so there is one form and it runs everywhere. Shell syntax is still read on the way in, when you convert a query back to SQL.

How does a JOIN convert?

An equality join becomes a $lookup followed by an $unwind. INNER JOIN drops a document with no match, which is what $unwind does by default. LEFT JOIN sets preserveNullAndEmptyArrays so the document stays. A condition on the base table runs in a $match before the $lookup, where an index can still serve it.

Why does it warn me about NULL?

A SQL row always has every column, and a MongoDB document does not. WHERE a != 1 excludes a NULL row in SQL, while { a: { $ne: 1 } } also matches a document where the field is null or absent. On a sparse field that returns extra rows, so the tool says so and offers the $exists form.

What does it refuse to convert?

Subqueries, UNION, window functions, CROSS JOIN, RIGHT JOIN and FULL JOIN, COUNT(DISTINCT x), and a query placeholder such as ? or $1. Each one gets a message naming the construct and the line it sits on. A refusal is deliberate: a query that runs and returns the wrong rows is worse than one that will not convert.

Writing MongoDB queries every day?

Monghoul runs the query this page just wrote, with autocomplete that reads your real collection schemas, a visual pipeline builder for the stages a JOIN turns into, and an explain plan graded against the index it used. Free tier included.

Download Monghoul