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:
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
CREATE TABLEdefines a new table and every column it will ever hold. Rows that do not fit this shape are refused.SERIALis an auto-incrementing integer, so each new user gets the next number.PRIMARY KEYmakes it unique and indexed.NOT NULLmeans every user must have a name. Leaving it out is an error, not an empty value.UNIQUEmakes the database itself refuse a second account with the same email, even if two requests arrive at the same moment.- No
NOT NULLhere, socountryis optional and can beNULL(unknown). TIMESTAMPTZis a time with a time zone.DEFAULT now()fills it in automatically when a row is inserted.- Each post belongs to one user. This is the "many" side of a one-to-many relationship.
REFERENCES users(id)is the foreign key: a post cannot point at a user who does not exist.ON DELETE CASCADEdeletes a user's posts when the user is deleted.- A
CHECKconstraint is a rule the database enforces on every write. Here, the view count can never go negative. - Nullable on purpose: a post with no
published_atis still a draft. - Comments point at a post and at the user who wrote them, so one comment row links three tables together.
parent_idpoints 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:
{ _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
- Every document has an
_id. MongoDB generates anObjectIdautomatically: 12 bytes that encode a timestamp, so ids sort roughly by creation time. - 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
_idinside still lets us find the full user when we need to. - Arrays are ordinary values. In SQL, tags would need a separate
tagstable and apost_tagsjoin table. ISODateis the shell's way of writing a real date type, which can be sorted and compared, not a string.- 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,
UNIQUEandCHECKconstraints 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.
SELECT id, title, viewsFROM postsWHERE published_at IS NOT NULL AND views >= 100ORDER BY views DESCLIMIT 10;Line by line
- Name the columns you want back.
SELECT *returns every column, which is handy while exploring but wasteful in real code. FROMsays which table to read.WHEREfilters rows.NULLmeans "no value", and it can only be tested withIS NULLorIS NOT NULL, never with= NULL.ANDcombines conditions. Both must be true for a row to stay.ORDER BY ... DESCsorts from highest to lowest. WithoutORDER BY, SQL promises no order at all.LIMITkeeps only the first 10 rows of the sorted result: a "top 10 posts" list.
Beginner 2: filtering with patterns, lists and ranges
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
IN (...)matches any value in the list. It is a shorter way to writecountry = 'Myanmar' OR country = 'Thailand'.LIKEmatches a pattern:%means "any characters". So this finds every Gmail address. PostgreSQL'sILIKEdoes the same without caring about upper or lower case.BETWEENincludes both ends of the range. Text like'2026-01-01'is converted to a timestamp automatically.- Sorts alphabetically (ascending is the default).
Beginner 3: inserting, updating and deleting
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
INSERT INTOadds a row. We list only the columns we are giving values for:idandcreated_atfill themselves in.VALUESgives the values in the same order as the column list. Text goes in single quotes.RETURNING(PostgreSQL) hands back the generated values, so your app learns the new id without a second query.UPDATEchanges existing rows in a table.SETgives the new values. It can use the old value, so this is an atomic "add one", safe even with many visitors at once.- Always check the
WHEREon anUPDATE. Without it, every post in the table would be changed. DELETE FROMremoves rows. LikeUPDATE, it affects every row unless you filter.- 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.
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
- Columns can come from either table.
AS authorrenames the column in the result. AS pgives the table a short alias, so we can writep.titleinstead ofposts.title.INNER JOINkeeps only pairs where theONcondition is true: each post together with its author. A post whose user had been deleted would not appear.- The second query answers a classic question: which users have never written a post?
LEFT JOINkeeps every row from the left table (users). When a user has no posts, the post columns are filled withNULL.- Keeping only rows where the post side is
NULLleaves exactly the users with no posts. This "anti-join" pattern is worth memorising.
Intermediate 2: aggregation with GROUP BY and HAVING
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
- The column we group by. Each country becomes one row in the result.
COUNT(*)counts the rows in each group, which here is the number of posts.SUM,AVG,MINandMAXare aggregate functions: they squash many rows into one value per group.- A plain
JOINmeansINNER JOIN. WHEREfilters individual rows before they are grouped.GROUP BYcollects rows that share a country. Every selected column must either be grouped or be inside an aggregate.HAVINGfilters groups after aggregation, somethingWHEREcannot do. Only countries with at least five posts are kept.
Intermediate 3: subqueries
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
- A scalar subquery returns a single value, here the average views, which the outer query compares against. Result: posts that are above average.
EXISTSis true if the inner query returns at least one row. These lines find users who have commented at least once.SELECT 1because we only care whether a row exists, not what is in it.- This is a correlated subquery: it refers to
u.idfrom 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.
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
WITH monthly AS (...)defines a temporary named result: posts per user per month. It exists only for this one query.date_trunccuts a timestamp down to the first moment of its month, so every post in September lands in the same group.- CTEs can be chained with commas, and a later one can read an earlier one.
activekeeps users who posted in at least three different months.- The final
SELECTreads the CTEs like ordinary tables. USING (user_id)is shorthand forON m.user_id = a.user_idwhen 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.
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
RECURSIVElets the CTE refer to itself.- The anchor part: the starting rows. We also start a
depthcounter at 1. - The top-level comments on post 7 are the ones with no parent.
UNION ALLglues the anchor rows to the rows the recursive part keeps finding.- The recursive part: each round adds the children of the rows found in the previous round, one level deeper.
- Joining
commentstothreaditself finds every comment whose parent is already in the result. When a round finds nothing new, the recursion stops. repeatand||(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.
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
ROW_NUMBER()numbers rows 1, 2, 3 and so on.OVER (...)is what makes it a window function, and it describes the window.PARTITION BYrestarts the numbering for each author, likeGROUP BYbut without merging rows.- Inside each author, the most-viewed post gets number 1. Wrap this query and filter
rank_for_author <= 3to get each author's top three posts. - An ordinary aggregate (
SUM) used as a window function. - With an
ORDER BYin the window, the sum becomes running: each row adds its views to everything before it, giving a cumulative total over time. LAGreads a value from the previous row in the window order, which is perfect for "how much did this change since last time?".LEADreads the next row.
Advanced 4: transactions
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 BEGINLine by line
BEGINopens a transaction. Nothing after it is visible to other sessions until it is committed.- This uses a separate
accountstable to show the classic example: take 500 from account 1... - ...and give it to account 2. If the server crashed between these two lines without a transaction, the money would simply vanish.
- Record the transfer as part of the same unit of work.
COMMITmakes all four changes permanent together. This is the "A" in ACID.- If anything fails, for example a
CHECK (balance >= 0)constraint, the app sendsROLLBACKand the database returns to exactly how it was beforeBEGIN.
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.
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
- Give indexes descriptive names. You will see them in query plans later.
- 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. EXPLAINshows the plan the database chose.ANALYZEactually runs the query and reports real timings.- A line like this is what you want to see. A
Seq Scanon 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
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
use blogswitches to theblogdatabase. If it does not exist yet, MongoDB creates it the first time you write to it. There is noCREATE DATABASEstep.db.usersis theuserscollection, also created on first write.insertOneadds one document and returns its new_id.- A document is a set of field–value pairs, written like a JavaScript object. No schema had to be declared first.
new Date()stores a real date value, which can be sorted and compared, not a string.insertManyadds an array of documents in a single round trip to the server.tagsis an array stored right inside the document.- 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
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
find()with no filter returns every document in the collection, likeSELECT * FROM posts. The shell prints the first 20 and lets you typeitfor more.- A filter object. Listing two fields means both must match, an implicit
AND. This isWHERE author = 'Zin' AND published = true. findtakes up to two arguments: the filter, then a projection.- Operators are nested objects.
{ $gte: 100 }means "greater than or equal to 100". Others include$gt,$lt,$lte,$ne(not equal) and$eq. - The projection picks the fields to return:
1includes a field and0excludes it._idis included unless you turn it off. sorttakes field: direction pairs, where1is ascending and-1descending. This isORDER BY views DESC.limit(10)returns at most ten documents. Together these lines are the "top 10 posts" query from the SQL section.findOnereturns the first matching document itself (ornull) instead of a cursor, which is handy when you expect a single result.- Counts matching documents without returning them, like
SELECT COUNT(*) ... WHERE.
Beginner 3: updating and deleting
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
updateOnechanges the first document that matches the filter.- The first argument is the filter: which document to change.
- 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.
$setwrites fields, creating them if they do not exist yet. Writing the update without$setwould replace the whole document, a classic beginner mistake.$unsetremoves a field completely. The value ("") is ignored.$incadds to a number atomically, likeSET views = views + 1in SQL.$pushappends a value to an array.updateManychanges every matching document.- Querying an array field with a plain value matches documents whose array contains that value. No join table is needed.
$addToSetadds to an array only if the value is not already there, so running this twice does not create duplicates.deleteOneremoves the first match.deleteManyremoves every match. As with SQL, double-check the filter:deleteMany({})empties the collection.
Intermediate 1: query operators and arrays
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
$inmatches any value in the list, the same as SQL'sIN.$regexmatches a regular expression. Theiflag ignores case, and$anchors the match to the end. Note that only patterns anchored at the start (^...) can use an index efficiently.- A filter can mix several conditions.
$ortakes an array of conditions and matches if any of them is true. There is also$andand$nor.$allmatches arrays that contain every listed value, in any order.- Searching inside an array of sub-documents.
$elemMatchrequires that one single array element meets all the conditions. Without it,user: "Aye"could match one comment andlikes >= 5a different one.- Dot notation reaches into embedded documents. The quotes are required because the key contains a dot.
$existsmatches documents that have (or, withfalse, 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.
// 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
- Embed when the child data is small, belongs to exactly one parent and is read together with it.
- Adding a comment is a single atomic update to one document, and reading the post brings every comment with it.
- 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.
postIdplays the role of a foreign key. The difference is that MongoDB does not enforce it. Keeping it valid is your code's job.- 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.
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
aggregatetakes an array of stages. Read it from top to bottom: each stage receives the previous stage's output.$matchfilters documents, likeWHERE. Put it as early as possible so later stages handle fewer documents and it can use an index.$groupmerges documents into groups, likeGROUP BY._idis the group key."$author", a string starting with$, means "the value of theauthorfield", so there is one group per author.$sum: 1adds 1 per document, which counts the documents in the group (COUNT(*)).$sum: "$views"adds up the field's values (SUM(views)).$avg,$min,$maxand$pushwork the same way.- A second
$match, after grouping, filters the groups. This is exactly whatHAVINGdoes in SQL. $sortorders the groups by total views, highest first.$projectreshapes each output document: which fields to keep, rename or compute.- Renames the group key from
_idto a readableauthorfield. $roundis an expression operator. Expressions take arguments in an array, here the value and the number of decimal places.
Advanced 2: $unwind and $lookup
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
$unwindturns one document with an array into one document per element. A post with three tags becomes three documents, each with a singletagsvalue.- Now each tag can be grouped and counted like any other field.
- Result: the five most-used tags across all posts. In SQL this would need the tags join table.
$lookupis MongoDB's left outer join: it pulls matching documents from another collection into each document.fromnames the other collection.localFieldis the field in the current documents (the post's_id)...- ...and
foreignFieldis the field incommentsthat should equal it. asis the name of the new array field that holds the matches. A post with no comments gets an empty array, just likeNULLs in aLEFT JOIN.$addFieldsadds a computed field while keeping all the others.$sizereturns the length of an array.- 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
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
- One shared first stage: only published posts go into every report.
$facetruns 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.- Sub-pipeline 1: the three most-viewed posts.
- Sub-pipeline 2: posts per month.
$dateToStringformats a date as text such as"2026-09", which makes a convenient group key for a month.- Sub-pipeline 3:
$countreturns a single document holding the number of inputs, for a "showing 1–10 of N" line.
Advanced 4: indexes and explain()
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
- A compound index, the same idea as the SQL one: posts by author, newest first. Field order matters here too.
- A unique index makes MongoDB refuse duplicate emails, bringing back one of the guarantees SQL gives you from a schema.
- A text index across two fields enables simple full-text search.
$textwith$searchfinds documents that contain any of the words, using that index.- A query we want to check...
- ...and
explainshows how MongoDB ran it. Look forIXSCAN(it used an index) rather thanCOLLSCAN(it read the whole collection), and comparetotalDocsExaminedwithnReturned.
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.
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
- A session groups operations together, and every transaction belongs to one.
withTransactionstarts the transaction, runs the callback, and commits it. If the callback throws, it aborts. It also retries automatically on temporary errors.- The filter only matches if account 1 has enough money, like a
CHECKconstraint. In real code you would check the result'smatchedCountand throw when it is 0, which aborts the transfer. - Passing
{ session }is what puts this operation inside the transaction. Forget it, and the write happens on its own. - Credit the second account in the same transaction.
- The audit record commits or disappears together with the two balance changes: all or nothing, just like SQL's
BEGIN ... COMMIT. - 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.
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:htmlLine by line
SETstores a string under a key. The colon-separated name (post:7:html) is only a naming convention.EX 3600makes the key expire after an hour, which is the heart of a cache.GETreads it back. If the key is missing or expired, you getnil, and the app falls back to the main database and fills the cache again.TTLshows how many seconds the key has left to live.INCRadds 1 atomically and returns the new value, even with thousands of requests at once. This is perfect for view counters and rate limits.- A hash stores several fields under one key, like a tiny document. It is a common way to keep session data.
HGETALLreturns every field and value in the hash.- A sorted set keeps members ordered by a score, here the players' points.
ZREVRANGE ... 0 2returns the top three, highest score first, together with their scores. A live leaderboard costs one command.DELremoves 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.
-- 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;// 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 အတွက် ရေးထားပါတယ်—
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());တစ်ကြောင်းချင်း ရှင်းလင်းချက်
CREATE TABLEက table အသစ်တစ်ခုနဲ့ သူ ထိန်းထားမယ့် column အားလုံးကို သတ်မှတ်ပါတယ်။ ဒီပုံစံနဲ့ မကိုက်တဲ့ row တွေကို ငြင်းပယ်ပါတယ်။SERIALက အလိုလို တိုးသွားတဲ့ integer ပါ၊ user အသစ်တိုင်း နောက်ဂဏန်းကို ရပါတယ်။PRIMARY KEYက တစ်မူထူးခြားစေပြီး index လုပ်ပေးပါတယ်။NOT NULLဆိုတာ user တိုင်းမှာ နာမည် ရှိရမယ်။ မထည့်ရင် error ဖြစ်ပါတယ်၊ တန်ဖိုးအလွတ် မဟုတ်ပါဘူး။UNIQUEက email တူတဲ့ ဒုတိယ account ကို request နှစ်ခု တစ်ချိန်တည်း ရောက်လာရင်တောင် database ကိုယ်တိုင် ငြင်းပယ်ပေးပါတယ်။- ဒီမှာ
NOT NULLမပါလို့countryက မထည့်လည်း ရပြီးNULL(မသိ) ဖြစ်နိုင်ပါတယ်။ TIMESTAMPTZက time zone ပါတဲ့ အချိန်ပါ။DEFAULT now()က row ထည့်တဲ့အခါ အလိုလို ဖြည့်ပေးပါတယ်။- Post တစ်ခုစီက user တစ်ယောက်ပိုင်ပါတယ်။ One-to-many relationship ရဲ့ "many" ဘက်ပါ။
REFERENCES users(id)က foreign key ပါ — မရှိတဲ့ user ကို post က ညွှန်လို့ မရပါဘူး။ON DELETE CASCADEက user ကို ဖျက်ရင် သူ့ post တွေကိုပါ ဖျက်ပေးပါတယ်။CHECKconstraint က write တိုင်းမှာ database က စစ်ပေးတဲ့ rule ပါ။ ဒီမှာ view အရေအတွက် ဘယ်တော့မှ အနုတ် မဖြစ်နိုင်ပါဘူး။- ရည်ရွယ်ချက်ရှိရှိ null ဖြစ်ခွင့် ပေးထားတာပါ —
published_atမရှိတဲ့ post က draft သာ ဖြစ်ပါသေးတယ်။ - Comment တွေက post တစ်ခုနဲ့ ရေးတဲ့ user ကို ညွှန်လို့ comment row တစ်ခုက table သုံးခုကို ချိတ်ဆက်ထားပါတယ်။
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 တွေက အတူတူ နေတာကို သတိထားကြည့်ပါ—
{ _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") } ]}တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Document တိုင်းမှာ
_idရှိပါတယ်။ MongoDB ကObjectIdကို အလိုလို ထုတ်ပေးပါတယ် — timestamp ပါတဲ့ 12 byte ဖြစ်လို့ id တွေက ဖန်တီးတဲ့ အချိန်အလိုက် အကြမ်းဖျင်း စီပြီးသား ဖြစ်ပါတယ်။ - Author ကို sub-document အသေးလေးအဖြစ် embed လုပ်ထားပါတယ်။ ဒုတိယ lookup မလုပ်ဘဲ post ကို ပြနိုင်ဖို့ နာမည် မိတ္တူကို ထားတာပါ။ အထဲက
_idနဲ့ လိုတဲ့အခါ user အပြည့်အစုံကို ရှာလို့ ရပါသေးတယ်။ - Array တွေက ပုံမှန် value တွေပါပဲ။ SQL မှာဆိုရင် tag တွေအတွက်
tagstable သီးသန့်နဲ့post_tagsjoin table လိုပါလိမ့်မယ်။ ISODateက shell မှာ string မဟုတ်ဘဲ စီလို့၊ နှိုင်းယှဉ်လို့ ရတဲ့ ရက်စွဲ type အစစ်ကို ရေးတဲ့ နည်းပါ။- 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နဲ့CHECKconstraint တွေက 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 တွေ၊ ဘယ်အစီအစဉ်နဲ့၊ ဘယ်နှခု။
SELECT id, title, viewsFROM postsWHERE published_at IS NOT NULL AND views >= 100ORDER BY views DESCLIMIT 10;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- ပြန်လိုချင်တဲ့ column တွေကို နာမည်တပ်ပါ။
SELECT *က column အားလုံးကို ပြန်ပေးပါတယ်၊ စူးစမ်းနေတုန်း အဆင်ပြေပေမဲ့ တကယ့် code မှာ ဖြုန်းတီးရာ ရောက်ပါတယ်။ FROMက ဘယ် table ကို ဖတ်မလဲ ပြောပါတယ်။WHEREက row တွေကို စစ်ထုတ်ပါတယ်။NULLဆိုတာ "တန်ဖိုး မရှိ" ဖြစ်ပြီးIS NULLဒါမှမဟုတ်IS NOT NULLနဲ့သာ စစ်လို့ ရပါတယ်၊= NULLနဲ့ ဘယ်တော့မှ မရပါဘူး။ANDက condition တွေကို ပေါင်းပါတယ်။ Row တစ်ခု ကျန်ဖို့ နှစ်ခုလုံး မှန်ရပါမယ်။ORDER BY ... DESCက အများဆုံးကနေ အနည်းဆုံးကို စီပါတယ်။ORDER BYမပါရင် SQL က ဘာအစီအစဉ်မှ အာမမခံပါဘူး။LIMITက စီပြီးသား ရလဒ်ရဲ့ ပထမ row ၁၀ ခုကိုသာ ယူပါတယ် — "top 10 posts" စာရင်းပါ။
Beginner ၂: pattern၊ list နဲ့ range တွေနဲ့ စစ်ထုတ်ခြင်း
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
IN (...)က list ထဲက တန်ဖိုး တစ်ခုခုနဲ့ ကိုက်ပါတယ်။country = 'Myanmar' OR country = 'Thailand'ကို တိုတိုရေးတဲ့ နည်းပါ။LIKEက pattern နဲ့ ကိုက်ပါတယ် —%က "ဘယ် စာလုံးမဆို" ဆိုတဲ့ အဓိပ္ပာယ်ပါ။ ဒါကြောင့် Gmail လိပ်စာ အားလုံးကို ရှာပေးပါတယ်။ PostgreSQL ရဲ့ILIKEက စာလုံးအကြီးအသေး ဂရုမစိုက်ဘဲ အတူတူ လုပ်ပါတယ်။BETWEENက range ရဲ့ အစွန်း နှစ်ဖက်လုံး ပါပါတယ်။'2026-01-01'လို စာသားကို timestamp အဖြစ် အလိုလို ပြောင်းပေးပါတယ်။- အက္ခရာစဉ်အတိုင်း စီပါတယ် (ascending က default ပါ)။
Beginner ၃: ထည့်ခြင်း၊ ပြင်ခြင်းနဲ့ ဖျက်ခြင်း
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';တစ်ကြောင်းချင်း ရှင်းလင်းချက်
INSERT INTOက row တစ်ခု ထည့်ပါတယ်။ တန်ဖိုးပေးမယ့် column တွေကိုသာ စာရင်းပြုထားပါတယ် —idနဲ့created_atက ကိုယ့်ဘာသာ ဖြည့်သွားပါတယ်။VALUESက column စာရင်းနဲ့ အစီအစဉ်တူ တန်ဖိုးတွေ ပေးပါတယ်။ စာသားကို single quote ထဲ ထည့်ပါတယ်။RETURNING(PostgreSQL) က ထုတ်ပေးလိုက်တဲ့ တန်ဖိုးတွေကို ပြန်ပေးလို့ app က ဒုတိယ query မလိုဘဲ id အသစ်ကို သိပါတယ်။UPDATEက table ထဲက ရှိပြီးသား row တွေကို ပြောင်းပါတယ်။SETက တန်ဖိုးအသစ်တွေ ပေးပါတယ်။ တန်ဖိုးဟောင်းကို သုံးလို့ရလို့ ဒါက atomic "တစ်ပေါင်း" ပါ၊ ဧည့်သည် အများကြီး တစ်ပြိုင်နက် ရှိရင်တောင် စိတ်ချရပါတယ်။UPDATEမှာWHEREကို အမြဲ စစ်ပါ။ မပါရင် table ထဲက post အားလုံး ပြောင်းသွားပါလိမ့်မယ်။DELETE FROMက row တွေကို ဖယ်ရှားပါတယ်။UPDATEလိုပဲ မစစ်ထုတ်ရင် row အားလုံးကို သက်ရောက်ပါတယ်။- ရက်စွဲ တွက်ချက်ခြင်း — "အခု၊ နှစ်နှစ် နုတ်"။ Comment အဟောင်းတွေကိုသာ ဖျက်ပါတယ်။
Intermediate ၁: JOIN များ
Join က condition ကိုက်တဲ့ နေရာမှာ table နှစ်ခုက row တွေကို ပေါင်းပါတယ်၊ များသောအားဖြင့် foreign key က primary key နဲ့ ညီတဲ့နေရာ။
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Column တွေက table နှစ်ခုထဲက ဘယ်ဟာကမဆို လာနိုင်ပါတယ်။
AS authorက ရလဒ်ထဲက column နာမည်ကို ပြောင်းပါတယ်။ AS pက table ကို alias အတိုလေး ပေးလို့posts.titleအစားp.titleလို့ ရေးလို့ ရပါတယ်။INNER JOINကONcondition မှန်တဲ့ အတွဲတွေကိုသာ ထားပါတယ် — post တစ်ခုစီနဲ့ သူ့ author။ User ဖျက်ခံရပြီးသား post ဆိုရင် မပေါ်ပါဘူး။- ဒုတိယ query က မေးခွန်းဟောင်းတစ်ခုကို ဖြေပါတယ် — ဘယ် user တွေ post တစ်ခါမှ မရေးဖူးလဲ?
LEFT JOINက ဘယ်ဘက် table (users) က row အားလုံး ကို ထားပါတယ်။ User မှာ post မရှိရင် post column တွေကိုNULLနဲ့ ဖြည့်ပါတယ်။- Post ဘက်က
NULLဖြစ်တဲ့ row တွေကိုသာ ထားရင် post မရှိတဲ့ user တွေ အတိအကျ ကျန်ပါတယ်။ ဒီ "anti-join" pattern ကို မှတ်ထားသင့်ပါတယ်။
Intermediate ၂: GROUP BY နဲ့ HAVING ဖြင့် aggregation
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Group လုပ်မယ့် column ပါ။ နိုင်ငံ တစ်ခုစီက ရလဒ်ထဲမှာ row တစ်ခု ဖြစ်လာပါတယ်။
COUNT(*)က group တစ်ခုစီထဲက row တွေကို ရေတွက်ပါတယ်၊ ဒီမှာတော့ post အရေအတွက်ပါ။SUM၊AVG၊MINနဲ့MAXတွေက aggregate function တွေပါ — row အများကြီးကို group တစ်ခုလျှင် တန်ဖိုး တစ်ခုအဖြစ် ချုံ့ပါတယ်။- ရိုးရိုး
JOINဆိုတာINNER JOINပါ။ WHEREက group မလုပ်ခင် row တစ်ခုချင်းကို အရင် စစ်ထုတ်ပါတယ်။GROUP BYက နိုင်ငံ တူတဲ့ row တွေကို စုပါတယ်။ ရွေးထားတဲ့ column တိုင်း group လုပ်ထားရမယ် ဒါမှမဟုတ် aggregate ထဲမှာ ရှိရမယ်။HAVINGက aggregation ပြီးမှ group တွေကို စစ်ထုတ်ပါတယ်၊WHEREလုပ်လို့ မရတဲ့ အရာပါ။ Post အနည်းဆုံး ငါးခု ရှိတဲ့ နိုင်ငံတွေကိုသာ ထားပါတယ်။
Intermediate ၃: subquery များ
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);တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Scalar subquery က တန်ဖိုး တစ်ခုတည်း ပြန်ပေးပါတယ်၊ ဒီမှာ ပျမ်းမျှ view ကို အပြင်က query က နှိုင်းယှဉ်ပါတယ်။ ရလဒ်က ပျမ်းမျှထက် များတဲ့ post တွေပါ။
EXISTSက အထဲက query က row အနည်းဆုံး တစ်ခု ပြန်ပေးရင် မှန်ပါတယ်။ ဒီစာကြောင်းတွေက အနည်းဆုံး တစ်ခါ comment ပေးဖူးတဲ့ user တွေကို ရှာပါတယ်။- Row ထဲမှာ ဘာရှိလဲ မဟုတ်ဘဲ row ရှိ မရှိ ကိုသာ ဂရုစိုက်လို့
SELECT 1လို့ ရေးတာပါ။ - ဒါက correlated subquery ပါ — အပြင် query က
u.idကို ရည်ညွှန်လို့ user တစ်ယောက်ချင်းအလိုက် တွက်ပါတယ်။ Planner က များသောအားဖြင့် ထိရောက်တဲ့ join အဖြစ် ပြောင်းပေးပါတယ်။
Advanced ၁: WITH နဲ့ CTE များ
Common table expression (CTE) က subquery ကို နာမည်ပေးလို့ query ရှည်ကြီးကို အဆင့်ငယ်လေးတွေလို အပေါ်ကနေ အောက်ကို ဖတ်လို့ ရစေပါတယ်။
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
WITH monthly AS (...)က ယာယီ နာမည်ပေးထားတဲ့ ရလဒ်ကို သတ်မှတ်ပါတယ် — user တစ်ယောက်ချင်း၊ လတစ်လချင်းအလိုက် post အရေအတွက်။ ဒီ query တစ်ခုအတွက်သာ ရှိပါတယ်။date_truncက timestamp ကို သူ့လရဲ့ ပထမဆုံး အချိန်အထိ ဖြတ်ချလို့ စက်တင်ဘာလထဲက post အားလုံး group တစ်ခုတည်းထဲ ရောက်ပါတယ်။- CTE တွေကို comma နဲ့ ဆက်လို့ ရပြီး နောက်က တစ်ခုက အရင်တစ်ခုကို ဖတ်လို့ ရပါတယ်။
activeက အနည်းဆုံး လ သုံးလ ကွဲကွဲ post တင်ခဲ့တဲ့ user တွေကို ထားပါတယ်။- နောက်ဆုံး
SELECTက CTE တွေကို ရိုးရိုး table တွေလို ဖတ်ပါတယ်။ - Column နှစ်ခုလုံး နာမည်တူတဲ့အခါ
USING (user_id)ကON m.user_id = a.user_idရဲ့ အတိုကောက်ပါ။
Advanced ၂: tree များအတွက် recursive CTE
comments.parent_id ကို မှတ်မိသေးလား? Reply တွေကို ပြန်ဖြေတဲ့ reply တွေက အနက် ဘယ်လောက်မဆို ရှိနိုင်တဲ့ tree ဖြစ်လာပါတယ်။ Recursive CTE က query တစ်ခုတည်းနဲ့ အဲဒါကို လျှောက်ကြည့်ပါတယ်။
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
RECURSIVEက CTE ကို ကိုယ့်ကိုယ်ကိုယ် ပြန်ရည်ညွှန်ခွင့် ပေးပါတယ်။- Anchor အပိုင်း — စတင်တဲ့ row တွေပါ။
depthcounter ကိုလည်း 1 ကနေ စပါတယ်။ - Post 7 ပေါ်က ထိပ်ဆုံးအဆင့် comment တွေက parent မရှိတဲ့ ဟာတွေပါ။
UNION ALLက anchor row တွေကို recursive အပိုင်းက ဆက်ရှာတွေ့နေတဲ့ row တွေနဲ့ ကပ်ပါတယ်။- Recursive အပိုင်း — အလှည့်တိုင်း အရင်အလှည့်က တွေ့ခဲ့တဲ့ row တွေရဲ့ child တွေကို တစ်ဆင့် ပိုနက်ပြီး ထည့်ပါတယ်။
commentsကိုthreadကိုယ်တိုင်နဲ့ join လုပ်လို့ parent က ရလဒ်ထဲ ရှိပြီးသား comment တိုင်းကို တွေ့ပါတယ်။ အလှည့်တစ်ခုမှာ အသစ် ဘာမှ မတွေ့တော့ရင် recursion ရပ်ပါတယ်။repeatနဲ့||(string ဆက်ခြင်း) က reply တစ်ခုစီကို သူ့ depth အလိုက် အထဲတိုးလို့ ရလဒ်က thread တစ်ခုလို ဖတ်ရပါတယ်။
Advanced ၃: window function များ
Window function တွေက row အစုတစ်ခုကို ဖြတ်ပြီး တွက်ပါတယ်၊ ဒါပေမဲ့ GROUP BY လို row တွေကို မချုံ့ ပါဘူး။ Row တစ်ခုစီက သူ့ကိုယ်ပိုင် အမှတ်လက္ခဏာ ထိန်းထားပြီး တွက်ထားတဲ့ တန်ဖိုး တစ်ခု ထပ်ရပါတယ်။
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;တစ်ကြောင်းချင်း ရှင်းလင်းချက်
ROW_NUMBER()က row တွေကို 1၊ 2၊ 3 စသည်ဖြင့် နံပါတ်တပ်ပါတယ်။OVER (...)က window function ဖြစ်စေတာပါ၊ window ကို ဖော်ပြပါတယ်။PARTITION BYက author တစ်ယောက်ချင်းအတွက် နံပါတ်တပ်တာကို ပြန်စပါတယ်၊GROUP BYနဲ့တူပေမဲ့ row တွေကို မပေါင်းပါဘူး။- Author တစ်ယောက်ချင်းထဲမှာ view အများဆုံး post က နံပါတ် 1 ရပါတယ်။ ဒီ query ကို အုပ်ပြီး
rank_for_author <= 3နဲ့ စစ်ထုတ်ရင် author တစ်ယောက်ချင်းရဲ့ top 3 post ကို ရပါတယ်။ - ရိုးရိုး aggregate (
SUM) ကို window function အဖြစ် သုံးထားတာပါ။ - Window ထဲမှာ
ORDER BYပါရင် sum က running ဖြစ်သွားပါတယ် — row တစ်ခုစီက သူ့ view ကို အရင်က အားလုံးနဲ့ ပေါင်းလို့ အချိန်အလိုက် စုစုပေါင်း ရပါတယ်။ LAGက window အစီအစဉ်ထဲက အရင် row ရဲ့ တန်ဖိုးကို ဖတ်ပါတယ်၊ "ပြီးခဲ့တဲ့အကြိမ်ကနေ ဘယ်လောက် ပြောင်းသွားလဲ?" အတွက် အကောင်းဆုံးပါ။LEADက နောက် row ကို ဖတ်ပါတယ်။
Advanced ၄: transaction များ
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တစ်ကြောင်းချင်း ရှင်းလင်းချက်
BEGINက transaction ကို ဖွင့်ပါတယ်။ Commit မလုပ်မချင်း သူ့နောက်က ဘာမှ တခြား session တွေကို မမြင်ရပါဘူး။- ဥပမာ ဂန္ထဝင်ကို ပြဖို့
accountstable သီးသန့် သုံးထားပါတယ် — account 1 ကနေ 500 ယူ... - ...account 2 ကို ပေးပါ။ Transaction မပါဘဲ ဒီစာကြောင်း နှစ်ကြောင်းကြား server ပျက်သွားရင် ပိုက်ဆံက ပျောက်သွားရုံပါပဲ။
- လွှဲပြောင်းမှုကို အလုပ်အစု တစ်ခုတည်းရဲ့ အစိတ်အပိုင်းအဖြစ် မှတ်တမ်းတင်ပါတယ်။
COMMITက အပြောင်းအလဲ လေးခုလုံးကို အတူတကွ အမြဲတမ်း ဖြစ်စေပါတယ်။ ဒါက ACID ထဲက "A" ပါ။- ဥပမာ
CHECK (balance >= 0)constraint လို တစ်ခုခု မအောင်မြင်ရင် app ကROLLBACKပို့ပြီး database ကBEGINမတိုင်ခင်က အခြေအနေအတိုင်း အတိအကျ ပြန်ဖြစ်သွားပါတယ်။
Advanced ၅: index များနဲ့ EXPLAIN
Query တွေက သူတို့နောက်က index တွေလောက်သာ မြန်ပါတယ်။ Index က table တစ်ခုလုံး မဖတ်ဘဲ ကိုက်တဲ့ row တွေဆီ တန်းခုန်သွားနိုင်စေတဲ့ စီထားတဲ့ ဖွဲ့စည်းပုံ (များသောအားဖြင့် B-tree) ပါ။
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)တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Index တွေကို ဖော်ပြချက်ပါတဲ့ နာမည် ပေးပါ။ နောက်ပိုင်း query plan ထဲမှာ မြင်ရပါလိမ့်မယ်။
- Column နှစ်ခုပေါ်က composite index ပါ၊ ဒီအစီအစဉ်အတိုင်း။ "User X ရဲ့ post တွေ၊ အသစ်ဆုံး အရင်" ကို ပြည့်ပြည့်ဝဝ ဆောင်ရွက်ပေးပါတယ်။ Column အစီအစဉ် အရေးကြီးပါတယ် —
published_atနဲ့သာ စစ်ထုတ်တဲ့ query ကို မကူညီပါဘူး။ EXPLAINက database ရွေးလိုက်တဲ့ plan ကို ပြပါတယ်။ANALYZEက query ကို တကယ် run ပြီး အချိန် အစစ်တွေကို ပြပါတယ်။- ဒီလို စာကြောင်းမျိုး မြင်ချင်တာပါ။ 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 များ ထည့်ခြင်း
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" }])တစ်ကြောင်းချင်း ရှင်းလင်းချက်
use blogကblogdatabase ကို ပြောင်းပါတယ်။ မရှိသေးရင် ပထမဆုံး write လုပ်တဲ့အခါ MongoDB က ဖန်တီးပေးပါတယ်။CREATE DATABASEအဆင့် မရှိပါဘူး။db.usersကuserscollection ပါ၊ ပထမဆုံး write မှာ ဖန်တီးပါတယ်။insertOneက document တစ်ခု ထည့်ပြီး သူ့_idအသစ်ကို ပြန်ပေးပါတယ်။- Document ဆိုတာ JavaScript object လို ရေးထားတဲ့ field–value အတွဲ အစုပါ။ Schema ကို ကြိုကြေညာစရာ မလိုခဲ့ပါဘူး။
new Date()က string မဟုတ်ဘဲ စီလို့၊ နှိုင်းယှဉ်လို့ ရတဲ့ ရက်စွဲ တန်ဖိုး အစစ်ကို သိမ်းပါတယ်။insertManyက document array တစ်ခုကို server ဆီ တစ်ခေါက်တည်းနဲ့ ထည့်ပါတယ်။tagsက document ထဲမှာ တိုက်ရိုက် သိမ်းထားတဲ့ array ပါ။- ဒုတိယ post မှာ ပထမ post မှာ မရှိတဲ့ field (
draftNote) ရှိပါတယ်။ Collection တစ်ခုတည်းထဲမှာ လုံးဝ တရားဝင်ပါတယ်။ ဒါက schema ပြောင်းလွယ်မှုကို လက်တွေ့ ပြတာပါ။
Beginner ၂: document များ ရှာခြင်း
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 })တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Filter မပါတဲ့
find()က collection ထဲက document အားလုံးကို ပြန်ပေးပါတယ်၊SELECT * FROM postsလိုပါပဲ။ Shell က ပထမ ၂၀ ကို ပြပြီး နောက်ထပ်အတွက်itရိုက်ခိုင်းပါတယ်။ - Filter object ပါ။ Field နှစ်ခု စာရင်းပြုရင် နှစ်ခုလုံး ကိုက်ရမယ်၊ အလိုလို
ANDပါ။ ဒါကWHERE author = 'Zin' AND published = trueပါ။ findက argument နှစ်ခုအထိ ယူပါတယ် — filter နဲ့ နောက်မှ projection။- Operator တွေက nested object တွေပါ။
{ $gte: 100 }က "100 နဲ့ ညီ ဒါမှမဟုတ် ပိုကြီး" ပါ။ တခြား$gt၊$lt၊$lte၊$ne(မညီ) နဲ့$eqတို့ ရှိပါတယ်။ - Projection က ပြန်ပေးမယ့် field တွေကို ရွေးပါတယ် —
1က ထည့်ပြီး0က ဖယ်ပါတယ်။_idကို ပိတ်မထားရင် ပါပါတယ်။ sortက field: direction အတွဲတွေ ယူပါတယ်၊1က ငယ်ရာကနေ ကြီးရာ၊-1က ကြီးရာကနေ ငယ်ရာ။ ဒါကORDER BY views DESCပါ။limit(10)က document အများဆုံး ၁၀ ခု ပြန်ပေးပါတယ်။ ဒီစာကြောင်းတွေ ပေါင်းရင် SQL အပိုင်းက "top 10 posts" query ပါ။findOneက cursor အစား ကိုက်တဲ့ ပထမ document ကိုယ်တိုင် (ဒါမှမဟုတ်null) ကို ပြန်ပေးလို့ ရလဒ် တစ်ခုတည်း မျှော်လင့်တဲ့အခါ အဆင်ပြေပါတယ်။- ကိုက်တဲ့ document တွေကို ပြန်မပေးဘဲ ရေတွက်ပါတယ်၊
SELECT COUNT(*) ... WHEREလိုပါ။
Beginner ၃: ပြင်ခြင်းနဲ့ ဖျက်ခြင်း
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 })တစ်ကြောင်းချင်း ရှင်းလင်းချက်
updateOneက filter နဲ့ ကိုက်တဲ့ ပထမ document ကို ပြောင်းပါတယ်။- ပထမ argument က filter ပါ — ဘယ် document ကို ပြောင်းမလဲ။
- ဒုတိယ argument က update ပါ၊ update operator အစုတစ်ခုပါ။ Operator တစ်ခုစီက document ကို မတူတဲ့နည်းနဲ့ ပြောင်းပြီး အားလုံး atomic အနေနဲ့ အတူတကွ သက်ရောက်ပါတယ်။
$setက field တွေကို ရေးပါတယ်၊ မရှိသေးရင် ဖန်တီးပါတယ်။$setမပါဘဲ update ကို ရေးရင် document တစ်ခုလုံးကို အစားထိုး သွားပါတယ် — beginner တွေ မှားလေ့ရှိတဲ့ အမှားပါ။$unsetက field ကို လုံးဝ ဖယ်ရှားပါတယ်။ တန်ဖိုး ("") ကို လျစ်လျူရှုပါတယ်။$incက ဂဏန်းကို atomic အနေနဲ့ ပေါင်းပါတယ်၊ SQL ကSET views = views + 1လိုပါ။$pushက array ထဲ တန်ဖိုးကို နောက်ဆုံးမှာ ထပ်ထည့်ပါတယ်။updateManyက ကိုက်တဲ့ document တိုင်း ကို ပြောင်းပါတယ်။- Array field ကို ရိုးရိုး တန်ဖိုးနဲ့ query လုပ်ရင် array ထဲမှာ အဲဒီတန်ဖိုး ပါတဲ့ document တွေနဲ့ ကိုက်ပါတယ်။ Join table မလိုပါဘူး။
$addToSetက တန်ဖိုး မရှိသေးမှသာ array ထဲ ထည့်လို့ ဒါကို နှစ်ခါ run ရင်တောင် ထပ်နေတာ မဖြစ်ပါဘူး။deleteOneက ပထမ ကိုက်တဲ့ ဟာကို ဖယ်ရှားပါတယ်။deleteManyက ကိုက်တဲ့ ဟာ အားလုံးကို ဖယ်ရှားပါတယ်။ SQL လိုပဲ filter ကို ထပ်စစ်ပါ —deleteMany({})က collection ကို ရှင်းလင်းသွားပါတယ်။
Intermediate ၁: query operator များနဲ့ array များ
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 } })တစ်ကြောင်းချင်း ရှင်းလင်းချက်
$inက list ထဲက တန်ဖိုး တစ်ခုခုနဲ့ ကိုက်ပါတယ်၊ SQL ရဲ့INနဲ့ တူတူပါ။$regexက regular expression နဲ့ ကိုက်ပါတယ်။iflag က စာလုံးအကြီးအသေး လျစ်လျူရှုပြီး$က အဆုံးမှာ ချည်ပါတယ်။ အစမှာ ချည်ထားတဲ့ (^...) pattern တွေသာ index ကို ထိရောက်စွာ သုံးနိုင်တာ သတိပြုပါ။- Filter တစ်ခုမှာ condition အများကြီး ရောနိုင်ပါတယ်။
$orက condition array တစ်ခု ယူပြီး တစ်ခုခု မှန်ရင် ကိုက်ပါတယ်။$andနဲ့$norလည်း ရှိပါတယ်။$allက စာရင်းထဲက တန်ဖိုး အားလုံး ပါတဲ့ array တွေနဲ့ ကိုက်ပါတယ်၊ ဘယ်အစီအစဉ်မဆို။- Sub-document array ထဲမှာ ရှာခြင်းပါ။
$elemMatchက array element တစ်ခုတည်း က condition အားလုံးကို ပြည့်မီရမယ်လို့ တောင်းဆိုပါတယ်။ မပါရင်user: "Aye"က comment တစ်ခုနဲ့၊likes >= 5က တခြား comment တစ်ခုနဲ့ ကိုက်သွားနိုင်ပါတယ်။- Dot notation က embed လုပ်ထားတဲ့ document ထဲကို ရောက်ပါတယ်။ Key ထဲမှာ dot ပါလို့ quote လိုပါတယ်။
$existsက field ရှိတဲ့ (falseနဲ့ဆိုရင် မရှိတဲ့) document တွေနဲ့ ကိုက်ပါတယ်။ ဒီမေးခွန်းက schema ပြောင်းလွယ်တဲ့နေရာမှာသာ အဓိပ္ပာယ် ရှိပါတယ်။
Intermediate ၂: embedding နဲ့ referencing
MongoDB မှာ အကြီးဆုံး ဒီဇိုင်း ဆုံးဖြတ်ချက်က query တစ်ခု မဟုတ်ပါဘူး။ ဆက်စပ်တဲ့ data ကို document တစ်ခုထဲ embed လုပ်မလား ဒါမှမဟုတ် တခြား collection ကနေ id နဲ့ reference လုပ်မလား ဆိုတာပါ။
// 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 })တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Child data က သေးငယ်ပြီး parent တစ်ခုတည်းပိုင်ကာ အတူတူ ဖတ်တဲ့အခါ embed လုပ်ပါ။
- Comment ထည့်တာက document တစ်ခုပေါ်က atomic update တစ်ခုတည်းပါ၊ post ကို ဖတ်ရင် comment အားလုံး ပါလာပါတယ်။
- Child စာရင်းက အကန့်အသတ်မရှိ ကြီးလာနိုင်တဲ့အခါ ဒါမှမဟုတ် သီးသန့် query လုပ်တဲ့အခါ reference လုပ်ပါ။ MongoDB document တစ်ခုက 16 MB အထိသာ ရလို့ comment ၁ သိန်း ရှိတဲ့ post က သူတို့ကို embed မလုပ်ရပါဘူး။
postIdက foreign key အခန်းကဏ္ဍ ပါဝင်ပါတယ်။ ကွာခြားချက်က MongoDB က အတင်းအကျပ် မစစ်တာပါ။ မှန်ကန်အောင် ထိန်းတာ သင့် code ရဲ့ တာဝန်ပါ။- Comment တွေ ဖတ်တာ အခု ဒုတိယ query ဖြစ်သွားပြီး (အောက်က)
$lookupက အဲဒါကို တစ်ခုတည်းအဖြစ် ပေါင်းပေးပါတယ်။ လက်ကိုင်စည်းမျဉ်း — data ကို ဘယ်လို ဖတ်မလဲပေါ် မူတည်ပြီး model လုပ်ပါ။
Advanced ၁: aggregation pipeline
ရိုးရိုး စစ်ထုတ်တာထက် ကျော်တဲ့ ဘာမဆိုအတွက် MongoDB က aggregation pipeline ကို သုံးပါတယ် — stage တွေရဲ့ array ပါ။ Document တွေက stage တွေကို အစီအစဉ်အတိုင်း ဖြတ်စီးပြီး stage တစ်ခုစီက လက်ခံရတာကို ပြောင်းလဲပါတယ်၊ Unix pipe တွေလိုပါ။ ဒါက GROUP BY၊ HAVING နဲ့ အများကြီး ပိုတာတွေနဲ့ ညီမျှပါတယ်။
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] } } }])တစ်ကြောင်းချင်း ရှင်းလင်းချက်
aggregateက stage array တစ်ခု ယူပါတယ်။ အပေါ်ကနေ အောက်ကို ဖတ်ပါ — stage တစ်ခုစီက အရင် stage ရဲ့ output ကို လက်ခံပါတယ်။$matchက document တွေကို စစ်ထုတ်ပါတယ်၊WHEREလိုပါ။ နောက် stage တွေ document နည်းနည်းပဲ ကိုင်ရအောင်နဲ့ index သုံးနိုင်အောင် တတ်နိုင်သမျှ စောစော ထားပါ။$groupက document တွေကို group တွေအဖြစ် ပေါင်းပါတယ်၊GROUP BYလိုပါ။_idက group key ပါ။$နဲ့ စတဲ့ string"$author"က "authorfield ရဲ့ တန်ဖိုး" ဆိုလိုလို့ author တစ်ယောက်လျှင် group တစ်ခု ရပါတယ်။$sum: 1က document တစ်ခုလျှင် 1 ပေါင်းလို့ group ထဲက document တွေကို ရေတွက်ပါတယ် (COUNT(*))။$sum: "$views"က field ရဲ့ တန်ဖိုးတွေကို ပေါင်းပါတယ် (SUM(views))။$avg၊$min၊$maxနဲ့$pushတို့လည်း ဒီအတိုင်း အလုပ်လုပ်ပါတယ်။- Group လုပ်ပြီးနောက် ဒုတိယ
$matchက group တွေကို စစ်ထုတ်ပါတယ်။ SQL မှာHAVINGလုပ်တာ အတိအကျပါ။ $sortက group တွေကို စုစုပေါင်း view အလိုက် အများဆုံးကနေ စီပါတယ်။$projectက output document တစ်ခုစီကို ပုံစံပြန်ချပါတယ် — ဘယ် field တွေ ထားမလဲ၊ နာမည်ပြောင်းမလဲ၊ တွက်မလဲ။- Group key ကို
_idကနေ ဖတ်ရလွယ်တဲ့authorfield အဖြစ် နာမည်ပြောင်းပါတယ်။ $roundက expression operator ပါ။ Expression တွေက argument တွေကို array ထဲမှာ ယူပါတယ်၊ ဒီမှာ တန်ဖိုးနဲ့ ဒသမနေရာ အရေအတွက်။
Advanced ၂: $unwind နဲ့ $lookup
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 } }])တစ်ကြောင်းချင်း ရှင်းလင်းချက်
$unwindက array ပါတဲ့ document တစ်ခုကို element တစ်ခုလျှင် document တစ်ခု ဖြစ်အောင် ပြောင်းပါတယ်။ Tag သုံးခုပါတဲ့ post ကtagsတန်ဖိုး တစ်ခုစီပါတဲ့ document သုံးခု ဖြစ်သွားပါတယ်။- အခု tag တစ်ခုစီကို တခြား field တွေလိုပဲ group လုပ်ပြီး ရေတွက်လို့ ရပါပြီ။
- ရလဒ် — post အားလုံးမှာ အသုံးအများဆုံး tag ငါးခု။ SQL မှာဆိုရင် tag join table လိုပါလိမ့်မယ်။
$lookupက MongoDB ရဲ့ left outer join ပါ — တခြား collection က ကိုက်တဲ့ document တွေကို document တစ်ခုစီထဲ ဆွဲထည့်ပါတယ်။fromက တခြား collection ရဲ့ နာမည်ပါ။localFieldက လက်ရှိ document တွေထဲက field (post ရဲ့_id) ပါ...- ...
foreignFieldက အဲဒါနဲ့ ညီရမယ့်commentsထဲက field ပါ။ asက ကိုက်တာတွေကို ထိန်းမယ့် array field အသစ်ရဲ့ နာမည်ပါ။ Comment မရှိတဲ့ post ကLEFT JOINထဲကNULLတွေလိုပဲ array အလွတ် ရပါတယ်။$addFieldsက တခြား field အားလုံးကို ထိန်းထားရင်း တွက်ထားတဲ့ field ထည့်ပါတယ်။$sizeက array ရဲ့ အလျားကို ပြန်ပေးပါတယ်။- ပြီးမှ comment array အပြည့်ကို ဖယ်ပြီး အရေအတွက်ကိုသာ ထားလို့ network ပေါ် data နည်းနည်းပဲ သွားပါတယ်။
Advanced ၃: $facet နဲ့ report အများကြီးကို တစ်ခါတည်း
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" } ] } }])တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- မျှဝေထားတဲ့ ပထမ stage တစ်ခု — published post တွေသာ report တိုင်းထဲ ဝင်ပါတယ်။
$facetက တူညီတဲ့ input document တွေပေါ်မှာ sub-pipeline အများကြီး run ပြီး sub-pipeline တစ်ခုလျှင် array တစ်ခုပါတဲ့ document တစ်ခု ပြန်ပေးပါတယ်။ Dashboard တွေနဲ့ filter count ပါတဲ့ search page တွေအတွက် အကောင်းဆုံးပါ။- Sub-pipeline ၁ — view အများဆုံး post သုံးခု။
- Sub-pipeline ၂ — လတစ်လချင်းအလိုက် post အရေအတွက်။
$dateToStringက ရက်စွဲကို"2026-09"လို စာသားအဖြစ် ပုံစံချလို့ လအတွက် အဆင်ပြေတဲ့ group key ဖြစ်ပါတယ်။- Sub-pipeline ၃ —
$countက input အရေအတွက် ပါတဲ့ document တစ်ခုတည်း ပြန်ပေးပါတယ်၊ "N ခုထဲက 1–10 ကို ပြနေတယ်" စာကြောင်းအတွက်။
Advanced ၄: index များနဲ့ explain()
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")တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Compound index ပါ၊ SQL က ဟာနဲ့ အယူအဆ တူပါတယ် — author အလိုက် post တွေ၊ အသစ်ဆုံး အရင်။ ဒီမှာလည်း field အစီအစဉ် အရေးကြီးပါတယ်။
- Unique index က email ထပ်တာကို MongoDB ငြင်းအောင် လုပ်ပေးလို့ SQL က schema ကနေ ပေးတဲ့ အာမခံချက်တစ်ခုကို ပြန်ယူလာပါတယ်။
- Field နှစ်ခုပေါ်က text index က ရိုးရှင်းတဲ့ full-text search ကို ဖြစ်နိုင်စေပါတယ်။
$searchပါတဲ့$textက အဲဒီ index ကို သုံးပြီး စကားလုံး တစ်ခုခု ပါတဲ့ document တွေကို ရှာပါတယ်။- စစ်ကြည့်ချင်တဲ့ query တစ်ခု...
- ...
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 ကို သုံးပါတယ်။
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();}တစ်ကြောင်းချင်း ရှင်းလင်းချက်
- Session က operation တွေကို အုပ်စုဖွဲ့ပါတယ်၊ transaction တိုင်းက session တစ်ခုပိုင်ပါတယ်။
withTransactionက transaction ကို စ၊ callback ကို run ပြီး commit လုပ်ပါတယ်။ Callback က throw ရင် abort လုပ်ပါတယ်။ ယာယီ error တွေမှာ အလိုလို ပြန်ကြိုးစားပါတယ်။- Account 1 မှာ ပိုက်ဆံ လုံလောက်မှသာ filter က ကိုက်ပါတယ်၊
CHECKconstraint လိုပါ။ တကယ့် code မှာ ရလဒ်ရဲ့matchedCountကို စစ်ပြီး 0 ဖြစ်ရင် throw လုပ်လို့ လွှဲပြောင်းမှုကို abort လုပ်ပါတယ်။ { session }ပေးတာက ဒီ operation ကို transaction ထဲ ထည့်တာပါ။ မေ့သွားရင် write က သီးသန့် ဖြစ်သွားပါတယ်။- ဒုတိယ account ကို transaction တစ်ခုတည်းထဲမှာ ငွေဖြည့်ပါတယ်။
- Audit record က balance ပြောင်းလဲမှု နှစ်ခုနဲ့ အတူတကွ commit ဖြစ်မယ် ဒါမှမဟုတ် ပျောက်မယ် — အကုန် ဒါမှမဟုတ် ဘာမှမဟုတ်၊ SQL ရဲ့
BEGIN ... COMMITလိုပါပဲ။ - တစ်ခုခု မအောင်မြင်ရင်တောင် 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 ပါတယ်။
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တစ်ကြောင်းချင်း ရှင်းလင်းချက်
SETက key တစ်ခုအောက်မှာ string တစ်ခု သိမ်းပါတယ်။ Colon ခြားထားတဲ့ နာမည် (post:7:html) က နာမည်ပေးပုံ စည်းမျဉ်းသာပါ။EX 3600က key ကို တစ်နာရီကြာရင် သက်တမ်းကုန်စေပါတယ် — cache ရဲ့ အနှစ်သာရပါ။GETက ပြန်ဖတ်ပါတယ်။ Key မရှိရင် ဒါမှမဟုတ် သက်တမ်းကုန်ရင်nilရပြီး app က main database ကို ပြန်သွားကာ cache ကို ပြန်ဖြည့်ပါတယ်။TTLက key မှာ သက်တမ်း ဘယ်နှစက္ကန့် ကျန်သေးလဲ ပြပါတယ်။INCRက request ထောင်ချီ တစ်ပြိုင်နက် ရှိရင်တောင် atomic အနေနဲ့ 1 ပေါင်းပြီး တန်ဖိုးအသစ်ကို ပြန်ပေးပါတယ်။ View counter နဲ့ rate limit တွေအတွက် အကောင်းဆုံးပါ။- Hash က key တစ်ခုအောက်မှာ field အများကြီး သိမ်းပါတယ်၊ document အသေးလေး တစ်ခုလိုပါ။ Session data ထားဖို့ အသုံးများတဲ့ နည်းပါ။
HGETALLက hash ထဲက field နဲ့ value အားလုံးကို ပြန်ပေးပါတယ်။- Sorted set က member တွေကို score အလိုက် စီထားပါတယ်၊ ဒီမှာ ကစားသမားတွေရဲ့ အမှတ်ပါ။
ZREVRANGE ... 0 2က အမှတ် အများဆုံးကနေ ထိပ်ဆုံး သုံးယောက်ကို သူတို့ score နဲ့အတူ ပြန်ပေးပါတယ်။ Live leaderboard တစ်ခုက command တစ်ခုတည်းပါ။DELက key ကို ဖယ်ရှားပါတယ်၊ ဥပမာ post ကို ပြင်ပြီးချင်း cache ကို ရှင်းဖို့။
Cheat sheet: SQL နဲ့ MongoDB မှာ တူညီတဲ့ အလုပ်များ
နေ့စဉ် အလုပ် ခြောက်ခုကို SQL နဲ့ အရင်ရေးပြီး MongoDB နဲ့ ထပ်ရေးထားပါတယ်။ တိုတိုလေး ဖြစ်အောင် နှစ်ဖက်လုံးက author နာမည်ကို တိုက်ရိုက် သိမ်းထားတဲ့ ရိုးရှင်းတဲ့ posts table ကို သုံးပါတယ်။ Comment ထဲက နံပါတ်တွေ ကိုက်ပါတယ်။
-- 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;// 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 ကို ဖွင့်ပြီး ကိုယ်တိုင် ရိုက်ကြည့်တာပါပဲ။

