Back to campField Journalby Zin
All entries

sql · nosql · mongodb

SQL vs NoSQL: A Detailed Comparison

What SQL and NoSQL databases really are, when to pick each, and a guided tour of queries from a first SELECT to window functions, MongoDB aggregation pipelines and Redis, explained line by line.

23 min read0 likes

SQL နဲ့ NoSQL: အသေးစိတ် နှိုင်းယှဉ်ချက်

SQL နဲ့ NoSQL database တွေ တကယ်တမ်း ဘာလဲ၊ ဘယ်အချိန် ဘာကို ရွေးသင့်လဲ၊ ပထမဆုံး SELECT ကနေ window function၊ MongoDB aggregation pipeline နဲ့ Redis အထိ query တွေကို တစ်ကြောင်းချင်း ရှင်းပြထားပါတယ်။

ဖတ်ချိန် 22 မိနစ်0 likes

Every app you build ends up asking the same question sooner or later: where do I put the data? The two big families of answers are SQL (relational) databases like PostgreSQL and MySQL, and NoSQL databases like MongoDB and Redis. People argue about them as if one had to win. In practice they are tools with different shapes, and most real systems use more than one.

This entry walks through what each one is, where each one shines and hurts, and then the part most comparisons skip: actually writing queries in both, from the first line you would type as a beginner to the patterns you meet in production. Every code block has notes on its lines. Tap or hover a line to read what it does.

What is SQL?

SQL (Structured Query Language) is the language you use to talk to a relational database. A relational database stores data in tables. Each table has a fixed set of columns with types (a number, some text, a date), and every row is one record that fits those columns. The set of tables and columns is called the schema, and you define it before you store anything.

Tables are linked by keys. A primary key uniquely identifies each row, like users.id. A foreign key is a column that points at another table's primary key, like posts.user_id pointing at users.id. Instead of copying a user's name into every post, you store it once and join the tables when you read. Keeping each fact in one place is called normalisation.

Relational databases are built around transactions with ACID guarantees:

  • Atomicity: a group of changes happens completely or not at all.
  • Consistency: the data always obeys its rules (types, keys, constraints).
  • Isolation: transactions running at the same time do not see each other's half-finished work.
  • Durability: once the database says "committed", the data survives a crash.

Here is the schema we will use for the rest of this entry, a tiny blog with users, posts and comments, written for PostgreSQL:

sql
CREATE TABLE users (  id         SERIAL PRIMARY KEY,  name       TEXT NOT NULL,  email      TEXT NOT NULL UNIQUE,  country    TEXT,  created_at TIMESTAMPTZ NOT NULL DEFAULT now()); CREATE TABLE posts (  id           SERIAL PRIMARY KEY,  user_id      INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,  title        TEXT NOT NULL,  body         TEXT NOT NULL,  views        INT NOT NULL DEFAULT 0 CHECK (views >= 0),  published_at TIMESTAMPTZ); CREATE TABLE comments (  id         SERIAL PRIMARY KEY,  post_id    INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,  user_id    INT NOT NULL REFERENCES users(id),  parent_id  INT REFERENCES comments(id),  body       TEXT NOT NULL,  created_at TIMESTAMPTZ NOT NULL DEFAULT now());

Line by line

  1. CREATE TABLE defines a new table and every column it will ever hold. Rows that do not fit this shape are refused.
  2. SERIAL is an auto-incrementing integer, so each new user gets the next number. PRIMARY KEY makes it unique and indexed.
  3. NOT NULL means every user must have a name. Leaving it out is an error, not an empty value.
  4. UNIQUE makes the database itself refuse a second account with the same email, even if two requests arrive at the same moment.
  5. No NOT NULL here, so country is optional and can be NULL (unknown).
  6. TIMESTAMPTZ is a time with a time zone. DEFAULT now() fills it in automatically when a row is inserted.
  7. Each post belongs to one user. This is the "many" side of a one-to-many relationship.
  8. REFERENCES users(id) is the foreign key: a post cannot point at a user who does not exist. ON DELETE CASCADE deletes a user's posts when the user is deleted.
  9. A CHECK constraint is a rule the database enforces on every write. Here, the view count can never go negative.
  10. Nullable on purpose: a post with no published_at is still a draft.
  11. Comments point at a post and at the user who wrote them, so one comment row links three tables together.
  12. parent_id points back into the same table. A reply stores the id of the comment it answers, which turns flat rows into a tree. We will walk that tree later with a recursive query.

What is NoSQL?

NoSQL ("not only SQL") is an umbrella name for databases that do not store data as relational tables. They grew up in the late 2000s, when companies like Google, Amazon and Facebook needed to spread enormous amounts of data across many cheap machines, and found that strict schemas and joins were hard to scale that way. There is no single NoSQL language. Each family has its own model and its own API.

  • Document stores (MongoDB, Couchbase, Firestore) keep JSON-like documents. One document can hold nested objects and arrays, so a post can carry its tags and comments inside it.
  • Key-value stores (Redis, DynamoDB, Memcached) are a giant dictionary: you give a key, you get a value. Extremely fast, and mostly used for caches, sessions and counters.
  • Wide-column stores (Cassandra, HBase, ScyllaDB) store rows whose columns can differ, spread across many servers. Built for huge write volumes such as logs, metrics and sensor data.
  • Graph databases (Neo4j, Amazon Neptune) store nodes and the relationships between them as first-class data. Ideal for social networks, recommendations and fraud detection.

Most NoSQL databases are schema-flexible: two documents in the same collection may have different fields, and the shape is checked by your application (or optional validation rules) rather than by a fixed table definition. Many of them were designed to scale horizontally, meaning you add more servers and the data is split (sharded) across them, instead of buying one bigger server.

Distributed NoSQL systems often describe their guarantees as BASE (Basically Available, Soft state, Eventually consistent): the system stays up, and copies of the data agree eventually, rather than instantly. This is a trade-off, not a flaw, and modern MongoDB in fact supports ACID transactions across documents.

Here is the same blog modelled as a single MongoDB document. Notice how the data that is read together lives together:

js
{  _id: ObjectId("66f1c0a2e4b0a1b2c3d4e5f6"),  title: "Camping at Inle Lake",  author: { _id: ObjectId("66f1bf00e4b0a1b2c3d4e001"), name: "Zin" },  tags: ["travel", "camping"],  views: 42,  publishedAt: ISODate("2026-09-01T08:00:00Z"),  comments: [    { user: "Aye", body: "Beautiful photos!", likes: 6, at: ISODate("2026-09-02T10:00:00Z") },    { user: "Ko Ko", body: "Which month did you go?", likes: 2, at: ISODate("2026-09-03T12:30:00Z") }  ]}

Line by line

  1. Every document has an _id. MongoDB generates an ObjectId automatically: 12 bytes that encode a timestamp, so ids sort roughly by creation time.
  2. The author is embedded as a small sub-document. We keep a copy of the name so a post can be shown without a second lookup. The _id inside still lets us find the full user when we need to.
  3. Arrays are ordinary values. In SQL, tags would need a separate tags table and a post_tags join table.
  4. ISODate is the shell's way of writing a real date type, which can be sorted and compared, not a string.
  5. The comments live inside the post. Reading a post with its comments is now one read of one document, with no join.

When should you use SQL, and when NoSQL?

The honest answer starts with your data and how you read it, not with what is fashionable. Here are the strengths and weaknesses of each side.

SQL: advantages

  • Strong consistency and ACID transactions. Money, stock levels and bookings stay correct even when many users write at once.
  • Joins and ad-hoc questions. You can ask questions you did not plan for ("revenue by country for users who signed up in May") without changing how data is stored.
  • The schema protects you. Types, foreign keys, UNIQUE and CHECK constraints catch bad data at the door.
  • One mature, standard language. SQL skills move between PostgreSQL, MySQL, SQLite, SQL Server and many analytics tools.
  • Decades of tooling: query planners, backups, replication, reporting and BI tools.

SQL: disadvantages

  • Schema changes need migrations. Adding or reshaping columns on a large, busy table needs planning.
  • Horizontal scaling is harder. Spreading writes across many servers (sharding) is possible but rarely built in.
  • Object–relational mismatch. Nested objects in your code have to be split into tables and joined back, usually through an ORM.
  • Joins over huge tables can get expensive without the right indexes.

NoSQL: advantages

  • Flexible schema. You can add fields as the product changes, and store records with different shapes side by side.
  • Data shaped like your code. A document maps directly onto a JSON object, so there is little translation layer.
  • Built to scale out. Sharding and replication are part of the design, which suits very large or very write-heavy workloads.
  • Fast for known access patterns. Reading one document, or one key, with everything it needs is very quick.
  • A specialised model for each job: Redis for speed, Cassandra for write volume, Neo4j for relationships.

NoSQL: disadvantages

  • Data integrity is mostly your job. Without a strict schema, typos and half-shaped records slip in unless you add validation.
  • Duplication. Embedding copies data (like the author's name), and updating every copy is your responsibility.
  • Weaker or costlier joins and transactions. They exist in MongoDB ($lookup, multi-document transactions) but are not the natural path.
  • Eventual consistency in many distributed setups means a read may briefly return old data.
  • No single standard language. MongoDB, Redis and Cassandra queries are all different.

A quick decision guide

  • Pick SQL when your data is highly related (users, orders, invoices), correctness matters more than raw speed, you need reporting and ad-hoc queries, or you are not sure yet. PostgreSQL is a very safe default.
  • Pick a document store when records are naturally self-contained (articles, product catalogues, user profiles, CMS content), their shape varies or changes often, and you mostly read one whole record at a time.
  • Pick a key-value store when you need a cache, sessions, rate limits, counters, queues or leaderboards, usually next to a main database.
  • Pick wide-column or graph when you have massive write streams (time series, logs) or the relationships themselves are the question (friends of friends, recommendations).

And you do not have to choose only one. This very journal runs on MongoDB, because an entry is a self-contained document full of blocks of different shapes. A shop next to it would keep orders and payments in PostgreSQL, and both might use Redis as a cache. The phrase for this is polyglot persistence: the right store for each job.

SQL queries: from beginner to advanced

All the examples below use the users, posts and comments tables from earlier, in PostgreSQL. Most of it works the same in MySQL and SQLite. Each step builds on the last.

Beginner 1: reading rows with SELECT

Everything starts with SELECT: which columns, from which table, which rows, in what order, and how many.

sql
SELECT id, title, viewsFROM postsWHERE published_at IS NOT NULL  AND views >= 100ORDER BY views DESCLIMIT 10;

Line by line

  1. Name the columns you want back. SELECT * returns every column, which is handy while exploring but wasteful in real code.
  2. FROM says which table to read.
  3. WHERE filters rows. NULL means "no value", and it can only be tested with IS NULL or IS NOT NULL, never with = NULL.
  4. AND combines conditions. Both must be true for a row to stay.
  5. ORDER BY ... DESC sorts from highest to lowest. Without ORDER BY, SQL promises no order at all.
  6. LIMIT keeps only the first 10 rows of the sorted result: a "top 10 posts" list.

Beginner 2: filtering with patterns, lists and ranges

sql
SELECT name, email, countryFROM usersWHERE country IN ('Myanmar', 'Thailand')  AND email LIKE '%@gmail.com'  AND created_at BETWEEN '2026-01-01' AND '2026-06-30'ORDER BY name;

Line by line

  1. IN (...) matches any value in the list. It is a shorter way to write country = 'Myanmar' OR country = 'Thailand'.
  2. LIKE matches a pattern: % means "any characters". So this finds every Gmail address. PostgreSQL's ILIKE does the same without caring about upper or lower case.
  3. BETWEEN includes both ends of the range. Text like '2026-01-01' is converted to a timestamp automatically.
  4. Sorts alphabetically (ascending is the default).

Beginner 3: inserting, updating and deleting

sql
INSERT INTO users (name, email, country)VALUES ('Zin', 'zin@example.com', 'Myanmar')RETURNING id, created_at; UPDATE postsSET views = views + 1WHERE id = 7; DELETE FROM commentsWHERE created_at < now() - INTERVAL '2 years';

Line by line

  1. INSERT INTO adds a row. We list only the columns we are giving values for: id and created_at fill themselves in.
  2. VALUES gives the values in the same order as the column list. Text goes in single quotes.
  3. RETURNING (PostgreSQL) hands back the generated values, so your app learns the new id without a second query.
  4. UPDATE changes existing rows in a table.
  5. SET gives the new values. It can use the old value, so this is an atomic "add one", safe even with many visitors at once.
  6. Always check the WHERE on an UPDATE. Without it, every post in the table would be changed.
  7. DELETE FROM removes rows. Like UPDATE, it affects every row unless you filter.
  8. Date arithmetic: "now, minus two years". This deletes only old comments.

Intermediate 1: JOINs

A join combines rows from two tables where a condition matches, usually a foreign key equal to a primary key.

sql
SELECT p.title, u.name AS authorFROM posts AS pINNER JOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULL; SELECT u.name, p.titleFROM users AS uLEFT JOIN posts AS p ON p.user_id = u.idWHERE p.id IS NULL;

Line by line

  1. Columns can come from either table. AS author renames the column in the result.
  2. AS p gives the table a short alias, so we can write p.title instead of posts.title.
  3. INNER JOIN keeps only pairs where the ON condition is true: each post together with its author. A post whose user had been deleted would not appear.
  4. The second query answers a classic question: which users have never written a post?
  5. LEFT JOIN keeps every row from the left table (users). When a user has no posts, the post columns are filled with NULL.
  6. Keeping only rows where the post side is NULL leaves exactly the users with no posts. This "anti-join" pattern is worth memorising.

Intermediate 2: aggregation with GROUP BY and HAVING

sql
SELECT u.country,       COUNT(*)            AS posts,       SUM(p.views)        AS total_views,       ROUND(AVG(p.views)) AS avg_viewsFROM posts AS pJOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULLGROUP BY u.countryHAVING COUNT(*) >= 5ORDER BY total_views DESC;

Line by line

  1. The column we group by. Each country becomes one row in the result.
  2. COUNT(*) counts the rows in each group, which here is the number of posts.
  3. SUM, AVG, MIN and MAX are aggregate functions: they squash many rows into one value per group.
  4. A plain JOIN means INNER JOIN.
  5. WHERE filters individual rows before they are grouped.
  6. GROUP BY collects rows that share a country. Every selected column must either be grouped or be inside an aggregate.
  7. HAVING filters groups after aggregation, something WHERE cannot do. Only countries with at least five posts are kept.

Intermediate 3: subqueries

sql
SELECT title, viewsFROM postsWHERE views > (SELECT AVG(views) FROM posts); SELECT nameFROM users AS uWHERE EXISTS (  SELECT 1 FROM comments AS c  WHERE c.user_id = u.id);

Line by line

  1. A scalar subquery returns a single value, here the average views, which the outer query compares against. Result: posts that are above average.
  2. EXISTS is true if the inner query returns at least one row. These lines find users who have commented at least once.
  3. SELECT 1 because we only care whether a row exists, not what is in it.
  4. This is a correlated subquery: it refers to u.id from the outer query, so it is evaluated per user. The planner usually turns it into an efficient join.

Advanced 1: CTEs with WITH

A common table expression (CTE) names a subquery so a long query reads top to bottom, like small steps.

sql
WITH monthly AS (  SELECT user_id,         date_trunc('month', published_at) AS month,         COUNT(*) AS posts  FROM posts  WHERE published_at IS NOT NULL  GROUP BY user_id, month),active AS (  SELECT user_id FROM monthly  GROUP BY user_id  HAVING COUNT(*) >= 3)SELECT u.name, m.month, m.postsFROM monthly AS mJOIN active AS a USING (user_id)JOIN users AS u ON u.id = m.user_idORDER BY u.name, m.month;

Line by line

  1. WITH monthly AS (...) defines a temporary named result: posts per user per month. It exists only for this one query.
  2. date_trunc cuts a timestamp down to the first moment of its month, so every post in September lands in the same group.
  3. CTEs can be chained with commas, and a later one can read an earlier one.
  4. active keeps users who posted in at least three different months.
  5. The final SELECT reads the CTEs like ordinary tables.
  6. USING (user_id) is shorthand for ON m.user_id = a.user_id when both columns have the same name.

Advanced 2: recursive CTEs for trees

Remember comments.parent_id? Replies to replies form a tree of any depth. A recursive CTE walks it in a single query.

sql
WITH RECURSIVE thread AS (  SELECT id, parent_id, body, 1 AS depth  FROM comments  WHERE post_id = 7 AND parent_id IS NULL   UNION ALL   SELECT c.id, c.parent_id, c.body, t.depth + 1  FROM comments AS c  JOIN thread AS t ON c.parent_id = t.id)SELECT repeat('  ', depth - 1) || body AS replyFROM threadORDER BY depth, id;

Line by line

  1. RECURSIVE lets the CTE refer to itself.
  2. The anchor part: the starting rows. We also start a depth counter at 1.
  3. The top-level comments on post 7 are the ones with no parent.
  4. UNION ALL glues the anchor rows to the rows the recursive part keeps finding.
  5. The recursive part: each round adds the children of the rows found in the previous round, one level deeper.
  6. Joining comments to thread itself finds every comment whose parent is already in the result. When a round finds nothing new, the recursion stops.
  7. repeat and || (string concatenation) indent each reply by its depth, so the result reads like a thread.

Advanced 3: window functions

Window functions compute across a set of rows without collapsing them the way GROUP BY does. Each row keeps its identity and gains a calculated value.

sql
SELECT u.name,       p.title,       p.views,       ROW_NUMBER() OVER (         PARTITION BY p.user_id         ORDER BY p.views DESC       ) AS rank_for_author,       SUM(p.views) OVER (         PARTITION BY p.user_id         ORDER BY p.published_at       ) AS running_views,       p.views - LAG(p.views) OVER (ORDER BY p.published_at) AS change_from_previousFROM posts AS pJOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULL;

Line by line

  1. ROW_NUMBER() numbers rows 1, 2, 3 and so on. OVER (...) is what makes it a window function, and it describes the window.
  2. PARTITION BY restarts the numbering for each author, like GROUP BY but without merging rows.
  3. Inside each author, the most-viewed post gets number 1. Wrap this query and filter rank_for_author <= 3 to get each author's top three posts.
  4. An ordinary aggregate (SUM) used as a window function.
  5. With an ORDER BY in the window, the sum becomes running: each row adds its views to everything before it, giving a cumulative total over time.
  6. LAG reads a value from the previous row in the window order, which is perfect for "how much did this change since last time?". LEAD reads the next row.

Advanced 4: transactions

sql
BEGIN; UPDATE accounts SET balance = balance - 500 WHERE id = 1;UPDATE accounts SET balance = balance + 500 WHERE id = 2; INSERT INTO transfers (from_id, to_id, amount)VALUES (1, 2, 500); COMMIT;-- ROLLBACK; would undo everything since BEGIN

Line by line

  1. BEGIN opens a transaction. Nothing after it is visible to other sessions until it is committed.
  2. This uses a separate accounts table to show the classic example: take 500 from account 1...
  3. ...and give it to account 2. If the server crashed between these two lines without a transaction, the money would simply vanish.
  4. Record the transfer as part of the same unit of work.
  5. COMMIT makes all four changes permanent together. This is the "A" in ACID.
  6. If anything fails, for example a CHECK (balance >= 0) constraint, the app sends ROLLBACK and the database returns to exactly how it was before BEGIN.

Advanced 5: indexes and EXPLAIN

Queries are only as fast as the indexes behind them. An index is a sorted structure (usually a B-tree) that lets the database jump to matching rows instead of reading the whole table.

sql
CREATE INDEX posts_user_published_idx  ON posts (user_id, published_at DESC); EXPLAIN ANALYZESELECT title FROM postsWHERE user_id = 7ORDER BY published_at DESCLIMIT 5;-- Index Scan using posts_user_published_idx ... (actual time=0.03..0.05 rows=5)

Line by line

  1. Give indexes descriptive names. You will see them in query plans later.
  2. A composite index on two columns, in this order. It serves "posts by user X, newest first" perfectly. Column order matters: it does not help a query that filters only on published_at.
  3. EXPLAIN shows the plan the database chose. ANALYZE actually runs the query and reports real timings.
  4. A line like this is what you want to see. A Seq Scan on a large table usually means a missing index. Remember that indexes speed up reads but slow writes a little, so add the ones your queries really use.

NoSQL queries with MongoDB: from beginner to advanced

Now the same blog in MongoDB. The examples run in mongosh, MongoDB's shell. The Node.js driver uses almost the same syntax, with await in front. Instead of tables we have collections, instead of rows documents, and queries are written as JSON-like objects rather than as a language of their own.

Beginner 1: inserting documents

js
use blog db.users.insertOne({  name: "Zin",  email: "zin@example.com",  country: "Myanmar",  createdAt: new Date()}) db.posts.insertMany([  { title: "Camping at Inle Lake", author: "Zin", tags: ["travel", "camping"], views: 42, published: true },  { title: "Notes on Go", author: "Zin", tags: ["go"], views: 0, published: false, draftNote: "add examples" }])

Line by line

  1. use blog switches to the blog database. If it does not exist yet, MongoDB creates it the first time you write to it. There is no CREATE DATABASE step.
  2. db.users is the users collection, also created on first write. insertOne adds one document and returns its new _id.
  3. A document is a set of field–value pairs, written like a JavaScript object. No schema had to be declared first.
  4. new Date() stores a real date value, which can be sorted and compared, not a string.
  5. insertMany adds an array of documents in a single round trip to the server.
  6. tags is an array stored right inside the document.
  7. The second post has a field the first one does not (draftNote). In the same collection, that is perfectly legal. This is schema flexibility in action.

Beginner 2: finding documents

js
db.posts.find() db.posts.find({ author: "Zin", published: true }) db.posts.find(  { views: { $gte: 100 } },  { title: 1, views: 1, _id: 0 })  .sort({ views: -1 })  .limit(10) db.posts.findOne({ title: "Notes on Go" }) db.posts.countDocuments({ published: true })

Line by line

  1. find() with no filter returns every document in the collection, like SELECT * FROM posts. The shell prints the first 20 and lets you type it for more.
  2. A filter object. Listing two fields means both must match, an implicit AND. This is WHERE author = 'Zin' AND published = true.
  3. find takes up to two arguments: the filter, then a projection.
  4. Operators are nested objects. { $gte: 100 } means "greater than or equal to 100". Others include $gt, $lt, $lte, $ne (not equal) and $eq.
  5. The projection picks the fields to return: 1 includes a field and 0 excludes it. _id is included unless you turn it off.
  6. sort takes field: direction pairs, where 1 is ascending and -1 descending. This is ORDER BY views DESC.
  7. limit(10) returns at most ten documents. Together these lines are the "top 10 posts" query from the SQL section.
  8. findOne returns the first matching document itself (or null) instead of a cursor, which is handy when you expect a single result.
  9. Counts matching documents without returning them, like SELECT COUNT(*) ... WHERE.

Beginner 3: updating and deleting

js
db.posts.updateOne(  { title: "Notes on Go" },  {    $set: { published: true, publishedAt: new Date() },    $unset: { draftNote: "" },    $inc: { views: 1 },    $push: { tags: "tutorial" }  }) db.posts.updateMany(  { tags: "camping" },  { $addToSet: { tags: "outdoors" } }) db.posts.deleteOne({ title: "Old draft" })db.posts.deleteMany({ published: false, views: 0 })

Line by line

  1. updateOne changes the first document that matches the filter.
  2. The first argument is the filter: which document to change.
  3. The second argument is the update, a set of update operators. Each operator changes the document in a different way, and all of them apply atomically together.
  4. $set writes fields, creating them if they do not exist yet. Writing the update without $set would replace the whole document, a classic beginner mistake.
  5. $unset removes a field completely. The value ("") is ignored.
  6. $inc adds to a number atomically, like SET views = views + 1 in SQL.
  7. $push appends a value to an array.
  8. updateMany changes every matching document.
  9. Querying an array field with a plain value matches documents whose array contains that value. No join table is needed.
  10. $addToSet adds to an array only if the value is not already there, so running this twice does not create duplicates.
  11. deleteOne removes the first match.
  12. deleteMany removes every match. As with SQL, double-check the filter: deleteMany({}) empties the collection.

Intermediate 1: query operators and arrays

js
db.users.find({ country: { $in: ["Myanmar", "Thailand"] } }) db.users.find({ email: { $regex: /@gmail\.com$/i } }) db.posts.find({  $or: [    { views: { $gt: 1000 } },    { tags: { $all: ["go", "tutorial"] } }  ]}) db.posts.find({  comments: { $elemMatch: { user: "Aye", likes: { $gte: 5 } } }}) db.posts.find({ "author.name": "Zin" }) db.posts.find({ draftNote: { $exists: true } })

Line by line

  1. $in matches any value in the list, the same as SQL's IN.
  2. $regex matches a regular expression. The i flag ignores case, and $ anchors the match to the end. Note that only patterns anchored at the start (^...) can use an index efficiently.
  3. A filter can mix several conditions.
  4. $or takes an array of conditions and matches if any of them is true. There is also $and and $nor.
  5. $all matches arrays that contain every listed value, in any order.
  6. Searching inside an array of sub-documents.
  7. $elemMatch requires that one single array element meets all the conditions. Without it, user: "Aye" could match one comment and likes >= 5 a different one.
  8. Dot notation reaches into embedded documents. The quotes are required because the key contains a dot.
  9. $exists matches documents that have (or, with false, lack) a field. That question only makes sense in a flexible schema.

Intermediate 2: embedding versus referencing

The biggest design decision in MongoDB is not a query at all. It is whether related data is embedded in one document or referenced by id from another collection.

js
// Embedded: comments live inside the postdb.posts.updateOne(  { _id: postId },  { $push: { comments: { user: "Aye", body: "Nice!", at: new Date() } } }) // Referenced: comments are their own collectiondb.comments.insertOne({  postId: postId,  user: "Aye",  body: "Nice!",  at: new Date()})db.comments.find({ postId: postId }).sort({ at: 1 })

Line by line

  1. Embed when the child data is small, belongs to exactly one parent and is read together with it.
  2. Adding a comment is a single atomic update to one document, and reading the post brings every comment with it.
  3. Reference when the child list can grow without limit or is queried on its own. A MongoDB document is capped at 16 MB, so a post with 100,000 comments must not embed them.
  4. postId plays the role of a foreign key. The difference is that MongoDB does not enforce it. Keeping it valid is your code's job.
  5. Reading the comments is now a second query, and that is exactly what $lookup (below) combines into one. The rule of thumb: model your data around how you read it.

Advanced 1: the aggregation pipeline

For anything beyond simple filtering, MongoDB uses the aggregation pipeline: an array of stages. Documents flow through the stages in order, and each one transforms what it receives, like Unix pipes. This is the equivalent of GROUP BY, HAVING and much more.

js
db.posts.aggregate([  { $match: { published: true } },  { $group: {      _id: "$author",      posts: { $sum: 1 },      totalViews: { $sum: "$views" },      avgViews: { $avg: "$views" }  } },  { $match: { posts: { $gte: 5 } } },  { $sort: { totalViews: -1 } },  { $project: {      _id: 0,      author: "$_id",      posts: 1,      totalViews: 1,      avgViews: { $round: ["$avgViews", 0] }  } }])

Line by line

  1. aggregate takes an array of stages. Read it from top to bottom: each stage receives the previous stage's output.
  2. $match filters documents, like WHERE. Put it as early as possible so later stages handle fewer documents and it can use an index.
  3. $group merges documents into groups, like GROUP BY.
  4. _id is the group key. "$author", a string starting with $, means "the value of the author field", so there is one group per author.
  5. $sum: 1 adds 1 per document, which counts the documents in the group (COUNT(*)).
  6. $sum: "$views" adds up the field's values (SUM(views)). $avg, $min, $max and $push work the same way.
  7. A second $match, after grouping, filters the groups. This is exactly what HAVING does in SQL.
  8. $sort orders the groups by total views, highest first.
  9. $project reshapes each output document: which fields to keep, rename or compute.
  10. Renames the group key from _id to a readable author field.
  11. $round is an expression operator. Expressions take arguments in an array, here the value and the number of decimal places.

Advanced 2: $unwind and $lookup

js
db.posts.aggregate([  { $unwind: "$tags" },  { $group: { _id: "$tags", count: { $sum: 1 } } },  { $sort: { count: -1 } },  { $limit: 5 }]) db.posts.aggregate([  { $match: { published: true } },  { $lookup: {      from: "comments",      localField: "_id",      foreignField: "postId",      as: "comments"  } },  { $addFields: { commentCount: { $size: "$comments" } } },  { $project: { comments: 0 } }])

Line by line

  1. $unwind turns one document with an array into one document per element. A post with three tags becomes three documents, each with a single tags value.
  2. Now each tag can be grouped and counted like any other field.
  3. Result: the five most-used tags across all posts. In SQL this would need the tags join table.
  4. $lookup is MongoDB's left outer join: it pulls matching documents from another collection into each document.
  5. from names the other collection.
  6. localField is the field in the current documents (the post's _id)...
  7. ...and foreignField is the field in comments that should equal it.
  8. as is the name of the new array field that holds the matches. A post with no comments gets an empty array, just like NULLs in a LEFT JOIN.
  9. $addFields adds a computed field while keeping all the others. $size returns the length of an array.
  10. Then we drop the full comments array, keeping only the count, so less data travels over the network.

Advanced 3: several reports in one pass with $facet

js
db.posts.aggregate([  { $match: { published: true } },  { $facet: {      topPosts: [        { $sort: { views: -1 } },        { $limit: 3 },        { $project: { title: 1, views: 1 } }      ],      byMonth: [        { $group: {            _id: { $dateToString: { format: "%Y-%m", date: "$publishedAt" } },            posts: { $sum: 1 }        } },        { $sort: { _id: 1 } }      ],      totals: [ { $count: "published" } ]  } }])

Line by line

  1. One shared first stage: only published posts go into every report.
  2. $facet runs several sub-pipelines on the same input documents and returns one document with one array per sub-pipeline. This is ideal for dashboards and search pages with filter counts.
  3. Sub-pipeline 1: the three most-viewed posts.
  4. Sub-pipeline 2: posts per month.
  5. $dateToString formats a date as text such as "2026-09", which makes a convenient group key for a month.
  6. Sub-pipeline 3: $count returns a single document holding the number of inputs, for a "showing 1–10 of N" line.

Advanced 4: indexes and explain()

js
db.posts.createIndex({ author: 1, publishedAt: -1 }) db.users.createIndex({ email: 1 }, { unique: true }) db.posts.createIndex({ title: "text", body: "text" })db.posts.find({ $text: { $search: "camping lake" } }) db.posts.find({ author: "Zin" }).sort({ publishedAt: -1 })  .explain("executionStats")

Line by line

  1. A compound index, the same idea as the SQL one: posts by author, newest first. Field order matters here too.
  2. A unique index makes MongoDB refuse duplicate emails, bringing back one of the guarantees SQL gives you from a schema.
  3. A text index across two fields enables simple full-text search.
  4. $text with $search finds documents that contain any of the words, using that index.
  5. A query we want to check...
  6. ...and explain shows how MongoDB ran it. Look for IXSCAN (it used an index) rather than COLLSCAN (it read the whole collection), and compare totalDocsExamined with nReturned.

Advanced 5: multi-document transactions

Updates to a single document are always atomic in MongoDB. When a change must touch several documents at once, use a transaction (on a replica set, which every MongoDB Atlas cluster is). This example uses the Node.js driver.

js
const session = client.startSession();try {  await session.withTransaction(async () => {    const accounts = client.db("bank").collection("accounts");    await accounts.updateOne(      { _id: 1, balance: { $gte: 500 } },      { $inc: { balance: -500 } },      { session }    );    await accounts.updateOne({ _id: 2 }, { $inc: { balance: 500 } }, { session });    await client.db("bank").collection("transfers")      .insertOne({ from: 1, to: 2, amount: 500, at: new Date() }, { session });  });} finally {  await session.endSession();}

Line by line

  1. A session groups operations together, and every transaction belongs to one.
  2. withTransaction starts the transaction, runs the callback, and commits it. If the callback throws, it aborts. It also retries automatically on temporary errors.
  3. The filter only matches if account 1 has enough money, like a CHECK constraint. In real code you would check the result's matchedCount and throw when it is 0, which aborts the transfer.
  4. Passing { session } is what puts this operation inside the transaction. Forget it, and the write happens on its own.
  5. Credit the second account in the same transaction.
  6. The audit record commits or disappears together with the two balance changes: all or nothing, just like SQL's BEGIN ... COMMIT.
  7. Always end the session, even if something failed, to free its resources on the server.

A quick look at Redis (key-value)

Not every NoSQL database is a document store. Redis keeps data in memory as keys pointing at values of a few smart types (strings, hashes, lists, sets, sorted sets), which makes it extremely fast. It usually sits next to a main database rather than replacing it. These commands run in redis-cli.

redis
SET post:7:html "<article>...</article>" EX 3600GET post:7:htmlTTL post:7:html INCR post:7:views HSET session:abc123 userId 42 role adminHGETALL session:abc123 ZADD leaderboard 1520 "aye" 980 "kokko" 2210 "zin"ZREVRANGE leaderboard 0 2 WITHSCORES DEL post:7:html

Line by line

  1. SET stores a string under a key. The colon-separated name (post:7:html) is only a naming convention. EX 3600 makes the key expire after an hour, which is the heart of a cache.
  2. GET reads it back. If the key is missing or expired, you get nil, and the app falls back to the main database and fills the cache again.
  3. TTL shows how many seconds the key has left to live.
  4. INCR adds 1 atomically and returns the new value, even with thousands of requests at once. This is perfect for view counters and rate limits.
  5. A hash stores several fields under one key, like a tiny document. It is a common way to keep session data.
  6. HGETALL returns every field and value in the hash.
  7. A sorted set keeps members ordered by a score, here the players' points.
  8. ZREVRANGE ... 0 2 returns the top three, highest score first, together with their scores. A live leaderboard costs one command.
  9. DEL removes a key, for example to clear the cache right after the post is edited.

Cheat sheet: the same jobs in SQL and MongoDB

Six everyday tasks, written first in SQL and then in MongoDB. To keep them short, both sides use a simplified posts table that stores the author's name directly. The numbers in the comments line up.

sql
-- 1. Read with a filterSELECT * FROM posts WHERE author = 'Zin' AND views > 100; -- 2. Sort and paginate (page 3, 10 per page)SELECT * FROM posts ORDER BY published_at DESC LIMIT 10 OFFSET 20; -- 3. InsertINSERT INTO posts (title, author, views) VALUES ('Hello', 'Zin', 0); -- 4. UpdateUPDATE posts SET views = views + 1 WHERE id = 7; -- 5. Group and countSELECT author, COUNT(*) FROM posts GROUP BY author HAVING COUNT(*) > 2; -- 6. JoinSELECT p.title, c.body FROM posts p LEFT JOIN comments c ON c.post_id = p.id;
js
// 1. Read with a filterdb.posts.find({ author: "Zin", views: { $gt: 100 } }) // 2. Sort and paginate (page 3, 10 per page)db.posts.find().sort({ publishedAt: -1 }).skip(20).limit(10) // 3. Insertdb.posts.insertOne({ title: "Hello", author: "Zin", views: 0 }) // 4. Updatedb.posts.updateOne({ _id: 7 }, { $inc: { views: 1 } }) // 5. Group and countdb.posts.aggregate([{ $group: { _id: "$author", n: { $sum: 1 } } }, { $match: { n: { $gt: 2 } } }]) // 6. Joindb.posts.aggregate([{ $lookup: { from: "comments", localField: "_id", foreignField: "postId", as: "comments" } }])

Wrapping up

SQL and NoSQL are not rivals so much as different answers to "what shape is my data, and how will I read it?". Relational databases give you structure, joins and strong guarantees, and they remain the best default for most business data. Document stores like MongoDB give you flexibility and data shaped like your code. Key-value stores like Redis give you speed for the hot paths. Wide-column and graph databases solve the problems they were built for.

Learn SQL properly first. It is everywhere, it has barely changed in decades, and it teaches you to think about data. Then learn one NoSQL database by building something with it, paying attention to how you model your documents. The queries above are a map, and the best way to learn them is to open psql or mongosh and type them yourself.

App တစ်ခု ရေးတိုင်း မကြာခင် မေးရမယ့် မေးခွန်းတစ်ခု ရှိပါတယ် — data ကို ဘယ်မှာ ထားမလဲ? အဖြေ အုပ်စုကြီး နှစ်ခုက PostgreSQL၊ MySQL လို SQL (relational) database တွေနဲ့ MongoDB၊ Redis လို NoSQL database တွေပါ။ လူတွေက တစ်ခုခု နိုင်ရမယ့်ပုံ ငြင်းကြပေမဲ့ လက်တွေ့မှာ ပုံစံ မတူတဲ့ ကိရိယာတွေသာ ဖြစ်ပြီး တကယ့် system အများစုက တစ်ခုထက်ပိုပြီး သုံးကြပါတယ်။

ဒီ entry မှာ တစ်ခုချင်းစီ ဘာလဲ၊ ဘယ်နေရာမှာ ထူးချွန်ပြီး ဘယ်နေရာမှာ နာကျင်စေလဲ ကြည့်မယ်။ ပြီးရင် နှိုင်းယှဉ်ချက် အများစု ကျော်သွားတတ်တဲ့ အပိုင်း — နှစ်မျိုးလုံးမှာ query တကယ်ရေးတာကို beginner အနေနဲ့ ပထမဆုံး ရိုက်မယ့် စာကြောင်းကနေ production မှာ တွေ့ရမယ့် pattern တွေအထိ လေ့လာပါမယ်။ Code block တိုင်းမှာ စာကြောင်းတွေအတွက် မှတ်စု ပါပါတယ်။ စာကြောင်းတစ်ကြောင်းကို နှိပ်ပြီး (ဒါမှမဟုတ် hover လုပ်ပြီး) ဘာလုပ်လဲ ဖတ်ကြည့်ပါ။

SQL ဆိုတာ ဘာလဲ?

SQL (Structured Query Language) က relational database နဲ့ စကားပြောတဲ့ ဘာသာစကားပါ။ Relational database က data ကို table တွေထဲ သိမ်းပါတယ်။ Table တစ်ခုစီမှာ type (ဂဏန်း၊ စာသား၊ ရက်စွဲ) သတ်မှတ်ထားတဲ့ column အစုံ ရှိပြီး row တစ်ခုစီက အဲဒီ column တွေနဲ့ ကိုက်တဲ့ record တစ်ခုပါ။ Table နဲ့ column အစုအဝေးကို schema လို့ ခေါ်ပြီး ဘာမှ မသိမ်းခင် ကြိုသတ်မှတ်ရပါတယ်။

Table တွေကို key တွေနဲ့ ချိတ်ဆက်ပါတယ်။ Primary key က users.id လို row တစ်ခုချင်းကို တစ်မူထူးခြားစွာ ခွဲခြားပေးပါတယ်။ Foreign key က posts.user_id က users.id ကို ညွှန်သလို တခြား table ရဲ့ primary key ကို ညွှန်တဲ့ column ပါ။ Post တိုင်းထဲ user နာမည်ကို ကူးထည့်မယ့်အစား တစ်နေရာတည်းမှာ သိမ်းပြီး ဖတ်တဲ့အခါ table တွေကို join လုပ်ပါတယ်။ အချက်အလက် တစ်ခုကို နေရာတစ်ခုတည်းမှာ ထားတာကို normalisation လို့ ခေါ်ပါတယ်။

Relational database တွေက ACID အာမခံချက် ပါတဲ့ transaction တွေကို အခြေခံ တည်ဆောက်ထားပါတယ်—

  • Atomicity: အပြောင်းအလဲ တစ်စုက အကုန်ဖြစ်မယ်၊ ဒါမှမဟုတ် လုံးဝ မဖြစ်ဘူး။
  • Consistency: data က သူ့ rule တွေ (type၊ key၊ constraint) ကို အမြဲ လိုက်နာတယ်။
  • Isolation: တစ်ပြိုင်နက် run နေတဲ့ transaction တွေက တစ်ခုနဲ့တစ်ခု တစ်ဝက်တစ်ပျက် အလုပ်ကို မမြင်ရဘူး။
  • Durability: database က "commit ပြီးပြီ" လို့ ပြောပြီးရင် server ပျက်သွားလည်း data ကျန်ရစ်တယ်။

ဒီ entry တစ်လျှောက်လုံး သုံးမယ့် schema က user၊ post နဲ့ comment ပါတဲ့ blog အသေးလေးပါ၊ PostgreSQL အတွက် ရေးထားပါတယ်—

sql
CREATE TABLE users (  id         SERIAL PRIMARY KEY,  name       TEXT NOT NULL,  email      TEXT NOT NULL UNIQUE,  country    TEXT,  created_at TIMESTAMPTZ NOT NULL DEFAULT now()); CREATE TABLE posts (  id           SERIAL PRIMARY KEY,  user_id      INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,  title        TEXT NOT NULL,  body         TEXT NOT NULL,  views        INT NOT NULL DEFAULT 0 CHECK (views >= 0),  published_at TIMESTAMPTZ); CREATE TABLE comments (  id         SERIAL PRIMARY KEY,  post_id    INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,  user_id    INT NOT NULL REFERENCES users(id),  parent_id  INT REFERENCES comments(id),  body       TEXT NOT NULL,  created_at TIMESTAMPTZ NOT NULL DEFAULT now());

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. CREATE TABLE က table အသစ်တစ်ခုနဲ့ သူ ထိန်းထားမယ့် column အားလုံးကို သတ်မှတ်ပါတယ်။ ဒီပုံစံနဲ့ မကိုက်တဲ့ row တွေကို ငြင်းပယ်ပါတယ်။
  2. SERIAL က အလိုလို တိုးသွားတဲ့ integer ပါ၊ user အသစ်တိုင်း နောက်ဂဏန်းကို ရပါတယ်။ PRIMARY KEY က တစ်မူထူးခြားစေပြီး index လုပ်ပေးပါတယ်။
  3. NOT NULL ဆိုတာ user တိုင်းမှာ နာမည် ရှိရမယ်။ မထည့်ရင် error ဖြစ်ပါတယ်၊ တန်ဖိုးအလွတ် မဟုတ်ပါဘူး။
  4. UNIQUE က email တူတဲ့ ဒုတိယ account ကို request နှစ်ခု တစ်ချိန်တည်း ရောက်လာရင်တောင် database ကိုယ်တိုင် ငြင်းပယ်ပေးပါတယ်။
  5. ဒီမှာ NOT NULL မပါလို့ country က မထည့်လည်း ရပြီး NULL (မသိ) ဖြစ်နိုင်ပါတယ်။
  6. TIMESTAMPTZ က time zone ပါတဲ့ အချိန်ပါ။ DEFAULT now() က row ထည့်တဲ့အခါ အလိုလို ဖြည့်ပေးပါတယ်။
  7. Post တစ်ခုစီက user တစ်ယောက်ပိုင်ပါတယ်။ One-to-many relationship ရဲ့ "many" ဘက်ပါ။
  8. REFERENCES users(id) က foreign key ပါ — မရှိတဲ့ user ကို post က ညွှန်လို့ မရပါဘူး။ ON DELETE CASCADE က user ကို ဖျက်ရင် သူ့ post တွေကိုပါ ဖျက်ပေးပါတယ်။
  9. CHECK constraint က write တိုင်းမှာ database က စစ်ပေးတဲ့ rule ပါ။ ဒီမှာ view အရေအတွက် ဘယ်တော့မှ အနုတ် မဖြစ်နိုင်ပါဘူး။
  10. ရည်ရွယ်ချက်ရှိရှိ null ဖြစ်ခွင့် ပေးထားတာပါ — published_at မရှိတဲ့ post က draft သာ ဖြစ်ပါသေးတယ်။
  11. Comment တွေက post တစ်ခုနဲ့ ရေးတဲ့ user ကို ညွှန်လို့ comment row တစ်ခုက table သုံးခုကို ချိတ်ဆက်ထားပါတယ်။
  12. parent_id က table ကိုယ်တိုင်ကို ပြန်ညွှန်ပါတယ်။ Reply တစ်ခုက သူ ပြန်ဖြေတဲ့ comment ရဲ့ id ကို သိမ်းတာမို့ ပြားနေတဲ့ row တွေက tree ဖြစ်လာပါတယ်။ အဲဒီ tree ကို နောက်ပိုင်းမှာ recursive query နဲ့ လျှောက်ကြည့်ပါမယ်။

NoSQL ဆိုတာ ဘာလဲ?

NoSQL ("not only SQL") က data ကို relational table အနေနဲ့ မသိမ်းတဲ့ database တွေအတွက် ထီးကြီးနာမည်ပါ။ ၂၀၀၀ ပြည့်နှစ်များ နှောင်းပိုင်းမှာ Google၊ Amazon၊ Facebook လို ကုမ္ပဏီတွေ data အလွန်များများကို ဈေးသက်သာတဲ့ စက်အများကြီးပေါ် ဖြန့်ဖို့ လိုလာပြီး တင်းကျပ်တဲ့ schema နဲ့ join တွေက အဲဒီလို scale လုပ်ရ ခက်တာကို တွေ့ကြရာက ပေါ်လာတာပါ။ NoSQL ဘာသာစကား တစ်ခုတည်း မရှိပါဘူး။ အုပ်စုတစ်ခုစီမှာ ကိုယ်ပိုင် model နဲ့ API ရှိပါတယ်။

  • Document store (MongoDB၊ Couchbase၊ Firestore) တွေက JSON နဲ့တူတဲ့ document တွေကို သိမ်းပါတယ်။ Document တစ်ခုထဲမှာ nested object နဲ့ array တွေ ထည့်လို့ ရလို့ post တစ်ခုက သူ့ tag နဲ့ comment တွေကို အထဲမှာ သယ်ထားနိုင်ပါတယ်။
  • Key-value store (Redis၊ DynamoDB၊ Memcached) တွေက အဘိဓာန်ကြီး တစ်ခုလိုပါပဲ — key ပေးရင် value ရပါတယ်။ အလွန် မြန်ပြီး cache၊ session နဲ့ counter တွေအတွက် အများဆုံး သုံးပါတယ်။
  • Wide-column store (Cassandra၊ HBase၊ ScyllaDB) တွေက column မတူနိုင်တဲ့ row တွေကို server အများကြီးပေါ် ဖြန့်သိမ်းပါတယ်။ Log၊ metric၊ sensor data လို write အလွန်များတဲ့ အလုပ်အတွက် တည်ဆောက်ထားတာပါ။
  • Graph database (Neo4j၊ Amazon Neptune) တွေက node တွေနဲ့ သူတို့ကြား relationship တွေကို ပထမတန်းစား data အဖြစ် သိမ်းပါတယ်။ Social network၊ recommendation နဲ့ လိမ်လည်မှု ရှာဖွေရေးအတွက် အကောင်းဆုံးပါ။

NoSQL database အများစုက schema ပြောင်းလွယ် ပါတယ် — collection တစ်ခုတည်းထဲက document နှစ်ခုမှာ field မတူတာ ရှိနိုင်ပြီး ပုံစံကို fixed table သတ်မှတ်ချက်က မဟုတ်ဘဲ သင့် application (ဒါမှမဟုတ် ရွေးချယ်နိုင်တဲ့ validation rule) က စစ်ပါတယ်။ အများစုကို horizontal scale လုပ်ဖို့ ဒီဇိုင်းထုတ်ထားပါတယ် — server ပိုကြီးကြီး တစ်လုံး ဝယ်မယ့်အစား server တွေ ထပ်ထည့်ပြီး data ကို သူတို့ကြား ခွဲ (shard) လိုက်တာပါ။

Distributed NoSQL system တွေက သူတို့ အာမခံချက်ကို BASE (Basically Available, Soft state, Eventually consistent) လို့ မကြာခဏ ဖော်ပြပါတယ် — system က အမြဲ အလုပ်လုပ်နေပြီး data မိတ္တူတွေက ချက်ချင်း မဟုတ်ဘဲ နောက်ဆုံးမှာ တူညီသွားပါတယ်။ ဒါက အားနည်းချက် မဟုတ်ဘဲ အလဲအလှယ် (trade-off) တစ်ခုပါ၊ ခေတ်ပေါ် MongoDB က document အများကြီးကို ဖြတ်တဲ့ ACID transaction ကိုတောင် ထောက်ပံ့ပါတယ်။

ဒီမှာ blog တစ်ခုလုံးကို MongoDB document တစ်ခုတည်းအဖြစ် ပြထားပါတယ်။ အတူတူ ဖတ်တဲ့ data တွေက အတူတူ နေတာကို သတိထားကြည့်ပါ—

js
{  _id: ObjectId("66f1c0a2e4b0a1b2c3d4e5f6"),  title: "Camping at Inle Lake",  author: { _id: ObjectId("66f1bf00e4b0a1b2c3d4e001"), name: "Zin" },  tags: ["travel", "camping"],  views: 42,  publishedAt: ISODate("2026-09-01T08:00:00Z"),  comments: [    { user: "Aye", body: "Beautiful photos!", likes: 6, at: ISODate("2026-09-02T10:00:00Z") },    { user: "Ko Ko", body: "Which month did you go?", likes: 2, at: ISODate("2026-09-03T12:30:00Z") }  ]}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Document တိုင်းမှာ _id ရှိပါတယ်။ MongoDB က ObjectId ကို အလိုလို ထုတ်ပေးပါတယ် — timestamp ပါတဲ့ 12 byte ဖြစ်လို့ id တွေက ဖန်တီးတဲ့ အချိန်အလိုက် အကြမ်းဖျင်း စီပြီးသား ဖြစ်ပါတယ်။
  2. Author ကို sub-document အသေးလေးအဖြစ် embed လုပ်ထားပါတယ်။ ဒုတိယ lookup မလုပ်ဘဲ post ကို ပြနိုင်ဖို့ နာမည် မိတ္တူကို ထားတာပါ။ အထဲက _id နဲ့ လိုတဲ့အခါ user အပြည့်အစုံကို ရှာလို့ ရပါသေးတယ်။
  3. Array တွေက ပုံမှန် value တွေပါပဲ။ SQL မှာဆိုရင် tag တွေအတွက် tags table သီးသန့်နဲ့ post_tags join table လိုပါလိမ့်မယ်။
  4. ISODate က shell မှာ string မဟုတ်ဘဲ စီလို့၊ နှိုင်းယှဉ်လို့ ရတဲ့ ရက်စွဲ type အစစ်ကို ရေးတဲ့ နည်းပါ။
  5. Comment တွေက post ထဲမှာ နေပါတယ်။ Post ကို comment တွေနဲ့ ဖတ်တာက join မပါဘဲ document တစ်ခုကို တစ်ခါ ဖတ်ရုံပါပဲ။

SQL ကို ဘယ်အချိန် သုံးမလဲ၊ NoSQL ကို ဘယ်အချိန် သုံးမလဲ?

ရိုးသားတဲ့ အဖြေက ခေတ်စားနေတာကနေ မဟုတ်ဘဲ သင့် data နဲ့ ဘယ်လို ဖတ်မလဲကနေ စပါတယ်။ ဒီမှာ တစ်ဖက်စီရဲ့ အားသာချက်နဲ့ အားနည်းချက်တွေပါ။

SQL: အားသာချက်များ

  • ခိုင်မာတဲ့ consistency နဲ့ ACID transaction။ User အများကြီး တစ်ပြိုင်နက် write လုပ်ရင်တောင် ငွေ၊ stock အရေအတွက်နဲ့ booking တွေ မှန်ကန်နေပါတယ်။
  • Join နဲ့ ကြိုမစီစဉ်ထားတဲ့ မေးခွန်းများ။ ("မေလမှာ စာရင်းသွင်းတဲ့ user တွေရဲ့ နိုင်ငံအလိုက် ဝင်ငွေ") လို မထင်ထားတဲ့ မေးခွန်းတွေကို data သိမ်းပုံ မပြောင်းဘဲ မေးလို့ ရပါတယ်။
  • Schema က သင့်ကို ကာကွယ်ပေးတယ်။ Type၊ foreign key၊ UNIQUE နဲ့ CHECK constraint တွေက data မှားတွေကို တံခါးဝမှာတင် ဖမ်းပေးပါတယ်။
  • ရင့်ကျက်ပြီး စံသတ်မှတ်ထားတဲ့ ဘာသာစကား တစ်ခုတည်း။ SQL ကျွမ်းကျင်မှုက PostgreSQL၊ MySQL၊ SQLite၊ SQL Server နဲ့ analytics tool အများအပြားကြား ရွှေ့သုံးလို့ ရပါတယ်။
  • ဆယ်စုနှစ်ချီ tooling — query planner၊ backup၊ replication၊ reporting နဲ့ BI tool တွေ။

SQL: အားနည်းချက်များ

  • Schema ပြောင်းရင် migration လိုတယ်။ အလုပ်များနေတဲ့ table ကြီးမှာ column ထည့်တာ၊ ပုံစံပြောင်းတာကို ကြိုစီစဉ်ရပါတယ်။
  • Horizontal scale လုပ်ရ ပိုခက်တယ်။ Write တွေကို server အများကြီးကြား ဖြန့်တာ (sharding) ဖြစ်နိုင်ပေမဲ့ built-in ပါလေ့ မရှိပါဘူး။
  • Object–relational မကိုက်ညီမှု။ Code ထဲက nested object တွေကို table တွေအဖြစ် ခွဲပြီး ပြန် join ရပါတယ်၊ များသောအားဖြင့် ORM တစ်ခုကနေ။
  • Table အကြီးကြီးတွေကို join တာ index မှန်မှန် မရှိရင် ကုန်ကျစရိတ် များနိုင်ပါတယ်။

NoSQL: အားသာချက်များ

  • Schema ပြောင်းလွယ်တယ်။ Product ပြောင်းလဲလာသလို field တွေ ထပ်ထည့်နိုင်ပြီး ပုံစံမတူတဲ့ record တွေကို ဘေးချင်းကပ် သိမ်းနိုင်ပါတယ်။
  • Code နဲ့ ပုံစံတူ data။ Document က JSON object နဲ့ တိုက်ရိုက် ကိုက်လို့ ကြားက ဘာသာပြန် အလွှာ နည်းပါတယ်။
  • Scale out လုပ်ဖို့ တည်ဆောက်ထားတယ်။ Sharding နဲ့ replication က ဒီဇိုင်းရဲ့ အစိတ်အပိုင်း ဖြစ်လို့ အလွန်ကြီးတဲ့ ဒါမှမဟုတ် write အလွန်များတဲ့ workload တွေနဲ့ ကိုက်ပါတယ်။
  • သိပြီးသား access pattern တွေအတွက် မြန်တယ်။ လိုတာအားလုံး ပါတဲ့ document တစ်ခု ဒါမှမဟုတ် key တစ်ခုကို ဖတ်တာ အလွန်မြန်ပါတယ်။
  • အလုပ်တစ်ခုစီအတွက် အထူးပြု model — မြန်နှုန်းအတွက် Redis၊ write ပမာဏအတွက် Cassandra၊ relationship အတွက် Neo4j။

NoSQL: အားနည်းချက်များ

  • Data မှန်ကန်မှုက အများအားဖြင့် သင့်တာဝန်။ တင်းကျပ်တဲ့ schema မရှိရင် validation မထည့်ထားသရွေ့ စာလုံးပေါင်းမှားတာနဲ့ ပုံစံ တစ်ဝက်တစ်ပျက် record တွေ ဝင်လာနိုင်ပါတယ်။
  • ထပ်နေတဲ့ data။ Embed လုပ်ရင် (author နာမည်လို) data ကို ကူးရပြီး မိတ္တူတိုင်းကို update လုပ်တာ သင့်တာဝန်ပါ။
  • အားနည်းတဲ့ ဒါမှမဟုတ် ကုန်ကျစရိတ်များတဲ့ join နဲ့ transaction။ MongoDB မှာ ($lookup၊ multi-document transaction) ရှိပေမဲ့ သဘာဝ လမ်းကြောင်း မဟုတ်ပါဘူး။
  • Distributed setup အများစုမှာ eventual consistency ကြောင့် ဖတ်တဲ့အခါ data အဟောင်းကို ခဏ ပြန်ရနိုင်ပါတယ်။
  • စံ ဘာသာစကား တစ်ခုတည်း မရှိဘူး။ MongoDB၊ Redis နဲ့ Cassandra query တွေ အကုန် မတူကြပါဘူး။

အမြန် ဆုံးဖြတ်ချက် လမ်းညွှန်

  • SQL ကို ရွေးပါ — သင့် data က အပြန်အလှန် ဆက်စပ်မှု များတဲ့အခါ (user၊ order၊ invoice)၊ မြန်နှုန်းထက် မှန်ကန်မှုက ပိုအရေးကြီးတဲ့အခါ၊ reporting နဲ့ ad-hoc query လိုတဲ့အခါ ဒါမှမဟုတ် မသေချာသေးတဲ့အခါ။ PostgreSQL က အလွန် စိတ်ချရတဲ့ default ပါ။
  • Document store ကို ရွေးပါ — record တွေက သဘာဝအတိုင်း ကိုယ့်ဘာသာ ပြည့်စုံနေတဲ့အခါ (ဆောင်းပါး၊ product catalogue၊ user profile၊ CMS content)၊ ပုံစံ ကွဲပြားတာ ဒါမှမဟုတ် မကြာခဏ ပြောင်းတာ ရှိတဲ့အခါ၊ record တစ်ခုလုံးကို တစ်ခါမှာ တစ်ခု ဖတ်တာ များတဲ့အခါ။
  • Key-value store ကို ရွေးပါ — cache၊ session၊ rate limit၊ counter၊ queue ဒါမှမဟုတ် leaderboard လိုတဲ့အခါ၊ များသောအားဖြင့် main database ဘေးမှာ။
  • Wide-column ဒါမှမဟုတ် graph ကို ရွေးပါ — write stream အကြီးကြီးတွေ (time series၊ log) ရှိတဲ့အခါ ဒါမှမဟုတ် relationship ကိုယ်တိုင်က မေးခွန်း ဖြစ်တဲ့အခါ (သူငယ်ချင်းရဲ့ သူငယ်ချင်း၊ recommendation)။

ပြီးတော့ တစ်ခုတည်းကိုသာ ရွေးစရာ မလိုပါဘူး။ ဒီ journal ကိုယ်တိုင်က MongoDB ပေါ်မှာ run ပါတယ်၊ ဘာလို့လဲဆိုတော့ entry တစ်ခုက ပုံစံမတူတဲ့ block တွေ ပြည့်နေတဲ့ ကိုယ့်ဘာသာ ပြည့်စုံတဲ့ document ဖြစ်လို့ပါ။ ဘေးက ဆိုင်တစ်ခုဆိုရင် order နဲ့ payment တွေကို PostgreSQL မှာ ထားမှာ ဖြစ်ပြီး နှစ်ခုလုံးက Redis ကို cache အဖြစ် သုံးနိုင်ပါတယ်။ ဒါကို polyglot persistence လို့ ခေါ်ပါတယ် — အလုပ်တစ်ခုစီအတွက် သင့်တော်တဲ့ store။

SQL query များ: beginner ကနေ advanced အထိ

အောက်က ဥပမာ အားလုံးက အစောပိုင်းက users၊ posts နဲ့ comments table တွေကို PostgreSQL မှာ သုံးပါတယ်။ အများစုက MySQL နဲ့ SQLite မှာလည်း အတူတူ အလုပ်လုပ်ပါတယ်။ အဆင့်တစ်ခုစီက အရင်အဆင့်ပေါ် တည်ဆောက်ပါတယ်။

Beginner ၁: SELECT နဲ့ row တွေ ဖတ်ခြင်း

အရာအားလုံး SELECT ကနေ စပါတယ် — ဘယ် column တွေ၊ ဘယ် table က၊ ဘယ် row တွေ၊ ဘယ်အစီအစဉ်နဲ့၊ ဘယ်နှခု။

sql
SELECT id, title, viewsFROM postsWHERE published_at IS NOT NULL  AND views >= 100ORDER BY views DESCLIMIT 10;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. ပြန်လိုချင်တဲ့ column တွေကို နာမည်တပ်ပါ။ SELECT * က column အားလုံးကို ပြန်ပေးပါတယ်၊ စူးစမ်းနေတုန်း အဆင်ပြေပေမဲ့ တကယ့် code မှာ ဖြုန်းတီးရာ ရောက်ပါတယ်။
  2. FROM က ဘယ် table ကို ဖတ်မလဲ ပြောပါတယ်။
  3. WHERE က row တွေကို စစ်ထုတ်ပါတယ်။ NULL ဆိုတာ "တန်ဖိုး မရှိ" ဖြစ်ပြီး IS NULL ဒါမှမဟုတ် IS NOT NULL နဲ့သာ စစ်လို့ ရပါတယ်၊ = NULL နဲ့ ဘယ်တော့မှ မရပါဘူး။
  4. AND က condition တွေကို ပေါင်းပါတယ်။ Row တစ်ခု ကျန်ဖို့ နှစ်ခုလုံး မှန်ရပါမယ်။
  5. ORDER BY ... DESC က အများဆုံးကနေ အနည်းဆုံးကို စီပါတယ်။ ORDER BY မပါရင် SQL က ဘာအစီအစဉ်မှ အာမမခံပါဘူး။
  6. LIMIT က စီပြီးသား ရလဒ်ရဲ့ ပထမ row ၁၀ ခုကိုသာ ယူပါတယ် — "top 10 posts" စာရင်းပါ။

Beginner ၂: pattern၊ list နဲ့ range တွေနဲ့ စစ်ထုတ်ခြင်း

sql
SELECT name, email, countryFROM usersWHERE country IN ('Myanmar', 'Thailand')  AND email LIKE '%@gmail.com'  AND created_at BETWEEN '2026-01-01' AND '2026-06-30'ORDER BY name;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. IN (...) က list ထဲက တန်ဖိုး တစ်ခုခုနဲ့ ကိုက်ပါတယ်။ country = 'Myanmar' OR country = 'Thailand' ကို တိုတိုရေးတဲ့ နည်းပါ။
  2. LIKE က pattern နဲ့ ကိုက်ပါတယ် — % က "ဘယ် စာလုံးမဆို" ဆိုတဲ့ အဓိပ္ပာယ်ပါ။ ဒါကြောင့် Gmail လိပ်စာ အားလုံးကို ရှာပေးပါတယ်။ PostgreSQL ရဲ့ ILIKE က စာလုံးအကြီးအသေး ဂရုမစိုက်ဘဲ အတူတူ လုပ်ပါတယ်။
  3. BETWEEN က range ရဲ့ အစွန်း နှစ်ဖက်လုံး ပါပါတယ်။ '2026-01-01' လို စာသားကို timestamp အဖြစ် အလိုလို ပြောင်းပေးပါတယ်။
  4. အက္ခရာစဉ်အတိုင်း စီပါတယ် (ascending က default ပါ)။

Beginner ၃: ထည့်ခြင်း၊ ပြင်ခြင်းနဲ့ ဖျက်ခြင်း

sql
INSERT INTO users (name, email, country)VALUES ('Zin', 'zin@example.com', 'Myanmar')RETURNING id, created_at; UPDATE postsSET views = views + 1WHERE id = 7; DELETE FROM commentsWHERE created_at < now() - INTERVAL '2 years';

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. INSERT INTO က row တစ်ခု ထည့်ပါတယ်။ တန်ဖိုးပေးမယ့် column တွေကိုသာ စာရင်းပြုထားပါတယ် — id နဲ့ created_at က ကိုယ့်ဘာသာ ဖြည့်သွားပါတယ်။
  2. VALUES က column စာရင်းနဲ့ အစီအစဉ်တူ တန်ဖိုးတွေ ပေးပါတယ်။ စာသားကို single quote ထဲ ထည့်ပါတယ်။
  3. RETURNING (PostgreSQL) က ထုတ်ပေးလိုက်တဲ့ တန်ဖိုးတွေကို ပြန်ပေးလို့ app က ဒုတိယ query မလိုဘဲ id အသစ်ကို သိပါတယ်။
  4. UPDATE က table ထဲက ရှိပြီးသား row တွေကို ပြောင်းပါတယ်။
  5. SET က တန်ဖိုးအသစ်တွေ ပေးပါတယ်။ တန်ဖိုးဟောင်းကို သုံးလို့ရလို့ ဒါက atomic "တစ်ပေါင်း" ပါ၊ ဧည့်သည် အများကြီး တစ်ပြိုင်နက် ရှိရင်တောင် စိတ်ချရပါတယ်။
  6. UPDATE မှာ WHERE ကို အမြဲ စစ်ပါ။ မပါရင် table ထဲက post အားလုံး ပြောင်းသွားပါလိမ့်မယ်။
  7. DELETE FROM က row တွေကို ဖယ်ရှားပါတယ်။ UPDATE လိုပဲ မစစ်ထုတ်ရင် row အားလုံးကို သက်ရောက်ပါတယ်။
  8. ရက်စွဲ တွက်ချက်ခြင်း — "အခု၊ နှစ်နှစ် နုတ်"။ Comment အဟောင်းတွေကိုသာ ဖျက်ပါတယ်။

Intermediate ၁: JOIN များ

Join က condition ကိုက်တဲ့ နေရာမှာ table နှစ်ခုက row တွေကို ပေါင်းပါတယ်၊ များသောအားဖြင့် foreign key က primary key နဲ့ ညီတဲ့နေရာ။

sql
SELECT p.title, u.name AS authorFROM posts AS pINNER JOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULL; SELECT u.name, p.titleFROM users AS uLEFT JOIN posts AS p ON p.user_id = u.idWHERE p.id IS NULL;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Column တွေက table နှစ်ခုထဲက ဘယ်ဟာကမဆို လာနိုင်ပါတယ်။ AS author က ရလဒ်ထဲက column နာမည်ကို ပြောင်းပါတယ်။
  2. AS p က table ကို alias အတိုလေး ပေးလို့ posts.title အစား p.title လို့ ရေးလို့ ရပါတယ်။
  3. INNER JOIN က ON condition မှန်တဲ့ အတွဲတွေကိုသာ ထားပါတယ် — post တစ်ခုစီနဲ့ သူ့ author။ User ဖျက်ခံရပြီးသား post ဆိုရင် မပေါ်ပါဘူး။
  4. ဒုတိယ query က မေးခွန်းဟောင်းတစ်ခုကို ဖြေပါတယ် — ဘယ် user တွေ post တစ်ခါမှ မရေးဖူးလဲ?
  5. LEFT JOIN က ဘယ်ဘက် table (users) က row အားလုံး ကို ထားပါတယ်။ User မှာ post မရှိရင် post column တွေကို NULL နဲ့ ဖြည့်ပါတယ်။
  6. Post ဘက်က NULL ဖြစ်တဲ့ row တွေကိုသာ ထားရင် post မရှိတဲ့ user တွေ အတိအကျ ကျန်ပါတယ်။ ဒီ "anti-join" pattern ကို မှတ်ထားသင့်ပါတယ်။

Intermediate ၂: GROUP BY နဲ့ HAVING ဖြင့် aggregation

sql
SELECT u.country,       COUNT(*)            AS posts,       SUM(p.views)        AS total_views,       ROUND(AVG(p.views)) AS avg_viewsFROM posts AS pJOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULLGROUP BY u.countryHAVING COUNT(*) >= 5ORDER BY total_views DESC;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Group လုပ်မယ့် column ပါ။ နိုင်ငံ တစ်ခုစီက ရလဒ်ထဲမှာ row တစ်ခု ဖြစ်လာပါတယ်။
  2. COUNT(*) က group တစ်ခုစီထဲက row တွေကို ရေတွက်ပါတယ်၊ ဒီမှာတော့ post အရေအတွက်ပါ။
  3. SUM၊ AVG၊ MIN နဲ့ MAX တွေက aggregate function တွေပါ — row အများကြီးကို group တစ်ခုလျှင် တန်ဖိုး တစ်ခုအဖြစ် ချုံ့ပါတယ်။
  4. ရိုးရိုး JOIN ဆိုတာ INNER JOIN ပါ။
  5. WHERE က group မလုပ်ခင် row တစ်ခုချင်းကို အရင် စစ်ထုတ်ပါတယ်။
  6. GROUP BY က နိုင်ငံ တူတဲ့ row တွေကို စုပါတယ်။ ရွေးထားတဲ့ column တိုင်း group လုပ်ထားရမယ် ဒါမှမဟုတ် aggregate ထဲမှာ ရှိရမယ်။
  7. HAVING က aggregation ပြီးမှ group တွေကို စစ်ထုတ်ပါတယ်၊ WHERE လုပ်လို့ မရတဲ့ အရာပါ။ Post အနည်းဆုံး ငါးခု ရှိတဲ့ နိုင်ငံတွေကိုသာ ထားပါတယ်။

Intermediate ၃: subquery များ

sql
SELECT title, viewsFROM postsWHERE views > (SELECT AVG(views) FROM posts); SELECT nameFROM users AS uWHERE EXISTS (  SELECT 1 FROM comments AS c  WHERE c.user_id = u.id);

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Scalar subquery က တန်ဖိုး တစ်ခုတည်း ပြန်ပေးပါတယ်၊ ဒီမှာ ပျမ်းမျှ view ကို အပြင်က query က နှိုင်းယှဉ်ပါတယ်။ ရလဒ်က ပျမ်းမျှထက် များတဲ့ post တွေပါ။
  2. EXISTS က အထဲက query က row အနည်းဆုံး တစ်ခု ပြန်ပေးရင် မှန်ပါတယ်။ ဒီစာကြောင်းတွေက အနည်းဆုံး တစ်ခါ comment ပေးဖူးတဲ့ user တွေကို ရှာပါတယ်။
  3. Row ထဲမှာ ဘာရှိလဲ မဟုတ်ဘဲ row ရှိ မရှိ ကိုသာ ဂရုစိုက်လို့ SELECT 1 လို့ ရေးတာပါ။
  4. ဒါက correlated subquery ပါ — အပြင် query က u.id ကို ရည်ညွှန်လို့ user တစ်ယောက်ချင်းအလိုက် တွက်ပါတယ်။ Planner က များသောအားဖြင့် ထိရောက်တဲ့ join အဖြစ် ပြောင်းပေးပါတယ်။

Advanced ၁: WITH နဲ့ CTE များ

Common table expression (CTE) က subquery ကို နာမည်ပေးလို့ query ရှည်ကြီးကို အဆင့်ငယ်လေးတွေလို အပေါ်ကနေ အောက်ကို ဖတ်လို့ ရစေပါတယ်။

sql
WITH monthly AS (  SELECT user_id,         date_trunc('month', published_at) AS month,         COUNT(*) AS posts  FROM posts  WHERE published_at IS NOT NULL  GROUP BY user_id, month),active AS (  SELECT user_id FROM monthly  GROUP BY user_id  HAVING COUNT(*) >= 3)SELECT u.name, m.month, m.postsFROM monthly AS mJOIN active AS a USING (user_id)JOIN users AS u ON u.id = m.user_idORDER BY u.name, m.month;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. WITH monthly AS (...) က ယာယီ နာမည်ပေးထားတဲ့ ရလဒ်ကို သတ်မှတ်ပါတယ် — user တစ်ယောက်ချင်း၊ လတစ်လချင်းအလိုက် post အရေအတွက်။ ဒီ query တစ်ခုအတွက်သာ ရှိပါတယ်။
  2. date_trunc က timestamp ကို သူ့လရဲ့ ပထမဆုံး အချိန်အထိ ဖြတ်ချလို့ စက်တင်ဘာလထဲက post အားလုံး group တစ်ခုတည်းထဲ ရောက်ပါတယ်။
  3. CTE တွေကို comma နဲ့ ဆက်လို့ ရပြီး နောက်က တစ်ခုက အရင်တစ်ခုကို ဖတ်လို့ ရပါတယ်။
  4. active က အနည်းဆုံး လ သုံးလ ကွဲကွဲ post တင်ခဲ့တဲ့ user တွေကို ထားပါတယ်။
  5. နောက်ဆုံး SELECT က CTE တွေကို ရိုးရိုး table တွေလို ဖတ်ပါတယ်။
  6. Column နှစ်ခုလုံး နာမည်တူတဲ့အခါ USING (user_id) က ON m.user_id = a.user_id ရဲ့ အတိုကောက်ပါ။

Advanced ၂: tree များအတွက် recursive CTE

comments.parent_id ကို မှတ်မိသေးလား? Reply တွေကို ပြန်ဖြေတဲ့ reply တွေက အနက် ဘယ်လောက်မဆို ရှိနိုင်တဲ့ tree ဖြစ်လာပါတယ်။ Recursive CTE က query တစ်ခုတည်းနဲ့ အဲဒါကို လျှောက်ကြည့်ပါတယ်။

sql
WITH RECURSIVE thread AS (  SELECT id, parent_id, body, 1 AS depth  FROM comments  WHERE post_id = 7 AND parent_id IS NULL   UNION ALL   SELECT c.id, c.parent_id, c.body, t.depth + 1  FROM comments AS c  JOIN thread AS t ON c.parent_id = t.id)SELECT repeat('  ', depth - 1) || body AS replyFROM threadORDER BY depth, id;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. RECURSIVE က CTE ကို ကိုယ့်ကိုယ်ကိုယ် ပြန်ရည်ညွှန်ခွင့် ပေးပါတယ်။
  2. Anchor အပိုင်း — စတင်တဲ့ row တွေပါ။ depth counter ကိုလည်း 1 ကနေ စပါတယ်။
  3. Post 7 ပေါ်က ထိပ်ဆုံးအဆင့် comment တွေက parent မရှိတဲ့ ဟာတွေပါ။
  4. UNION ALL က anchor row တွေကို recursive အပိုင်းက ဆက်ရှာတွေ့နေတဲ့ row တွေနဲ့ ကပ်ပါတယ်။
  5. Recursive အပိုင်း — အလှည့်တိုင်း အရင်အလှည့်က တွေ့ခဲ့တဲ့ row တွေရဲ့ child တွေကို တစ်ဆင့် ပိုနက်ပြီး ထည့်ပါတယ်။
  6. comments ကို thread ကိုယ်တိုင်နဲ့ join လုပ်လို့ parent က ရလဒ်ထဲ ရှိပြီးသား comment တိုင်းကို တွေ့ပါတယ်။ အလှည့်တစ်ခုမှာ အသစ် ဘာမှ မတွေ့တော့ရင် recursion ရပ်ပါတယ်။
  7. repeat နဲ့ || (string ဆက်ခြင်း) က reply တစ်ခုစီကို သူ့ depth အလိုက် အထဲတိုးလို့ ရလဒ်က thread တစ်ခုလို ဖတ်ရပါတယ်။

Advanced ၃: window function များ

Window function တွေက row အစုတစ်ခုကို ဖြတ်ပြီး တွက်ပါတယ်၊ ဒါပေမဲ့ GROUP BY လို row တွေကို မချုံ့ ပါဘူး။ Row တစ်ခုစီက သူ့ကိုယ်ပိုင် အမှတ်လက္ခဏာ ထိန်းထားပြီး တွက်ထားတဲ့ တန်ဖိုး တစ်ခု ထပ်ရပါတယ်။

sql
SELECT u.name,       p.title,       p.views,       ROW_NUMBER() OVER (         PARTITION BY p.user_id         ORDER BY p.views DESC       ) AS rank_for_author,       SUM(p.views) OVER (         PARTITION BY p.user_id         ORDER BY p.published_at       ) AS running_views,       p.views - LAG(p.views) OVER (ORDER BY p.published_at) AS change_from_previousFROM posts AS pJOIN users AS u ON u.id = p.user_idWHERE p.published_at IS NOT NULL;

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. ROW_NUMBER() က row တွေကို 1၊ 2၊ 3 စသည်ဖြင့် နံပါတ်တပ်ပါတယ်။ OVER (...) က window function ဖြစ်စေတာပါ၊ window ကို ဖော်ပြပါတယ်။
  2. PARTITION BY က author တစ်ယောက်ချင်းအတွက် နံပါတ်တပ်တာကို ပြန်စပါတယ်၊ GROUP BY နဲ့တူပေမဲ့ row တွေကို မပေါင်းပါဘူး။
  3. Author တစ်ယောက်ချင်းထဲမှာ view အများဆုံး post က နံပါတ် 1 ရပါတယ်။ ဒီ query ကို အုပ်ပြီး rank_for_author <= 3 နဲ့ စစ်ထုတ်ရင် author တစ်ယောက်ချင်းရဲ့ top 3 post ကို ရပါတယ်။
  4. ရိုးရိုး aggregate (SUM) ကို window function အဖြစ် သုံးထားတာပါ။
  5. Window ထဲမှာ ORDER BY ပါရင် sum က running ဖြစ်သွားပါတယ် — row တစ်ခုစီက သူ့ view ကို အရင်က အားလုံးနဲ့ ပေါင်းလို့ အချိန်အလိုက် စုစုပေါင်း ရပါတယ်။
  6. LAG က window အစီအစဉ်ထဲက အရင် row ရဲ့ တန်ဖိုးကို ဖတ်ပါတယ်၊ "ပြီးခဲ့တဲ့အကြိမ်ကနေ ဘယ်လောက် ပြောင်းသွားလဲ?" အတွက် အကောင်းဆုံးပါ။ LEAD က နောက် row ကို ဖတ်ပါတယ်။

Advanced ၄: transaction များ

sql
BEGIN; UPDATE accounts SET balance = balance - 500 WHERE id = 1;UPDATE accounts SET balance = balance + 500 WHERE id = 2; INSERT INTO transfers (from_id, to_id, amount)VALUES (1, 2, 500); COMMIT;-- ROLLBACK; would undo everything since BEGIN

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. BEGIN က transaction ကို ဖွင့်ပါတယ်။ Commit မလုပ်မချင်း သူ့နောက်က ဘာမှ တခြား session တွေကို မမြင်ရပါဘူး။
  2. ဥပမာ ဂန္ထဝင်ကို ပြဖို့ accounts table သီးသန့် သုံးထားပါတယ် — account 1 ကနေ 500 ယူ...
  3. ...account 2 ကို ပေးပါ။ Transaction မပါဘဲ ဒီစာကြောင်း နှစ်ကြောင်းကြား server ပျက်သွားရင် ပိုက်ဆံက ပျောက်သွားရုံပါပဲ။
  4. လွှဲပြောင်းမှုကို အလုပ်အစု တစ်ခုတည်းရဲ့ အစိတ်အပိုင်းအဖြစ် မှတ်တမ်းတင်ပါတယ်။
  5. COMMIT က အပြောင်းအလဲ လေးခုလုံးကို အတူတကွ အမြဲတမ်း ဖြစ်စေပါတယ်။ ဒါက ACID ထဲက "A" ပါ။
  6. ဥပမာ CHECK (balance >= 0) constraint လို တစ်ခုခု မအောင်မြင်ရင် app က ROLLBACK ပို့ပြီး database က BEGIN မတိုင်ခင်က အခြေအနေအတိုင်း အတိအကျ ပြန်ဖြစ်သွားပါတယ်။

Advanced ၅: index များနဲ့ EXPLAIN

Query တွေက သူတို့နောက်က index တွေလောက်သာ မြန်ပါတယ်။ Index က table တစ်ခုလုံး မဖတ်ဘဲ ကိုက်တဲ့ row တွေဆီ တန်းခုန်သွားနိုင်စေတဲ့ စီထားတဲ့ ဖွဲ့စည်းပုံ (များသောအားဖြင့် B-tree) ပါ။

sql
CREATE INDEX posts_user_published_idx  ON posts (user_id, published_at DESC); EXPLAIN ANALYZESELECT title FROM postsWHERE user_id = 7ORDER BY published_at DESCLIMIT 5;-- Index Scan using posts_user_published_idx ... (actual time=0.03..0.05 rows=5)

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Index တွေကို ဖော်ပြချက်ပါတဲ့ နာမည် ပေးပါ။ နောက်ပိုင်း query plan ထဲမှာ မြင်ရပါလိမ့်မယ်။
  2. Column နှစ်ခုပေါ်က composite index ပါ၊ ဒီအစီအစဉ်အတိုင်း။ "User X ရဲ့ post တွေ၊ အသစ်ဆုံး အရင်" ကို ပြည့်ပြည့်ဝဝ ဆောင်ရွက်ပေးပါတယ်။ Column အစီအစဉ် အရေးကြီးပါတယ် — published_at နဲ့သာ စစ်ထုတ်တဲ့ query ကို မကူညီပါဘူး။
  3. EXPLAIN က database ရွေးလိုက်တဲ့ plan ကို ပြပါတယ်။ ANALYZE က query ကို တကယ် run ပြီး အချိန် အစစ်တွေကို ပြပါတယ်။
  4. ဒီလို စာကြောင်းမျိုး မြင်ချင်တာပါ။ Table ကြီးပေါ်မှာ Seq Scan ဆိုရင် များသောအားဖြင့် index ပျောက်နေတာပါ။ Index တွေက read ကို မြန်စေပေမဲ့ write ကို နည်းနည်း နှေးစေလို့ query တွေ တကယ်သုံးတဲ့ ဟာတွေကိုသာ ထည့်ပါ။

MongoDB ဖြင့် NoSQL query များ: beginner ကနေ advanced အထိ

အခု blog တစ်ခုတည်းကိုပဲ MongoDB မှာ ကြည့်ပါမယ်။ ဥပမာတွေကို MongoDB ရဲ့ shell ဖြစ်တဲ့ mongosh မှာ run ပါတယ်။ Node.js driver ကလည်း ရှေ့မှာ await ထည့်ရုံနဲ့ syntax အတော်လေး တူပါတယ်။ Table အစား collection၊ row အစား document တွေ ရှိပြီး query တွေကို ကိုယ်ပိုင် ဘာသာစကားတစ်ခု မဟုတ်ဘဲ JSON နဲ့တူတဲ့ object တွေအဖြစ် ရေးပါတယ်။

Beginner ၁: document များ ထည့်ခြင်း

js
use blog db.users.insertOne({  name: "Zin",  email: "zin@example.com",  country: "Myanmar",  createdAt: new Date()}) db.posts.insertMany([  { title: "Camping at Inle Lake", author: "Zin", tags: ["travel", "camping"], views: 42, published: true },  { title: "Notes on Go", author: "Zin", tags: ["go"], views: 0, published: false, draftNote: "add examples" }])

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. use blog က blog database ကို ပြောင်းပါတယ်။ မရှိသေးရင် ပထမဆုံး write လုပ်တဲ့အခါ MongoDB က ဖန်တီးပေးပါတယ်။ CREATE DATABASE အဆင့် မရှိပါဘူး။
  2. db.users က users collection ပါ၊ ပထမဆုံး write မှာ ဖန်တီးပါတယ်။ insertOne က document တစ်ခု ထည့်ပြီး သူ့ _id အသစ်ကို ပြန်ပေးပါတယ်။
  3. Document ဆိုတာ JavaScript object လို ရေးထားတဲ့ field–value အတွဲ အစုပါ။ Schema ကို ကြိုကြေညာစရာ မလိုခဲ့ပါဘူး။
  4. new Date() က string မဟုတ်ဘဲ စီလို့၊ နှိုင်းယှဉ်လို့ ရတဲ့ ရက်စွဲ တန်ဖိုး အစစ်ကို သိမ်းပါတယ်။
  5. insertMany က document array တစ်ခုကို server ဆီ တစ်ခေါက်တည်းနဲ့ ထည့်ပါတယ်။
  6. tags က document ထဲမှာ တိုက်ရိုက် သိမ်းထားတဲ့ array ပါ။
  7. ဒုတိယ post မှာ ပထမ post မှာ မရှိတဲ့ field (draftNote) ရှိပါတယ်။ Collection တစ်ခုတည်းထဲမှာ လုံးဝ တရားဝင်ပါတယ်။ ဒါက schema ပြောင်းလွယ်မှုကို လက်တွေ့ ပြတာပါ။

Beginner ၂: document များ ရှာခြင်း

js
db.posts.find() db.posts.find({ author: "Zin", published: true }) db.posts.find(  { views: { $gte: 100 } },  { title: 1, views: 1, _id: 0 })  .sort({ views: -1 })  .limit(10) db.posts.findOne({ title: "Notes on Go" }) db.posts.countDocuments({ published: true })

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Filter မပါတဲ့ find() က collection ထဲက document အားလုံးကို ပြန်ပေးပါတယ်၊ SELECT * FROM posts လိုပါပဲ။ Shell က ပထမ ၂၀ ကို ပြပြီး နောက်ထပ်အတွက် it ရိုက်ခိုင်းပါတယ်။
  2. Filter object ပါ။ Field နှစ်ခု စာရင်းပြုရင် နှစ်ခုလုံး ကိုက်ရမယ်၊ အလိုလို AND ပါ။ ဒါက WHERE author = 'Zin' AND published = true ပါ။
  3. find က argument နှစ်ခုအထိ ယူပါတယ် — filter နဲ့ နောက်မှ projection။
  4. Operator တွေက nested object တွေပါ။ { $gte: 100 } က "100 နဲ့ ညီ ဒါမှမဟုတ် ပိုကြီး" ပါ။ တခြား $gt၊ $lt၊ $lte၊ $ne (မညီ) နဲ့ $eq တို့ ရှိပါတယ်။
  5. Projection က ပြန်ပေးမယ့် field တွေကို ရွေးပါတယ် — 1 က ထည့်ပြီး 0 က ဖယ်ပါတယ်။ _id ကို ပိတ်မထားရင် ပါပါတယ်။
  6. sort က field: direction အတွဲတွေ ယူပါတယ်၊ 1 က ငယ်ရာကနေ ကြီးရာ၊ -1 က ကြီးရာကနေ ငယ်ရာ။ ဒါက ORDER BY views DESC ပါ။
  7. limit(10) က document အများဆုံး ၁၀ ခု ပြန်ပေးပါတယ်။ ဒီစာကြောင်းတွေ ပေါင်းရင် SQL အပိုင်းက "top 10 posts" query ပါ။
  8. findOne က cursor အစား ကိုက်တဲ့ ပထမ document ကိုယ်တိုင် (ဒါမှမဟုတ် null) ကို ပြန်ပေးလို့ ရလဒ် တစ်ခုတည်း မျှော်လင့်တဲ့အခါ အဆင်ပြေပါတယ်။
  9. ကိုက်တဲ့ document တွေကို ပြန်မပေးဘဲ ရေတွက်ပါတယ်၊ SELECT COUNT(*) ... WHERE လိုပါ။

Beginner ၃: ပြင်ခြင်းနဲ့ ဖျက်ခြင်း

js
db.posts.updateOne(  { title: "Notes on Go" },  {    $set: { published: true, publishedAt: new Date() },    $unset: { draftNote: "" },    $inc: { views: 1 },    $push: { tags: "tutorial" }  }) db.posts.updateMany(  { tags: "camping" },  { $addToSet: { tags: "outdoors" } }) db.posts.deleteOne({ title: "Old draft" })db.posts.deleteMany({ published: false, views: 0 })

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. updateOne က filter နဲ့ ကိုက်တဲ့ ပထမ document ကို ပြောင်းပါတယ်။
  2. ပထမ argument က filter ပါ — ဘယ် document ကို ပြောင်းမလဲ။
  3. ဒုတိယ argument က update ပါ၊ update operator အစုတစ်ခုပါ။ Operator တစ်ခုစီက document ကို မတူတဲ့နည်းနဲ့ ပြောင်းပြီး အားလုံး atomic အနေနဲ့ အတူတကွ သက်ရောက်ပါတယ်။
  4. $set က field တွေကို ရေးပါတယ်၊ မရှိသေးရင် ဖန်တီးပါတယ်။ $set မပါဘဲ update ကို ရေးရင် document တစ်ခုလုံးကို အစားထိုး သွားပါတယ် — beginner တွေ မှားလေ့ရှိတဲ့ အမှားပါ။
  5. $unset က field ကို လုံးဝ ဖယ်ရှားပါတယ်။ တန်ဖိုး ("") ကို လျစ်လျူရှုပါတယ်။
  6. $inc က ဂဏန်းကို atomic အနေနဲ့ ပေါင်းပါတယ်၊ SQL က SET views = views + 1 လိုပါ။
  7. $push က array ထဲ တန်ဖိုးကို နောက်ဆုံးမှာ ထပ်ထည့်ပါတယ်။
  8. updateMany က ကိုက်တဲ့ document တိုင်း ကို ပြောင်းပါတယ်။
  9. Array field ကို ရိုးရိုး တန်ဖိုးနဲ့ query လုပ်ရင် array ထဲမှာ အဲဒီတန်ဖိုး ပါတဲ့ document တွေနဲ့ ကိုက်ပါတယ်။ Join table မလိုပါဘူး။
  10. $addToSet က တန်ဖိုး မရှိသေးမှသာ array ထဲ ထည့်လို့ ဒါကို နှစ်ခါ run ရင်တောင် ထပ်နေတာ မဖြစ်ပါဘူး။
  11. deleteOne က ပထမ ကိုက်တဲ့ ဟာကို ဖယ်ရှားပါတယ်။
  12. deleteMany က ကိုက်တဲ့ ဟာ အားလုံးကို ဖယ်ရှားပါတယ်။ SQL လိုပဲ filter ကို ထပ်စစ်ပါ — deleteMany({}) က collection ကို ရှင်းလင်းသွားပါတယ်။

Intermediate ၁: query operator များနဲ့ array များ

js
db.users.find({ country: { $in: ["Myanmar", "Thailand"] } }) db.users.find({ email: { $regex: /@gmail\.com$/i } }) db.posts.find({  $or: [    { views: { $gt: 1000 } },    { tags: { $all: ["go", "tutorial"] } }  ]}) db.posts.find({  comments: { $elemMatch: { user: "Aye", likes: { $gte: 5 } } }}) db.posts.find({ "author.name": "Zin" }) db.posts.find({ draftNote: { $exists: true } })

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. $in က list ထဲက တန်ဖိုး တစ်ခုခုနဲ့ ကိုက်ပါတယ်၊ SQL ရဲ့ IN နဲ့ တူတူပါ။
  2. $regex က regular expression နဲ့ ကိုက်ပါတယ်။ i flag က စာလုံးအကြီးအသေး လျစ်လျူရှုပြီး $ က အဆုံးမှာ ချည်ပါတယ်။ အစမှာ ချည်ထားတဲ့ (^...) pattern တွေသာ index ကို ထိရောက်စွာ သုံးနိုင်တာ သတိပြုပါ။
  3. Filter တစ်ခုမှာ condition အများကြီး ရောနိုင်ပါတယ်။
  4. $or က condition array တစ်ခု ယူပြီး တစ်ခုခု မှန်ရင် ကိုက်ပါတယ်။ $and နဲ့ $nor လည်း ရှိပါတယ်။
  5. $all က စာရင်းထဲက တန်ဖိုး အားလုံး ပါတဲ့ array တွေနဲ့ ကိုက်ပါတယ်၊ ဘယ်အစီအစဉ်မဆို။
  6. Sub-document array ထဲမှာ ရှာခြင်းပါ။
  7. $elemMatch က array element တစ်ခုတည်း က condition အားလုံးကို ပြည့်မီရမယ်လို့ တောင်းဆိုပါတယ်။ မပါရင် user: "Aye" က comment တစ်ခုနဲ့၊ likes >= 5 က တခြား comment တစ်ခုနဲ့ ကိုက်သွားနိုင်ပါတယ်။
  8. Dot notation က embed လုပ်ထားတဲ့ document ထဲကို ရောက်ပါတယ်။ Key ထဲမှာ dot ပါလို့ quote လိုပါတယ်။
  9. $exists က field ရှိတဲ့ (false နဲ့ဆိုရင် မရှိတဲ့) document တွေနဲ့ ကိုက်ပါတယ်။ ဒီမေးခွန်းက schema ပြောင်းလွယ်တဲ့နေရာမှာသာ အဓိပ္ပာယ် ရှိပါတယ်။

Intermediate ၂: embedding နဲ့ referencing

MongoDB မှာ အကြီးဆုံး ဒီဇိုင်း ဆုံးဖြတ်ချက်က query တစ်ခု မဟုတ်ပါဘူး။ ဆက်စပ်တဲ့ data ကို document တစ်ခုထဲ embed လုပ်မလား ဒါမှမဟုတ် တခြား collection ကနေ id နဲ့ reference လုပ်မလား ဆိုတာပါ။

js
// Embedded: comments live inside the postdb.posts.updateOne(  { _id: postId },  { $push: { comments: { user: "Aye", body: "Nice!", at: new Date() } } }) // Referenced: comments are their own collectiondb.comments.insertOne({  postId: postId,  user: "Aye",  body: "Nice!",  at: new Date()})db.comments.find({ postId: postId }).sort({ at: 1 })

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Child data က သေးငယ်ပြီး parent တစ်ခုတည်းပိုင်ကာ အတူတူ ဖတ်တဲ့အခါ embed လုပ်ပါ။
  2. Comment ထည့်တာက document တစ်ခုပေါ်က atomic update တစ်ခုတည်းပါ၊ post ကို ဖတ်ရင် comment အားလုံး ပါလာပါတယ်။
  3. Child စာရင်းက အကန့်အသတ်မရှိ ကြီးလာနိုင်တဲ့အခါ ဒါမှမဟုတ် သီးသန့် query လုပ်တဲ့အခါ reference လုပ်ပါ။ MongoDB document တစ်ခုက 16 MB အထိသာ ရလို့ comment ၁ သိန်း ရှိတဲ့ post က သူတို့ကို embed မလုပ်ရပါဘူး။
  4. postId က foreign key အခန်းကဏ္ဍ ပါဝင်ပါတယ်။ ကွာခြားချက်က MongoDB က အတင်းအကျပ် မစစ်တာပါ။ မှန်ကန်အောင် ထိန်းတာ သင့် code ရဲ့ တာဝန်ပါ။
  5. Comment တွေ ဖတ်တာ အခု ဒုတိယ query ဖြစ်သွားပြီး (အောက်က) $lookup က အဲဒါကို တစ်ခုတည်းအဖြစ် ပေါင်းပေးပါတယ်။ လက်ကိုင်စည်းမျဉ်း — data ကို ဘယ်လို ဖတ်မလဲပေါ် မူတည်ပြီး model လုပ်ပါ။

Advanced ၁: aggregation pipeline

ရိုးရိုး စစ်ထုတ်တာထက် ကျော်တဲ့ ဘာမဆိုအတွက် MongoDB က aggregation pipeline ကို သုံးပါတယ် — stage တွေရဲ့ array ပါ။ Document တွေက stage တွေကို အစီအစဉ်အတိုင်း ဖြတ်စီးပြီး stage တစ်ခုစီက လက်ခံရတာကို ပြောင်းလဲပါတယ်၊ Unix pipe တွေလိုပါ။ ဒါက GROUP BY၊ HAVING နဲ့ အများကြီး ပိုတာတွေနဲ့ ညီမျှပါတယ်။

js
db.posts.aggregate([  { $match: { published: true } },  { $group: {      _id: "$author",      posts: { $sum: 1 },      totalViews: { $sum: "$views" },      avgViews: { $avg: "$views" }  } },  { $match: { posts: { $gte: 5 } } },  { $sort: { totalViews: -1 } },  { $project: {      _id: 0,      author: "$_id",      posts: 1,      totalViews: 1,      avgViews: { $round: ["$avgViews", 0] }  } }])

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. aggregate က stage array တစ်ခု ယူပါတယ်။ အပေါ်ကနေ အောက်ကို ဖတ်ပါ — stage တစ်ခုစီက အရင် stage ရဲ့ output ကို လက်ခံပါတယ်။
  2. $match က document တွေကို စစ်ထုတ်ပါတယ်၊ WHERE လိုပါ။ နောက် stage တွေ document နည်းနည်းပဲ ကိုင်ရအောင်နဲ့ index သုံးနိုင်အောင် တတ်နိုင်သမျှ စောစော ထားပါ။
  3. $group က document တွေကို group တွေအဖြစ် ပေါင်းပါတယ်၊ GROUP BY လိုပါ။
  4. _id က group key ပါ။ $ နဲ့ စတဲ့ string "$author" က "author field ရဲ့ တန်ဖိုး" ဆိုလိုလို့ author တစ်ယောက်လျှင် group တစ်ခု ရပါတယ်။
  5. $sum: 1 က document တစ်ခုလျှင် 1 ပေါင်းလို့ group ထဲက document တွေကို ရေတွက်ပါတယ် (COUNT(*))။
  6. $sum: "$views" က field ရဲ့ တန်ဖိုးတွေကို ပေါင်းပါတယ် (SUM(views))။ $avg၊ $min၊ $max နဲ့ $push တို့လည်း ဒီအတိုင်း အလုပ်လုပ်ပါတယ်။
  7. Group လုပ်ပြီးနောက် ဒုတိယ $match က group တွေကို စစ်ထုတ်ပါတယ်။ SQL မှာ HAVING လုပ်တာ အတိအကျပါ။
  8. $sort က group တွေကို စုစုပေါင်း view အလိုက် အများဆုံးကနေ စီပါတယ်။
  9. $project က output document တစ်ခုစီကို ပုံစံပြန်ချပါတယ် — ဘယ် field တွေ ထားမလဲ၊ နာမည်ပြောင်းမလဲ၊ တွက်မလဲ။
  10. Group key ကို _id ကနေ ဖတ်ရလွယ်တဲ့ author field အဖြစ် နာမည်ပြောင်းပါတယ်။
  11. $round က expression operator ပါ။ Expression တွေက argument တွေကို array ထဲမှာ ယူပါတယ်၊ ဒီမှာ တန်ဖိုးနဲ့ ဒသမနေရာ အရေအတွက်။

Advanced ၂: $unwind နဲ့ $lookup

js
db.posts.aggregate([  { $unwind: "$tags" },  { $group: { _id: "$tags", count: { $sum: 1 } } },  { $sort: { count: -1 } },  { $limit: 5 }]) db.posts.aggregate([  { $match: { published: true } },  { $lookup: {      from: "comments",      localField: "_id",      foreignField: "postId",      as: "comments"  } },  { $addFields: { commentCount: { $size: "$comments" } } },  { $project: { comments: 0 } }])

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. $unwind က array ပါတဲ့ document တစ်ခုကို element တစ်ခုလျှင် document တစ်ခု ဖြစ်အောင် ပြောင်းပါတယ်။ Tag သုံးခုပါတဲ့ post က tags တန်ဖိုး တစ်ခုစီပါတဲ့ document သုံးခု ဖြစ်သွားပါတယ်။
  2. အခု tag တစ်ခုစီကို တခြား field တွေလိုပဲ group လုပ်ပြီး ရေတွက်လို့ ရပါပြီ။
  3. ရလဒ် — post အားလုံးမှာ အသုံးအများဆုံး tag ငါးခု။ SQL မှာဆိုရင် tag join table လိုပါလိမ့်မယ်။
  4. $lookup က MongoDB ရဲ့ left outer join ပါ — တခြား collection က ကိုက်တဲ့ document တွေကို document တစ်ခုစီထဲ ဆွဲထည့်ပါတယ်။
  5. from က တခြား collection ရဲ့ နာမည်ပါ။
  6. localField က လက်ရှိ document တွေထဲက field (post ရဲ့ _id) ပါ...
  7. ...foreignField က အဲဒါနဲ့ ညီရမယ့် comments ထဲက field ပါ။
  8. as က ကိုက်တာတွေကို ထိန်းမယ့် array field အသစ်ရဲ့ နာမည်ပါ။ Comment မရှိတဲ့ post က LEFT JOIN ထဲက NULL တွေလိုပဲ array အလွတ် ရပါတယ်။
  9. $addFields က တခြား field အားလုံးကို ထိန်းထားရင်း တွက်ထားတဲ့ field ထည့်ပါတယ်။ $size က array ရဲ့ အလျားကို ပြန်ပေးပါတယ်။
  10. ပြီးမှ comment array အပြည့်ကို ဖယ်ပြီး အရေအတွက်ကိုသာ ထားလို့ network ပေါ် data နည်းနည်းပဲ သွားပါတယ်။

Advanced ၃: $facet နဲ့ report အများကြီးကို တစ်ခါတည်း

js
db.posts.aggregate([  { $match: { published: true } },  { $facet: {      topPosts: [        { $sort: { views: -1 } },        { $limit: 3 },        { $project: { title: 1, views: 1 } }      ],      byMonth: [        { $group: {            _id: { $dateToString: { format: "%Y-%m", date: "$publishedAt" } },            posts: { $sum: 1 }        } },        { $sort: { _id: 1 } }      ],      totals: [ { $count: "published" } ]  } }])

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. မျှဝေထားတဲ့ ပထမ stage တစ်ခု — published post တွေသာ report တိုင်းထဲ ဝင်ပါတယ်။
  2. $facet က တူညီတဲ့ input document တွေပေါ်မှာ sub-pipeline အများကြီး run ပြီး sub-pipeline တစ်ခုလျှင် array တစ်ခုပါတဲ့ document တစ်ခု ပြန်ပေးပါတယ်။ Dashboard တွေနဲ့ filter count ပါတဲ့ search page တွေအတွက် အကောင်းဆုံးပါ။
  3. Sub-pipeline ၁ — view အများဆုံး post သုံးခု။
  4. Sub-pipeline ၂ — လတစ်လချင်းအလိုက် post အရေအတွက်။
  5. $dateToString က ရက်စွဲကို "2026-09" လို စာသားအဖြစ် ပုံစံချလို့ လအတွက် အဆင်ပြေတဲ့ group key ဖြစ်ပါတယ်။
  6. Sub-pipeline ၃ — $count က input အရေအတွက် ပါတဲ့ document တစ်ခုတည်း ပြန်ပေးပါတယ်၊ "N ခုထဲက 1–10 ကို ပြနေတယ်" စာကြောင်းအတွက်။

Advanced ၄: index များနဲ့ explain()

js
db.posts.createIndex({ author: 1, publishedAt: -1 }) db.users.createIndex({ email: 1 }, { unique: true }) db.posts.createIndex({ title: "text", body: "text" })db.posts.find({ $text: { $search: "camping lake" } }) db.posts.find({ author: "Zin" }).sort({ publishedAt: -1 })  .explain("executionStats")

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Compound index ပါ၊ SQL က ဟာနဲ့ အယူအဆ တူပါတယ် — author အလိုက် post တွေ၊ အသစ်ဆုံး အရင်။ ဒီမှာလည်း field အစီအစဉ် အရေးကြီးပါတယ်။
  2. Unique index က email ထပ်တာကို MongoDB ငြင်းအောင် လုပ်ပေးလို့ SQL က schema ကနေ ပေးတဲ့ အာမခံချက်တစ်ခုကို ပြန်ယူလာပါတယ်။
  3. Field နှစ်ခုပေါ်က text index က ရိုးရှင်းတဲ့ full-text search ကို ဖြစ်နိုင်စေပါတယ်။
  4. $search ပါတဲ့ $text က အဲဒီ index ကို သုံးပြီး စကားလုံး တစ်ခုခု ပါတဲ့ document တွေကို ရှာပါတယ်။
  5. စစ်ကြည့်ချင်တဲ့ query တစ်ခု...
  6. ...explain က MongoDB ဘယ်လို run ခဲ့လဲ ပြပါတယ်။ COLLSCAN (collection တစ်ခုလုံး ဖတ်ခဲ့) မဟုတ်ဘဲ IXSCAN (index သုံးခဲ့) ကို ရှာပြီး totalDocsExamined ကို nReturned နဲ့ နှိုင်းယှဉ်ပါ။

Advanced ၅: multi-document transaction များ

MongoDB မှာ document တစ်ခုတည်း ကို update လုပ်တာ အမြဲ atomic ဖြစ်ပါတယ်။ အပြောင်းအလဲ တစ်ခုက document အများကြီးကို တစ်ပြိုင်နက် ထိရမယ်ဆိုရင် transaction ကို သုံးပါ (replica set ပေါ်မှာ၊ MongoDB Atlas cluster တိုင်းက replica set ပါ)။ ဒီဥပမာက Node.js driver ကို သုံးပါတယ်။

js
const session = client.startSession();try {  await session.withTransaction(async () => {    const accounts = client.db("bank").collection("accounts");    await accounts.updateOne(      { _id: 1, balance: { $gte: 500 } },      { $inc: { balance: -500 } },      { session }    );    await accounts.updateOne({ _id: 2 }, { $inc: { balance: 500 } }, { session });    await client.db("bank").collection("transfers")      .insertOne({ from: 1, to: 2, amount: 500, at: new Date() }, { session });  });} finally {  await session.endSession();}

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. Session က operation တွေကို အုပ်စုဖွဲ့ပါတယ်၊ transaction တိုင်းက session တစ်ခုပိုင်ပါတယ်။
  2. withTransaction က transaction ကို စ၊ callback ကို run ပြီး commit လုပ်ပါတယ်။ Callback က throw ရင် abort လုပ်ပါတယ်။ ယာယီ error တွေမှာ အလိုလို ပြန်ကြိုးစားပါတယ်။
  3. Account 1 မှာ ပိုက်ဆံ လုံလောက်မှသာ filter က ကိုက်ပါတယ်၊ CHECK constraint လိုပါ။ တကယ့် code မှာ ရလဒ်ရဲ့ matchedCount ကို စစ်ပြီး 0 ဖြစ်ရင် throw လုပ်လို့ လွှဲပြောင်းမှုကို abort လုပ်ပါတယ်။
  4. { session } ပေးတာက ဒီ operation ကို transaction ထဲ ထည့်တာပါ။ မေ့သွားရင် write က သီးသန့် ဖြစ်သွားပါတယ်။
  5. ဒုတိယ account ကို transaction တစ်ခုတည်းထဲမှာ ငွေဖြည့်ပါတယ်။
  6. Audit record က balance ပြောင်းလဲမှု နှစ်ခုနဲ့ အတူတကွ commit ဖြစ်မယ် ဒါမှမဟုတ် ပျောက်မယ် — အကုန် ဒါမှမဟုတ် ဘာမှမဟုတ်၊ SQL ရဲ့ BEGIN ... COMMIT လိုပါပဲ။
  7. တစ်ခုခု မအောင်မြင်ရင်တောင် server ပေါ်က resource တွေ လွတ်အောင် session ကို အမြဲ ပိတ်ပါ။

Redis (key-value) ကို အမြန်ကြည့်ခြင်း

NoSQL database တိုင်းက document store မဟုတ်ပါဘူး။ Redis က data ကို memory ထဲမှာ type ဉာဏ်ကောင်း အနည်းငယ် (string၊ hash၊ list၊ set၊ sorted set) ရှိတဲ့ value တွေကို ညွှန်တဲ့ key တွေအဖြစ် ထားလို့ အလွန် မြန်ပါတယ်။ များသောအားဖြင့် main database ကို အစားထိုးတာ မဟုတ်ဘဲ ဘေးမှာ ထိုင်ပါတယ်။ ဒီ command တွေကို redis-cli မှာ run ပါတယ်။

redis
SET post:7:html "<article>...</article>" EX 3600GET post:7:htmlTTL post:7:html INCR post:7:views HSET session:abc123 userId 42 role adminHGETALL session:abc123 ZADD leaderboard 1520 "aye" 980 "kokko" 2210 "zin"ZREVRANGE leaderboard 0 2 WITHSCORES DEL post:7:html

တစ်ကြောင်းချင်း ရှင်းလင်းချက်

  1. SET က key တစ်ခုအောက်မှာ string တစ်ခု သိမ်းပါတယ်။ Colon ခြားထားတဲ့ နာမည် (post:7:html) က နာမည်ပေးပုံ စည်းမျဉ်းသာပါ။ EX 3600 က key ကို တစ်နာရီကြာရင် သက်တမ်းကုန်စေပါတယ် — cache ရဲ့ အနှစ်သာရပါ။
  2. GET က ပြန်ဖတ်ပါတယ်။ Key မရှိရင် ဒါမှမဟုတ် သက်တမ်းကုန်ရင် nil ရပြီး app က main database ကို ပြန်သွားကာ cache ကို ပြန်ဖြည့်ပါတယ်။
  3. TTL က key မှာ သက်တမ်း ဘယ်နှစက္ကန့် ကျန်သေးလဲ ပြပါတယ်။
  4. INCR က request ထောင်ချီ တစ်ပြိုင်နက် ရှိရင်တောင် atomic အနေနဲ့ 1 ပေါင်းပြီး တန်ဖိုးအသစ်ကို ပြန်ပေးပါတယ်။ View counter နဲ့ rate limit တွေအတွက် အကောင်းဆုံးပါ။
  5. Hash က key တစ်ခုအောက်မှာ field အများကြီး သိမ်းပါတယ်၊ document အသေးလေး တစ်ခုလိုပါ။ Session data ထားဖို့ အသုံးများတဲ့ နည်းပါ။
  6. HGETALL က hash ထဲက field နဲ့ value အားလုံးကို ပြန်ပေးပါတယ်။
  7. Sorted set က member တွေကို score အလိုက် စီထားပါတယ်၊ ဒီမှာ ကစားသမားတွေရဲ့ အမှတ်ပါ။
  8. ZREVRANGE ... 0 2 က အမှတ် အများဆုံးကနေ ထိပ်ဆုံး သုံးယောက်ကို သူတို့ score နဲ့အတူ ပြန်ပေးပါတယ်။ Live leaderboard တစ်ခုက command တစ်ခုတည်းပါ။
  9. DEL က key ကို ဖယ်ရှားပါတယ်၊ ဥပမာ post ကို ပြင်ပြီးချင်း cache ကို ရှင်းဖို့။

Cheat sheet: SQL နဲ့ MongoDB မှာ တူညီတဲ့ အလုပ်များ

နေ့စဉ် အလုပ် ခြောက်ခုကို SQL နဲ့ အရင်ရေးပြီး MongoDB နဲ့ ထပ်ရေးထားပါတယ်။ တိုတိုလေး ဖြစ်အောင် နှစ်ဖက်လုံးက author နာမည်ကို တိုက်ရိုက် သိမ်းထားတဲ့ ရိုးရှင်းတဲ့ posts table ကို သုံးပါတယ်။ Comment ထဲက နံပါတ်တွေ ကိုက်ပါတယ်။

sql
-- 1. Read with a filterSELECT * FROM posts WHERE author = 'Zin' AND views > 100; -- 2. Sort and paginate (page 3, 10 per page)SELECT * FROM posts ORDER BY published_at DESC LIMIT 10 OFFSET 20; -- 3. InsertINSERT INTO posts (title, author, views) VALUES ('Hello', 'Zin', 0); -- 4. UpdateUPDATE posts SET views = views + 1 WHERE id = 7; -- 5. Group and countSELECT author, COUNT(*) FROM posts GROUP BY author HAVING COUNT(*) > 2; -- 6. JoinSELECT p.title, c.body FROM posts p LEFT JOIN comments c ON c.post_id = p.id;
js
// 1. Read with a filterdb.posts.find({ author: "Zin", views: { $gt: 100 } }) // 2. Sort and paginate (page 3, 10 per page)db.posts.find().sort({ publishedAt: -1 }).skip(20).limit(10) // 3. Insertdb.posts.insertOne({ title: "Hello", author: "Zin", views: 0 }) // 4. Updatedb.posts.updateOne({ _id: 7 }, { $inc: { views: 1 } }) // 5. Group and countdb.posts.aggregate([{ $group: { _id: "$author", n: { $sum: 1 } } }, { $match: { n: { $gt: 2 } } }]) // 6. Joindb.posts.aggregate([{ $lookup: { from: "comments", localField: "_id", foreignField: "postId", as: "comments" } }])

နိဂုံးချုပ်

SQL နဲ့ NoSQL က ပြိုင်ဘက်တွေ ဆိုတာထက် "ငါ့ data က ဘယ်ပုံစံလဲ၊ ဘယ်လို ဖတ်မလဲ?" ဆိုတဲ့ မေးခွန်းအတွက် မတူတဲ့ အဖြေတွေပါ။ Relational database တွေက ဖွဲ့စည်းပုံ၊ join နဲ့ ခိုင်မာတဲ့ အာမခံချက်တွေ ပေးပြီး business data အများစုအတွက် အကောင်းဆုံး default အဖြစ် ရှိနေဆဲပါ။ MongoDB လို document store တွေက ပြောင်းလွယ်မှုနဲ့ code နဲ့ ပုံစံတူ data ကို ပေးပါတယ်။ Redis လို key-value store တွေက hot path တွေအတွက် မြန်နှုန်းကို ပေးပါတယ်။ Wide-column နဲ့ graph database တွေက သူတို့ တည်ဆောက်ထားတဲ့ ပြဿနာတွေကို ဖြေရှင်းပါတယ်။

SQL ကို အရင် သေချာ သင်ပါ။ နေရာတိုင်းမှာ ရှိတယ်၊ ဆယ်စုနှစ်ချီ မပြောင်းသလောက်ပဲ ရှိပြီး data အကြောင်း စဉ်းစားတတ်အောင် သင်ပေးပါတယ်။ ပြီးရင် NoSQL database တစ်ခုကို တစ်ခုခု တည်ဆောက်ရင်း သင်ပါ၊ document တွေကို ဘယ်လို model လုပ်လဲ အာရုံစိုက်ပါ။ အပေါ်က query တွေက မြေပုံတစ်ခုပါ၊ အကောင်းဆုံး သင်ယူနည်းက psql ဒါမှမဟုတ် mongosh ကို ဖွင့်ပြီး ကိုယ်တိုင် ရိုက်ကြည့်တာပါပဲ။

Filed undersqlnosqlmongodbredisdatabases

Written byZin Min Htun (Wine Chanz)Senior Full-Stack Developer · Da Nang, VietnamSay hello at campMore entries