B-tree зберігає значення відсортованими. Тому запит «перші N за порядком» може взяти рядки прямо з індексу й зупинитися, не сортуючи всю таблицю:
CREATE INDEX posts_published_idx ON posts (published_at);
SELECT * FROM posts ORDER BY published_at DESC LIMIT 20;
-- Index Scan Backward: 20 рядків з кінця індексу, без сортування
B-tree можна читати в обох напрямках, тож для однієї колонки DESC в індексі не обов'язковий.
Коли напрямок у індексі важливий - змішане сортування кількох колонок:
SELECT * FROM products ORDER BY category_id ASC, price DESC LIMIT 50;
CREATE INDEX products_cat_price_idx ON products (category_id ASC, price DESC);
Індекс (category_id, price) для такого запиту не підійде: обхід у будь-якому напрямку дасть price в тому ж напрямку, що й category_id.
NULLS FIRST / NULLS LAST. За замовчуванням у PostgreSQL NULL більші за всі значення: при ASC вони в кінці, при DESC - на початку. Якщо запит пише ORDER BY published_at DESC NULLS LAST, індекс має збігатися:
CREATE INDEX posts_pub_idx ON posts (published_at DESC NULLS LAST);
Інакше планувальник змушений сортувати.
Разом з фільтром: WHERE user_id = ? ORDER BY created_at DESC LIMIT 10 найкраще обслуговує індекс (user_id, created_at): спершу рівність, потім колонка сортування.
Як перевірити: у EXPLAIN не має бути вузла Sort над великою кількістю рядків - лише Index Scan з Limit. Саме на цьому тримається keyset-пагінація.