Справочник основных конструкций SQL с примерами, выполненными в PostgreSQL 17 на учебной базе магазина: SELECT, WHERE, JOIN, GROUP BY, оконные функции, CTE, INSERT … ON CONFLICT, UPDATE и DELETE с соединением, DDL, индексы, транзакции и функции. Запрос выполняется не в порядке написания: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT, поэтому псевдоним из SELECT не виден в WHERE. Там, где MySQL 8.4 ведёт себя иначе, дан проверенный аналог.
После материала вы сможете
- найти синтаксис и проверенный пример для
SELECT,JOIN,GROUP BY, оконных функций, CTE,ON CONFLICT, DDL и транзакций - объяснить по порядку выполнения запроса, почему псевдоним из
SELECTне работает вWHERE, а оконную функцию нельзя фильтровать вWHERE - перевести запрос с PostgreSQL на MySQL 8.4 и обратно по таблице отличий
Что знать заранее
Проверено на
PostgreSQL 17 (17.11, образ postgres:17-alpine)psql 17MySQL 8.4 (8.4.11, образ mysql:8.4) — для пометок об отличияхDocker Engine 29
Сверено с документацией: 12 октября 2026 г.
Содержание
SELECT: шаблон запроса и порядок выполнения
Все примеры выполнены в PostgreSQL 17.11 (образ postgres:17-alpine) на учебной базе магазина из урока «SQL с нуля»: таблицы clients, products, orders и order_items, скрипт shop.sql — там же. Перед каждым примером база загружалась заново, поэтому изменения одного запроса не влияют на другой. Под кодом — реальный вывод psql. Отличия MySQL проверены на MySQL 8.4.11.
SELECT category,
count(*) AS items,
min(price) AS from_price
FROM products
WHERE price < 15000
GROUP BY category
HAVING count(*) > 1
ORDER BY from_price
LIMIT 3;tea
- items
- 2
- from_price
- 3200.00
coffee
- items
- 3
- from_price
- 4900.00
equipment не попала в результат: после WHERE в ней остался один товар, и HAVING её отсеял.1
- Часть запроса
WITH- Что происходит
- Вычисляются CTE — временные именованные результаты
2
- Часть запроса
FROM,JOIN- Что происходит
- Собираются строки из таблиц и соединений
3
- Часть запроса
WHERE- Что происходит
- Отбрасываются строки, для которых условие не истинно
4
- Часть запроса
GROUP BY,HAVING- Что происходит
- Строки собираются в группы, считаются агрегаты,
HAVINGфильтрует группы
5
- Часть запроса
SELECT- Что происходит
- Вычисляются выражения, псевдонимы
ASи оконные функции
6
- Часть запроса
DISTINCT- Что происходит
- Удаляются повторяющиеся строки
7
- Часть запроса
UNION/INTERSECT/EXCEPT- Что происходит
- Результаты нескольких запросов объединяются
8
- Часть запроса
ORDER BY- Что происходит
- Сортировка; здесь псевдонимы из
SELECTуже доступны
9
- Часть запроса
LIMIT/OFFSET- Что происходит
- Возвращается нужная часть строк
SELECT name, price * 0.9 AS sale_price
FROM products
WHERE sale_price < 4000;
-- ERROR: column "sale_price" does not exist
-- LINE 3: WHERE sale_price < 4000;
-- ^WHERE и HAVING выражение повторяют, в ORDER BY и GROUP BY имя выходного столбца использовать можно.SELECT id, name, price
FROM products
ORDER BY price DESC, id
LIMIT 3 OFFSET 3;7
- name
- French Press 600 ml
- price
- 8900.00
3
- name
- Ethiopia Yirgacheffe 250 g
- price
- 5600.00
2
- name
- Colombia Supremo 250 g
- price
- 4900.00
id делает порядок однозначным.SELECT id FROM products ORDER BY id LIMIT 3, 3;
-- ERROR: LIMIT #,# syntax is not supported
-- LINE 1: SELECT id FROM products ORDER BY id LIMIT 3, 3;
-- ^
-- HINT: Use separate LIMIT and OFFSET clauses.WHERE: операторы сравнения и NULL
price = 4900
- Строк
- 1
- Что важно
- Равенство. Строки и даты пишут в одинарных кавычках
category <> 'coffee'
- Строк
- 6
- Что важно
- «Не равно»;
!=— то же самое
price BETWEEN 3000 AND 6000
- Строк
- 4
- Что важно
- Обе границы входят в диапазон
category IN ('tea', 'gifts')
- Строк
- 3
- Что важно
- Короткая запись нескольких
ORпо одному столбцу
name LIKE '%250 g'
- Строк
- 2
- Что важно
%— любая подстрока,_— ровно один символ; регистр учитывается
name ILIKE '%MUG%'
- Строк
- 1
- Что важно
- Без учёта регистра. Только PostgreSQL
weight_g IS NULL
- Строк
- 4
- Что важно
- Единственный способ найти
NULL
weight_g <> 250
- Строк
- 3
- Что важно
- Только 3: строки с
NULLвыпали, сравнение сNULLдаётNULL
weight_g IS DISTINCT FROM 250
- Строк
- 7
- Что важно
- 7 = 3 + 4 строки с
NULL: здесьNULL— обычное значение
id = ANY (ARRAY[1, 2, 3])
- Строк
- 3
- Что важно
- Массив удобно передавать из приложения одним параметром
NOT (category = 'coffee' OR price > 10000)
- Строк
- 4
- Что важно
NOTи скобки:ANDсвязывает сильнее, чемOR
SELECT count(*) FROM products WHERE <условие> на учебной базе (9 товаров, у 4 вес NULL).- MySQL 8.4:
ILIKEнет — запрос завершается ошибкой синтаксиса 1064. Учитывать ли регистр вLIKE, решают правила сравнения (collation) столбца. - MySQL 8.4:
IS DISTINCT FROMтоже даёт ошибку 1064. Аналог — оператор<=>(«равно с учётомNULL»):NOT (city <=> 'Almaty'). - Значения от пользователя передают параметрами (
$1в PostgreSQL,?в драйверах MySQL), а не склеивают со строкой запроса — иначе это SQL-инъекция.
JOIN, GROUP BY и HAVING
JOIN (INNER JOIN)
- Что возвращает
- Только пары, нашедшие друг друга по условию
ON - Пример
clients JOIN orders- Строк
- 10
LEFT JOIN
- Что возвращает
- Все строки левой таблицы; если пары нет —
NULLв столбцах правой - Пример
clients LEFT JOIN orders- Строк
- 12 (два клиента без заказов)
RIGHT JOIN
- Что возвращает
- Зеркало
LEFT JOIN: все строки правой таблицы - Пример
orders RIGHT JOIN clients- Строк
- 12
FULL JOIN
- Что возвращает
- Все строки обеих таблиц, недостающая сторона —
NULL - Пример
order_items FULL JOIN products- Строк
- 16 (15 позиций и товар без продаж)
CROSS JOIN
- Что возвращает
- Каждая строка с каждой, без условия
- Пример
clients CROSS JOIN products- Строк
- 72 = 8 × 9
SELECT c.name
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
WHERE o.id IS NULL
ORDER BY c.id;Zarina Kaliyeva
Sergey Pak
LEFT JOIN и проверка IS NULL по ключу правой таблицы.SELECT count(*) AS all_rows,
count(weight_g) AS with_weight,
count(DISTINCT category) AS categories,
sum(weight_g) AS total_g,
round(avg(weight_g)) AS avg_g
FROM products;9
- with_weight
- 5
- categories
- 5
- total_g
- 1800
- avg_g
- 360
count(*) считает строки, count(столбец) — только не-NULL; sum и avg тоже пропускают NULL.SELECT p.category,
sum(oi.quantity * oi.price) AS revenue,
count(DISTINCT o.id) FILTER (WHERE o.promo_code IS NOT NULL) AS promo_orders
FROM order_items AS oi
JOIN orders AS o ON o.id = oi.order_id
JOIN products AS p ON p.id = oi.product_id
WHERE o.status IN ('paid', 'shipped')
GROUP BY p.category
HAVING sum(oi.quantity * oi.price) > 10000
ORDER BY revenue DESC;coffee
- revenue
- 77500.00
- promo_orders
- 3
equipment
- revenue
- 27400.00
- promo_orders
- 1
tea
- revenue
- 10800.00
- promo_orders
- 1
HAVING убрал категории с выручкой до 10 000 ₸.FILTER (WHERE …) ограничивает вход одного агрегата, не трогая остальные. В MySQL 8.4 FILTER нет — то же считают через CASE: count(CASE WHEN status = 'paid' THEN 1 END). Подробно о соединениях, дублях после JOIN и группировке — в уроке «SQL: JOIN, GROUP BY и агрегаты».
Оконные функции: ROW_NUMBER, RANK, SUM OVER
Оконная функция считает значение по набору строк, связанных с текущей, но, в отличие от GROUP BY, не сворачивает строки. Запись: функция() OVER (PARTITION BY … ORDER BY …). PARTITION BY делит строки на группы, ORDER BY задаёт порядок внутри группы.
row_number()
- Что возвращает
- Номер строки в разделе: 1, 2, 3… без повторов
rank()
- Что возвращает
- Место с пропусками после равных: 1, 2, 2, 4
dense_rank()
- Что возвращает
- Место без пропусков: 1, 2, 2, 3
lag(x) / lead(x)
- Что возвращает
- Значение
xиз предыдущей / следующей строки илиNULL
first_value(x) / last_value(x)
- Что возвращает
- Первое / последнее значение в рамке окна
ntile(n)
- Что возвращает
- Номер одной из
nпримерно равных частей
sum(x) OVER (…), avg, count
- Что возвращает
- Агрегат по окну: с
ORDER BY— нарастающий итог, без него — итог по всему разделу
SELECT client_id, id AS order_id, ordered_on, status
FROM (
SELECT o.*,
row_number() OVER (PARTITION BY client_id ORDER BY ordered_on DESC) AS rn
FROM orders AS o
) AS t
WHERE rn = 1
ORDER BY client_id;1
- order_id
- 9
- ordered_on
- 2026-10-08
- status
- paid
2
- order_id
- 10
- ordered_on
- 2026-10-09
- status
- new
3
- order_id
- 7
- ordered_on
- 2026-10-03
- status
- shipped
4
- order_id
- 5
- ordered_on
- 2026-09-28
- status
- shipped
5
- order_id
- 6
- ordered_on
- 2026-10-01
- status
- paid
6
- order_id
- 8
- ordered_on
- 2026-10-05
- status
- new
rn = 1 во внешнем запросе.WITH totals AS (
SELECT o.id, o.ordered_on, sum(oi.quantity * oi.price) AS total
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status IN ('paid', 'shipped')
GROUP BY o.id
)
SELECT id, ordered_on, total,
sum(total) OVER (ORDER BY ordered_on) AS running_total,
total - lag(total) OVER (ORDER BY ordered_on) AS diff_prev,
round(100 * total / sum(total) OVER (), 1) AS pct
FROM totals
ORDER BY ordered_on;1
- ordered_on
- 2026-09-02
- total
- 17900.00
- running_total
- 17900.00
- diff_prev
- NULL
- pct
- 14.5
2
- ordered_on
- 2026-09-05
- total
- 13000.00
- running_total
- 30900.00
- diff_prev
- -4900.00
- pct
- 10.6
3
- ordered_on
- 2026-09-14
- total
- 8900.00
- running_total
- 39800.00
- diff_prev
- -4100.00
- pct
- 7.2
5
- ordered_on
- 2026-09-28
- total
- 15700.00
- running_total
- 55500.00
- diff_prev
- 6800.00
- pct
- 12.7
6
- ordered_on
- 2026-10-01
- total
- 25800.00
- running_total
- 81300.00
- diff_prev
- 10100.00
- pct
- 20.9
7
- ordered_on
- 2026-10-03
- total
- 10500.00
- running_total
- 91800.00
- diff_prev
- -15300.00
- pct
- 8.5
9
- ordered_on
- 2026-10-08
- total
- 31400.00
- running_total
- 123200.00
- diff_prev
- 20900.00
- pct
- 25.5
SELECT p.name, oi.price,
rank() OVER (ORDER BY oi.price DESC) AS rnk,
dense_rank() OVER (ORDER BY oi.price DESC) AS dense,
row_number() OVER (ORDER BY oi.price DESC, p.name) AS rn
FROM order_items AS oi
JOIN products AS p ON p.id = oi.product_id
WHERE oi.order_id IN (1, 5, 9)
ORDER BY oi.price DESC, p.name;Hand Grinder
- price
- 18500.00
- rnk
- 1
- dense
- 1
- rn
- 1
Espresso Blend 1 kg
- price
- 12900.00
- rnk
- 2
- dense
- 2
- rn
- 2
Espresso Blend 1 kg
- price
- 12900.00
- rnk
- 2
- dense
- 2
- rn
- 3
Ethiopia Yirgacheffe 250 g
- price
- 5600.00
- rnk
- 4
- dense
- 3
- rn
- 4
Masala Chai 200 g
- price
- 3800.00
- rnk
- 5
- dense
- 4
- rn
- 5
Ceramic Mug 350 ml
- price
- 2500.00
- rnk
- 6
- dense
- 5
- rn
- 6
Ceramic Mug 350 ml
- price
- 2500.00
- rnk
- 6
- dense
- 5
- rn
- 7
rank пропускает места, dense_rank — нет, row_number всегда уникален.SELECT name, row_number() OVER (ORDER BY price) AS rn
FROM products
WHERE row_number() OVER (ORDER BY price) <= 3;
-- ERROR: window functions are not allowed in WHERE
-- LINE 3: WHERE row_number() OVER (ORDER BY price) <= 3;
-- ^WHERE, GROUP BY и HAVING, поэтому фильтруют по ним во внешнем запросе или CTE, как в примере с последним заказом.Подзапросы и CTE (WITH)
Скалярный подзапрос
- Пример
price - (SELECT avg(price) FROM products)- Когда
- Сравнить с одним вычисленным значением
IN (подзапрос)
- Пример
id IN (SELECT client_id FROM orders …)- Когда
- Значение входит в список из другой таблицы
EXISTS / NOT EXISTS
- Пример
NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.product_id = p.id)- Когда
- Есть ли пара;
NOT EXISTSбезопаснееNOT IN, если в списке бываетNULL
Подзапрос в FROM
- Пример
FROM (SELECT …, row_number() OVER (…) AS rn …) AS t- Когда
- Отфильтровать результат оконной функции или агрегата
WITH (CTE)
- Пример
WITH totals AS (…) SELECT … FROM totals- Когда
- Разбить отчёт на именованные шаги
WITH RECURSIVE
- Пример
- Календарь дат, дерево категорий
- Когда
- Повторять шаг, пока он возвращает строки
LATERAL
- Пример
CROSS JOIN LATERAL (… LIMIT 1)- Когда
- Подзапрос для каждой строки слева: «последние N на группу»
SELECT name, price, round(price - (SELECT avg(price) FROM products), 2) AS vs_avg
FROM products WHERE category = 'coffee' ORDER BY id;
SELECT name FROM clients
WHERE id IN (SELECT client_id FROM orders WHERE promo_code IS NOT NULL) ORDER BY id;
SELECT p.name FROM products AS p
WHERE NOT EXISTS (SELECT 1 FROM order_items AS oi WHERE oi.product_id = p.id);
-- name | price | vs_avg
-- ----------------------------+----------+----------
-- Espresso Blend 1 kg | 12900.00 | 4533.33
-- Colombia Supremo 250 g | 4900.00 | -3466.67
-- Ethiopia Yirgacheffe 250 g | 5600.00 | -2766.67
-- (3 rows)
--
-- name
-- ------------------
-- Aigerim Seitova
-- Daniyar Akhmetov
-- Aliya Bekova
-- (3 rows)
--
-- name
-- ----------
-- Gift Box
-- (1 row)WITH RECURSIVE days AS (
SELECT date '2026-10-01' AS day
UNION ALL
SELECT day + 1 FROM days WHERE day < date '2026-10-07'
)
SELECT d.day, count(o.id) AS orders
FROM days AS d
LEFT JOIN orders AS o ON o.ordered_on = d.day
GROUP BY d.day
ORDER BY d.day;2026-10-01
- orders
- 1
2026-10-02
- orders
- 0
2026-10-03
- orders
- 1
2026-10-04
- orders
- 0
2026-10-05
- orders
- 1
2026-10-06
- orders
- 0
2026-10-07
- orders
- 0
LEFT JOIN показывает и дни без заказов.SELECT c.name, last.id AS order_id, last.ordered_on
FROM clients AS c
CROSS JOIN LATERAL (
SELECT o.id, o.ordered_on
FROM orders AS o
WHERE o.client_id = c.id
ORDER BY o.ordered_on DESC
LIMIT 1
) AS last
ORDER BY c.id
LIMIT 3;Aigerim Seitova
- order_id
- 9
- ordered_on
- 2026-10-08
Daniyar Akhmetov
- order_id
- 10
- ordered_on
- 2026-10-09
Madina Omarova
- order_id
- 7
- ordered_on
- 2026-10-03
LATERAL: для каждой строки clients подзапрос выполняется со своим c.id.CTE без рекурсии и побочных эффектов, на который ссылаются один раз, PostgreSQL встраивает в основной запрос; поведение меняют подсказки MATERIALIZED и NOT MATERIALIZED. Рекурсивная часть обязана когда-то вернуть ноль строк, иначе запрос зациклится. WITH, WITH RECURSIVE и LATERAL из этих примеров работают и в MySQL 8.4.
INSERT, UPSERT (ON CONFLICT), UPDATE и DELETE с JOIN
INSERT INTO clients (name, city, email)
VALUES ('Asel Nurlanova', 'Almaty', 'asel@example.com'),
('Bauyrzhan Ospanov', 'Astana', NULL)
RETURNING id, name;
-- id | name
-- ----+-------------------
-- 9 | Asel Nurlanova
-- 10 | Bauyrzhan Ospanov
-- (2 rows)
--
-- INSERT 0 2RETURNING возвращает созданные строки, в том числе сгенерированный id, без второго запроса.INSERT INTO clients (name, city, email)
VALUES ('Madina Omarova', 'Astana', 'madina@example.com')
ON CONFLICT (email) DO UPDATE
SET city = EXCLUDED.city
RETURNING id, name, city;
INSERT INTO clients (name, city, email)
VALUES ('Madina O.', 'Shymkent', 'madina@example.com')
ON CONFLICT (email) DO NOTHING
RETURNING id;
INSERT INTO clients (name, city, email)
VALUES ('Nurlan Abenov', 'Almaty', 'nurlan@example.com')
RETURNING id;
-- id | name | city
-- ----+----------------+--------
-- 3 | Madina Omarova | Astana
-- (1 row)
--
-- INSERT 0 1
-- id
-- ----
-- (0 rows)
--
-- INSERT 0 0
-- id
-- ----
-- 11
-- (1 row)
--
-- INSERT 0 1email: обновление, пропуск и обычная вставка.EXCLUDED — строка, которую пытались вставить; имя таблицы или её псевдоним — строка, которая уже есть. DO NOTHING молча пропускает конфликт и ничего не возвращает в RETURNING. Обратите внимание на последний id: 11, а не 9 — значения идентификатора тратятся и на вставки, которые закончились конфликтом. Номера не обязаны идти без пропусков, и строить на этом логику нельзя.
INSERT INTO products (name, category, price)
VALUES ('Gift Box', 'gifts', 16000)
ON CONFLICT (name) DO UPDATE SET price = EXCLUDED.price;
-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specificationON CONFLICT (столбцы) работает только при уникальном индексе или ограничении ровно на этих столбцах.UPDATE orders AS o
SET status = 'shipped'
FROM clients AS c
WHERE c.id = o.client_id
AND c.city = 'Almaty'
AND o.status = 'paid'
RETURNING o.id, c.name, o.status;
-- id | name | status
-- ----+-----------------+---------
-- 3 | Aigerim Seitova | shipped
-- 9 | Aigerim Seitova | shipped
-- (2 rows)
--
-- UPDATE 2UPDATE … FROM: соединение с другой таблицей прямо в изменении.DELETE FROM order_items AS oi
USING orders AS o
WHERE o.id = oi.order_id
AND o.status = 'cancelled'
RETURNING oi.order_id, oi.product_id, oi.price;
-- order_id | product_id | price
-- ----------+------------+----------
-- 4 | 8 | 18500.00
-- (1 row)
--
-- DELETE 1DELETE … USING — удаление по условию из другой таблицы. Для очистки всей таблицы быстрее TRUNCATE.INSERT INTO clients (name, city, email)
VALUES ('Madina Omarova', 'Astana', 'madina@example.com') AS new
ON DUPLICATE KEY UPDATE city = new.city;
UPDATE orders AS o
JOIN clients AS c ON c.id = o.client_id
SET o.status = 'shipped'
WHERE c.city = 'Almaty' AND o.status = 'paid';
DELETE o FROM orders AS o
JOIN clients AS c ON c.id = o.client_id
WHERE o.status = 'cancelled';VALUES() в ON DUPLICATE KEY UPDATE объявлен устаревшим — используют псевдоним строки AS new. RETURNING в MySQL нет: запрос с ним завершается ошибкой синтаксиса 1064.DDL, ограничения, индексы и транзакции
CREATE TABLE reviews (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_id bigint NOT NULL REFERENCES products (id) ON DELETE CASCADE,
client_id bigint NOT NULL REFERENCES clients (id),
rating smallint NOT NULL CHECK (rating BETWEEN 1 AND 5),
body text,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (product_id, client_id)
);
ALTER TABLE reviews ADD COLUMN is_published boolean NOT NULL DEFAULT false;
ALTER TABLE reviews ALTER COLUMN body TYPE varchar(2000);
ALTER TABLE reviews RENAME COLUMN body TO comment;
ALTER TABLE reviews ADD CONSTRAINT comment_not_blank CHECK (comment <> '');
ALTER TABLE reviews DROP COLUMN is_published;ALTER TABLE. На каждую команду psql ответил CREATE TABLE и ALTER TABLE без ошибок.UNIQUE
- SQLSTATE
- 23505
- Сообщение PostgreSQL
duplicate key value violates unique constraint "reviews_product_id_client_id_key"
CHECK
- SQLSTATE
- 23514
- Сообщение PostgreSQL
new row for relation "reviews" violates check constraint "reviews_rating_check"
FOREIGN KEY
- SQLSTATE
- 23503
- Сообщение PostgreSQL
insert or update on table "reviews" violates foreign key constraint "reviews_product_id_fkey"
NOT NULL
- SQLSTATE
- 23502
- Сообщение PostgreSQL
null value in column "rating" of relation "reviews" violates not-null constraint
reviews (вывод с \set VERBOSITY verbose). В приложении надёжнее проверять код SQLSTATE, а не текст.CHECKпропускает строку, если выражение истинно или равноNULL, — для обязательного поля добавляютNOT NULL.UNIQUEпо умолчанию допускает несколькоNULL; запретить повторNULLможно сNULLS NOT DISTINCT.PRIMARY KEY=UNIQUE+NOT NULLи автоматически создаёт индекс. Столбцы внешнего ключа индекс не получают — его обычно создают вручную.- Действия внешнего ключа:
ON DELETE NO ACTION(по умолчанию),RESTRICT,CASCADE,SET NULL,SET DEFAULT.
CREATE INDEX orders_client_id_idx ON orders (client_id);
CREATE INDEX orders_status_date_idx ON orders (status, ordered_on DESC);
CREATE INDEX orders_new_idx ON orders (ordered_on) WHERE status = 'new';
CREATE UNIQUE INDEX clients_email_lower_key ON clients (lower(email));
CREATE INDEX CONCURRENTLY products_category_idx ON products (category);
DROP INDEX IF EXISTS products_category_idx;
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders' ORDER BY indexname;orders_client_id_idx
- indexdef
- CREATE INDEX orders_client_id_idx ON public.orders USING btree (client_id)
orders_new_idx
- indexdef
- CREATE INDEX orders_new_idx ON public.orders USING btree (ordered_on) WHERE (status = 'new'::text)
orders_pkey
- indexdef
- CREATE UNIQUE INDEX orders_pkey ON public.orders USING btree (id)
orders_status_date_idx
- indexdef
- CREATE INDEX orders_status_date_idx ON public.orders USING btree (status, ordered_on DESC)
orders после команд: обычный, составной, частичный и индекс первичного ключа.CREATE INDEX CONCURRENTLY строит индекс, не блокируя запись, но внутри BEGIN … COMMIT его запустить нельзя: PostgreSQL отвечает CREATE INDEX CONCURRENTLY cannot run inside a transaction block. В MySQL 8.4 индекс по выражению пишут в двойных скобках — ((lower(email))), а частичных индексов с WHERE нет (ошибка 1064). Как выбирать индексы и проверять их через EXPLAIN — в статье «Индексы».
BEGIN;
UPDATE products SET price = price * 1.1 WHERE category = 'tea';
SAVEPOINT before_mugs;
UPDATE products SET price = 0 WHERE id = 6;
ROLLBACK TO SAVEPOINT before_mugs;
COMMIT;
SELECT id, name, price FROM products WHERE id IN (4, 5, 6) ORDER BY id;
-- BEGIN
-- UPDATE 2
-- SAVEPOINT
-- ERROR: new row for relation "products" violates check constraint "products_price_check"
-- DETAIL: Failing row contains (6, Ceramic Mug 350 ml, tableware, 0.00, null).
-- ROLLBACK
-- COMMIT
-- id | name | price
-- ----+--------------------+---------
-- 4 | Green Sencha 100 g | 3520.00
-- 5 | Masala Chai 200 g | 4180.00
-- 6 | Ceramic Mug 350 ml | 2500.00
-- (3 rows)SAVEPOINT: откат до точки сохранения спасает остальные изменения, чай подорожал на 10%, кружка не изменилась.BEGIN;
INSERT INTO clients (name, email) VALUES ('Test', 'aigerim@example.com');
SELECT count(*) FROM clients;
ROLLBACK;
-- BEGIN
-- ERROR: duplicate key value violates unique constraint "clients_email_key"
-- DETAIL: Key (email)=(aigerim@example.com) already exists.
-- ERROR: current transaction is aborted, commands ignored until end of transaction block
-- ROLLBACKROLLBACK.BEGIN;
CREATE TABLE tmp_promo (code text PRIMARY KEY);
ROLLBACK;
SELECT to_regclass('tmp_promo') AS table_after_rollback;
-- BEGIN
-- CREATE TABLE
-- ROLLBACK
-- table_after_rollback
-- ----------------------
-- NULL
-- (1 row)ROLLBACK исчезла. В MySQL 8.4 CREATE TABLE вызывает неявный COMMIT — после такого же ROLLBACK таблица осталась.BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id, status FROM orders WHERE id = 8 FOR UPDATE;
UPDATE orders SET status = 'paid' WHERE id = 8;
COMMIT;
-- BEGIN
-- id | status
-- ----+--------
-- 8 | new
-- (1 row)
--
-- UPDATE 1
-- COMMITBEGIN, SELECT … FOR UPDATE блокирует строку до конца транзакции. Когда это нужно — в статье «Транзакции и изоляция».Функции: даты, строки, NULL и CASE
SELECT id,
ordered_on,
ordered_on + 7 AS plus_7_days,
ordered_on + interval '1 month' AS plus_month,
date_trunc('month', ordered_on)::date AS month_start,
extract(isodow FROM ordered_on) AS iso_weekday,
to_char(ordered_on, 'DD.MM.YYYY') AS ru_format,
date '2026-10-12' - ordered_on AS days_ago
FROM orders
WHERE id IN (1, 10)
ORDER BY id;1
- ordered_on
- 2026-09-02
- plus_7_days
- 2026-09-09
- plus_month
- 2026-10-02 00:00:00
- month_start
- 2026-09-01
- iso_weekday
- 3
- ru_format
- 02.09.2026
- days_ago
- 40
10
- ordered_on
- 2026-10-09
- plus_7_days
- 2026-10-16
- plus_month
- 2026-11-09 00:00:00
- month_start
- 2026-10-01
- iso_weekday
- 5
- ru_format
- 09.10.2026
- days_ago
- 3
date + целое — дата, date + interval — timestamp; isodow — день недели от 1 (пн) до 7 (вс).SELECT name,
upper(split_part(name, ' ', 1)) AS first_word,
length(name) AS len,
left(name, 8) || '...' AS short,
replace(name, ' g', ' гр') AS ru_unit,
coalesce(weight_g::text, 'n/a') AS weight,
nullif(category, 'gifts') AS cat_or_null
FROM products
WHERE id IN (2, 4, 9)
ORDER BY id;Colombia Supremo 250 g
- first_word
- COLOMBIA
- len
- 22
- short
- Colombia...
- ru_unit
- Colombia Supremo 250 гр
- weight
- 250
- cat_or_null
- coffee
Green Sencha 100 g
- first_word
- GREEN
- len
- 18
- short
- Green Se...
- ru_unit
- Green Sencha 100 гр
- weight
- 100
- cat_or_null
- tea
Gift Box
- first_word
- GIFT
- len
- 8
- short
- Gift Box...
- ru_unit
- Gift Box
- weight
- n/a
- cat_or_null
- NULL
coalesce возвращает первый не-NULL аргумент, nullif(a, b) — NULL, если a = b.SELECT category, string_agg(name, ', ' ORDER BY price) AS items
FROM products
GROUP BY category
ORDER BY category;coffee
- items
- Colombia Supremo 250 g, Ethiopia Yirgacheffe 250 g, Espresso Blend 1 kg
equipment
- items
- French Press 600 ml, Hand Grinder
gifts
- items
- Gift Box
tableware
- items
- Ceramic Mug 350 ml
tea
- items
- Green Sencha 100 g, Masala Chai 200 g
string_agg склеивает значения группы; порядок задаётся внутри вызова.SELECT name, price,
CASE
WHEN price >= 15000 THEN 'premium'
WHEN price >= 5000 THEN 'mid'
ELSE 'budget'
END AS segment,
greatest(price - 3000, 0) AS after_coupon,
price::int AS as_int
FROM products
WHERE id IN (1, 4, 8)
ORDER BY price;Green Sencha 100 g
- price
- 3200.00
- segment
- budget
- after_coupon
- 200.00
- as_int
- 3200
Espresso Blend 1 kg
- price
- 12900.00
- segment
- mid
- after_coupon
- 9900.00
- as_int
- 12900
Hand Grinder
- price
- 18500.00
- segment
- premium
- after_coupon
- 15500.00
- as_int
- 18500
CASE проверяет условия сверху вниз; ::int и CAST(x AS тип) — приведение типов.Склеить строки
- PostgreSQL 17
'a' || 'b',concat('a', 'b')- MySQL 8.4
CONCAT('a', 'b');'a' || 'b'вернул0и предупреждение:||здесь — устаревший синонимOR
Значение вместо NULL
- PostgreSQL 17
coalesce(x, 'n/a')- MySQL 8.4
COALESCEилиIFNULL(x, 'n/a')
Значения группы через запятую
- PostgreSQL 17
string_agg(name, ', ' ORDER BY name)- MySQL 8.4
GROUP_CONCAT(name ORDER BY name SEPARATOR ', ')
Дата как ДД.ММ.ГГГГ
- PostgreSQL 17
to_char(d, 'DD.MM.YYYY')- MySQL 8.4
DATE_FORMAT(d, '%d.%m.%Y')
Автоинкремент
- PostgreSQL 17
GENERATED ALWAYS AS IDENTITY- MySQL 8.4
AUTO_INCREMENT
UPSERT
- PostgreSQL 17
ON CONFLICT (email) DO UPDATE SET city = EXCLUDED.city- MySQL 8.4
… AS new ON DUPLICATE KEY UPDATE city = new.city
Вернуть изменённые строки
- PostgreSQL 17
RETURNING- MySQL 8.4
- Нет
Условный агрегат
- PostgreSQL 17
count(*) FILTER (WHERE …)- MySQL 8.4
count(CASE WHEN … THEN 1 END)
DDL в транзакции
- PostgreSQL 17
- Откатывается вместе с транзакцией
- MySQL 8.4
- Неявный
COMMIT
Как проверено2 · пройденосверено 12 октября 2026 г.
- запущено, работает
Все запросы PostgreSQL в статье (32 файла: шаблон SELECT, ошибка псевдонима в WHERE, 11 операторов WHERE, пагинация, 5 видов JOIN, антисоединение, агрегаты, GROUP BY/HAVING/FILTER, 4 оконных запроса и ошибка в WHERE, подзапросы, WITH RECURSIVE, LATERAL, INSERT … RETURNING, ON CONFLICT DO UPDATE/DO NOTHING, ошибка ON CONFLICT без уникального индекса, UPDATE … FROM, DELETE … USING, DDL и 4 нарушения ограничений, индексы, 4 сценария транзакций, функции дат/строк/CASE, LIMIT #,#, CONCURRENTLY в транзакции)
docker postgres:17-alpine (PostgreSQL 17.11), контейнер kb-pg-sqlref (--rm); seed shop.sql взят из sql-select.json без изменений; перед каждым базовым запросом схема пересоздавалась (DROP SCHEMA public CASCADE + shop.sql); каждый запрос — отдельный файл, `psql -X -P null=NULL`; код статьи берётся из этих файлов, таблицы и комментарии с выводом собраны скриптом из реальных файлов вывода. Ошибки в статье — намеренные примеры, их текст из реального вывода
- запущено, работает
Пометки MySQL: INSERT … AS new ON DUPLICATE KEY UPDATE, UPDATE … JOIN, DELETE o FROM … JOIN, LIMIT 1, 2, CONCAT/IFNULL/GROUP_CONCAT/DATE_FORMAT,
'a' || 'b'(0 + предупреждение 1287), оконная функция, WITH RECURSIVE, LATERAL, индекс ((lower(email))), неявный COMMIT после CREATE TABLE, NOT (a <=> b), count(CASE …); ошибки 1064 для ILIKE, RETURNING, FILTER, частичного индекса, FULL JOIN, IS DISTINCT FROMdocker mysql:8.4 (8.4.11), контейнер kb-mysql-sqlref (--rm), мини-схема clients/orders в mysql.sql; блок кода MySQL в статье вырезан из выполненного файла
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.