← Назад до блогу
·8 хв читання·

П’ять SQL-патернів, які замінюють цикл на Python – і чому вони виграють

Найдорожчий Python, який я рев’юю, – це for order in orders після SELECT *. Останній рядок у групі, місяць до місяця, умовні лічильники й upsert – це запити. Як я пишу їх у Postgres, замість тягнути таблицю.

SQLPostgreSQLPythonШвидкодіяІнженерія

Найдорожчий Python, який я рев’юю, – не хендлер FastAPI без типів. Це for order in orders, який живе, бо ніхто не написав віконну функцію. Цикл виглядає чесно. Так само звіт, що тривав чотири секунди, стає сорока, а в понеділок після вдалого вікенда продажів – таймаутом.

Я вже писала, як обираю тип бази. Це наступний шар: SQL, який я справді пишу, коли питання продукту – «останнє замовлення клієнта», «виручка цього місяця проти минулого» або «встав бронювання, якщо слот вільний». Postgres відповідає за один round trip. Python теж – після того як завантажив таблицю. Парна стаття – як цей запит має вийти з Python, не тягнучи купу назад.

1. Цикл, який завантажує таблицю

Типовий скрипт: SELECT * FROM orders, далі групування в dict, далі найсвіжіший created_at на customer_id. На 800 рядках здається нормальним. На 800 000 – це тікет у підтримку. База вже має індекс, сортування і пам’ять для цього. Ви за це заплатили. Python як другий рушій запитів – так вартість вилазить у затримці, не в рахунку, який планували.

  • Якщо результат менший за джерело, а правило має назву – last, sum, rank, exists – це запит.
  • Якщо правило – бізнес-есе (повернення з людським винятком, абзац політики) – може лишитись у Python.

2. Останній рядок у групі – DISTINCT ON і ROW_NUMBER

Поточна підписка на робочий простір. Останній статус інвойсу на замовлення. Водій, який пінгував востаннє. Це перший цикл, який я видаляю. У Postgres пишу DISTINCT ON (workspace_id) і ORDER BY workspace_id, created_at DESC. Портативний родич – ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at DESC), далі лишаю rn = 1.

DISTINCT ON коротший, і я ставлю його, коли Postgres мій. ROW_NUMBER пишу, коли той самий запит має пережити переїзд, або коли потрібні ранг 1 і 2 – попередній план, другий результат. Не тягніть усі підписки й не зводьте їх у Python. На нічиїх помилитеся і завантажите колонки, які ніколи не малюєте.

3. Накопичувальні суми і «з минулого місяця» – SUM OVER і LAG

Місяць до місяця – не джойн таблиці на себе всередині циклу. LAG(revenue) OVER (PARTITION BY workspace_id ORDER BY month) кладе минулий місяць у той самий рядок. SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) – накопичувальний баланс. Віконні функції лишають зерно рядка і все одно бачать сусідів. GROUP BY згортає. Цикл на Python збирає сусідів у dict і забуває часові пояси.

Якщо потрібні і детальний рядок, і підсумок групи, SUM(amount) OVER (PARTITION BY order_id) б’є другий запит і б’є N+1 з ORM. Увесь трюк: лишити зерно, яке малюватимете, а сусідство порахувати в рушії.

4. Умовні лічильники – FILTER замість if

COUNT(*) FILTER (WHERE status = paid) поруч із COUNT(*) FILTER (WHERE status = refunded) – один прохід. Версія на Python – один прохід і два лічильники, доки хтось не додасть третій статус у другому циклі або експорт не виросте і ви не пройдете двічі. FILTER читається. CASE у SUM працює на рушіях без FILTER. У Postgres я ставлю FILTER, бо намір на сторінці: цей підрахунок має предикат.

5. Upsert замість select-потім-insert

Гонка: прочитати «чи вільний цей email», потім вставити. Дві вкладки, два воркери, один unique, який знаходите в проді. INSERT ... ON CONFLICT (email) DO UPDATE – або DO NOTHING – транзакція, яку база вже вміє. Та сама родина: UPDATE ... WHERE version = $version для оптимістичного лока. Check-then-write у Python – це гірша база, написана вручну.

  • Слоти бронювання, ключі ідемпотентності, «створи клієнта, якщо немає» – це upsert, не if.
  • Унікальний індекс – частина продукту. Без нього ON CONFLICT – театр.

6. Як я перевіряю, що не вигадала повільніший запит

EXPLAIN (ANALYZE, BUFFERS) на повільному шляху. Якщо бачите sequential scan на таблиці, яка виросла, потрібен індекс під партицію і порядок – (workspace_id, created_at DESC) для DISTINCT ON. Віконна функція не безкоштовна. Вона все одно дешевша за відправку купи в застосунок. Якщо план сортує два мільйони рядків для дашборда на двадцять – додайте межу по даті. SQL, що замінює цикл, усе ще може бути поганим циклом усередині рушія.

Висновок: назвіть результат – напишіть запит

Python – добре місце для HTTP, політик і речення, яке бачить користувач. Погане місце, щоб заново винаходити GROUP BY. П’ять форм – last-per-group, lag, накопичувальна сума, умовні агрегати, upsert – і більшість «розумних» скриптів, які я рев’юю, стають в’ю або одним запитом у функції репозиторію. Інша стаття – Python, який відмовляється це скасувати.

Найняти в 1 клік

Найміть мене як розробницю в один клік. Безпечний контракт на Upwork - і зробимо вебрішення з високою конверсією.

Найняти на Upwork

Готові обговорити проєкт?

Я senior веброзробниця, спеціалізуюсь на React і Next.js - відкрита до фриланс-проєктів по всьому світу.

Забронювати кол у Google Календар

Виберіть дату й час — посилання на Google Meet створиться автоматично.