Например, Primary Key гарантирует уникальность записей, Foreign Key — обеспечивает ссылочную целостность, а JOIN’ы позволяют собирать распределённые данные в единую выборку.
На картинке — основные концепции, которые используются при проектировании и работе с БД.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤8🤝8
Проверяем дубликаты и считаем уникальные значения!
В больших таблицах важно быстро находить повторяющиеся записи и понимать, сколько уникальных элементов. Это полезно для контроля качества данных и аналитики.
Создадим таблицу пользователей:
Запрос для выявления дубликатов и подсчёта уникальных email:
Функция
Результат:
🔥 Это простой способ контролировать качество данных, выявлять ошибки и готовить отчёты для команды.
➡️ SQL Ready | #практика
В больших таблицах важно быстро находить повторяющиеся записи и понимать, сколько уникальных элементов. Это полезно для контроля качества данных и аналитики.
Создадим таблицу пользователей:
CREATE TABLE users (
user_id INT,
email VARCHAR(100)
);
INSERT INTO users VALUES
(1, 'alice@mail.com'),
(2, 'bob@mail.com'),
(3, 'alice@mail.com'),
(4, 'carol@mail.com'),
(5, 'bob@mail.com');
Запрос для выявления дубликатов и подсчёта уникальных email:
SELECT email, COUNT(*) AS cnt,
CASE WHEN COUNT(*)>1 THEN 'Duplicate' ELSE 'Unique' END AS status
FROM users
GROUP BY email;
Функция
COUNT() + GROUP BY группирует одинаковые значения, а CASE сразу классифицирует их как дубликаты или уникальные.Результат:
email | cnt | status
-----------------------------
alice@mail.com | 2 | Duplicate
bob@mail.com | 2 | Duplicate
carol@mail.com | 1 | Unique
🔥 Это простой способ контролировать качество данных, выявлять ошибки и готовить отчёты для команды.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18🔥7🤝6❤5
Использование data-modifying CTE для цепочек операций в одном запросе!
Обычно изменения и чтение делают разными запросами, что создаёт лишние
Теперь можно атомарно обновить данные и сразу записать аудит, отправить в очередь или выполнить дополнительную логику без повторных
🔥
➡️ SQL Ready | #совет
Обычно изменения и чтение делают разными запросами, что создаёт лишние
round-trip и риск рассинхронизации данных между операциями:UPDATE orders
SET status = 'processing'
WHERE id = 123
RETURNING *;
RETURNING уже даёт доступ к изменённым строкам, но data-modifying CTE позволяет пойти дальше и использовать их в следующих шагах:WITH updated AS (...)
CTE фиксирует результат изменения и гарантирует, что последующие операции работают с тем же набором строк в рамках одного statement:INSERT INTO audit_log (order_id, new_status)
SELECT id, status
FROM updated;
Теперь можно атомарно обновить данные и сразу записать аудит, отправить в очередь или выполнить дополнительную логику без повторных
SELECT.data-modifying CTE — это способ строить сложные, но безопасные цепочки операций внутри одного SQL-запроса.Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥10🤝8
В этой статье:
• Разбирается, почему классические RAG-системы на базе векторных БД часто ограничиваются поверхностным поиском;
• Показан подход HippoRAG 2, где память и связи между фактами организованы ближе к графовой модели;
• Объясняется, как эволюционирует работа с данными;
• Рассматривается, как это влияет на SQL/NoSQL слой, архитектуру хранения и построение более осмысленных запросов к данным.🔊 Продолжайте читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍7🤝5❤3
Дубликаты после JOIN — откуда берутся и как контролировать!
Одна из частых проблем — внезапное размножение строк после JOIN. Это базовое поведение, если не учтена кардинальность связей.
Таблицы:
Задача: получить заказы с информацией об оплате.
Если у одного заказа несколько платежей — в результат попадёт несколько строк. Фактически вы получаете по одной строке на каждое совпадение
Для связи 1:N это абсолютно ожидаемо. Где начинаются проблемы — агрегация:
Здесь
Корректный вариант:
Так считаются уникальные заказы, независимо от числа платежей. Но важно помнить, что
И отдельный момент, если вам нужно просто количество заказов без условий по
Если задача — проверить наличие успешной оплаты, проще и дешевле использовать
Если
Здесь мы сначала приводим
Типичная ошибка:
Это не решит проблему. Такой
🔥
➡️ SQL Ready | #практика
Одна из частых проблем — внезапное размножение строк после JOIN. Это базовое поведение, если не учтена кардинальность связей.
Таблицы:
orders(id, customer_id, amount)
payments(id, order_id, status)
Задача: получить заказы с информацией об оплате.
SELECT
o.id,
o.amount,
p.status
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
Если у одного заказа несколько платежей — в результат попадёт несколько строк. Фактически вы получаете по одной строке на каждое совпадение
orders — payments. То есть один заказ повторится столько раз, сколько у него записей в payments.Для связи 1:N это абсолютно ожидаемо. Где начинаются проблемы — агрегация:
SELECT
COUNT(*) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
Здесь
COUNT(*) считает строки уже после JOIN, а не заказы. Если у заказа 3 платежа — он попадёт в счёт 3 раза. Это одна из самых частых причин кривых метрик.Корректный вариант:
SELECT
COUNT(DISTINCT o.id) AS total_orders
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.id;
Так считаются уникальные заказы, независимо от числа платежей. Но важно помнить, что
DISTINCT — это дополнительная операция, и на больших объёмах она может стоить дорого.И отдельный момент, если вам нужно просто количество заказов без условий по
payments, JOIN здесь вообще лишний. Лучше контролировать кардинальность до JOINЕсли задача — проверить наличие успешной оплаты, проще и дешевле использовать
EXISTS:SELECT
o.id,
o.amount
FROM orders o
WHERE EXISTS (
SELECT 1
FROM payments p
WHERE p.order_id = o.id
AND p.status = 'success'
);
EXISTS работает как semi-join: он проверяет факт наличия строки, но не тянет её в результат. За счёт этого одна строка заказа остаётся одной строкой.Если
JOIN всё-таки нужен — агрегируем заранее:SELECT
o.id,
o.amount,
COALESCE(p.has_success, 0) AS has_success
FROM orders o
LEFT JOIN (
SELECT
order_id,
MAX(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS has_success
FROM payments
GROUP BY order_id
) p ON p.order_id = o.id;
Здесь мы сначала приводим
payments к одной строке на order_id, и только потом делаем JOIN. После этого результат становится понятным: одна строка на заказ, без раздувания.Типичная ошибка:
GROUP BY o.id, o.amount, p.status
Это не решит проблему. Такой
GROUP BY просто фиксирует текущую детализацию. Если у заказа было несколько статусов — строки никуда не денутся.JOIN не создаёт дубликаты сам по себе. Он возвращает строки в соответствии с числом совпадений по условию ON. Если после JOIN строк стало больше — значит реальная связь между таблицами не 1:1, а 1:N или даже N:M.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17👍11🤝7❤3
This media is not supported in your browser
VIEW IN TELEGRAM
Репозиторий представляет собой обширную коллекцию скриптов, статей, рекомендаций и утилит для администрирования и разработки на SQL. Внутри собраны решения для диагностики производительности, оптимизации запросов, мониторинга, резервного копирования, работы с индексами и анализа состояния базы данных. Также представлены ссылки на проверенные источники и лучшие практики работы с СУБД.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥8🤝6❤4
This media is not supported in your browser
VIEW IN TELEGRAM
Это структурированное обучение по PostgreSQL: от базовых запросов до JOIN’ов, подзапросов и проектирования базы данных. Материал выстроен последовательно, поэтому легко идти шаг за шагом и не теряться в теме. Здесь есть практика с заданиями и ответами, за счёт чего обучение закрепляется.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
❤17🤝10👍9
Диапазоны как тип данных: убираем двойные условия и баги!
Когда работаешь с интервалами, обычно пишут два условия, и почти всегда где-то ошибаются с границами:
Такой код легко сломать и плохо масштабируется:
PostgreSQL имеет встроенные range-типы, которые позволяют хранить и сравнивать интервалы как единое значение.
tstzrange — для timestamptz, tsrange — для timestamp:
Оператор
По умолчанию границы [ ) — аналог
С
🔥
➡️ SQL Ready | #совет
Когда работаешь с интервалами, обычно пишут два условия, и почти всегда где-то ошибаются с границами:
WHERE created_at <= now()
AND shipped_at > now()
Такой код легко сломать и плохо масштабируется:
tstzrange(created_at, shipped_at)
PostgreSQL имеет встроенные range-типы, которые позволяют хранить и сравнивать интервалы как единое значение.
tstzrange — для timestamptz, tsrange — для timestamp:
@> now()
Оператор
@> проверяет содержит ли диапазон значение, заменяя сразу два условия.По умолчанию границы [ ) — аналог
created_at <= x AND shipped_at > x:CREATE INDEX ON orders USING GIST (tstzrange(created_at, shipped_at));
С
GiST-индексом такие запросы работают эффективно даже на больших объёмах.range-типы — это способ убрать класс ошибок с датами и упростить сложную логику.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍10❤7🤝1
Например, каждое подключение создаёт отдельный процесс, а любые изменения сначала попадают в WAL, и только потом гарантированно пишутся в данные.
На картинке — базовая архитектура: подключения и backend-процессы, shared memory, background и auxiliary процессы и физическое хранение.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍6🔥5🤝3
Исключающие ограничения: гарантия отсутствия пересечений на уровне БД!
Проверять пересекаются ли интервалы через SELECT перед INSERT — классическая ошибка, которая ломается при конкурентных транзакциях.
Даже если проверка есть, два параллельных запроса могут пройти её одновременно и записать конфликтующие данные:
PostgreSQL решает это на уровне ограничений через exclusion constraint, который работает корректно даже при конкуренции:
🔥
➡️ SQL Ready | #совет
Проверять пересекаются ли интервалы через SELECT перед INSERT — классическая ошибка, которая ломается при конкурентных транзакциях.
SELECT 1
FROM bookings
WHERE room_id = 101
AND start_at < $end
AND end_at > $start;
Даже если проверка есть, два параллельных запроса могут пройти её одновременно и записать конфликтующие данные:
CREATE EXTENSION IF NOT EXISTS btree_gist;
PostgreSQL решает это на уровне ограничений через exclusion constraint, который работает корректно даже при конкуренции:
ALTER TABLE bookings
ADD CONSTRAINT no_overlap
EXCLUDE USING gist (
room_id WITH =,
tsrange(start_at, end_at) WITH &&
);
exclusion constraint — это способ переложить сложную логику (пересечения, диапазоны, уникальность по условиям) с приложения на уровень СУБД с полной защитой от состояния гонки.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14❤7🤝6
This media is not supported in your browser
VIEW IN TELEGRAM
В этом репозитории собраны лучшие инструменты, библиотеки, GUI-клиенты, ORM, статьи, курсы и сервисы для работы с PostgreSQL. По сути — это подборка всего полезного для backend-разработчика и DBA в одном месте. Внутри есть материалы не только по базовой работе с БД, но и по мониторингу, репликации, производительности, миграциям и архитектуре.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤7🤝5
LAG() в SQL — сравниваем строку с предыдущей без JOIN!
Оконная функция
Таблица:
payments(id, user_id, amount, created_at)
Посмотрим предыдущий платёж каждого пользователя:
Теперь можно сразу посчитать разницу между текущим и прошлым платежом:
Так удобно анализировать рост, падение и аномалии в данных без дополнительных JOIN.
Можно находить моменты изменения значения, например смену статуса:
Запрос покажет только строки, где статус изменился относительно предыдущего события.
🔥
➡️ SQL Ready | #практика
Оконная функция
LAG() позволяет получить значение из предыдущей строки внутри группы. Это один из самых полезных инструментов для аналитики, логов и временных рядов.Таблица:
payments(id, user_id, amount, created_at)
Посмотрим предыдущий платёж каждого пользователя:
SELECT
user_id,
amount,
LAG(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS prev_amount
FROM payments;
LAG() сдвигает значение на одну строку назад внутри окна пользователя.Теперь можно сразу посчитать разницу между текущим и прошлым платежом:
SELECT
user_id,
amount,
amount - LAG(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS diff
FROM payments;
Так удобно анализировать рост, падение и аномалии в данных без дополнительных JOIN.
Можно находить моменты изменения значения, например смену статуса:
SELECT *
FROM (
SELECT
user_id,
status,
LAG(status) OVER (
PARTITION BY user_id
ORDER BY created_at
) AS prev_status
FROM user_logs
) t
WHERE status <> prev_status;
Запрос покажет только строки, где статус изменился относительно предыдущего события.
LAG() часто используют для анализа трендов, поиска изменений, расчёта дельт и работы с временными рядами.Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍11🤝5
Шардинг помогает масштабировать базу данных, распределяя данные между несколькими серверами. На картинке — основные стратегии партиционирования и шардинга.
Например, key-based sharding использует hash-функцию для равномерного распределения записей, а range-based sharding делит данные по диапазонам значений.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍8🤝6
Генерация пропущенных дат в отчётах: почему LEFT JOIN не помогает!
Классическая задача — построить отчёт по дням. Например, количество заказов по датам. И внезапно оказывается, что в выдаче нет некоторых дней.
Таблица:
Плохой вариант:
Проблема: если в какой-то день не было заказов — строки просто не будет. В аналитике это почти всегда ошибка, потому что отсутствие строки не равно ноль значений.
Попытка через
В PostgreSQL для этого есть
Теперь: каждый день присутствует; если заказов нет —
Почему именно
Ещё один важный момент — условие в
Лучше диапазон:
Это позволяет использовать индекс и ускоряет запрос на больших таблицах.
Расширение задачи — накопительный итог по дням (используется тот же CTE calendar):
Если нужно ограничение по периоду по данным, а не вручную:
Календарь строится автоматически по данным. Если
🔥 Если в отчёте есть время — сначала формируйте ось времени, потом присоединяйте данные. Иначе вы работаете не с нулями, а с отсутствием строк, что даёт искажённую картину.
➡️ SQL Ready | #практика
Классическая задача — построить отчёт по дням. Например, количество заказов по датам. И внезапно оказывается, что в выдаче нет некоторых дней.
Таблица:
orders(id, created_at)
Плохой вариант:
SELECT
DATE(created_at) AS day,
COUNT(*) AS orders_count
FROM orders
GROUP BY DATE(created_at)
ORDER BY day;
Проблема: если в какой-то день не было заказов — строки просто не будет. В аналитике это почти всегда ошибка, потому что отсутствие строки не равно ноль значений.
Попытка через
LEFT JOIN сама по себе не помогает — без таблицы дат нечего достраивать. Правильный подход — сначала явно сгенерировать календарь, а потом присоединить данные.В PostgreSQL для этого есть
generate_series:WITH calendar AS (
SELECT generate_series(
DATE '2026-01-01',
DATE '2026-01-10',
INTERVAL '1 day'
)::date AS day
)
SELECT
c.day,
COUNT(o.id) AS orders_count
FROM calendar c
LEFT JOIN orders o
ON DATE(o.created_at) = c.day
GROUP BY c.day
ORDER BY c.day;
Теперь: каждый день присутствует; если заказов нет —
COUNT(o.id) вернёт 0.Почему именно
COUNT(o.id), а не COUNT(*): COUNT(*) посчитает строку календаря даже без совпадений — всегда ≥ 1; COUNT(o.id) считает только реальные заказы — корректный 0 при отсутствии данных.Ещё один важный момент — условие в
JOIN: ON DATE(o.created_at) = c.day. Так писать удобно, но это ломает использование индекса по created_at.Лучше диапазон:
ON o.created_at >= c.day
AND o.created_at < c.day + INTERVAL '1 day'
Это позволяет использовать индекс и ускоряет запрос на больших таблицах.
Расширение задачи — накопительный итог по дням (используется тот же CTE calendar):
SELECT
c.day,
COUNT(o.id) AS orders_count,
SUM(COUNT(o.id)) OVER (ORDER BY c.day) AS running_total
FROM calendar c
LEFT JOIN orders o
ON o.created_at >= c.day
AND o.created_at < c.day + INTERVAL '1 day'
GROUP BY c.day
ORDER BY c.day;
Если нужно ограничение по периоду по данным, а не вручную:
WITH bounds AS (
SELECT
MIN(created_at)::date AS min_day,
MAX(created_at)::date AS max_day
FROM orders
),
calendar AS (
SELECT generate_series(min_day, max_day, INTERVAL '1 day')::date AS day
FROM bounds
)
SELECT ...
Календарь строится автоматически по данным. Если
created_at — timestamptz, границы дня зависят от timezone.Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🔥7🤝4❤2
This media is not supported in your browser
VIEW IN TELEGRAM
Удобный и понятный справочник, где разобраны основные SQL-запросы и конструкции: SELECT, WHERE, JOIN, INSERT, UPDATE и другие ключевые команды. Всё объясняется простым языком с примерами, чтобы быстро понять логику работы с данными и начать применять это в реальных задачах
Please open Telegram to view this post
VIEW IN TELEGRAM
❤17🔥7👍6
BUFFERS: как увидеть цену запроса?
Обычный
Опция
Такой запрос почти полностью работает из памяти, что хорошо.
А здесь видно узкое место: запрос массово читает диск, даже если по времени более менее.
🔥 Два запроса с одинаковым
➡️ SQL Ready | #совет
Обычный
EXPLAIN ANALYZE показывает время выполнения, но почти ничего не говорит о том, сколько данных было прочитано с диска и из памяти.EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
Опция
BUFFERS добавляет статистику по страницам памяти:shared hit — страницы найдены в PostgreSQL shared buffers.shared read — страницы пришлось загрузить в shared buffers; это может быть физический диск или OS page cache.Buffers: shared hit=15000 read=2
Такой запрос почти полностью работает из памяти, что хорошо.
А здесь видно узкое место: запрос массово читает диск, даже если по времени более менее.
Buffers: shared hit=10 read=15000
execution time могут иметь абсолютно разную нагрузку на систему.Please open Telegram to view this post
VIEW IN TELEGRAM
🤝11❤7👍7
Например, Data Warehouse хранит уже очищенные и структурированные данные для BI и отчетности, Data Lake позволяет складировать сырые данные любого типа и обрабатывать их по мере необходимости, а Data Mesh распределяет владение данными между бизнес-доменами и превращает данные в полноценные data products.
На картинке — основные отличия: как хранятся данные, кто ими управляет, как происходит обработка и для каких задач лучше подходит каждый подход.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14👍6🔥5
Почему NOT IN может вернуть пустой результат из-за NULL!
Одна из самых неприятных ловушек SQL — поведение
Таблицы:
Допустим, нужно получить пользователей, которых нет в
Пока в
Теперь запрос внезапно вернет:
Хотя пользователи без бана есть. Почему так происходит — SQL сравнивает условие примерно как:
Но:
не даёт TRUE или FALSE. Результат: UNKNOWN. А в
Это особенность трёхзначной логики SQL: TRUE, FALSE, UNKNOWN
Любое сравнение с NULL даёт UNKNOWN:
Поэтому
Безопасный вариант —
Почему
Ещё вариант — явно убрать NULL:
Но на практике:
Отдельный момент:
Быстрая проверка проблемы:
Если такие строки есть —
🔥
➡️ SQL Ready | #практика
Одна из самых неприятных ловушек SQL — поведение
NOT IN при наличии NULL. Запрос выглядит абсолютно корректно, но внезапно перестаёт возвращать строки.Таблицы:
users(id)
bans(user_id)
Допустим, нужно получить пользователей, которых нет в
bans. Часто пишут так:SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
);
Пока в
bans.user_id нет NULL — всё работает нормально. Но представим данные:bans
-------
1
2
NULL
Теперь запрос внезапно вернет:
0 rows
Хотя пользователи без бана есть. Почему так происходит — SQL сравнивает условие примерно как:
id <> 1
AND id <> 2
AND id <> NULL
Но:
id <> NULL
не даёт TRUE или FALSE. Результат: UNKNOWN. А в
WHERE проходят только TRUE. Из-за этого всё условие целиком перестаёт выполняться.Это особенность трёхзначной логики SQL: TRUE, FALSE, UNKNOWN
Любое сравнение с NULL даёт UNKNOWN:
NULL = 1
NULL <> 1
NULL = NULL
Поэтому
NOT IN с NULL внутри подзапроса становится опасным.Безопасный вариант —
NOT EXISTS:SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM bans b
WHERE b.user_id = u.id
);
Почему
EXISTS работает нормально: сравнение идёт построчно; NULL не ломает всю проверку; оптимизатор обычно хорошо превращает это в anti-join.Ещё вариант — явно убрать NULL:
SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM bans
WHERE user_id IS NOT NULL
);
Но на практике:
NOT EXISTS обычно считается более безопасным и читаемым решением.Отдельный момент:
NOT IN () и NOT EXISTS могут давать разные планы выполнения в зависимости от СУБД. Но логически для nullable данных: NOT EXISTS почти всегда предпочтительнее.Быстрая проверка проблемы:
SELECT COUNT(*)
FROM bans
WHERE user_id IS NULL;
Если такие строки есть —
NOT IN уже потенциально опасен.NOT IN и NULL плохо сочетаются. Если в подзапросе появляется хотя бы один NULL, условие может перестать возвращать строки вообще. Для таких проверок надёжнее использовать NOT EXISTS.Please open Telegram to view this post
VIEW IN TELEGRAM
👍21❤10🔥5🤝1
This media is not supported in your browser
VIEW IN TELEGRAM
Здесь собрано огромное количество материалов по SQL: запросы, JOIN’ы, подзапросы, оконные функции, индексы, оптимизация и работа с PostgreSQL. Репозиторий отлично подходит как для изучения базы, так и для углубления в более сложные темы. Особенно полезно то, что здесь много практических примеров и разборов реальных SQL-конструкций.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤7🤝5
Например, индексы ускоряют поиск данных, а репликация помогает распределять нагрузку и повышать отказоустойчивость.
На картинке — 9 основных подходов для улучшения производительности базы данных, которые полезно держать под рукой.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍6🤝6