Junior 7 min · March 05, 2026

SQL LIMIT Without ORDER BY — Your Top-N Query Is Broken

Top-10 widget changed on every refresh because LIMIT had no ORDER BY — heap order shifts.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • ORDER BY sorts result rows — ASC (default, smallest first) or DESC (largest first)
  • Without ORDER BY, SQL returns rows in undefined order — 'consistent' results on small datasets are an illusion
  • LIMIT restricts the number of rows returned — always pair with ORDER BY for deterministic results
  • LIMIT with OFFSET implements pagination: LIMIT 10 OFFSET 20 returns rows 21-30
  • OFFSET pagination degrades at large page numbers — OFFSET 1000000 scans and discards 1M rows
  • Biggest mistake: LIMIT without ORDER BY — 'Top 5' queries return different random rows on each execution
✦ Definition~90s read
What is SQL ORDER BY and LIMIT?

SQL's LIMIT clause restricts how many rows a query returns, but without an accompanying ORDER BY, the result set is non-deterministic — the database is free to return any N rows that satisfy the WHERE clause. This isn't a theoretical edge case; it's a bug waiting to happen.

Imagine you're at a school prize ceremony.

PostgreSQL, MySQL, SQLite, and most other databases make no guarantees about row order without explicit sorting. If you run the same LIMIT query twice on a live table, you may get different rows due to page splits, index scans, or parallel execution plans.

Your "top-N" query isn't broken because of a syntax error — it's broken because you didn't tell the database what "top" means.

ORDER BY is the mechanism that gives LIMIT semantic meaning. It sorts the entire result set by one or more columns before the database discards all but N rows. Without it, LIMIT is just a random sample, not a deterministic subset. When you add ORDER BY created_at DESC LIMIT 10, you're saying "give me the ten most recent rows" — a precise, repeatable contract.

The database will use an index on created_at to avoid sorting the whole table, making this pattern efficient at scale.

Pagination via LIMIT and OFFSET is the classic anti-pattern that breaks under real-world load. OFFSET 10000 LIMIT 20 forces the database to generate and then discard 10,000 rows before returning the 20 you want — a linear performance degradation that becomes catastrophic at high offsets. This is why production pagination at companies like Stripe, GitHub, and Slack uses keyset pagination (cursor-based) instead: WHERE id > last_seen_id ORDER BY id LIMIT 20.

This approach leverages the index directly, avoiding the scan-and-discard overhead entirely. If you're building pagination for anything beyond a toy app, OFFSET is a performance trap you should avoid.

Plain-English First

Imagine you're at a school prize ceremony. The teacher has a list of every student's score, but she doesn't read them all out — she sorts the list from highest to lowest, then announces only the top 3 winners. That's exactly what ORDER BY and LIMIT do in SQL. ORDER BY sorts your data (highest to lowest, A to Z, newest to oldest), and LIMIT says 'stop after this many rows'. Together, they turn a chaotic pile of database records into a clean, useful result.

Every real application that touches a database needs to sort data and control how much of it comes back. Your favourite music app shows your 'Top 10 Most Played' songs — that's ORDER BY and LIMIT. An e-commerce site shows products sorted by price, lowest first. A leaderboard ranks players by score. Without these two tools, your database would just dump every row in whatever random order it felt like, and your users would get an unusable wall of unsorted data.

The problem ORDER BY solves is simple: databases store rows in insertion order, not in any order that's useful to humans. Add LIMIT on top of that and you solve a second problem — returning millions of rows when the user only needs the first twenty is slow, wasteful, and will crash your app under load. These two clauses let you be surgical: get exactly what you need, in exactly the order you need it.

By the end of this article you'll be able to write queries that sort results by one or multiple columns in ascending or descending order, cap how many rows come back, and combine both techniques to build paginated results or leaderboard-style queries — the kind you'll write on day one of a real job.

Why LIMIT Without ORDER BY Is a Bug

LIMIT restricts the number of rows returned by a query. Without ORDER BY, the database is free to return any subset of rows that satisfy the WHERE clause — typically the first N rows it happens to scan. This is not random, but it is implementation-defined and non-deterministic across executions, storage engines, or after data modifications.

In practice, the rows returned depend on physical storage order, index scan order, or parallel execution plan choices. A query that returns the 'top 5' today may return a different set tomorrow after an index rebuild or a new row insertion. There is no guarantee of consistency unless ORDER BY is present.

Use LIMIT with ORDER BY when you need a specific top-N set — e.g., 'highest revenue customers' or 'most recent orders'. Without ORDER BY, LIMIT is only safe for sampling or when any N rows are acceptable, like a quick peek at table structure. In production reporting or pagination, omitting ORDER BY silently corrupts results.

Not Random, But Not Deterministic
Without ORDER BY, the same query can return different rows after an index rebuild, a new row insertion, or a database version upgrade.
Production Insight
A paginated API endpoint returning 'top 10' results without ORDER BY caused duplicate and missing items across pages.
Users reported seeing the same record on two different pages and missing records entirely — the database returned a different physical order per query.
Always pair LIMIT with a deterministic ORDER BY clause that includes a unique column to guarantee stable pagination.
Key Takeaway
LIMIT without ORDER BY is not a top-N query — it's an arbitrary-N query.
The only guarantee is that you get N rows, not which N rows.
If the result set must be meaningful, always specify ORDER BY with a unique tiebreaker.
SQL LIMIT Without ORDER BY Is Broken THECODEFORGE.IO SQL LIMIT Without ORDER BY Is Broken Why top-N queries need ORDER BY and a tiebreaker LIMIT Without ORDER BY Returns arbitrary rows, not top-N ORDER BY Clause Specifies sort column for deterministic results Sorting by Multiple Columns Breaks ties with secondary sort key LIMIT with OFFSET Builds pagination by skipping rows OFFSET Performance Trap Scans skipped rows, slow on large offsets ORDER BY + LIMIT Without Tiebreaker Non-deterministic results on ties ⚠ LIMIT without ORDER BY returns unpredictable rows Always add ORDER BY with a unique tiebreaker for determinism THECODEFORGE.IO
thecodeforge.io
SQL LIMIT Without ORDER BY Is Broken
Sql Order By Limit

ORDER BY — Telling SQL Which Column to Sort On

By default, SQL has no guaranteed sort order. When you run SELECT * FROM products, the database returns rows in whatever order it stored them internally. That might look consistent in development but it'll bite you the moment data is inserted, deleted, or the database optimiser changes its mind. Never rely on the default order.

ORDER BY fixes this. You add it at the very end of your SELECT statement and tell SQL which column to use as the sorting key. SQL then rearranges the result set before handing it back to you.

The two directions are ASC (ascending — smallest to largest, A to Z, oldest to newest) and DESC (descending — largest to smallest, Z to A, newest to oldest). ASC is the default, so you can omit it, but writing it explicitly makes your intention crystal clear to anyone reading your query later — including future you.

Think of it like sorting a spreadsheet column. You click the column header once to sort A→Z (ASC), twice to sort Z→A (DESC). ORDER BY is that click, but inside SQL.

order_by_basics.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Imagine we have a table called 'products' that looks like this:
-- | product_id | product_name        | price  | stock_quantity |
-- |------------|---------------------|--------|----------------|
-- | 1          | Wireless Headphones | 79.99  | 120            |
-- | 2          | USB-C Cable         | 9.99   | 500            |
-- | 3          | Mechanical Keyboard | 129.99 | 45             |
-- | 4          | Webcam HD           | 59.99  | 200            |
-- | 5          | Mouse Pad XL        | 19.99  | 310            |

-- QUERY 1: Sort products cheapest first (ASC is the default)
SELECT product_name, price
FROM products
ORDER BY price ASC;  -- ASC = ascending = smallest number first

-- QUERY 2: Sort products most expensive first
SELECT product_name, price
FROM products
ORDER BY price DESC;  -- DESC = descending = largest number first

-- QUERY 3: Sort alphabetically by product name
SELECT product_name, price
FROM products
ORDER BY product_name ASC;  -- Works on text too — sorts A to Z
Output
-- QUERY 1 result (cheapest first):
product_name | price
---------------------+--------
USB-C Cable | 9.99
Mouse Pad XL | 19.99
Webcam HD | 59.99
Wireless Headphones | 79.99
Mechanical Keyboard | 129.99
-- QUERY 2 result (most expensive first):
product_name | price
---------------------+--------
Mechanical Keyboard | 129.99
Wireless Headphones | 79.99
Webcam HD | 59.99
Mouse Pad XL | 19.99
USB-C Cable | 9.99
-- QUERY 3 result (alphabetical):
product_name | price
---------------------+--------
Mechanical Keyboard | 129.99
Mouse Pad XL | 19.99
USB-C Cable | 9.99
Webcam HD | 59.99
Wireless Headphones | 79.99
Watch Out: Never assume default order
SQL has NO guaranteed row order unless you use ORDER BY. Two identical queries run seconds apart can return rows in a different sequence. If order matters to your feature — and it usually does — always be explicit with ORDER BY.
Production Insight
SQL has no guaranteed row order without ORDER BY — results that look consistent in development will vary in production as data grows and storage pages rearrange.
Any user-facing 'sorted' feature (leaderboards, recent items, top products) must have ORDER BY.
Ordering by an unindexed column on a large table forces a full sort — add an index on the ORDER BY column for repeated queries.
Key Takeaway
SQL returns rows in undefined order without ORDER BY — consistent results in dev are coincidence, not guarantee.
Always add ORDER BY for any user-facing query or report where row order matters.
Index your ORDER BY column to avoid full-table sorts in production.

Sorting by Multiple Columns — Breaking Ties the Right Way

What happens when two rows have the same value in your sort column? SQL sorts them in an undefined order among themselves unless you give it a tiebreaker. That's where multi-column sorting comes in.

You list the columns after ORDER BY separated by commas. SQL sorts by the first column first. If two rows have the same value there, it then looks at the second column to decide their order. Think of it like sorting a contact list: first by last name, then by first name within the same last name.

Each column in a multi-sort can have its own direction. So you could sort by category ASC (A to Z) and then by price DESC (most expensive first within each category). The directions are completely independent.

This pattern is everywhere in real apps. An order history page might sort by order_date DESC (newest first), then by order_id DESC to break ties on the same date. A scoreboard sorts by score DESC, then by completion_time ASC so the fastest player wins a tiebreak.

multi_column_sort.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- Our products table now includes a category column:
-- | product_id | product_name        | category    | price  |
-- |------------|---------------------|-------------|--------|
-- | 1          | Wireless Headphones | Audio       | 79.99  |
-- | 2          | USB-C Cable         | Accessories | 9.99   |
-- | 3          | Mechanical Keyboard | Input       | 129.99 |
-- | 4          | Webcam HD           | Video       | 59.99  |
-- | 5          | Mouse Pad XL        | Accessories | 19.99  |
-- | 6          | Microphone USB      | Audio       | 49.99  |
-- | 7          | Laptop Stand        | Accessories | 34.99  |

-- Sort by category A-Z, then within each category by price highest first
-- Real-world use: a product listing page grouped by category
SELECT product_name, category, price
FROM products
ORDER BY
    category ASC,   -- PRIMARY sort: categories in alphabetical order
    price DESC;     -- TIEBREAKER: most expensive product first within each category
Output
product_name | category | price
---------------------+-------------+--------
Laptop Stand | Accessories | 34.99
Mouse Pad XL | Accessories | 19.99
USB-C Cable | Accessories | 9.99
Wireless Headphones | Audio | 79.99
Microphone USB | Audio | 49.99
Mechanical Keyboard | Input | 129.99
Webcam HD | Video | 59.99
Pro Tip: Column position shorthand
You can use column position numbers instead of names in ORDER BY — e.g. ORDER BY 2 DESC sorts by the second column in your SELECT list. It's handy for quick queries but avoid it in production code because if you reorder your SELECT columns, the sort silently breaks.
Production Insight
Multi-column ORDER BY is essential for deterministic results when the primary sort column has ties.
Always add the primary key as the final tiebreaker: ORDER BY created_at DESC, id DESC.
For NULLS handling: ORDER BY col NULLS LAST (PostgreSQL) puts NULLs at the end; NULLS FIRST puts them at the start — MySQL always sorts NULLs first in ASC, last in DESC.
Key Takeaway
Always add the primary key as a tiebreaker in ORDER BY — no other column guarantees unique ordering.
NULL sort order differs by database: PostgreSQL supports NULLS LAST/FIRST explicitly.
Multi-column sort: the second column only applies to groups of identical values in the first column.

LIMIT — Stopping SQL After N Rows

Sorting is great, but what if your products table has 50,000 rows? Returning all of them sorted just to show a user the 5 cheapest items is like ordering an entire restaurant menu to decide what to eat. LIMIT tells SQL to stop after returning a specific number of rows.

LIMIT goes at the very end of your query, after ORDER BY. The database sorts the full result set first, then hands back only the first N rows. This is crucial to understand: LIMIT restricts what you receive, not what the database sorts internally.

LIMIT is not just a convenience — it's a performance tool. Sending fewer rows over a network connection is faster. Your application uses less memory parsing a smaller result. And for users, a fast page that loads 20 results beats a slow page loading 10,000.

One important note: LIMIT is the standard used by MySQL, PostgreSQL, and SQLite. Microsoft SQL Server uses TOP instead, and Oracle uses FETCH FIRST n ROWS ONLY. The concept is identical — the syntax just differs. This article uses the MySQL/PostgreSQL/SQLite syntax since those are the most common environments beginners encounter.

limit_basics.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- ============================================================
-- SCENARIO: Build a 'Top 3 Most Expensive Products' widget
-- ============================================================

-- Without LIMIT — returns ALL 7 products (overkill for a widget)
SELECT product_name, price
FROM products
ORDER BY price DESC;

-- With LIMIT — returns exactly 3 rows: the podium finishers
SELECT product_name, price
FROM products
ORDER BY price DESC   -- Sort so the most expensive rises to the top
LIMIT 3;              -- Then cut the list off after 3 rows

-- ============================================================
-- SCENARIO: Show the single cheapest product (minimum price)
-- ============================================================
-- You could use MIN(), but LIMIT + ORDER BY is more flexible
-- because you can also return other columns alongside it
SELECT product_name, category, price
FROM products
ORDER BY price ASC  -- Cheapest first
LIMIT 1;            -- Only the very cheapest one
Output
-- Query 1 (no LIMIT) result — all 7 rows:
product_name | price
---------------------+--------
Mechanical Keyboard | 129.99
Wireless Headphones | 79.99
Webcam HD | 59.99
Microphone USB | 49.99
Laptop Stand | 34.99
Mouse Pad XL | 19.99
USB-C Cable | 9.99
-- Query 2 (LIMIT 3) result — top 3 only:
product_name | price
---------------------+--------
Mechanical Keyboard | 129.99
Wireless Headphones | 79.99
Webcam HD | 59.99
-- Query 3 (cheapest single product):
product_name | category | price
--------------+-------------+-------
USB-C Cable | Accessories | 9.99
SQL Server / Oracle Note:
In SQL Server use SELECT TOP 3 ... FROM products ORDER BY price DESC. In Oracle 12c+ use ORDER BY price DESC FETCH FIRST 3 ROWS ONLY. The logic is identical — only the syntax changes between database engines.
Production Insight
LIMIT without ORDER BY is functionally meaningless for any deterministic use case — do not ship it.
LIMIT 1 with ORDER BY is the fastest way to get the most recent or highest-value row without a subquery.
LIMIT is evaluated after WHERE, GROUP BY, HAVING, SELECT, and ORDER BY — it takes rows from the sorted result, not randomly.
Key Takeaway
LIMIT without ORDER BY returns arbitrary rows — always pair them.
LIMIT 1 ORDER BY col DESC is the correct pattern for 'most recent record'.
LIMIT is applied after sorting — the database must complete the sort before slicing.

LIMIT with OFFSET — Building Pagination from Scratch

LIMIT alone gives you the first page of results. But every real app needs page 2, page 3, and beyond. That's where OFFSET comes in. OFFSET tells SQL 'skip this many rows before you start counting'.

Think of it like a library shelf. LIMIT 5 says 'give me 5 books'. OFFSET 10 says 'start from the 11th book on the shelf'. Together, LIMIT 5 OFFSET 10 means 'skip the first 10, then give me the next 5' — that's page 3 of a 5-items-per-page layout.

The formula for any page is: OFFSET = (page_number - 1) × items_per_page. Page 1 = OFFSET 0. Page 2 = OFFSET 5. Page 3 = OFFSET 10. And so on.

This pattern powers the pagination you see on every website. It does have a performance gotcha at scale (scanning and discarding millions of rows to reach a high offset is slow), but for most beginner and intermediate use cases it works perfectly and is the standard approach to learn first.

pagination_with_offset.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- ============================================================
-- SCENARIO: A product listing page, 3 products per page,
-- sorted by price lowest-first
-- ============================================================

-- PAGE 1: Skip 0 rows, take the first 3
SELECT product_name, price
FROM products
ORDER BY price ASC
LIMIT 3 OFFSET 0;   -- OFFSET 0 = start from the very beginning

-- PAGE 2: Skip the first 3 rows, take the next 3
SELECT product_name, price
FROM products
ORDER BY price ASC
LIMIT 3 OFFSET 3;   -- OFFSET 3 = jump past the first 3 results

-- PAGE 3: Skip 6 rows, take the next 3
-- (only 1 product remains in our 7-product table)
SELECT product_name, price
FROM products
ORDER BY price ASC
LIMIT 3 OFFSET 6;   -- OFFSET 6 = jump past 6 results, take up to 3 more

-- ============================================================
-- FORMULA REMINDER:
-- OFFSET = (page_number - 1) * items_per_page
-- Page 1: (1-1)*3 = 0   → OFFSET 0
-- Page 2: (2-1)*3 = 3   → OFFSET 3
-- Page 3: (3-1)*3 = 6   → OFFSET 6
-- ============================================================
Output
-- PAGE 1 result:
product_name | price
--------------+-------
USB-C Cable | 9.99
Mouse Pad XL | 19.99
Laptop Stand | 34.99
-- PAGE 2 result:
product_name | price
---------------------+-------
Microphone USB | 49.99
Webcam HD | 59.99
Wireless Headphones | 79.99
-- PAGE 3 result (only 1 row left — SQL won't error, it just returns what exists):
product_name | price
---------------------+--------
Mechanical Keyboard | 129.99
Pro Tip: LIMIT + OFFSET always needs ORDER BY
If you use OFFSET without ORDER BY, you have no guarantee which rows get skipped. Page 2 might return rows you already saw on page 1. Always pair OFFSET with a stable ORDER BY so each page reliably shows different, predictably ordered rows.
Production Insight
OFFSET pagination has O(n) cost: OFFSET 1000000 LIMIT 10 scans and discards 1 million rows before returning 10.
For high-page-number pagination, switch to keyset pagination: WHERE id > $last_id ORDER BY id LIMIT 10.
Keyset pagination is O(log n) with an index on id — dramatically faster than OFFSET at large page numbers.
Key Takeaway
OFFSET pagination is O(n) — page 10,000 is 10,000x slower than page 1 on large datasets.
Keyset/cursor pagination (WHERE id > last_id) is O(log n) — the correct pattern for production pagination.
ROW_NUMBER() window function enables keyset pagination with complex multi-column sort keys.

OFFSET Is a Performance Trap — Why Your "Pagination" Breaks at Scale

LIMIT with OFFSET looks clean on page 2 of your 10-row test set. It's a lie. By page 50 the database is still reading and discarding every row before your target offset. That's not pagination, it's a full table scan wearing a costume. The problem gets worse as your offset grows because Postgres, MySQL, and SQL Server all fetch the skipped rows internally. They just don't show them to you. The fix is keyset pagination — also called "seek method" — where you filter on the last seen value from the previous page. Instead of OFFSET 5000, you write WHERE created_at < last_timestamp. That turns an O(n) scan into an index seek. If your users jump to page 200, don't let them. Force search filters or keyset. OFFSET has its place — admin exports, small datasets — but front-facing pagination over 10k rows needs keyset. Production databases die on OFFSET 100000. Don't be the one holding the deploy bag.

KeysetPagination.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — database tutorial

-- The slow way — OFFSET grows linearly with table size
SELECT order_id, created_at, total
FROM orders
ORDER BY created_at DESC, order_id DESC
LIMIT 50 OFFSET 5000;  -- Still reads ~5050 rows internally

-- The fast way — keyset pagination using last page's cursor
-- Assume the previous page returned created_at = '2024-11-01 14:23:00', order_id = 8723
SELECT order_id, created_at, total
FROM orders
WHERE created_at < '2024-11-01 14:23:00'
   OR (created_at = '2024-11-01 14:23:00' AND order_id < 8723)  -- tiebreaker
ORDER BY created_at DESC, order_id DESC
LIMIT 50;  -- Only reads 50 rows, full index seek
Output
order_id | created_at | total
--------|---------------------|-------
8722 | 2024-11-01 14:22:45 | 329.99
8721 | 2024-11-01 14:22:10 | 150.00
... 48 more rows. No OFFSET overhead.
Production Trap: OFFSET 100000
If your users complain about page 200 loading for 30 seconds, check your query plan. You'll see a Sequential Scan on the entire table. Keyset pagination drops that to an Index Only Scan. Swap now.
Key Takeaway
Never use OFFSET for pagination beyond the first few pages. Use keyset (seek) pagination with WHERE clauses on your ORDER BY columns.

ORDER BY + LIMIT Without a Tiebreaker Is a Non-Deterministic Landmine

You sorted by created_at DESC, grabbed the top 10, and deployed. Next week, two customer invoices were created at the same second. Which one made the cut? Depends on the index order, the moon phase, and whether the buffer pool had coffee that morning. SQL guarantees nothing about row order for equal values in the ORDER BY clause. The database returns whichever row it finds first in storage. That changes with indexes, updates, and vacuum operations. You've now shipped a feature that flips results randomly. The fix is boring and mandatory: always include a unique column as the final sort key. An auto-increment ID, a UUID, anything guaranteed unique. This isn't just for correctness — it prevents subtle bugs in pagination offset calculations and batch processing jobs. Ever had a nightly script miss a row because it assumed an ordered set was stable? That's this. If you're building a report, a queue, or an API endpoint that returns a "top N" list, tiebreak on uniqueness. It costs nothing and saves a midnight Slack ping.

TiebreakerOrderBy.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — database tutorial

-- WRONG: duplicate created_at values = random output
SELECT invoice_id, customer_name, amount, created_at
FROM invoices
ORDER BY created_at DESC
LIMIT 5;

-- RIGHT: tiebreak with a unique column for deterministic results
SELECT invoice_id, customer_name, amount, created_at
FROM invoices
ORDER BY created_at DESC, invoice_id DESC  -- invoice_id breaks ties
LIMIT 5;
Output
invoice_id | customer_name | amount | created_at
-----------|---------------|---------|-------------------
10045 | Acme Corp | 1500.00 | 2024-11-15 10:00:00
10044 | Beta Inc | 1200.00 | 2024-11-15 10:00:00 -- Both at same time, but ID 10044 comes second deterministically
10043 | Gamma Ltd | 800.00 | 2024-11-15 09:59:58
Senior Shortcut: Always Add a Tiebreaker
When writing ORDER BY with LIMIT, ask: "Could two rows have the same value for this column?" If yes, add a unique column. Do it now, not after the bug report.
Key Takeaway
Always append a unique column (like a primary key) as the final sort key to guarantee deterministic results with LIMIT.
● Production incidentPOST-MORTEMseverity: high

Inconsistent Dashboard Top-10 Widget That Changed on Every Refresh

Symptom
Users noticed the trending products list changed every time they refreshed the page. Support tickets came in claiming 'the site is broken.' The SQL was technically correct — it returned exactly 10 products.
Assumption
The developer added LIMIT 10 assuming the database would 'naturally' return the same rows consistently. On the development machine with a small dataset it did; in production the storage engine's internal page ordering varied.
Root cause
SELECT * FROM products LIMIT 10 with no ORDER BY — the database returns rows in heap/storage order which changes as rows are inserted, updated, and vacuumed. In production, the PostgreSQL autovacuum was rearranging heap pages, changing which 10 rows appeared first.
Fix
Added ORDER BY view_count DESC, product_id ASC (the id tiebreaker ensures total determinism). Added a linter rule: any query with LIMIT must have ORDER BY.
Key lesson
  • LIMIT without ORDER BY is not a valid query for any user-facing feature — results are undefined and unstable
  • Always add a tiebreaker column (usually primary key) to ORDER BY to ensure complete determinism
  • Add a query linter rule: LIMIT requires ORDER BY in application code
Production debug guideDiagnosing pagination gaps, duplicates, and sort inconsistencies3 entries
Symptom · 01
Pagination shows duplicate rows or skips rows between pages
Fix
OFFSET-based pagination is broken when rows are inserted/deleted between page requests. Switch to keyset/cursor pagination: WHERE id > last_seen_id ORDER BY id LIMIT 10. This is immune to row insertion between requests.
Symptom · 02
ORDER BY is slow on a large table
Fix
Run EXPLAIN — look for Sort in the plan. If the sort column isn't indexed, the database sorts all rows in memory (or to disk for large datasets). Add CREATE INDEX idx_orders_created_at ON orders(created_at DESC). For covering queries, include all SELECT columns in the index with INCLUDE.
Symptom · 03
ORDER BY returns non-deterministic results when values tie
Fix
Add a tiebreaker column to ORDER BY: ORDER BY status ASC, id ASC. Without a tiebreaker, rows with identical values in the sort column can appear in any order across executions.
Feature / AspectORDER BYLIMIT / OFFSET
PurposeControls the sort order of returned rowsControls how many rows are returned
Position in queryAfter WHERE / GROUP BY / HAVINGVery last clause in the query
Required?No, but almost always needed for predictable resultsNo, but essential for performance at scale
Works alone?Yes — sorts all rows and returns them allYes — but without ORDER BY the rows returned are unpredictable
Works together?Yes — ORDER BY runs first, then LIMIT slices the top NYes — this combo is the standard pattern for leaderboards and pagination
Database supportAll major SQL databases (identical syntax)MySQL, PostgreSQL, SQLite. SQL Server uses TOP. Oracle uses FETCH FIRST.
Performance impactAdds sorting cost — index on sort column removes thisReduces data transferred — huge win on large tables
Tiebreaker possible?Yes — list multiple columns: ORDER BY a ASC, b DESCNo — LIMIT just counts rows, it doesn't break ties

Key takeaways

1
SQL has no default sort order
without ORDER BY, two identical queries can return rows in different sequences. Always be explicit when row order matters to your feature.
2
ORDER BY column DESC LIMIT N is the fundamental pattern for every leaderboard, top-N widget, and 'most recent item' query you'll ever write in a real application.
3
LIMIT reduces the number of rows your application receives over the network
it's a performance tool, not just a convenience. But it doesn't remove the sorting cost; add an index on your ORDER BY column for large tables.
4
Pagination requires LIMIT + OFFSET + ORDER BY working together. The formula is OFFSET = (page_number - 1) × items_per_page, and page 1 always starts at OFFSET 0
never OFFSET 1.

Common mistakes to avoid

3 patterns
×

Using LIMIT without ORDER BY

Symptom
Top-N widget shows different rows on each request — the database returns rows in storage order which changes as rows are inserted, updated, and autovacuumed
Fix
Always pair LIMIT with ORDER BY: ORDER BY created_at DESC LIMIT 10. Add a primary key tiebreaker for complete determinism: ORDER BY created_at DESC, id DESC LIMIT 10.
×

Misunderstanding the order of SQL clause execution

Symptom
Developers add LIMIT expecting it to reduce how many rows are scanned — but LIMIT runs last, after the sort. The database still sorts all rows before slicing.
Fix
LIMIT reduces the result, not the work. To reduce work, use WHERE (filter rows early) and ensure ORDER BY columns are indexed (avoid full-table sorts). LIMIT alone does not make a slow query fast.
×

Off-by-one errors in OFFSET pagination

Symptom
Page 2 accidentally repeats the last item from page 1, or skips the first item of page 2 — fencepost error in the OFFSET calculation
Fix
Page N with page size K: OFFSET (N-1)*K LIMIT K. Page 1: OFFSET 0 LIMIT 10. Page 2: OFFSET 10 LIMIT 10. Better: switch to keyset pagination for production APIs where rows may be inserted between page requests.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between ORDER BY ASC and DESC, and what does SQL ...
Q02JUNIOR
How would you write a query to get the 3rd, 4th, and 5th most expensive ...
Q03JUNIOR
If you run SELECT * FROM orders LIMIT 10 without ORDER BY, are you guara...
Q01 of 03JUNIOR

What is the difference between ORDER BY ASC and DESC, and what does SQL do with no ORDER BY?

ANSWER
ORDER BY ASC (the default) sorts from smallest to largest — numbers ascending, dates oldest first, strings A-Z. ORDER BY DESC sorts from largest to smallest — numbers descending, dates newest first, strings Z-A. Without any ORDER BY, SQL returns rows in storage order (heap order) which is determined by the database engine's internal page layout. This order is not guaranteed to be consistent between queries — it can change as rows are inserted, updated, or vacuumed. Any query that depends on a particular row order must have an explicit ORDER BY.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Does LIMIT affect query performance in SQL?
02
Can I use ORDER BY on a column that isn't in my SELECT list?
03
What's the difference between LIMIT in MySQL and TOP in SQL Server?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's SQL Basics. Mark it forged?

7 min read · try the examples if you haven't

Previous
SQL WHERE Clause and Filtering
6 / 16 · SQL Basics
Next
SQL JOINs Explained