Skip to main content

10 posts tagged with "Database"

Schema design, data types, constraints and choosing the right storage for each problem.

View All Tags

Four Indexes Made INSERT 6× Slower: The Bill Nobody Mentions

· 12 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Indexes speed up reads by charging you on every write, and the charge is larger than most people picture. Measured on PostgreSQL 16 with the same 200,000 rows: a table with no indexes finished inserting in 287.8 ms and occupied 10.2 MB; a table with four indexes took 1,722.3 ms and occupied 27 MB. That is 6× slower and 2.6× bigger — just to carry four structures that no query may ever touch. The right question is not "should this column have an index?" but "does the read time saved cover the write time paid?"

Every post about database optimization ends with advice to add an index. Very few mention the bill that comes with it, and fewer still put a number on it.

This post goes deeper into two lessons from the 30-day SQL series: Database design and Index. Every figure below was measured on PostgreSQL 16.11.

Your Running Total Is Wrong on the First Row: Window Functions, RANGE, and the Default Nobody Reads

· 12 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

GROUP BY collapses rows; a window function keeps every row while still computing the group total. Measured: on the same 200,000-row table, GROUP BY returns 200 rows and the window returns 200,000 rows. It is also faster than the old approach — replacing a self-join with a window took the query from 79.2 ms down to 22.7 ms. But there is one default that quietly corrupts numbers and almost nobody reads it: OVER (ORDER BY ...) uses a RANGE frame, so every peer row receives the same running total. If you want a row-by-row cumulative sum, you must spell out ROWS.

Window functions turn long-winded reporting queries into a few readable lines. They also carry exactly one trap subtle enough to pass code review: a running total that looks plausible in the middle and is wrong wherever values tie.

This post goes deeper into Recursive Queries and Window Functions from the 30-day SQL series. Every number here was measured on PostgreSQL 16.11.

Hai giao dịch cùng cộng 100, số dư chỉ tăng 100: isolation level qua thí nghiệm thật

· 14 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

Isolation level quyết định một giao dịch nhìn thấy gì khi có giao dịch khác chạy song song. Ở mức mặc định của hầu hết database là READ COMMITTED, hai phiên cùng đọc số dư 1000 rồi cùng ghi 1100 sẽ cho kết quả cuối là 1100 — một lần cộng biến mất, không có lỗi nào được ném. Nâng lên REPEATABLE READ thì phiên thứ hai nhận ERROR: could not serialize access due to concurrent update: câu trả lời sai âm thầm biến thành một lỗi rõ ràng mà ứng dụng phải thử lại. Toàn bộ số liệu dưới đây đo bằng hai session psql song song trên PostgreSQL 16.11.

Đây là loại lỗi gần như không thể tái hiện trên máy dev, vì ở đó bạn chỉ có một người dùng. Nó chỉ xuất hiện khi hai request thật chạm vào cùng một dòng trong cùng một khoảnh khắc — và lúc đó nó không sập, không log, chỉ làm sai số liệu.

Bài này đào sâu bài Transactions và ACID trong series học SQL 30 ngày.

Index Exists but the Query Still Scans the Whole Table? Sargability and One Function Call

· 13 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

An index is a structure sorted by the column's values. Wrap a function around the column — date_part('year', created_at), LOWER(email), CAST(...) — and the thing you are comparing is no longer the sorted value, so the database cannot use the index and falls back to scanning the whole table. A condition that can use an index is called sargable. Measured on PostgreSQL 16 with 200,000 rows: the function-wrapped version runs in 29.6 ms with a Seq Scan, the range-rewritten version runs in 5.4 ms with a Bitmap Index Scan — about 5.4× faster, returning the same 37,518 rows.

A familiar situation: the query is slow, you add an index on exactly the column being filtered, you run it again — still just as slow. The index is right there, \d shows it, but the execution plan never mentions it.

This post drills into the Index lesson and the query optimization lesson from my 30-day SQL series. Every number is real output from PostgreSQL 16.11.

Your LEFT JOIN Became an INNER JOIN and Nothing Warned You

· 11 min read
Nguyễn Huỳnh Minh Tiến
Middle Fullstack Developer @ Utop.vn
Summary

LEFT JOIN keeps every row from the left table and fills in NULL where nothing matches. But WHERE runs after the join, so any condition placed on a right-hand column also removes those NULL rows — and the LEFT JOIN becomes an INNER JOIN with no warning whatsoever. Measured on PostgreSQL 16: a plain LEFT JOIN gives 4 rows, adding WHERE leaves 2 rows, but moving that same condition into ON gives 3 rows — which is the correct answer.

This is the bug I see most often in reporting queries. It is not a syntax error, it is not slow, it throws nothing. It simply returns too few rows, and usually it is precisely the important ones that go missing: customers with no orders yet, products that have never sold, salespeople who have not closed anything.

This post drills into the JOIN lesson from my 30-day SQL series. Every number below is real output from PostgreSQL 16.11.