Перейти к основному содержимому

Примеры запросов в SQL

1. Базовые SELECT-запросы

1.1. Выбор всех столбцов из таблицы

SELECT * FROM employees;

1.2. Выбор конкретных столбцов

SELECT first_name, last_name, email FROM employees;

1.3. Выбор с псевдонимами столбцов

SELECT first_name AS name, department_id AS dept FROM employees;

1.4. Выбор уникальных значений (DISTINCT)

SELECT DISTINCT department_id FROM employees;

1.5. Выбор с ограничением количества строк (LIMIT / TOP / FETCH)

-- PostgreSQL, MySQL, SQLite
SELECT * FROM employees LIMIT 10;

-- MS SQL Server (до 2012)
SELECT TOP 10 * FROM employees;

-- MS SQL Server (2012+), стандартный SQL:2008
SELECT * FROM employees
ORDER BY employee_id
FETCH FIRST 10 ROWS ONLY;

2. Фильтрация: WHERE

2.1. Простое условие

SELECT * FROM employees WHERE salary > 50000;

2.2. Составное условие

SELECT * FROM employees
WHERE salary BETWEEN 40000 AND 70000
AND department_id = 3;

2.3. Поиск по шаблону (LIKE)

SELECT * FROM employees
WHERE last_name LIKE 'Sm%'; -- начинается на Sm

-- Регистронезависимый поиск (PostgreSQL)
SELECT * FROM employees
WHERE last_name ILIKE 'sm%';

-- MySQL: регистронезависимый по умолчанию (если collation _ci)
-- MS SQL: чувствителен к регистру — используй UPPER() или COLLATE
SELECT * FROM employees
WHERE UPPER(last_name) LIKE 'SM%';

2.4. Проверка на NULL

SELECT * FROM employees WHERE manager_id IS NULL;
SELECT * FROM employees WHERE manager_id IS NOT NULL;

2.5. Множественное условие (IN)

SELECT * FROM employees
WHERE department_id IN (1, 3, 5);

2.6. Отрицание (NOT)

SELECT * FROM employees
WHERE NOT (salary > 60000 AND department_id = 2);
-- Эквивалент:
SELECT * FROM employees
WHERE salary <= 60000 OR department_id <> 2;

3. Сортировка: ORDER BY

3.1. Сортировка по одному полю

SELECT * FROM employees
ORDER BY last_name ASC;

3.2. Сортировка по нескольким полям

SELECT * FROM employees
ORDER BY department_id ASC, salary DESC;

3.3. Сортировка с NULL в нужной позиции

-- PostgreSQL: NULLS LAST / NULLS FIRST
SELECT * FROM employees
ORDER BY commission_pct DESC NULLS LAST;

-- MS SQL: используйте CASE
SELECT * FROM employees
ORDER BY
CASE WHEN commission_pct IS NULL THEN 1 ELSE 0 END,
commission_pct DESC;

4. Агрегация и группировка: GROUP BY, HAVING

4.1. Простая агрегация

SELECT COUNT(*) AS total_employees FROM employees;
SELECT AVG(salary) AS avg_salary FROM employees;
SELECT MIN(salary), MAX(salary), SUM(salary) FROM employees;

4.2. Группировка по столбцу

SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;

4.3. Группировка с фильтрацией по агрегатам (HAVING)

SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 50000;

4.4. Агрегация с COALESCE для NULL

SELECT department_id,
COUNT(*) AS total,
AVG(COALESCE(commission_pct, 0)) AS avg_commission
FROM employees
GROUP BY department_id;

5. Соединения (JOIN)

5.1. INNER JOIN

SELECT e.first_name, e.last_name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id;

5.2. LEFT JOIN (все строки из левой таблицы)

SELECT e.first_name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id;
-- Покажет всех сотрудников, даже без департамента (department_name = NULL)

5.3. RIGHT JOIN (аналогично, но сохраняет все из правой)

SELECT e.first_name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id;
-- Покажет все департаменты, даже без сотрудников

5.4. FULL OUTER JOIN (все строки из обеих таблиц)

-- Поддерживается в PostgreSQL, MS SQL
SELECT e.employee_id, d.department_id
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.department_id;

-- В MySQL: эмулируется через UNION
SELECT e.employee_id, d.department_id
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
UNION
SELECT e.employee_id, d.department_id
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id
WHERE e.employee_id IS NULL;

5.5. Самосоединение (SELF JOIN)

SELECT e1.first_name AS employee,
e2.first_name AS manager
FROM employees e1
LEFT JOIN employees e2 ON e1.manager_id = e2.employee_id;

6. Подзапросы

6.1. Скалярный подзапрос (возвращает одно значение)

SELECT first_name, last_name,
(SELECT department_name
FROM departments d
WHERE d.department_id = e.department_id) AS dept_name
FROM employees e;

✅ Работает, если связь 1:1 или гарантируется единственность.
⚠️ Может быть медленнее JOIN, особенно без индекса.

6.2. Подзапрос в WHERE — поиск по списку (IN)

SELECT * FROM employees
WHERE department_id IN (
SELECT department_id
FROM departments
WHERE location_id = 1700
);

6.3. Коррелированный подзапрос (зависит от внешнего запроса)

SELECT e1.first_name, e1.last_name, e1.salary
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e1.department_id
);
-- Сотрудники, зарабатывающие выше среднего в своём департаменте

6.4. EXISTS — проверка наличия

SELECT d.department_name
FROM departments d
WHERE EXISTS (
SELECT 1
FROM employees e
WHERE e.department_id = d.department_id
AND e.salary > 10000
);
-- Департаменты, где есть хотя бы один сотрудник с ЗП > 10 000

EXISTS часто эффективнее IN, особенно при работе с NULL.

6.5. Подзапрос в FROM (производная таблица / inline view)

SELECT dept_name, avg_sal
FROM (
SELECT d.department_name AS dept_name,
AVG(e.salary) AS avg_sal
FROM employees e
JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_id, d.department_name
) sub
WHERE avg_sal > 50000;

7. Общие табличные выражения (WITH, CTE)

7.1. Простой CTE

WITH high_salary AS (
SELECT employee_id, first_name, salary
FROM employees
WHERE salary > 8000
)
SELECT * FROM high_salary
ORDER BY salary DESC;

7.2. Множественные CTE

WITH 
dept_avg AS (
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
),
above_avg AS (
SELECT e.first_name, e.last_name, e.salary, da.avg_sal
FROM employees e
JOIN dept_avg da ON e.department_id = da.department_id
WHERE e.salary > da.avg_sal
)
SELECT * FROM above_avg;

7.3. Рекурсивный CTE (иерархия: например, оргструктура)

WITH RECURSIVE emp_hierarchy AS (
-- Якорь: топ-менеджеры (без руководителя)
SELECT employee_id, first_name, last_name, manager_id, 0 AS level
FROM employees
WHERE manager_id IS NULL

UNION ALL

-- Рекурсия: подчинённые
SELECT e.employee_id, e.first_name, e.last_name, e.manager_id, eh.level + 1
FROM employees e
INNER JOIN emp_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT REPEAT(' ', level) || first_name || ' ' || last_name AS tree_view
FROM emp_hierarchy
ORDER BY level, employee_id;

✅ Поддерживается в PostgreSQL, MS SQL Server, SQLite (3.8.3+), Oracle.
❌ MySQL поддерживает с 8.0.


8. Оконные функции (OVER)

8.1. Ранжирование

SELECT 
first_name, last_name, department_id, salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS drnk
FROM employees;
  • ROW_NUMBER() — уникальный номер (1,2,3…)
  • RANK() — одинаковые значения → одинаковый ранг, следующий пропускается (1,1,3)
  • DENSE_RANK() — одинаковые значения → одинаковый ранг, без пропусков (1,1,2)

8.2. Накопительная сумма

SELECT 
hire_date, salary,
SUM(salary) OVER (ORDER BY hire_date ROWS UNBOUNDED PRECEDING) AS running_total
FROM employees
ORDER BY hire_date;

8.3. Сравнение с предыдущей строкой (LAG/LEAD)

SELECT 
first_name, hire_date, salary,
LAG(salary, 1) OVER (ORDER BY hire_date) AS prev_salary,
salary - LAG(salary, 1) OVER (ORDER BY hire_date) AS diff
FROM employees
ORDER BY hire_date;

8.4. Доля в группе (PERCENT_RANK, CUME_DIST, % от суммы)

SELECT 
department_id,
first_name,
salary,
ROUND(
100.0 * salary / SUM(salary) OVER (PARTITION BY department_id),
2
) AS pct_of_dept_total
FROM employees;

9. Изменение данных (DML)

9.1. Вставка одной строки

INSERT INTO employees (
employee_id, first_name, last_name, email, hire_date, job_id, salary, department_id
) VALUES (
999, 'Алексей', 'Петров', 'apetrov@example.com', '2025-11-13', 'IT_PROG', 75000, 60
);

9.2. Вставка из другой таблицы

INSERT INTO archive_employees
SELECT * FROM employees
WHERE hire_date < '2020-01-01';

9.3. Обновление с JOIN (не ANSI, но часто используется)

-- PostgreSQL, MySQL
UPDATE employees e
SET salary = salary * 1.1
FROM departments d
WHERE e.department_id = d.department_id
AND d.department_name = 'IT';

-- MS SQL Server
UPDATE e
SET salary = salary * 1.1
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.department_name = 'IT';

-- ANSI-совместимый (через подзапрос)
UPDATE employees
SET salary = salary * 1.1
WHERE department_id = (
SELECT department_id
FROM departments
WHERE department_name = 'IT'
);

9.4. Условное обновление (CASE)

UPDATE employees
SET salary = CASE
WHEN job_id = 'MANAGER' THEN salary * 1.15
WHEN job_id = 'SA_REP' THEN salary * 1.10
ELSE salary
END
WHERE department_id = 80;

9.5. Удаление с подзапросом

DELETE FROM employees
WHERE department_id IN (
SELECT department_id
FROM departments
WHERE location_id = 1800
);

9.6. Умное обновление/вставка (UPSERT / MERGE)

PostgreSQL (ON CONFLICT)

INSERT INTO employees (employee_id, first_name, salary)
VALUES (999, 'Иван', 60000)
ON CONFLICT (employee_id)
DO UPDATE SET
first_name = EXCLUDED.first_name,
salary = EXCLUDED.salary;

MySQL (ON DUPLICATE KEY UPDATE)

INSERT INTO employees (employee_id, first_name, salary)
VALUES (999, 'Иван', 60000)
ON DUPLICATE KEY UPDATE
first_name = VALUES(first_name),
salary = VALUES(salary);

MS SQL / Oracle (MERGE)

MERGE INTO employees AS target
USING (SELECT 999 AS id, 'Иван' AS name, 60000 AS sal) AS source
ON target.employee_id = source.id
WHEN MATCHED THEN
UPDATE SET first_name = source.name, salary = source.sal
WHEN NOT MATCHED THEN
INSERT (employee_id, first_name, salary)
VALUES (source.id, source.name, source.sal);

10. Работа с датами и временем

10.1. Текущая дата/время

-- Стандартный SQL (совместимо с большинством СУБД)
SELECT CURRENT_DATE; -- дата
SELECT CURRENT_TIMESTAMP; -- дата + время + часовой пояс (если поддерживается)

-- PostgreSQL
SELECT NOW(); -- = CURRENT_TIMESTAMP
SELECT CLOCK_TIMESTAMP(); -- точное время выполнения (даже внутри одного запроса)

-- MySQL
SELECT CURDATE();
SELECT NOW(); -- DATETIME (без часового пояса)
SELECT UTC_TIMESTAMP();

-- MS SQL Server
SELECT GETDATE(); -- DATETIME (локальное)
SELECT GETUTCDATE();
SELECT SYSDATETIME(); -- более высокая точность (до 100 нс)

10.2. Извлечение компонентов (EXTRACT, DATE_PART)

-- Стандарт SQL / PostgreSQL
SELECT
EXTRACT(YEAR FROM hire_date) AS yr,
EXTRACT(MONTH FROM hire_date) AS mth,
EXTRACT(DAY FROM hire_date) AS day,
EXTRACT(DOW FROM hire_date) AS weekday -- 0=вс (PostgreSQL), 1=пн (MS SQL)
FROM employees;

-- MySQL
SELECT
YEAR(hire_date),
MONTH(hire_date),
DAY(hire_date),
WEEKDAY(hire_date); -- 0=пн, 6=вс

-- MS SQL Server
SELECT
YEAR(hire_date),
MONTH(hire_date),
DAY(hire_date),
DATEPART(WEEKDAY, hire_date); -- зависит от SET DATEFIRST

10.3. Арифметика дат

-- PostgreSQL
SELECT hire_date + INTERVAL '1 year' AS next_year,
hire_date + INTERVAL '3 months 5 days' AS adjusted
FROM employees;

-- MySQL
SELECT
DATE_ADD(hire_date, INTERVAL 1 YEAR) AS next_year,
hire_date + INTERVAL 3 MONTH + INTERVAL 5 DAY AS adjusted
FROM employees;

-- MS SQL Server
SELECT
DATEADD(YEAR, 1, hire_date) AS next_year,
DATEADD(DAY, 5, DATEADD(MONTH, 3, hire_date)) AS adjusted
FROM employees;

10.4. Группировка по периодам

-- Месяцы (PostgreSQL)
SELECT
DATE_TRUNC('month', hire_date)::DATE AS month_start,
COUNT(*) AS hires
FROM employees
GROUP BY month_start
ORDER BY month_start;

-- MySQL
SELECT
DATE_FORMAT(hire_date, '%Y-%m-01') AS month_start,
COUNT(*)
FROM employees
GROUP BY month_start
ORDER BY month_start;

-- MS SQL Server
SELECT
DATEFROMPARTS(YEAR(hire_date), MONTH(hire_date), 1) AS month_start,
COUNT(*)
FROM employees
GROUP BY YEAR(hire_date), MONTH(hire_date)
ORDER BY month_start;

11. Работа со строками

11.1. Конкатенация

-- Стандарт (SQL-92)
SELECT first_name || ' ' || last_name AS full_name FROM employees;

-- MySQL, MS SQL (<2012): CONCAT()
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM employees;

-- MS SQL (2012+): CONCAT() + ISNULL/COALESCE для NULL-safe
SELECT CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name)
FROM employees;

-- PostgreSQL: CONCAT() и CONCAT_WS()
SELECT CONCAT_WS(' ', first_name, middle_name, last_name) FROM employees;

11.2. Регулярные выражения

PostgreSQL

-- Совпадение
SELECT * FROM employees
WHERE email ~* '^[a-z0-9._%+-]+@example\.com$';

-- Замена
SELECT REGEXP_REPLACE(phone_number, '\D', '', 'g') AS digits_only
FROM employees;

MySQL (8.0+)

SELECT * FROM employees
WHERE email REGEXP '^[a-z0-9._%+-]+@example\\.com$';

SELECT REGEXP_REPLACE(phone_number, '[^0-9]', '') AS digits_only
FROM employees;

MS SQL Server — без встроенных regexp; используйте LIKE или CLR-функции. Пример с LIKE:

-- Простой паттерн: email с @example.com
SELECT * FROM employees
WHERE email LIKE '%@example.com';
-- Для сложных случаев — внешняя обработка или CLR.

11.3. Разбор строк (разделение по символу)

PostgreSQL (string_to_array, unnest)

SELECT id, UNNEST(STRING_TO_ARRAY(tags, ',')) AS tag
FROM products;

MySQL (8.0+, JSON_TABLE или REGEXP_SUBSTR)

-- Через генерацию последовательности и REGEXP_SUBSTR
WITH RECURSIVE nums AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums WHERE n < 10
)
SELECT
p.id,
TRIM(REGEXP_SUBSTR(p.tags, '[^,]+', 1, n.n)) AS tag
FROM products p
JOIN nums n ON n.n <= 1 + (LENGTH(p.tags) - LENGTH(REPLACE(p.tags, ',', '')))
WHERE tag IS NOT NULL AND tag <> '';

MS SQL Server (2016+, STRING_SPLIT)

SELECT p.id, TRIM(value) AS tag
FROM products p
CROSS APPLY STRING_SPLIT(p.tags, ',')
WHERE TRIM(value) <> '';

12. Работа с JSON

12.1. PostgreSQL (JSONB — рекомендуется)

-- Вставка JSON-объекта
INSERT INTO logs (event_data)
VALUES ('{"user_id": 101, "action": "login", "params": {"ip": "192.168.1.5"}}'::JSONB);

-- Извлечение полей
SELECT
event_data->>'user_id' AS user_id,
event_data->'params'->>'ip' AS ip
FROM logs;

-- Фильтрация по JSON-ключу
SELECT * FROM logs
WHERE event_data @> '{"action": "login"}';

-- Индексация (ускоряет поиск по JSON)
CREATE INDEX idx_logs_action ON logs USING GIN ((event_data->'action'));

12.2. MySQL (5.7+, JSON тип)

-- Извлечение
SELECT
JSON_UNQUOTE(JSON_EXTRACT(event_data, '$.user_id')) AS user_id,
JSON_UNQUOTE(JSON_EXTRACT(event_data, '$.params.ip')) AS ip
FROM logs;

-- Упрощённый синтаксис (MySQL 8.0+)
SELECT
event_data->>'$.user_id' AS user_id,
event_data->>'$.params.ip' AS ip
FROM logs;

-- Поиск по JSON-ключу
SELECT * FROM logs
WHERE JSON_CONTAINS_PATH(event_data, 'one', '$.action')
AND JSON_UNQUOTE(JSON_EXTRACT(event_data, '$.action')) = 'login';

-- Индекс (функциональный)
ALTER TABLE logs
ADD COLUMN action VARCHAR(32) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(event_data, '$.action')));
CREATE INDEX idx_logs_action ON logs(action);

12.3. MS SQL Server (2016+, NVARCHAR + JSON_* функции)

-- Проверка
SELECT * FROM logs
WHERE ISJSON(event_data) = 1;

-- Извлечение
SELECT
JSON_VALUE(event_data, '$.user_id') AS user_id,
JSON_VALUE(event_data, '$.params.ip') AS ip
FROM logs;

-- Фильтрация
SELECT * FROM logs
WHERE JSON_VALUE(event_data, '$.action') = 'login';

-- Индекс — только по вычисляемому столбцу
ALTER TABLE logs
ADD action AS JSON_VALUE(event_data, '$.action');
CREATE INDEX idx_logs_action ON logs(action);

13. Поворот таблиц (Pivot / Cross-tabulation)

13.1. Условная агрегация (универсально)

SELECT 
department_id,
COUNT(*) FILTER (WHERE gender = 'M') AS male_count,
COUNT(*) FILTER (WHERE gender = 'F') AS female_count,
AVG(salary) FILTER (WHERE job_id = 'IT_PROG') AS avg_it_salary
FROM employees
GROUP BY department_id;

✅ Поддерживается в PostgreSQL (начиная с 9.4), MS SQL Server (через CASE), SQLite.
MySQL: используйте CASE WHEN.

Альтернатива для MySQL / MS SQL:

SELECT 
department_id,
SUM(CASE WHEN gender = 'M' THEN 1 ELSE 0 END) AS male_count,
SUM(CASE WHEN gender = 'F' THEN 1 ELSE 0 END) AS female_count,
AVG(CASE WHEN job_id = 'IT_PROG' THEN salary END) AS avg_it_salary
FROM employees
GROUP BY department_id;

13.2. PIVOT (MS SQL Server)

SELECT department_id, [IT_PROG], [SA_REP], [ST_CLERK]
FROM (
SELECT department_id, job_id, salary
FROM employees
) src
PIVOT (
AVG(salary)
FOR job_id IN ([IT_PROG], [SA_REP], [ST_CLERK])
) pvt;

13.3. Динамический PIVOT (MS SQL — через динамический SQL)

DECLARE @cols AS NVARCHAR(MAX),
@query AS NVARCHAR(MAX);

SELECT @cols = STRING_AGG(QUOTENAME(job_id), ', ')
FROM (SELECT DISTINCT job_id FROM employees) AS jobs;

SET @query = '
SELECT department_id, ' + @cols + '
FROM (
SELECT department_id, job_id, salary
FROM employees
) src
PIVOT (
AVG(salary)
FOR job_id IN (' + @cols + ')
) pvt;';

EXEC sp_executesql @query;

14. Генерация данных / последовательностей

14.1. Генерация дней за период (PostgreSQL)

SELECT day::DATE
FROM GENERATE_SERIES(
'2025-01-01'::DATE,
'2025-12-31'::DATE,
'1 day'
) AS day;

14.2. Генерация последовательности (рекурсивный CTE, кроссплатформенно)

WITH RECURSIVE nums AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums WHERE n < 365
)
SELECT n FROM nums;

14.3. Генерация временных слотов (каждые 15 минут)

WITH RECURSIVE slots AS (
SELECT '09:00'::TIME AS slot
UNION ALL
SELECT (slot + INTERVAL '15 minutes')::TIME
FROM slots
WHERE slot < '18:00'
)
SELECT slot FROM slots;

15. Практические отчётные шаблоны

15.1. TOP-N в каждой группе (например: ТОП-3 з/п в отделе)

WITH ranked AS (
SELECT
first_name, last_name, department_id, salary,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn
FROM employees
)
SELECT * FROM ranked
WHERE rn <= 3;

15.2. Скользящее среднее (3-дневное)

SELECT 
hire_date,
COUNT(*) AS hires_today,
AVG(COUNT(*)) OVER (
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS moving_avg_3d
FROM employees
GROUP BY hire_date
ORDER BY hire_date;

15.3. Retention (повторное действие в течение N дней)

Пример: пользователи, сделавшие повторную покупку в течение 7 дней после первой.

WITH first_orders AS (
SELECT
customer_id,
MIN(order_date) AS first_date
FROM orders
GROUP BY customer_id
),
repeat_customers AS (
SELECT DISTINCT o.customer_id
FROM orders o
JOIN first_orders f ON o.customer_id = f.customer_id
WHERE o.order_date > f.first_date
AND o.order_date <= f.first_date + INTERVAL '7 days'
)
SELECT
COUNT(DISTINCT f.customer_id) AS total_customers,
COUNT(DISTINCT r.customer_id) AS retained,
ROUND(100.0 * COUNT(r.customer_id) / COUNT(f.customer_id), 2) AS retention_pct
FROM first_orders f
LEFT JOIN repeat_customers r ON f.customer_id = r.customer_id;

16. Анализ плана выполнения: EXPLAIN

16.1. Базовый план

EXPLAIN SELECT * FROM employees WHERE department_id = 60;
-- Выводит дерево операций (Seq Scan, Index Scan и т.д.)

EXPLAIN ANALYZE SELECT * FROM employees WHERE department_id = 60;
-- Выполняет запрос и показывает реальное время, число строк и т.п.

16.2. Интерпретация ключевых узлов

УзелЧто означаетНа что смотреть
Seq ScanПолное сканирование таблицыИзбегать на больших таблицах без LIMIT
Index Scan / Index Only ScanИспользование индексаIndex Only — лучше (нет обращения к таблице)
Bitmap Index Scan + Bitmap Heap ScanДля множественных условийЭффективно при среднем селективном фильтре
Hash Join / Nested Loop / Merge JoinАлгоритм соединенияNested Loop — хорошо при малом правом множестве
SortЯвная сортировка (без индекса)Может быть дорого — добавьте индекс под ORDER BY

16.3. Советы по оптимизации

  • ✅ Добавляйте индексы на WHERE, JOIN, ORDER BY.
  • ✅ Избегайте SELECT * в production-запросах — явно указывайте поля.
  • ✅ Не оборачивайте колонку в функцию в WHERE:
    WHERE UPPER(name) = 'IVAN' → индекс не используется
    WHERE name = 'Ivan' (и используйте регистронезависимый collation или функциональный индекс).
  • ✅ Для пагинации на больших данных — курсоры (DECLARE CURSOR) или ключевая пагинация (WHERE id > last_seen_id), а не OFFSET.