This media is not supported in your browser
VIEW IN TELEGRAM
На сайте собрано множество обучающих материал по SQL: от базовых запросов SELECT и WHERE до JOIN, подзапросов, функций, сортировки и работы с таблицами. Всё объясняется простым языком с примерами запросов и постепенным усложнением тем, поэтому материал подойдёт как новичкам, так и тем, кто хочет систематизировать знания по бд.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍5🤝3
Почему CHECK constraint может пропустить неправильные данные из-за NULL!
Редкая, но неприятная ловушка SQL: многие думают, что
Именно поэтому nullable-колонки внутри
Допустим, есть таблица товаров:
Хотим запретить скидку больше цены:
На первый взгляд всё выглядит правильно.
Теперь такой
Потому что проверка:
Вот такой
И такой тоже:
Многие ожидают, что
То есть:
И вот здесь самая важная мысль:
Это поведение связано с SQL three-valued logic — логикой с тремя состояниями: TRUE, FALSE и UNKNOWN. Именно поэтому
Правильный вариант:
Теперь NULL уже не сможет пройти, потому что NOT NULL сработает раньше
В таком случае
Такой вариант намного понятнее при чтении схемы. Он явно показывает бизнес-логику: либо скидки нет, либо она не больше цены. Но здесь есть ещё один тонкий момент.
Если
Результат проверки:
Поэтому если цена обязательна — нужен отдельный NOT NULL:
Похожая ситуация встречается с датами. Например:
Разработчик может думать, что
потому что результат сравнения снова UNKNOWN.
И это может быть абсолютно нормальным поведением. Например, если NULL означает: период ещё не завершён. Но если обе даты обязательны, это нужно фиксировать явно:
🔥 Вывод: если в
➡️ SQL Ready | #практика
Редкая, но неприятная ловушка SQL: многие думают, что
CHECK constraint требует, чтобы условие всегда было TRUE. Но это не так, CHECK запрещает только FALSE. А результат UNKNOWN — пропускается.Именно поэтому nullable-колонки внутри
CHECK могут вести себя не так, как ожидает разработчик.Допустим, есть таблица товаров:
products(
id,
price,
discount
)
Хотим запретить скидку больше цены:
ALTER TABLE products
ADD CONSTRAINT chk_discount_price
CHECK (discount <= price);
На первый взгляд всё выглядит правильно.
Теперь такой
INSERT действительно не пройдёт:INSERT INTO products(id, price, discount)
VALUES (1, 100, 150);
Потому что проверка:
150 <= 100 даёт FALSE. А CHECK constraint запрещает строки, где выражение возвращает FALSE. Но дальше начинается важный нюанс SQL и трёхзначной логики.Вот такой
INSERT уже может пройти:INSERT INTO products(id, price, discount)
VALUES (2, 100, NULL);
И такой тоже:
INSERT INTO products(id, price, discount)
VALUES (3, NULL, 50);
Многие ожидают, что
CHECK отклонит такие строки. Но SQL работает иначе, если в сравнении участвует NULL, результатом становится не TRUE и не FALSE, а: UNKNOWNТо есть:
150 <= 100 -- FALSE
NULL <= 100 -- UNKNOWN
50 <= NULL -- UNKNOWN
NULL <= NULL -- UNKNOWN
И вот здесь самая важная мысль:
CHECK constraint считает строку валидной, если результат выражения — TRUE или UNKNOWN. Запрещается только явно FALSE.Это поведение связано с SQL three-valued logic — логикой с тремя состояниями: TRUE, FALSE и UNKNOWN. Именно поэтому
CHECK сам по себе НЕ заменяет NOT NULL. Если колонка обязательная — это нужно указывать отдельно.Правильный вариант:
CREATE TABLE products(
id bigint PRIMARY KEY,
price numeric NOT NULL,
discount numeric NOT NULL,
CONSTRAINT chk_discount_price
CHECK (discount <= price)
);
Теперь NULL уже не сможет пройти, потому что NOT NULL сработает раньше
CHECK. Но в реальных системах скидка часто может отсутствовать. То есть NULL — это нормальное состояние: скидки нет.В таком случае
constraint лучше писать явно и читаемо:ALTER TABLE products
ADD CONSTRAINT chk_discount_price
CHECK (
discount IS NULL
OR discount <= price
);
Такой вариант намного понятнее при чтении схемы. Он явно показывает бизнес-логику: либо скидки нет, либо она не больше цены. Но здесь есть ещё один тонкий момент.
Если
price остаётся nullable: price numeric, то выражение: discount <= price снова может вернуть UNKNOWN. Например:discount = 50
price = NULL
Результат проверки:
50 <= NULL, будет UNKNOWN, а строка снова станет валидной.Поэтому если цена обязательна — нужен отдельный NOT NULL:
price numeric NOT NULL
Похожая ситуация встречается с датами. Например:
CHECK (end_date >= start_date)
Разработчик может думать, что
constraint гарантирует корректный диапазон дат. Но если end_date nullable, такой CHECK спокойно пропускает:end_date = NULL
потому что результат сравнения снова UNKNOWN.
И это может быть абсолютно нормальным поведением. Например, если NULL означает: период ещё не завершён. Но если обе даты обязательны, это нужно фиксировать явно:
start_date date NOT NULL,
end_date date NOT NULL,
CHECK (end_date >= start_date)
CHECK участвуют nullable-поля, constraint может пропускать строки из-за UNKNOWN, CHECK не заменяет NOT NULL. Для обязательных значений всегда нужен отдельный NOT NULL constraint.Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍7🔥4
This media is not supported in your browser
VIEW IN TELEGRAM
Это обучающий материал с теорией, объяснениями и практическими задачами. Здесь разбираются устройство баз данных, связи между таблицами, индексы, нормализация, проектирование схем и многое др. Большой акцент сделан на понимании того, как правильно проектировать бд и решать задачи, которые часто встречаются на собеседованиях и в работе.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍7🤝4❤1
Например, кэширование помогает ускорить чтение и снизить нагрузку на БД, а CDN уменьшает задержки для пользователей из разных регионов.
На картинке — 8 распространённых проблем проектирования систем и практические способы их решения: кэширование, балансировка нагрузки, репликация, шардинг, централизованное логирование и другие базовые архитектурные паттерны.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15❤7👍7
PostgreSQL умеет замораживать часть запроса и запрещать оптимизатору его разворачивать!
Начиная с PostgreSQL 12 оптимизатор получил право разворачивать
Такой
Например, если внутри
А обратная фича:
наоборот подсказывает оптимизатору агрессивно встраивать
🔥 Одна и та же
➡️ SQL Ready | #совет
Начиная с PostgreSQL 12 оптимизатор получил право разворачивать
CTE прямо внутрь основного запроса.WITH data AS (
SELECT *
FROM orders
)
SELECT *
FROM data
WHERE user_id = 42;
Такой
WITH может вообще исчезнуть из плана выполнения, потому что PostgreSQL встроит его обратно в запрос. Но иногда это плохо.Например, если внутри
CTE дорогой расчёт, который нельзя выполнять повторно. Тут появляется малоизвестная фича:WITH expensive AS MATERIALIZED (
SELECT *
FROM huge_events
WHERE created_at >= now() - interval '1 day'
)
SELECT COUNT(*)
FROM expensive;
MATERIALIZED заставляет PostgreSQL сначала физически вычислить CTE, а потом использовать результат дальше.А обратная фича:
WITH data AS NOT MATERIALIZED (
SELECT *
FROM orders
)
SELECT *
FROM data
WHERE user_id = 42;
наоборот подсказывает оптимизатору агрессивно встраивать
CTE обратно в запрос.CTE с MATERIALIZED и без него иногда отличается по производительности в десятки раз на больших объёмах данных.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍6❤5
This media is not supported in your browser
VIEW IN TELEGRAM
Это подборка материалов по PostgreSQL: настройка и администрирование баз данных, оптимизация запросов, репликация, резервное копирование, индексы, мониторинг и др. темы. Помимо теории, здесь много практических статей и кейсов, которые помогают лучше понять работу PostgreSQL и применять полученные знания.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍6🔥6
N+1 проблема в SQL: почему приложение внезапно начинает делать тысячи запросов!
Одна из самых частых проблем backend-приложений — N+1 queries. Особенно часто это появляется при работе через ORM, потому что код выглядит нормально, а реальные SQL-запросы скрыты внутри слоя абстракции.
Например, есть таблицы:
Сначала приложение получает пользователей:
Допустим, запрос вернул 1000 пользователей. Дальше приложение начинает отдельно загружать заказы для каждого пользователя:
И этот запрос выполняется уже 1000 раз. То есть итоговая схема выглядит так: 1 запрос на получение
В ORM это обычно выглядит примерно так:
Внешне код выглядит абсолютно нормально, но внутри ORM может выполнять отдельный
На маленьких объемах данных проблема почти незаметна. Но на продакшене начинают быстро расти: latency, network overhead, нагрузка на connection pool, время ответа API, нагрузка на БД.
Особенно неприятно это проявляется при pagination, background jobs и high-load API. Обычно данные эффективнее загружать набором.
Например, через
Либо через batch loading:
Во многих случаях
Поэтому современные ORM обычно уже имеют встроенные механизмы борьбы с N+1.
Например, в Django:
В SQLAlchemy:
Отдельная проблема — nested N+1:
И приложение внезапно начинает выполнять уже сотни, тысячи или даже десятки тысяч SQL-запросов. Самое опасное — проблема часто долго остается незаметной, пока объем данных не вырастает.
🔥 Если внутри цикла потенциально выполняется SQL-запрос — почти всегда стоит проверить код на N+1. Именно поэтому profiling SQL-запросов и понимание того, как ORM реально работает с базой, критично для продакшн backend-разработки.
➡️ SQL Ready | #практика
Одна из самых частых проблем backend-приложений — N+1 queries. Особенно часто это появляется при работе через ORM, потому что код выглядит нормально, а реальные SQL-запросы скрыты внутри слоя абстракции.
Например, есть таблицы:
users(id, name)
orders(id, user_id, amount)
Сначала приложение получает пользователей:
SELECT
id,
name
FROM users;
Допустим, запрос вернул 1000 пользователей. Дальше приложение начинает отдельно загружать заказы для каждого пользователя:
SELECT
id,
user_id,
amount
FROM orders
WHERE user_id = ?;
И этот запрос выполняется уже 1000 раз. То есть итоговая схема выглядит так: 1 запрос на получение
users, N запросов на получение orders. Это и есть классическая N+1 problem.В ORM это обычно выглядит примерно так:
users = User.objects.all()
for user in users:
orders = list(user.order_set.all())
print(orders)
Внешне код выглядит абсолютно нормально, но внутри ORM может выполнять отдельный
SELECT для каждого user.order_set.all().На маленьких объемах данных проблема почти незаметна. Но на продакшене начинают быстро расти: latency, network overhead, нагрузка на connection pool, время ответа API, нагрузка на БД.
Особенно неприятно это проявляется при pagination, background jobs и high-load API. Обычно данные эффективнее загружать набором.
Например, через
JOIN:SELECT
u.id,
u.name,
o.id,
o.amount
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id;
Либо через batch loading:
SELECT
id,
user_id,
amount
FROM orders
WHERE user_id IN (?, ?, ?, ...);
Во многих случаях
batch loading даже эффективнее огромного JOIN, потому что JOIN может раздувать result set и создавать большое количество дублирующихся строк при one-to-many связях.Поэтому современные ORM обычно уже имеют встроенные механизмы борьбы с N+1.
Например, в Django:
User.objects.prefetch_related("order_set")
User.objects.select_related("profile")
prefetch_related() обычно используется для reverse FK и many-to-many; select_related() — для FK и OneToOne.В SQLAlchemy:
select(User).options(
selectinload(User.orders)
)
Отдельная проблема — nested N+1:
users
→ orders
→ payments
→ items
И приложение внезапно начинает выполнять уже сотни, тысячи или даже десятки тысяч SQL-запросов. Самое опасное — проблема часто долго остается незаметной, пока объем данных не вырастает.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10👍8❤6
This media is not supported in your browser
VIEW IN TELEGRAM
Репозиторий представляет собой структурированную базу знаний по MySQL, где собраны как основы работы с базами данных, так и более сложные темы. Материал подан в формате конспекта, поэтому его удобно использовать и для изучения, и для быстрого повторения перед собеседованием или рабочими задачами.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍6🤝5
Например, Query Router (mongos) принимает запросы от приложения и распределяет их по нужным шардам, а Replica Set внутри каждого shard обеспечивает отказоустойчивость и репликацию данных.
На картинке — базовая архитектура MongoDB Cluster: Client Application, Driver, Query Router, Config Server и Shards с Primary/Secondary-нодами.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤16👍6🔥4🤝3
This media is not supported in your browser
VIEW IN TELEGRAM
Отличный ресурс для тех, кто хочет разобраться в SQL и освоить оконные функции. На сайте подробно объясняются инструменты для аналитики и сложной обработки данных. Всё сопровождается наглядными примерами и практическими кейсами.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍7🔥4
Транзакционная блокировка без блокировки строк!
Иногда нужно запретить параллельную обработку одного объекта, но подходящей строки для
Пока транзакция не завершится, другой процесс с тем же ключом будет ждать. После
Если ждать нельзя, используй
🔥 Полезно для идемпотентных операций, генерации документов, биллинга, обработки вебхуков и любых мест, где один и тот же объект нельзя обрабатывать параллельно.
➡️ SQL Ready | #совет
Иногда нужно запретить параллельную обработку одного объекта, но подходящей строки для
FOR UPDATE ещё может не быть.SELECT pg_advisory_xact_lock(10, 42);
pg_advisory_xact_lock создаёт транзакционную пользовательскую блокировку. Первый аргумент удобно использовать как namespace, второй — как id объекта.SELECT pg_advisory_xact_lock(20, user_id);
Пока транзакция не завершится, другой процесс с тем же ключом будет ждать. После
COMMIT или ROLLBACK блокировка снимается автоматически.SELECT pg_try_advisory_xact_lock(20, user_id);
Если ждать нельзя, используй
pg_try_advisory_xact_lock: он сразу вернёт true или false, и приложение сможет аккуратно пропустить задачу.BEGIN;
SELECT pg_advisory_xact_lock(30, 123);
UPDATE invoices SET status = 'paid' WHERE id = 123;
COMMIT;
advisory lock не блокирует таблицу и не заменяет ограничения БД. Это кооперативная блокировка, поэтому все участники должны использовать один и тот же ключ.Please open Telegram to view this post
VIEW IN TELEGRAM
👍13❤5🔥4
This media is not supported in your browser
VIEW IN TELEGRAM
Этот репозиторий особенно полезен тем, кто уже работает с PostgreSQL и хочет больше разобраться в производительности базы данных. Здесь хорошо показано, как анализировать запросы, понимать execution plan и находить узкие места, которые замедляют работу приложения.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10👍7🤝5
Lost Update: почему транзакций недостаточно без правильной модели конкурентного доступа!
Очень распространённое заблуждение: если запросы выполняются внутри транзакции — значит данные уже защищены от гонок, но это не так.
Одна из классических проблем конкурентного доступа —
Текущий баланс:
Два запроса одновременно читают баланс:
Оба получают: 1000. Дальше: первый процесс хочет списать 100; второй — 200. Первый считает: 1000 - 100 = 900. Второй: 1000 - 200 = 800.
После этого выполняются:
а также:
В итоге финальный баланс: 800, хотя математически должен быть: 700, одно обновление потерялось. Это и есть
И самое неприятное — такое может происходить даже внутри транзакций. Всё зависит от уровня изоляции, паттерна обновления и механики блокировок конкретной СУБД.
Очень частая ошибка выглядит так:
Проблема в том, что между
Один из самых надёжных вариантов — атомарное обновление:
Теперь вычисление происходит внутри самого
СУБД выполняет обновление на основе актуального значения строки и использует необходимые механизмы блокировок для корректной синхронизации конкурентных изменений. Это намного безопаснее.
Ещё один вариант — pessimistic locking:
Но здесь есть trade-off, чем больше блокировок: тем выше contention; тем ниже concurrency; тем выше риск deadlock.
Ещё один подход — optimistic locking. Например:
Чтение:
Обновление:
Если другая транзакция уже изменила строку —
Например, PostgreSQL и MySQL (InnoDB) используют разные механизмы MVCC и имеют различия в поведении блокировок и уровней изоляции.
🔥 Главное правило, если логика выглядит как: прочитал значение — изменил в приложении — записал обратно, то всегда стоит проверять, не появляется ли
➡️ SQL Ready | #практика
Очень распространённое заблуждение: если запросы выполняются внутри транзакции — значит данные уже защищены от гонок, но это не так.
Одна из классических проблем конкурентного доступа —
lost update. Например, есть таблица:accounts(id, balance)
Текущий баланс:
id | balance
1 | 1000
Два запроса одновременно читают баланс:
SELECT balance
FROM accounts
WHERE id = 1;
Оба получают: 1000. Дальше: первый процесс хочет списать 100; второй — 200. Первый считает: 1000 - 100 = 900. Второй: 1000 - 200 = 800.
После этого выполняются:
UPDATE accounts
SET balance = 900
WHERE id = 1;
а также:
UPDATE accounts
SET balance = 800
WHERE id = 1;
В итоге финальный баланс: 800, хотя математически должен быть: 700, одно обновление потерялось. Это и есть
lost update.И самое неприятное — такое может происходить даже внутри транзакций. Всё зависит от уровня изоляции, паттерна обновления и механики блокировок конкретной СУБД.
Очень частая ошибка выглядит так:
BEGIN;
SELECT balance
FROM accounts
WHERE id = 1;
-- вычисления в приложении
UPDATE accounts
SET balance = :new_balance
WHERE id = 1;
COMMIT;
Проблема в том, что между
SELECT и UPDATE другая транзакция может изменить строку.Один из самых надёжных вариантов — атомарное обновление:
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
Теперь вычисление происходит внутри самого
UPDATE.СУБД выполняет обновление на основе актуального значения строки и использует необходимые механизмы блокировок для корректной синхронизации конкурентных изменений. Это намного безопаснее.
Ещё один вариант — pessimistic locking:
SELECT balance
FROM accounts
WHERE id = 1
FOR UPDATE;
FOR UPDATE ставит row-level lock. Пока транзакция не завершится, другая транзакция не сможет изменить эту строку или получить несовместимую блокировку на неё.Но здесь есть trade-off, чем больше блокировок: тем выше contention; тем ниже concurrency; тем выше риск deadlock.
Ещё один подход — optimistic locking. Например:
accounts(id, balance, version)
Чтение:
SELECT balance, version
FROM accounts
WHERE id = 1;
Обновление:
UPDATE accounts
SET
balance = :new_balance,
version = version + 1
WHERE
id = 1
AND version = 5;
Если другая транзакция уже изменила строку —
UPDATE затронет 0 строк. Приложение понимает: данные устарели, нужно перечитать и повторить операцию. Ещё важно понимать: READ COMMITTED, REPEATABLE READ, SERIALIZABLE ведут себя по-разному в разных СУБД.Например, PostgreSQL и MySQL (InnoDB) используют разные механизмы MVCC и имеют различия в поведении блокировок и уровней изоляции.
lost update при параллельных запросах.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍6🤝6
This media is not supported in your browser
VIEW IN TELEGRAM
На странице собрана официальная документация ClickHouse по работе с PostgreSQL. Здесь подробно разбираются импорт данных, репликация таблиц, синхронизация и выполнение запросов к PostgreSQL напрямую из ClickHouse. Также есть примеры настройки и практические сценарии использования.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13🔥8👍7👎2
Каждый найдет что-то по душе:
1202 ГБ — Python
1811 ГБ — Frontend
1100 ГБ — C / C++ / C#
804 ГБ — Java
411 ГБ — SQL & БД
309 ГБ — DevOps
998 ГБ — ИБ & Хакинг
773 ГБ — Kotlin / Swift
189 ГБ — PHP
201 ГБ — GoLang
170 ГБ — Rust
167 ГБ — QA / Тестирование
310 ГБ — 1C + Лицензии
495 ГБ — Машинное обучение
704 ГБ — Аналитика Данных
991 ГБ — Дизайн
Материалы в закрепе, постоянно пополняются👆🏻
Please open Telegram to view this post
VIEW IN TELEGRAM
👎7❤1
Разбор JSON в строки на стороне PostgreSQL!
Когда из API прилетает массив объектов, не обязательно разбирать его в коде и делать много отдельных
На вход можно передать
Это особенно удобно для пакетных загрузок, импорта данных, обработки вебхуков и синхронизации с внешними сервисами.
Главный плюс в том, что приложение передаёт один JSON-параметр, а вся пакетная обработка происходит внутри базы одним запросом.
🔥 Такой приём убирает циклы в коде, уменьшает количество запросов к базе и делает массовые операции намного чище.
➡️ SQL Ready | #совет
Когда из API прилетает массив объектов, не обязательно разбирать его в коде и делать много отдельных
INSERT или UPDATE. PostgreSQL умеет превратить JSON-массив в обычную табличную выборку.SELECT *
FROM jsonb_to_recordset($1::jsonb)
AS x(id bigint, amount numeric);
На вход можно передать
JSON вида [{"id":1,"amount":500},{"id":2,"amount":900}], а на выходе получить нормальные типизированные строки.INSERT INTO payments (id, amount)
SELECT id, amount
FROM jsonb_to_recordset($1::jsonb)
AS x(id bigint, amount numeric);
Это особенно удобно для пакетных загрузок, импорта данных, обработки вебхуков и синхронизации с внешними сервисами.
UPDATE payments p
SET amount = x.amount
FROM jsonb_to_recordset($1::jsonb)
AS x(id bigint, amount numeric)
WHERE p.id = x.id;
Главный плюс в том, что приложение передаёт один JSON-параметр, а вся пакетная обработка происходит внутри базы одним запросом.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14👍8🤝5
Разбираем почему ORDER BY RANDOM() на больших таблицах не лучшая идея!
Если нужно вытащить случайные строки из таблицы, многие пишут так, например в PostgreSQL:
На первый взгляд всё отлично: перемешали строки и взяли первые 10. Но под капотом всё не так красиво. Обычно база читает много строк, а часто вообще всю таблицу, затем вычисляет случайное значение для каждой строки, сортирует результат и только после этого применяет
То есть даже если нужна всего одна строка:
на большой таблице это всё равно может оказаться тяжёлым запросом. Если строк миллионы, начинаются проблемы с CPU, памятью, сортировками и latency. Индексы здесь обычно не помогают.
Один из более дешёвых вариантов — случайный выбор через id:
Этот вариант уже может использовать индекс по id. Но есть нюанс: если после удалений в id много дырок, распределение будет неидеальным.
Например:
В таком случае чаще будет выпадать первая существующая строка после большого разрыва — здесь это 10000. То есть выборка получается смещённой.
Ещё один вариант — случайный
Звучит неплохо. Но у
В PostgreSQL есть ещё
Или:
На больших таблицах это часто работает заметно быстрее. Но важно понимать:
Если нужно, например, 10 строк, обычно добавляют
Но и тут есть нюанс: если sample слишком маленький, запрос может вернуть меньше 10 строк.
И здесь тоже есть компромисс между скоростью и качеством случайной выборки.
выглядит красиво и удобно. Но на больших таблицах это один из тех запросов, которые начинают стоить очень дорого.
🔥 Именно такие простые запросы часто становятся причиной деградации производительности в продакшн.
➡️ SQL Ready | #практика
Если нужно вытащить случайные строки из таблицы, многие пишут так, например в PostgreSQL:
SELECT *
FROM products
ORDER BY RANDOM()
LIMIT 10;
На первый взгляд всё отлично: перемешали строки и взяли первые 10. Но под капотом всё не так красиво. Обычно база читает много строк, а часто вообще всю таблицу, затем вычисляет случайное значение для каждой строки, сортирует результат и только после этого применяет
LIMIT.То есть даже если нужна всего одна строка:
SELECT *
FROM products
ORDER BY RANDOM()
LIMIT 1;
на большой таблице это всё равно может оказаться тяжёлым запросом. Если строк миллионы, начинаются проблемы с CPU, памятью, сортировками и latency. Индексы здесь обычно не помогают.
Один из более дешёвых вариантов — случайный выбор через id:
SELECT *
FROM products
WHERE id >= 1 + FLOOR(RANDOM() * (SELECT MAX(id) FROM products))::int
ORDER BY id
LIMIT 1;
Этот вариант уже может использовать индекс по id. Но есть нюанс: если после удалений в id много дырок, распределение будет неидеальным.
Например:
id
1
2
3
10000
10001
В таком случае чаще будет выпадать первая существующая строка после большого разрыва — здесь это 10000. То есть выборка получается смещённой.
Ещё один вариант — случайный
OFFSET:SELECT *
FROM products
OFFSET FLOOR(RANDOM() * (SELECT COUNT(*) FROM products))::int
LIMIT 1;
Звучит неплохо. Но у
OFFSET тоже есть проблема: чем больше offset, тем больше строк базе придётся пропустить. Плюс в PostgreSQL COUNT(*) на большой таблице тоже может быть дорогой операцией.В PostgreSQL есть ещё
TABLESAMPLE:SELECT *
FROM products TABLESAMPLE SYSTEM (1);
Или:
SELECT *
FROM products TABLESAMPLE BERNOULLI (1);
На больших таблицах это часто работает заметно быстрее. Но важно понимать:
TABLESAMPLE выбирает примерный процент таблицы, а не конкретное количество строк.Если нужно, например, 10 строк, обычно добавляют
LIMIT:SELECT *
FROM products TABLESAMPLE SYSTEM (1)
LIMIT 10;
Но и тут есть нюанс: если sample слишком маленький, запрос может вернуть меньше 10 строк.
И здесь тоже есть компромисс между скоростью и качеством случайной выборки.
SYSTEM работает быстрее, но выбирает данные блоками, поэтому распределение менее равномерное. BERNOULLI ближе к построчной случайной выборке, но обычно дороже. В общем, мысль простая:ORDER BY RANDOM()
выглядит красиво и удобно. Но на больших таблицах это один из тех запросов, которые начинают стоить очень дорого.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🔥10🤝6
Почему оконные функции могут давать неверный результат из-за RANGE вместо ROWS!
Одна из самых неприятных особенностей window functions — frame по умолчанию. Запрос выглядит корректно, проходит проверку глазами, но накопительная сумма может считаться совсем не так, как ожидает разработчик.
Особенно часто это встречается там, где важен порядок операций: расчёт балансов, транзакции, аналитика событий и любые накопительные показатели.
Есть таблица:
Допустим, нужно получить обычный running total — сумму всех предыдущих платежей до текущей строки.
Запрос выглядит очевидно:
Логика кажется простой: первая строка берёт свой amount, вторая прибавляет своё значение к предыдущей сумме, третья делает то же самое. То есть ожидается: 100 — 300 — 600. Но у оконных функций есть скрытая настройка — frame.
Если после
И именно здесь появляется разница между ожиданием и реальным результатом.
Например:
Здесь первые две записи имеют одинаковое время. Для человека это две разные операции. Но для
При таком запросе:
результат будет:
Почему? Потому что при расчёте первой строки
Но чаще всего для накопительных сумм ожидается другое поведение: каждая строка должна учитываться отдельно. Для этого используется
Он не смотрит, одинаковые ли значения сортировки у соседних строк. Для него важен порядок строк после сортировки. Например:
Теперь база получает явную инструкцию: сначала отсортировать записи, затем считать накопление строка за строкой.
Результат становится ожидаемым:
Ещё один важный момент — сама сортировка. Даже если используется
Если несколько событий произошли в одну секунду, SQL не обязан выбирать между ними определённый порядок. Например:
Какая операция должна идти первой? Без дополнительного поля ответа нет. Поэтому обычно добавляют уникальный идентификатор:
Теперь порядок строк становится однозначным, а результат расчёта стабильным.
🔥 Вывод: если
➡️ SQL Ready | #практика
Одна из самых неприятных особенностей window functions — frame по умолчанию. Запрос выглядит корректно, проходит проверку глазами, но накопительная сумма может считаться совсем не так, как ожидает разработчик.
Особенно часто это встречается там, где важен порядок операций: расчёт балансов, транзакции, аналитика событий и любые накопительные показатели.
Есть таблица:
payments(id, created_at, amount)
Допустим, нужно получить обычный running total — сумму всех предыдущих платежей до текущей строки.
Запрос выглядит очевидно:
SELECT
id,
created_at,
amount,
SUM(amount) OVER (
ORDER BY created_at
) AS running_total
FROM payments;
Логика кажется простой: первая строка берёт свой amount, вторая прибавляет своё значение к предыдущей сумме, третья делает то же самое. То есть ожидается: 100 — 300 — 600. Но у оконных функций есть скрытая настройка — frame.
Если после
ORDER BY не указать его явно, SQL использует значение по умолчанию:RANGE BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
И именно здесь появляется разница между ожиданием и реальным результатом.
RANGE работает не по физическому положению строк, а по значениям сортировки. То есть база смотрит на поле из ORDER BY и объединяет строки с одинаковым значением в одну логическую группу.Например:
id=1, created_at='2026-01-01 10:00:00', amount=100
id=2, created_at='2026-01-01 10:00:00', amount=200
id=3, created_at='2026-01-01 11:00:00', amount=300
Здесь первые две записи имеют одинаковое время. Для человека это две разные операции. Но для
RANGE они являются одной группой, потому что значение сортировки совпадает.При таком запросе:
SUM(amount) OVER (
ORDER BY created_at
)
результат будет:
300
300
600
Почему? Потому что при расчёте первой строки
RANGE уже включает обе записи с одинаковым created_at. Получается: 100 + 200 = 300. Поэтому первая строка сразу показывает итог двух операций. Это не ошибка SQL, это стандартное поведение RANGE.Но чаще всего для накопительных сумм ожидается другое поведение: каждая строка должна учитываться отдельно. Для этого используется
ROWS, который работает именно с физическими строками окна.Он не смотрит, одинаковые ли значения сортировки у соседних строк. Для него важен порядок строк после сортировки. Например:
SELECT
id,
created_at,
amount,
SUM(amount) OVER (
ORDER BY created_at, id
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
) AS running_total
FROM payments;
Теперь база получает явную инструкцию: сначала отсортировать записи, затем считать накопление строка за строкой.
Результат становится ожидаемым:
100
300
600
Ещё один важный момент — сама сортировка. Даже если используется
ROWS, одного created_at может быть недостаточно.Если несколько событий произошли в одну секунду, SQL не обязан выбирать между ними определённый порядок. Например:
id=1, created_at='10:00:00', amount=100
id=2, created_at='10:00:00', amount=200
Какая операция должна идти первой? Без дополнительного поля ответа нет. Поэтому обычно добавляют уникальный идентификатор:
ORDER BY created_at, id
Теперь порядок строк становится однозначным, а результат расчёта стабильным.
RANGE удобен, когда нужно работать с группами одинаковых значений сортировки. ROWS нужен, когда важен порядок конкретных строк. Для обычного running total почти всегда стоит явно указывать ROWS.cumulative sum показывает одинаковые значения на нескольких строках подряд — проверьте ORDER BY и frame окна. Чаще всего причина в том, что ожидали ROWS, а получили поведение RANGE.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤7👍6
Например, сначала изучают основы SQL и работу с таблицами, затем переходят к JOIN, сложным запросам, подзапросам и оконным функциям, а дальше — к проектированию баз данных, транзакциям и оптимизации.
На картинке — путь изучения от базового уровня до продвинутого: основные команды, структура базы данных, работа с данными, производительность запросов и др.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12👍9🤝6
Разработчики из Anthropic обновили и структурировали масштабный хаб готовых запросов для своей языковой модели. База создана для того, чтобы пользователи могли выжать максимум из Claude при решении прикладных и технических задач, не тратя время на самостоятельный подбор формулировок.
🟦 Для безопасности: готовые конструкции для проверки кода на уязвимости и анализа граничных условий работы алгоритмов.
🟦 Для разработчиков: шаблоны для глубокого ревью кода, автоматического поиска багов, рефакторинга, написания юнит-тестов и проектирования архитектуры.
🟦 Для автоматизации и менеджмента: промпты под стратегическое планирование, парсинг неструктурированных данных, генерацию технической документации и выстраивание логики для ИИ-агентов.
Каждый шаблон снабжен подробным разбором: авторы пошагово объясняют, почему выбрана именно такая структура запроса, как модель интерпретирует переменные и как правильно передавать контекст. Поскольку библиотека официальная, все промпты оптимизированы под особенности контекстного окна и логику мышления последних моделей семейства Claude.
Нейросети отлично автоматизируют рутину и помогают искать уязвимости, но управлять ими может только тот, кто понимает саму базу и суть киберугроз.
Если вы хотите заложить мощный фундамент начните с нашего бесплатного курса (его прошли уже 1108 человек!).
🟦 Разберетесь, как работают кибератаки и как грамотно защитить свои данные.
🟦 Познакомитесь с направлениями и профессиями в кибербезопасности, чтобы выбрать свой трек.
🟦 Получите скидку, если решите продолжить обучение в CyberYozh Academy.
👉 Начните бесплатно прямо сейчас
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2