Обычное сравнение через = и != ломается, как только появляется NULL!
В SQL
Оба выражения вернут не
Кажется, что запрос покажет все изменённые email. Но если одно из значений
Для таких случаев в PostgreSQL есть специальный оператор.
Теперь
Пригодится в аудитах изменений, UPSERT-логике, ETL и триггерах, где важно понять: значение реально изменилось или нет.
🔥 Оператор, который убирает большое количество багов при работе с
➡️ SQL Ready | #совет
old_value IS DISTINCT FROM new_value
В SQL
NULL никогда не равен даже другому NULL, поэтому сравнение начинает вести себя не так, как ожидается.SELECT
NULL = NULL,
NULL != NULL;
Оба выражения вернут не
TRUE и не FALSE, а NULL. Именно из-за этого часто ломается логика сравнения.SELECT *
FROM users
WHERE old_email != new_email;
Кажется, что запрос покажет все изменённые email. Но если одно из значений
NULL, строка может просто не попасть в выборку.Для таких случаев в PostgreSQL есть специальный оператор.
SELECT *
FROM users
WHERE old_email IS DISTINCT FROM new_email;
Теперь
NULL корректно участвует в сравнении. PostgreSQL считает NULL обычным сравниваемым состоянием.NULL IS DISTINCT FROM NULL -- false
NULL IS DISTINCT FROM 'test' -- true
'a' IS DISTINCT FROM 'b' -- true
Пригодится в аудитах изменений, UPSERT-логике, ETL и триггерах, где важно понять: значение реально изменилось или нет.
NULL.Please open Telegram to view this post
VIEW IN TELEGRAM
❤11👍7🔥4
Например, range-based шардинг делит данные по диапазонам значений, hash-based — распределяет записи через хэш-функцию, а consistent hashing помогает минимизировать перераспределение данных при добавлении новых нод.
На картинке — 4 алгоритма шардинга, которые полезно знать при проектировании масштабируемых систем и высоконагруженных баз данных.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍4🤝2
Почему CONCAT_WS в PostgreSQL часто лучше обычной конкатенации строк!
Есть такие SQL-функции, которые вроде бы выглядят слишком простыми, чтобы о них писать отдельный пост. Но именно они часто убирают кучу мелких проблем в реальных проектах,
Когда работаешь с тестовыми данными, кажется, что собрать строку очень просто. Есть имя, фамилия, отчество — соединяем их через пробел и получаем красивый результат.
Обычно первый вариант выглядит так:
На первый взгляд всё нормально. Но потом база начинает жить настоящими данными. У одного пользователя нет отчества. У другого не заполнена фамилия. После миграции часть полей приходит пустыми. И вот здесь простой запрос начинает вести себя не так, как ожидает разработчик.
Например:
Кажется логичным получить:
Но PostgreSQL вернёт:
Причина в том, что NULL — это не пустое значение. Это отсутствие значения. При обычной конкатенации строк неизвестная часть делает неизвестным весь результат.
Конечно, можно начать добавлять
Для двух-трёх полей это ещё нормально. Но в реальном запросе таких частей может быть намного больше: адрес, название компании, описание товара, дополнительные параметры.
И постепенно вместо простой сборки строки появляется большая конструкция из проверок.
В PostgreSQL для таких задач есть более удобный вариант:
Главная особенность в том, что функция сама пропускает NULL. То есть:
превратится в:
а не в строку с лишним пробелом или неожиданный NULL.
На практике это особенно удобно, когда собираете данные для отображения пользователю. Например, адрес:
может состоять из нескольких частей, но квартира или дополнительный номер могут отсутствовать. Не хочется писать отдельные проверки для каждого поля только ради красивого вывода.
Но есть нюанс, который часто забывают.
Например, после загрузки данных из CSV можно получить:
Для пользователя это выглядит так же, как отсутствие значения. Но PostgreSQL различает эти случаи. В результате можно получить:
Поэтому в системах, где данные приходят из разных источников, часто сначала нормализуют значения:
После этого пустые строки становятся NULL, и
🔥
➡️ SQL Ready | #практика
Есть такие SQL-функции, которые вроде бы выглядят слишком простыми, чтобы о них писать отдельный пост. Но именно они часто убирают кучу мелких проблем в реальных проектах,
CONCAT_WS — хороший пример.Когда работаешь с тестовыми данными, кажется, что собрать строку очень просто. Есть имя, фамилия, отчество — соединяем их через пробел и получаем красивый результат.
Обычно первый вариант выглядит так:
SELECT
first_name || ' ' || middle_name || ' ' || last_name AS full_name
FROM users;
На первый взгляд всё нормально. Но потом база начинает жить настоящими данными. У одного пользователя нет отчества. У другого не заполнена фамилия. После миграции часть полей приходит пустыми. И вот здесь простой запрос начинает вести себя не так, как ожидает разработчик.
Например:
first_name = 'John'
middle_name = NULL
last_name = 'Smith'
Кажется логичным получить:
John Smith
Но PostgreSQL вернёт:
NULL
Причина в том, что NULL — это не пустое значение. Это отсутствие значения. При обычной конкатенации строк неизвестная часть делает неизвестным весь результат.
Конечно, можно начать добавлять
COALESCE:SELECT
first_name || ' ' ||
COALESCE(middle_name || ' ', '') ||
last_name
FROM users;
Для двух-трёх полей это ещё нормально. Но в реальном запросе таких частей может быть намного больше: адрес, название компании, описание товара, дополнительные параметры.
И постепенно вместо простой сборки строки появляется большая конструкция из проверок.
В PostgreSQL для таких задач есть более удобный вариант:
SELECT
CONCAT_WS(
' ',
first_name,
middle_name,
last_name
) AS full_name
FROM users;
CONCAT_WS расшифровывается как "concatenate with separator". Первый аргумент — разделитель, который нужно использовать между значениями.Главная особенность в том, что функция сама пропускает NULL. То есть:
John + NULL + Smith
превратится в:
John Smith
а не в строку с лишним пробелом или неожиданный NULL.
На практике это особенно удобно, когда собираете данные для отображения пользователю. Например, адрес:
Germany, Berlin, Alexanderplatz 10
может состоять из нескольких частей, но квартира или дополнительный номер могут отсутствовать. Не хочется писать отдельные проверки для каждого поля только ради красивого вывода.
Но есть нюанс, который часто забывают.
CONCAT_WS пропускает NULL, но пустую строку считает настоящим значением.Например, после загрузки данных из CSV можно получить:
middle_name = ''
Для пользователя это выглядит так же, как отсутствие значения. Но PostgreSQL различает эти случаи. В результате можно получить:
John Smith с двойным пробелом.Поэтому в системах, где данные приходят из разных источников, часто сначала нормализуют значения:
NULLIF(middle_name, '')
После этого пустые строки становятся NULL, и
CONCAT_WS уже работает предсказуемо. На небольших примерах это кажется мелочью. Но когда у тебя несколько миллионов записей и данные собираются из разных сервисов, именно такие детали начинают влиять на качество данных и количество лишнего кода.CONCAT_WS — хороший пример того, как правильный инструмент делает SQL проще. Если нужно собрать строку из нескольких необязательных полей — часто лучше использовать её, чем вручную обрабатывать каждый NULL.Please open Telegram to view this post
VIEW IN TELEGRAM
👍13❤9🔥5
Используй RETURNING как временную таблицу!
Многие после
Но PostgreSQL уже умеет вернуть результат изменения через
Один запрос вместо двух.
Например, обновили заказ и сразу записали историю изменения:
Без промежуточных переменных и без повторного поиска строки.
Также можно использовать это для атомарного переноса данных:
🔥 Получается "забрать строку и положить в другую таблицу" одной транзакционной операцией. Часто заменяет сложный код в сервисах и убирает лишние запросы к базе.
➡️ SQL Ready | #совет
Многие после
UPDATE делают отдельный SELECT, чтобы получить изменённые данные.Но PostgreSQL уже умеет вернуть результат изменения через
RETURNING.UPDATE orders
SET status = 'paid',
paid_at = now()
WHERE id = 1001
RETURNING id, status, paid_at;
Один запрос вместо двух.
RETURNING можно сразу передавать дальше.Например, обновили заказ и сразу записали историю изменения:
WITH changed AS (
UPDATE orders
SET status = 'cancelled'
WHERE id = 1001
RETURNING id, status
)
INSERT INTO order_history(order_id, new_status)
SELECT id, status
FROM changed;
Без промежуточных переменных и без повторного поиска строки.
Также можно использовать это для атомарного переноса данных:
WITH moved AS (
DELETE FROM queue
WHERE id = 55
RETURNING *
)
INSERT INTO archive_queue
SELECT *
FROM moved;
Please open Telegram to view this post
VIEW IN TELEGRAM
❤18🔥8👍7
This media is not supported in your browser
VIEW IN TELEGRAM
На сайте опубликованы статьи, руководства, инструкции и практические материалы по работе с одной из самых популярных СУБД. Здесь можно найти информацию по установке, настройке, администрированию, оптимизации производительности, репликации, резервному копированию, расширениям и др.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥5🤝3❤2
This media is not supported in your browser
VIEW IN TELEGRAM
Этот репозиторий поможет разобраться с PostgreSQL с нуля и понять, как работать с базой данных на практике. Здесь простым языком объясняются SQL-запросы, создание таблиц, связи между ними, индексы, объединения, агрегатные функции и др. Материал построен последовательно и с большим количеством примеров.
Оставляю ссылочку: GitHub📱
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤6🔥4
Почему ROW_NUMBER(), RANK() и DENSE_RANK() в SQL дают разный результат!
Есть такие оконные функции в SQL, которые выглядят почти одинаково, но используются для разных задач.
На тестовых данных разница может быть незаметной. Но в реальных задачах это влияет на рейтинги пользователей, отчёты, аналитику и любые запросы, где важно правильное место записи.
Например, есть таблица с результатами:
Если нужно просто пронумеровать строки после сортировки, обычно используют
Результат:
Это удобно, когда нужна обычная последовательная нумерация. Например, для пагинации, выбора последней записи из группы или получения одной строки после сортировки. Но для рейтингов такой вариант часто подходит не всегда.
Если два пользователя набрали одинаковое количество баллов, обычно ожидается, что они займут одинаковое место. Для такой задачи используется
Результат:
Иногда такое поведение является правильным. Например, в спортивных рейтингах, где нужно учитывать фактическое количество участников с одинаковым результатом. Но бывают случаи, когда пропуски не нужны. Тогда используется
Результат:
Есть ещё один практический момент, который часто забывают при использовании
то порядок таких строк может быть нестабильным. База данных не обязана всегда возвращать их в одинаковой последовательности.
Поэтому в реальных проектах часто добавляют дополнительное поле для сортировки:
🔥
➡️ SQL Ready | #практика
Есть такие оконные функции в SQL, которые выглядят почти одинаково, но используются для разных задач.
ROW_NUMBER(), RANK() и DENSE_RANK() все умеют добавлять номера к строкам, но поведение меняется, когда в данных появляются одинаковые значения.На тестовых данных разница может быть незаметной. Но в реальных задачах это влияет на рейтинги пользователей, отчёты, аналитику и любые запросы, где важно правильное место записи.
Например, есть таблица с результатами:
score
-----
100
100
90
80
Если нужно просто пронумеровать строки после сортировки, обычно используют
ROW_NUMBER():SELECT
score,
ROW_NUMBER() OVER (
ORDER BY score DESC
) AS position
FROM results;
Результат:
score | position
------+---------
100 | 1
100 | 2
90 | 3
80 | 4
ROW_NUMBER() всегда выдаёт уникальный номер для каждой строки. Даже если два значения одинаковые, они всё равно получат разные позиции.Это удобно, когда нужна обычная последовательная нумерация. Например, для пагинации, выбора последней записи из группы или получения одной строки после сортировки. Но для рейтингов такой вариант часто подходит не всегда.
Если два пользователя набрали одинаковое количество баллов, обычно ожидается, что они займут одинаковое место. Для такой задачи используется
RANK():SELECT
score,
RANK() OVER (
ORDER BY score DESC
) AS position
FROM results;
Результат:
score | position
------+---------
100 | 1
100 | 1
90 | 3
80 | 4
RANK() учитывает одинаковые значения и присваивает им одинаковый номер. Но после одинаковых результатов появляются пропуски. В примере выше два участника заняли первое место, поэтому следующий результат получает третье место.Иногда такое поведение является правильным. Например, в спортивных рейтингах, где нужно учитывать фактическое количество участников с одинаковым результатом. Но бывают случаи, когда пропуски не нужны. Тогда используется
DENSE_RANK():SELECT
score,
DENSE_RANK() OVER (
ORDER BY score DESC
) AS position
FROM results;
Результат:
score | position
------+---------
100 | 1
100 | 1
90 | 2
80 | 3
DENSE_RANK() работает похоже на RANK(), но продолжает нумерацию без пропусков. Получается простое правило: ROW_NUMBER() — когда каждая строка должна получить свой уникальный номер; RANK() — когда одинаковые значения должны иметь одинаковое место, а пропуски после них допустимы; DENSE_RANK() — когда одинаковые значения должны иметь одинаковое место, но нумерация должна идти без пропусков.Есть ещё один практический момент, который часто забывают при использовании
ROW_NUMBER(). Если несколько строк имеют одинаковое значение для сортировки:SELECT
id,
score,
ROW_NUMBER() OVER (
ORDER BY score DESC
) AS position
FROM results;
то порядок таких строк может быть нестабильным. База данных не обязана всегда возвращать их в одинаковой последовательности.
Поэтому в реальных проектах часто добавляют дополнительное поле для сортировки:
SELECT
id,
score,
ROW_NUMBER() OVER (
ORDER BY score DESC, id
) AS position
FROM results;
ROW_NUMBER(), RANK() и DENSE_RANK() решают похожую задачу, но предназначены для разных сценариев. Главное — понимать, нужна ли вам обычная нумерация строк или именно логика рейтинга.Please open Telegram to view this post
VIEW IN TELEGRAM
❤21🔥8👍6👎1
Например,
INNER JOIN возвращает только совпадающие записи, LEFT/RIGHT JOIN сохраняют данные одной из сторон, а FULL JOIN объединяет полный набор данных из обеих таблиц. Дополнительные проверки через NULL позволяют находить отсутствующие связи между сущностями.На картинке — основные типы
JOIN и визуальное представление того, какие строки попадут в результат выполнения запроса.Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15❤7🔥6👎1
В этой статье:
• Рассматриваются возможности PostgreSQL, которые помогают решать типовые backend-задачи на уровне базы;• Разбираются эффективные инструменты для работы с очередями, запросами, индексами и поиском;• Показывается, как использовать встроенные механизмы PostgreSQL для повышения производительности приложений.Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍7🔥5
Индексы по выражениям — ускоряем поиск по вычисляемым значениям!
Обычный индекс работает только тогда, когда база может сравнить значение напрямую с колонкой. Если в условии
Таблица:
Создадим индекс на email:
Теперь простой поиск по точному значению использует этот индекс:
Но в реальных проектах часто нужна нормализация данных. Например, искать email без учёта регистра через
Проблема в том, что PostgreSQL должен применить
Решение — создать индекс не на колонку, а на результат выражения:
Теперь база хранит вычисленные значения в индексе и может быстро находить совпадения:
Та же техника работает и для других преобразований. Например, если часто ищем заказы по году создания:
Теперь запросы по этому выражению могут использовать индекс вместо полного прохода таблицы.
Индекс нужно создавать под реальные запросы. Лишние увеличивают время
🔥 Индексы по выражениям полезны, когда одно и то же вычисление постоянно используется в
➡️ SQL Ready | #практика
Обычный индекс работает только тогда, когда база может сравнить значение напрямую с колонкой. Если в условии
WHERE применяется функция к полю, оптимизатор часто не может использовать обычный индекс.Таблица:
users(id, email)
Создадим индекс на email:
CREATE INDEX idx_users_email
ON users(email);
Теперь простой поиск по точному значению использует этот индекс:
SELECT *
FROM users
WHERE email = 'test@mail.com';
Но в реальных проектах часто нужна нормализация данных. Например, искать email без учёта регистра через
LOWER():SELECT *
FROM users
WHERE LOWER(email) = 'test@mail.com';
Проблема в том, что PostgreSQL должен применить
LOWER() к каждой строке, а потом сравнить результат. Обычный индекс по email для такого условия уже не подходит.Решение — создать индекс не на колонку, а на результат выражения:
CREATE INDEX idx_users_lower_email
ON users(LOWER(email));
Теперь база хранит вычисленные значения в индексе и может быстро находить совпадения:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE LOWER(email) = 'test@mail.com';
Та же техника работает и для других преобразований. Например, если часто ищем заказы по году создания:
CREATE INDEX idx_orders_year
ON orders(EXTRACT(YEAR FROM created_at));
Теперь запросы по этому выражению могут использовать индекс вместо полного прохода таблицы.
Индекс нужно создавать под реальные запросы. Лишние увеличивают время
INSERT/UPDATE и занимают место.WHERE, JOIN или ORDER BY.Please open Telegram to view this post
VIEW IN TELEGRAM
❤15👍8🔥7
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15❤7🤝4🔥3😁2
This media is not supported in your browser
VIEW IN TELEGRAM
На сайте собрана крупная библиотека материалов по базам данных: SQL, проектирование БД, транзакции, индексы, оптимизация запросов и администрирование различных СУБД. Здесь можно найти статьи, учебные пособия, техническую документацию и обзоры по PostgreSQL, MySQL, Oracle, Microsoft SQL Server и другим системам управления базами данных.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤6🔥6🤝1
Например, MongoDB подходит для работы с гибкими документными структурами, Redis — для высокопроизводительного кэширования и хранения данных в памяти, а Cassandra — для распределённых систем с огромными объёмами данных и высокой доступностью.
На картинке — сравнение популярных NoSQL решений, их особенностей и основных сценариев использования: от поиска и аналитики до IoT, социальных сетей, e-commerce и real-time приложений.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍6🤝5
Вычисляемые значения на стороне PostgreSQL!
Если значение полностью зависит от других колонок, не обязательно считать его перед каждым
PostgreSQL умеет делать это сам и всегда гарантирует корректный результат.
Поле
После изменения исходных данных значение пересчитается автоматически.
Это удобно для нормализованных значений, поисковых ключей, вычисляемых сумм и любых других детерминированных данных, которые должны всегда оставаться консистентными.
🔥 Если колонка полностью вычисляется из других колонок, пусть этим занимается PostgreSQL. Логику проще поддерживать, когда вычисление в одном месте.
➡️ SQL Ready | #совет
Если значение полностью зависит от других колонок, не обязательно считать его перед каждым
INSERT и UPDATE.PostgreSQL умеет делать это сам и всегда гарантирует корректный результат.
INSERT INTO orders(price, quantity)
VALUES (100, 3);
SELECT total
FROM orders
WHERE id = 1;
Поле
total заполнится автоматически. Его нельзя случайно забыть обновить или вычислить по старой формуле.UPDATE orders
SET quantity = 5
WHERE id = 1;
SELECT total
FROM orders
WHERE id = 1;
После изменения исходных данных значение пересчитается автоматически.
ALTER TABLE products
ADD COLUMN search_name text
GENERATED ALWAYS AS (lower(name)) STORED;
Это удобно для нормализованных значений, поисковых ключей, вычисляемых сумм и любых других детерминированных данных, которые должны всегда оставаться консистентными.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍8❤5🤝1
This media is not supported in your browser
VIEW IN TELEGRAM
На сайте можно изучать SQL на практике, выполняя запросы прямо в браузере. Здесь собраны уроки по основным темам: подзапросы, агрегатные функции и другие конструкции, которые используются при работе с базами данных. Ресурс подойдёт новичкам, а также разработчикам и аналитикам, желающим закрепить знания с помощью практики.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤝12🔥6❤3👍2
Например,
groupby() в Pandas и Polars выполняет роль GROUP BY в SQL, merge() аналогичен JOIN, sort_values() заменяет ORDER BY, а unique() работает как SELECT DISTINCT.На картинке — основные операции для работы с таблицами: загрузка данных, просмотр строк, фильтрация, сортировка, группировка, объединение таблиц, переименование и удаление колонок в Pandas, Polars и SQL.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍8🔥5👎1
Почему EXISTS в SQL часто лучше использовать для проверки наличия данных, чем COUNT(*)!
Одна из распространённых задач в SQL — определить, существует ли хотя бы одна запись, соответствующая заданному условию. Для этого иногда используют
Например, есть таблица пользователей:
Проверка наличия пользователя через
Такой запрос вернёт количество найденных строк. Если требуется только узнать, существует ли запись, получение точного количества всех совпадений не является необходимым.
Для проверки факта существования данных лучше подходит
Разница особенно заметна в больших таблицах и в часто выполняемых проверках внутри бизнес-логики: при создании пользователей, проверке связей между сущностями, наличии связанных данных и других условных операциях. Например, проверка наличия заказов пользователя через
Этот запрос отвечает на вопрос: сколько заказов существует у пользователя.
Если требуется только проверить наличие хотя бы одного заказа:
В этом случае запрос точно отражает бизнес-логику: нужно проверить существование данных, а не выполнять подсчёт всех строк.
Например, для формирования отчётов, статистики или отображения количества элементов. При наличии индекса по колонкам, используемым в условии
🔥 Важно понимать разницу:
➡️ SQL Ready | #практика
Одна из распространённых задач в SQL — определить, существует ли хотя бы одна запись, соответствующая заданному условию. Для этого иногда используют
COUNT(*), хотя его основное назначение — вычисление количества строк, а не проверка факта существования данных.Например, есть таблица пользователей:
id | email
---+-----------------
1 | user@test.com
2 | admin@test.com
3 | test@test.com
Проверка наличия пользователя через
COUNT(*):SELECT
COUNT(*)
FROM users
WHERE email = 'user@test.com';
Такой запрос вернёт количество найденных строк. Если требуется только узнать, существует ли запись, получение точного количества всех совпадений не является необходимым.
Для проверки факта существования данных лучше подходит
EXISTS:SELECT EXISTS (
SELECT 1
FROM users
WHERE email = 'user@test.com'
);
EXISTS возвращает логическое значение: TRUE, если найдена хотя бы одна подходящая строка, и FALSE, если совпадений нет. При выполнении такого запроса оптимизатор базы данных может использовать стратегию с ранним завершением проверки после нахождения первого совпадения, так как дальнейший поиск для этой операции не имеет смысла.Разница особенно заметна в больших таблицах и в часто выполняемых проверках внутри бизнес-логики: при создании пользователей, проверке связей между сущностями, наличии связанных данных и других условных операциях. Например, проверка наличия заказов пользователя через
COUNT(*):SELECT
COUNT(*)
FROM orders
WHERE user_id = 100;
Этот запрос отвечает на вопрос: сколько заказов существует у пользователя.
Если требуется только проверить наличие хотя бы одного заказа:
SELECT EXISTS (
SELECT 1
FROM orders
WHERE user_id = 100
);
В этом случае запрос точно отражает бизнес-логику: нужно проверить существование данных, а не выполнять подсчёт всех строк.
COUNT(*) остаётся правильным выбором, когда необходимо получить фактическое количество записей:SELECT
COUNT(*) AS orders_count
FROM orders
WHERE user_id = 100;
Например, для формирования отчётов, статистики или отображения количества элементов. При наличии индекса по колонкам, используемым в условии
WHERE, EXISTS может эффективно использовать возможности оптимизатора и быстро определить наличие подходящей записи.COUNT(*) отвечает на вопрос: сколько строк существует? EXISTS отвечает на вопрос: существует ли хотя бы одна строка? Выбор правильного оператора делает SQL-запросы более выразительными и лучше соответствует реальной задаче приложения.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤6👍6