Модуль 10 · Урок 38 із 58

SELECT: filters, order, NULL і safe parameters

SQL-запит має контракт так само, як функція Python: які rows він повертає, у якому порядку, за яких parameters і що означає порожній результат. Без цього навіть правильний SELECT стає нестабільним API.

SELECTWHERENULLParameters

Безпечна PostgreSQL-практика модуля 10

Пакет містить synthetic schema/seed/query files, parameterized Python repository, contract tests і read-only verifier. Локальні тести не виконують PostgreSQL; integration run проводиться лише у власній disposable database без персональних чи production-даних, credentials і production endpoints.

Завантажити практичний пакет →

Проєкція і відбір мають бути явними

SELECT * приховує output contract: порядок і набір columns можуть змінитися разом зі schema. Для application query перелічуйте потрібні columns, давайте зрозумілі aliases і описуйте, чи допускається zero, one або many rows.

SELECT id, project_id, title, status, created_at
FROM python_sql_lab.tickets
WHERE project_id = %s AND status = %s
ORDER BY created_at DESC, id DESC
LIMIT %s;

Додатковий id у ORDER BY робить порядок deterministic, коли timestamps однакові.

WHERE працює з SQL logic

Умови AND, OR і NOT треба групувати parentheses, коли priority не очевидний. NULL = NULL не повертає true; для nullable column використовують IS NULL. NOT IN із NULL у наборі може дати неочікуваний unknown, тому nullable semantics треба тестувати окремо.

ExactПорівняння має відповідати type і collation contract.
RangeМежі inclusive/exclusive названі явно.
NULLВідсутність значення має окремий test.
EmptyZero rows — нормальний result або domain error.

Параметри — не string formatting

Psycopg передає values окремо від query. Placeholder залишається %s незалежно від типу, а parameters передаються другим argument до execute(). Не використовуйте f-string, % чи format() для user-controlled values: escaping залежить від protocol і type adaptation, а ручне quoting легко породжує SQL injection.

cursor.execute(
    "SELECT id, title FROM python_sql_lab.tickets WHERE status = %s",
    (status,),
)

Один parameter теж потребує tuple

(status,) — one-item tuple. Сам рядок (status) ним не є. Placeholder не беруть у quotes, бо driver сам адаптує value.

Identifiers потребують composition API

Назву table або column не можна передати як звичайний value placeholder. Для обмеженого dynamic identifier спочатку застосовують allowlist, потім psycopg.sql.Identifier і SQL(...).format(). Direction сортування краще обирати з двох статичних query templates, а не приймати довільний fragment.

from psycopg import sql

allowed = {"created_at", "priority"}
if sort_key not in allowed:
    raise ValueError("unsupported_sort_key")
query = sql.SQL("SELECT id FROM python_sql_lab.tickets ORDER BY {} DESC")
cursor.execute(query.format(sql.Identifier(sort_key)))

Pagination має стабільний порядок

LIMIT без ORDER BY не обіцяє ті самі rows між runs. Великий OFFSET може бути дорогим і нестабільним під concurrent inserts. Для API часто краще keyset pagination: наступна сторінка починається після останньої пари (created_at, id). Contract має визначити tie-breaker, cursor encoding і snapshot expectations.

  1. Перелічіть output columns.
  2. Параметризуйте values.
  3. Allowlist + Identifier використовуйте лише для identifiers.
  4. Додайте deterministic ORDER BY.
  5. Перевірте empty, NULL, quote-containing і boundary inputs.

Методичні джерела

Урок, сценарії, пояснення й вправи створені SEOWORK. Посилання ведуть лише на офіційну документацію та Python Wiki.

Практична перевірка · урок 38 з 47

Закріпіть матеріал уроку

Три сценарні питання. Для зарахування уроку потрібно дати щонайменше дві правильні відповіді.

1. Чому application query краще перелічує id, title і status замість SELECT *?
2. Як правильно перевірити nullable closed_at на відсутність значення у PostgreSQL?
3. Який Psycopg виклик безпечно передає user-provided status як value parameter?