Как делать12 октября 2026 г.10 мин чтения
Справочник разработчикаШаг 6 из 8

Справочник SQL: запросы, изменения данных, DDL и транзакции

Коротко

Справочник основных конструкций 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 ведёт себя иначе, дан проверенный аналог.

СправочникУровень: НачальныйСправочник SQL

После материала вы сможете

  • найти синтаксис и проверенный пример для 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 г.

Содержание
  1. 01SELECT: шаблон запроса и порядок выполнения
  2. 02WHERE: операторы сравнения и NULL
  3. 03JOIN, GROUP BY и HAVING
  4. 04Оконные функции: ROW_NUMBER, RANK, SUM OVER
  5. 05Подзапросы и CTE (WITH)
  6. 06INSERT, UPSERT (ON CONFLICT), UPDATE и DELETE с JOIN
  7. 07DDL, ограничения, индексы и транзакции
  8. 08Функции: даты, строки, NULL и CASE

SELECT: шаблон запроса и порядок выполнения

Все примеры выполнены в PostgreSQL 17.11 (образ postgres:17-alpine) на учебной базе магазина из урока «SQL с нуля»: таблицы clients, products, orders и order_items, скрипт shop.sql — там же. Перед каждым примером база загружалась заново, поэтому изменения одного запроса не влияют на другой. Под кодом — реальный вывод psql. Отличия MySQL проверены на MySQL 8.4.11.

SQL
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.
SQL
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;
--               ^
Псевдоним появляется только на шаге 5. В WHERE и HAVING выражение повторяют, в ORDER BY и GROUP BY имя выходного столбца использовать можно.
SQL
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
Вторая «страница» по 3 товара, от дорогих к дешёвым. Второй ключ id делает порядок однозначным.
SQL
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
Число строк посчитано на учебной базе: 8 клиентов, 10 заказов, 9 товаров, 15 позиций заказов.
SQL
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 по ключу правой таблицы.
SQL
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.
SQL
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 — нарастающий итог, без него — итог по всему разделу
SQL
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 во внешнем запросе.
SQL
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
Нарастающий итог, разница с предыдущим заказом и доля каждого заказа в общей выручке.
SQL
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 всегда уникален.
SQL
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 на группу»
SQL
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)
Три подзапроса подряд, реальный вывод — в комментариях.
SQL
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
Рекурсивный CTE строит календарь, LEFT JOIN показывает и дни без заказов.
SQL
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

SQL
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 2
RETURNING возвращает созданные строки, в том числе сгенерированный id, без второго запроса.
SQL
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 1
UPSERT по уникальному email: обновление, пропуск и обычная вставка.

EXCLUDED — строка, которую пытались вставить; имя таблицы или её псевдоним — строка, которая уже есть. DO NOTHING молча пропускает конфликт и ничего не возвращает в RETURNING. Обратите внимание на последний id: 11, а не 9 — значения идентификатора тратятся и на вставки, которые закончились конфликтом. Номера не обязаны идти без пропусков, и строить на этом логику нельзя.

SQL
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 specification
ON CONFLICT (столбцы) работает только при уникальном индексе или ограничении ровно на этих столбцах.
SQL
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 2
UPDATE … FROM: соединение с другой таблицей прямо в изменении.
SQL
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 1
DELETE … USING — удаление по условию из другой таблицы. Для очистки всей таблицы быстрее TRUNCATE.
SQL
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';
MySQL 8.4: те же три операции. VALUES() в ON DUPLICATE KEY UPDATE объявлен устаревшим — используют псевдоним строки AS new. RETURNING в MySQL нет: запрос с ним завершается ошибкой синтаксиса 1064.

DDL, ограничения, индексы и транзакции

SQL
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.
SQL
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 — в статье «Индексы».

SQL
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%, кружка не изменилась.
SQL
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
-- ROLLBACK
Без точки сохранения после первой ошибки транзакция отвергает все команды до ROLLBACK.
SQL
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)
В PostgreSQL DDL транзакционен: таблица после ROLLBACK исчезла. В MySQL 8.4 CREATE TABLE вызывает неявный COMMIT — после такого же ROLLBACK таблица осталась.
SQL
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
-- COMMIT
Уровень изоляции задают в BEGIN, SELECT … FOR UPDATE блокирует строку до конца транзакции. Когда это нужно — в статье «Транзакции и изоляция».

Функции: даты, строки, NULL и CASE

SQL
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 (вс).
SQL
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.
SQL
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 склеивает значения группы; порядок задаётся внутри вызова.
SQL
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
Каждая запись для MySQL выполнена на MySQL 8.4.11.
Как проверено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 FROM

    docker mysql:8.4 (8.4.11), контейнер kb-mysql-sqlref (--rm), мини-схема clients/orders в mysql.sql; блок кода MySQL в статье вырезан из выполненного файла

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.