Skip to content

SQL #24

Description

@abzalimovrrr

SQL — Язык структурированных запросов: вопросы, практические задания и ответы с кодом

100 вопросов по SQL от основ до продвинутых техник — с практическими заданиями, примерами кода и комментариями на русском языке.


Темы

РазделОписание
1–10Основы SQLCREATE TABLE, INSERT, SELECT, WHERE, UPDATE, DELETE, ALTER/DROP TABLE, DISTINCT, ORDER BY, LIMIT/OFFSET
11–20Фильтрация и сортировкаAND/OR/NOT, IN, BETWEEN, LIKE, IS NULL, ORDER BY ASC/DESC, LIMIT/FETCH, OFFSET, CASE WHEN, NULLS FIRST/LAST
21–30Агрегация и GROUP BYCOUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING, ROLLUP, CUBE, GROUPING SETS, FILTER
31–40JOINsINNER, LEFT/RIGHT/FULL OUTER, CROSS, self-JOIN, NATURAL, LATERAL, anti-join, semi-join, множественные JOIN
41–50Подзапросы (subqueries)скалярный, коррелированный, EXISTS/NOT EXISTS, IN/ANY/ALL, производные таблицы, CTE, рекурсивный CTE, JOIN vs subquery
51–60Оконные функции (Window Functions)ROW_NUMBER, RANK/DENSE_RANK, NTILE, LEAD/LAG, FIRST_VALUE/LAST_VALUE, агрегаты OVER, PARTITION BY, фреймы ROWS/RANGE
61–70Индексы и производительностьCREATE INDEX, B-tree, UNIQUE, composite, partial, covering, EXPLAIN ANALYZE, Index Scan vs Seq Scan, BRIN/GIN/GiST, REINDEX
71–80Constraints и целостностьPRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK, DEFAULT, EXCLUDE, DEFERRABLE, ON DELETE CASCADE/SET NULL, проверка ограничений
81–90Транзакции и блокировкиBEGIN/COMMIT/ROLLBACK, SAVEPOINT, уровни изоляции, SERIALIZABLE, дедлоки, FOR UPDATE/SHARE, NOWAIT/SKIP LOCKED, advisory locks, MVCC
91–100Продвинутый SQLUPSERT (ON CONFLICT), MERGE, RETURNING, GENERATED, массивы/JSON, рекурсивные CTE, полнотекстовый поиск, партиционирование, наследование, NOTIFY/LISTEN

Вопросы и ответы

ВопросКод с комментариями
1Как создать таблицу в SQL?
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    salary NUMERIC(10, 2),
    hire_date DATE DEFAULT CURRENT_DATE
);
2Как вставить данные в таблицу?
INSERT INTO employees (name, salary, hire_date) VALUES
('Иван Петров', 75000.00, '2023-01-15'),
('Мария Смирнова', 82000.00, '2023-03-20'),
('Алексей Иванов', 65000.00, '2023-06-10');
3Как выполнить простой SELECT?
SELECT * FROM employees;
4Как использовать WHERE для фильтрации?
SELECT name, salary
FROM employees
WHERE salary > 70000;
5Как обновить существующие записи?
UPDATE employees
SET salary = salary * 1.1
WHERE name = 'Иван Петров';
6Как удалить записи из таблицы?
DELETE FROM employees
WHERE id = 3;
7Как изменить структуру таблицы?
ALTER TABLE employees
ADD COLUMN department VARCHAR(50);

ALTER TABLE employees
RENAME COLUMN department TO dept;

8Как удалить таблицу?
DROP TABLE IF EXISTS employees;
9Как очистить таблицу без удаления?
TRUNCATE TABLE employees;
10Как выбрать уникальные значения и отсортировать?
SELECT DISTINCT dept
FROM employees
ORDER BY dept
LIMIT 5

-- или с OFFSET:
-- LIMIT 5 OFFSET 0;

11Как комбинировать условия WHERE с AND/OR/NOT?
SELECT *
FROM employees
WHERE salary BETWEEN 50000 AND 100000
  AND (dept = 'IT' OR dept = 'Sales')
  AND NOT name = 'John';
12Как использовать IN для проверки множества значений?
SELECT name, dept, salary
FROM employees
WHERE dept IN ('HR', 'IT', 'Finance');
13Как отфильтровать по диапазону с BETWEEN?
SELECT name, hire_date
FROM employees
WHERE hire_date BETWEEN '2023-01-01' AND '2023-03-31';
14Как использовать LIKE для поиска по шаблону?
SELECT name
FROM employees
WHERE name LIKE 'А%';
15Как найти NULL-значения?
SELECT name, dept
FROM employees
WHERE dept IS NULL;
16Как сортировать по убыванию и возрастанию?
SELECT name, salary
FROM employees
ORDER BY salary DESC, name ASC;
17Как ограничить количество записей (LIMIT / FETCH)?
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;

-- или с FETCH:
-- FETCH FIRST 3 ROWS ONLY;

18Как пропустить записи (OFFSET)?
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 5 OFFSET 5;
19Как использовать CASE WHEN для условной логики?
SELECT name, salary,
    CASE
        WHEN salary < 50000 THEN 'Низкая'
        WHEN salary BETWEEN 50000 AND 100000 THEN 'Средняя'
        ELSE 'Высокая'
    END AS category
FROM employees;
20Как управлять порядком NULL-значений?
SELECT name, dept
FROM employees
ORDER BY dept NULLS FIRST;

-- или NULLS LAST (по умолчанию для ASC):

21Как посчитать количество записей?
SELECT
    COUNT(*) AS total,
    COUNT(dept) AS with_dept
FROM employees;
22Как вычислить сумму, среднее, минимум и максимум?
SELECT
    SUM(salary) AS total_salary,
    ROUND(AVG(salary), 2) AS avg_salary,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary
FROM employees;
23Как сгруппировать данные с GROUP BY?
SELECT dept, ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY dept;
24Как отфильтровать группы с HAVING?
SELECT dept, ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY dept
HAVING AVG(salary) > 80000;
25Как использовать ROLLUP для иерархической агрегации?
SELECT dept, SUM(salary) AS total
FROM employees
GROUP BY ROLLUP(dept)
ORDER BY dept;
26Как использовать CUBE для многомерной агрегации?
SELECT dept, position, SUM(salary) AS total
FROM employees
GROUP BY CUBE(dept, position)
ORDER BY dept, position;
27Как использовать GROUPING SETS?
SELECT dept, position, SUM(salary) AS total
FROM employees
GROUP BY GROUPING SETS ((dept), (position))
ORDER BY dept, position;
28Как агрегировать только часть строк с FILTER?
SELECT
    COUNT(*) FILTER (WHERE salary > 70000) AS high_earners,
    ROUND(AVG(salary) FILTER (WHERE dept = 'IT'), 2) AS avg_it_salary
FROM employees;
29Как использовать GROUP BY с несколькими столбцами?
SELECT dept, position, COUNT(*) AS cnt
FROM employees
GROUP BY dept, position
ORDER BY dept, position;
30Как работает порядок выполнения SQL-запроса?
SELECT dept, COUNT(*) AS cnt
FROM employees
WHERE salary > 40000
GROUP BY dept
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 5;
31Как выполнить INNER JOIN двух таблиц?
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;
32Как выполнить LEFT JOIN?
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
33Как выполнить RIGHT JOIN?
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;
34Как выполнить FULL OUTER JOIN?
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.id;
35Как сделать CROSS JOIN?
SELECT e.name, p.project_name
FROM employees e
CROSS JOIN projects p;
36Как выполнить self-JOIN?
SELECT a.name AS employee1, b.name AS employee2, a.dept
FROM employees a
INNER JOIN employees b ON a.dept = b.dept AND a.id < b.id;
37Как работает NATURAL JOIN?
SELECT *
FROM employees
NATURAL JOIN departments;
38Как использовать LATERAL JOIN?
SELECT d.dept_name, e.name, e.salary
FROM departments d
CROSS JOIN LATERAL (
    SELECT name, salary
    FROM employees
    WHERE dept_id = d.id
    ORDER BY salary DESC
    LIMIT 3
) e;
39Как реализовать anti-join с NOT EXISTS?
SELECT e.name
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM departments d WHERE d.id = e.dept_id
);
40Как реализовать semi-join с EXISTS?
SELECT d.dept_name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.id
);
41Как объединить несколько таблиц и оптимизировать JOIN?
CREATE INDEX idx_emp_dept ON employees(dept_id);
CREATE INDEX idx_proj_lead ON projects(lead_id);

SELECT e.name, d.dept_name, p.project_name, m.name AS manager
FROM employees e
JOIN departments d ON e.dept_id = d.id
JOIN projects p ON p.team_id = d.id
LEFT JOIN employees m ON e.manager_id = m.id;

42Что такое скалярный подзапрос?
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
43Что такое коррелированный подзапрос?
SELECT e.name, e.salary, e.dept
FROM employees e
WHERE e.salary > (
    SELECT AVG(salary) FROM employees WHERE dept = e.dept
);
44Как использовать EXISTS / NOT EXISTS?
SELECT d.dept_name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e
    WHERE e.dept_id = d.id AND e.age < 25
);
45Как использовать IN с подзапросом?
SELECT name
FROM employees
WHERE id IN (
    SELECT employee_id FROM project_assignments
    WHERE project_id IN (SELECT id FROM projects WHERE budget > 100000)
);
46Как использовать ANY и ALL?
SELECT name, salary
FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE dept = 'HR')
  AND salary > ALL (SELECT salary FROM employees WHERE dept = 'Sales');
47Как использовать подзапрос в FROM (производная таблица)?
SELECT dept, max_sal
FROM (
    SELECT dept, MAX(salary) AS max_sal
    FROM employees
    GROUP BY dept
) AS dept_stats
WHERE max_sal > 90000;
48Как использовать подзапрос в SELECT?
SELECT name,
    (SELECT COUNT(*) FROM employees WHERE dept = e.dept) AS dept_count
FROM employees e;
49Что такое CTE (WITH)?
WITH dept_avg AS (
    SELECT dept, AVG(salary) AS avg_sal
    FROM employees
    GROUP BY dept
)
SELECT dept, avg_sal
FROM dept_avg
WHERE avg_sal > (SELECT AVG(salary) FROM employees);
50Что такое рекурсивный CTE?
WITH RECURSIVE numbers AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM numbers WHERE n < 10
)
SELECT * FROM numbers;
51Когда лучше использовать подзапрос, а когда JOIN?
-- Через JOIN (обычно эффективнее)
SELECT e.name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.dept_name = 'IT';

-- Через подзапрос
SELECT name
FROM employees
WHERE dept_id = (SELECT id FROM departments WHERE dept_name = 'IT');

52Как работает ROW_NUMBER()?
SELECT name, dept, salary,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees;
53Как использовать RANK() и DENSE_RANK()?
SELECT name, salary,
    RANK() OVER (ORDER BY salary DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;
54Как работает NTILE()?
SELECT name, salary,
    NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
55Как использовать LEAD() и LAG()?
SELECT name, salary,
    LAG(salary) OVER (ORDER BY salary) AS prev_salary,
    LEAD(salary) OVER (ORDER BY salary) AS next_salary
FROM employees;
56Как использовать FIRST_VALUE() и LAST_VALUE()?
SELECT name, dept, salary,
    FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS highest_paid,
    LAST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest_paid
FROM employees;
57Как использовать агрегатные функции с OVER?
SELECT name, dept, salary,
    AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
    SUM(salary) OVER () AS total_salary
FROM employees;
58Как работает PARTITION BY в оконных функциях?
SELECT name, dept, salary,
    SUM(salary) OVER (PARTITION BY dept ORDER BY salary
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM employees;
59Как работает ORDER BY внутри оконной функции?
SELECT name, dept, hire_date,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY hire_date) AS hire_seq
FROM employees;
60Как работают ROWS / RANGE / GROUPS фреймы?
SELECT name, salary,
    AVG(salary) OVER (ORDER BY salary
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg
FROM employees;
61Как использовать оконные функции для фильтрации?
SELECT name, dept, salary
FROM (
    SELECT name, dept, salary,
        ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
    FROM employees
) ranked
WHERE rn <= 2;
62Как создать индекс?
CREATE INDEX idx_employees_email ON employees(email);
63Что такое B-tree индекс и когда его использовать?
-- B-tree используется по умолчанию, подходит для:
-- равенства (=), диапазонов (<, >, BETWEEN), сортировки (ORDER BY)
CREATE INDEX idx_emp_hire_date ON employees(hire_date);

-- Проверка плана выполнения:
EXPLAIN ANALYZE SELECT * FROM employees WHERE hire_date > '2023-01-01';

64Что такое UNIQUE индекс?
CREATE UNIQUE INDEX idx_employees_email_unique ON employees(email);

-- Попытка вставить дубликат вызовет ошибку:
-- INSERT INTO employees (email) VALUES ('ivan@example.com'), ('ivan@example.com');

65Что такое составной (composite) индекс?
CREATE INDEX idx_emp_dept_salary ON employees(dept, salary);

-- Этот индекс ускорит запрос:
EXPLAIN ANALYZE
SELECT * FROM employees
WHERE dept = 'IT' AND salary > 70000;

66Что такое частичный (partial) индекс?
CREATE INDEX idx_emp_active ON employees(hire_date)
WHERE status = 'active';

-- Ускоряет запросы, фильтрующие активных сотрудников:
SELECT * FROM employees
WHERE status = 'active' AND hire_date < '2022-01-01';

67Что такое покрывающий (covering) индекс?
-- Включаем дополнительный столбец в INCLUDE (только для PostgreSQL)
CREATE INDEX idx_emp_email_cover ON employees(email) INCLUDE (name);

-- Запрос будет выполнен только по индексу (Index Only Scan):
EXPLAIN ANALYZE
SELECT name FROM employees WHERE email = 'ivan@example.com';

68Как использовать EXPLAIN ANALYZE?
EXPLAIN ANALYZE
SELECT name, salary
FROM employees
WHERE salary BETWEEN 60000 AND 80000
ORDER BY salary;
69В чём разница между index scan и sequential scan?
-- Seq Scan (полное сканирование) — когда выбрано много строк
EXPLAIN ANALYZE SELECT * FROM employees;

-- Index Scan — когда выбрано мало строк
EXPLAIN ANALYZE SELECT * FROM employees WHERE id = 42;

-- Bitmap Index Scan — средний случай
EXPLAIN ANALYZE SELECT * FROM employees WHERE salary > 90000;

70Когда использовать BRIN, GIN, GiST индексы?
-- BRIN: для больших таблиц с естественно упорядоченными данными
CREATE INDEX idx_emp_hire_brin ON employees USING BRIN(hire_date);

-- GIN: для массивов и полнотекстового поиска
CREATE INDEX idx_emp_skills_gin ON employees USING GIN(skills);

-- GiST: для геоданных и полнотекстового поиска
CREATE INDEX idx_locations_gist ON locations USING GIST(coord);

71Как обслуживать индексы (реиндексация, анализ)?
-- Сбор статистики для оптимизатора
ANALYZE employees;

-- Перестроение индекса (Pg 12+)
REINDEX INDEX idx_employees_email;

-- Проверка размера индексов
SELECT relname, pg_size_pretty(pg_relation_size(oid))
FROM pg_class
WHERE relkind = 'i'
ORDER BY pg_relation_size(oid) DESC;

72Что такое PRIMARY KEY?
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price NUMERIC(10, 2)
);
73Что такое FOREIGN KEY?
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
order_date DATE DEFAULT CURRENT_DATE,
amount NUMERIC(10, 2)
);

74Что такое UNIQUE constraint?
ALTER TABLE employees
ADD CONSTRAINT uq_employees_email UNIQUE (email);
75Что такое NOT NULL constraint?
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    body TEXT NOT NULL,
    published_at TIMESTAMP
);
76Что такое CHECK constraint?
ALTER TABLE employees
ADD CONSTRAINT chk_positive_salary CHECK (salary > 0),
ADD CONSTRAINT chk_adult_age CHECK (age >= 18);
77Что такое DEFAULT constraint?
CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    status VARCHAR(20) DEFAULT 'new',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    priority INTEGER DEFAULT 5
);
78Что такое EXCLUDE constraint?
CREATE TABLE bookings (
    room_id INTEGER,
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    booked_by VARCHAR(100),
    EXCLUDE USING gist (
        room_id WITH =,
        tstzrange(start_time, end_time) WITH &&
    )
);
79Что такое DEFERRABLE constraints?
CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE transactions (
id SERIAL PRIMARY KEY,
from_account INTEGER REFERENCES accounts(id) DEFERRABLE INITIALLY DEFERRED,
to_account INTEGER REFERENCES accounts(id) DEFERRABLE INITIALLY DEFERRED,
amount NUMERIC(10, 2)
);

-- Вставка без предварительного создания счетов возможна в транзакции:
BEGIN;
INSERT INTO transactions (from_account, to_account, amount) VALUES (1, 2, 100.00);
INSERT INTO accounts (id, name) VALUES (1, 'Alice'), (2, 'Bob');
COMMIT;

80Как работают ON DELETE CASCADE / SET NULL?
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
created_by INTEGER REFERENCES users(id) ON DELETE CASCADE
);

-- При удалении категории: category_id = NULL
-- При удалении пользователя: товары удалятся автоматически

81Как проверить, что все ограничения выполняются?
-- Отключение (требует SUPERUSER)
ALTER TABLE orders DISABLE TRIGGER ALL;

-- Загрузка данных ...
INSERT INTO orders (customer_id) VALUES (999); -- нарушает FK

-- Включение и проверка
ALTER TABLE orders ENABLE TRIGGER ALL;

-- Поиск нарушений
SELECT * FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

82Как начать и завершить транзакцию?
BEGIN;

UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;

COMMIT;
-- При ошибке: ROLLBACK;

83Как использовать SAVEPOINT?
BEGIN;

INSERT INTO logs (message) VALUES ('Шаг 1 выполнен');
SAVEPOINT sp1;

INSERT INTO logs (message) VALUES ('Шаг 2 выполнен');
-- Если шаг 2 ошибочный:
ROLLBACK TO SAVEPOINT sp1;

INSERT INTO logs (message) VALUES ('Шаг 2 (исправленный)');
COMMIT;

84Какие уровни изоляции транзакций существуют?
-- Сессия 1 (READ COMMITTED — по умолчанию)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;  -- видит 1000
-- (в сессии 2 обновляют balance = 900 и коммитят)
SELECT balance FROM accounts WHERE id = 1;  -- видит 900 (неповторяющееся чтение)
COMMIT;

-- Сессия 1 (REPEATABLE READ)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- видит 1000
-- (в сессии 2 обновляют balance = 900 и коммитят)
SELECT balance FROM accounts WHERE id = 1; -- всё ещё 1000 (снапшот)
COMMIT;

85Что такое SERIALIZABLE уровень изоляции?
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Если две сессии одновременно выполняют этот код,
-- одна из них получит ошибку сериализации:
UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO order_items (order_id, product_id) VALUES (1, 10);

COMMIT;
-- При ошибке: повторить транзакцию

86Как возникают и обрабатываются дедлоки?
-- Сессия 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Сессия 2 делает:
-- UPDATE accounts SET balance = balance - 200 WHERE id = 2;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Если сессии обновляют ресурсы в разном порядке, возникает deadlock,
-- PostgreSQL автоматически завершит одну из транзакций с ошибкой
87Как использовать блокировки строк FOR UPDATE / FOR SHARE?
BEGIN;

-- FOR UPDATE — эксклюзивная блокировка (другие не могут читать/писать)
SELECT * FROM employees WHERE id = 10 FOR UPDATE;

UPDATE employees SET salary = salary * 1.1 WHERE id = 10;

COMMIT;

-- FOR SHARE — разделяемая блокировка (другие могут читать, но не изменять)
SELECT * FROM employees WHERE dept = 'IT' FOR SHARE;

88Как использовать NOWAIT и SKIP LOCKED?
-- SKIP LOCKED: взять только свободные строки (очередь задач)
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY priority DESC
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- NOWAIT: завершиться с ошибкой, если строка заблокирована
SELECT * FROM employees WHERE id = 5 FOR UPDATE NOWAIT;

89Что такое advisory locks?
-- Сессия 1
SELECT pg_advisory_lock(12345);

-- Выполнение критической секции
UPDATE employees SET salary = salary * 1.1;

-- Сессия 2 (ждёт)
SELECT pg_advisory_lock(12345); -- блокируется, пока сессия 1 не отпустит

-- Сессия 1
SELECT pg_advisory_unlock(12345);

-- Альтернатива: pg_try_advisory_lock (неблокирующая)

90Как работает MVCC в PostgreSQL?
-- PostgreSQL не перезаписывает строки, а создаёт новые версии.
-- Проверка mvp (минимальный xmin):
SELECT xmin, xmax, ctid, name, salary
FROM employees
ORDER BY ctid;

-- Количество "мёртвых" кортежей (нуждаются в VACUUM):
SELECT n_dead_tup, n_live_tup, relname
FROM pg_stat_user_tables
WHERE relname = 'employees';

91Как использовать INSERT ... ON CONFLICT (UPSERT)?
INSERT INTO employees (email, name, salary)
VALUES ('ivan@example.com', 'Иван Петров', 80000.00)
ON CONFLICT (email)
DO UPDATE SET
    name = EXCLUDED.name,
    salary = EXCLUDED.salary;
92Как использовать MERGE (UPSERT в SQL:2016)?
MERGE INTO employees AS target
USING employees_staging AS source
ON target.email = source.email
WHEN MATCHED THEN
    UPDATE SET salary = source.salary, name = source.name
WHEN NOT MATCHED THEN
    INSERT (email, name, salary)
    VALUES (source.email, source.name, source.salary);
93Как использовать RETURNING?
INSERT INTO employees (name, salary, dept)
VALUES ('Ольга Новикова', 72000.00, 'IT')
RETURNING id, name, salary, created_at;

-- Также работает с UPDATE и DELETE:
UPDATE employees SET salary = salary * 1.1 WHERE dept = 'IT'
RETURNING id, name, salary AS new_salary;

94Что такое GENERATED столбцы?
CREATE TABLE invoices (
    id SERIAL PRIMARY KEY,
    quantity INTEGER NOT NULL,
    unit_price NUMERIC(10, 2) NOT NULL,
    total NUMERIC(10, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

INSERT INTO invoices (quantity, unit_price) VALUES (3, 150.00);
SELECT * FROM invoices; -- total = 450.00

95Как работать с массивами и JSON в PostgreSQL?
-- Массивы
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    skills TEXT[]
);

INSERT INTO employees (name, skills) VALUES
('Иван', ARRAY['Python', 'SQL', 'Docker']),
('Мария', ARRAY['Java', 'SQL', 'Kubernetes']);

SELECT name FROM employees WHERE 'SQL' = ANY(skills);
SELECT name FROM employees WHERE skills @> ARRAY['Python', 'Docker'];

-- JSONB
CREATE TABLE events (
id SERIAL PRIMARY KEY,
payload JSONB
);

INSERT INTO events (payload) VALUES
('{"user_id": 42, "tags": ["premium", "vip"]}');

SELECT payload->>'user_id' AS uid FROM events;
SELECT * FROM events WHERE payload @> '{"tags": ["vip"]}';
CREATE INDEX idx_events_payload ON events USING GIN(payload);

96Как сделать рекурсивный CTE для дерева?
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    parent_id INTEGER REFERENCES categories(id)
);

INSERT INTO categories VALUES
(1, 'Электроника', NULL),
(2, 'Компьютеры', 1),
(3, 'Ноутбуки', 2),
(4, 'Планшеты', 2),
(5, 'Телефоны', 1);

WITH RECURSIVE cat_tree AS (
SELECT id, name, parent_id, 1 AS level, name::TEXT AS path
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1,
ct.path || ' -> ' || c.name
FROM categories c
JOIN cat_tree ct ON c.parent_id = ct.id
)
SELECT * FROM cat_tree ORDER BY path;

97Как использовать полнотекстовый поиск (tsvector/tsquery)?
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT,
    body TEXT,
    search_vector TSVECTOR
);

-- Заполнение tsvector
UPDATE articles SET search_vector =
to_tsvector('russian', title || ' ' || body);

-- Индекс GIN для ускорения
CREATE INDEX idx_articles_fts ON articles USING GIN(search_vector);

-- Поиск
SELECT title
FROM articles
WHERE search_vector @@ to_tsquery('russian', 'база & данных');

-- Триггер для автоматического обновления
CREATE FUNCTION update_search_vector() RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('russian', NEW.title || ' ' || NEW.body);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_articles_fts
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();

98Как использовать партиционирование таблиц?
CREATE TABLE logs (
    id SERIAL,
    level TEXT,
    message TEXT,
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

CREATE TABLE logs_2024_q1 PARTITION OF logs
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE logs_2024_q2 PARTITION OF logs
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE logs_2024_q3 PARTITION OF logs
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
CREATE TABLE logs_2024_q4 PARTITION OF logs
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');

-- Запрос автоматически использует только нужные партиции
SELECT * FROM logs WHERE created_at BETWEEN '2024-02-01' AND '2024-02-28';

99Как использовать наследование таблиц (table inheritance)?
CREATE TABLE vehicles (
    id SERIAL PRIMARY KEY,
    brand VARCHAR(100),
    model VARCHAR(100),
    year INTEGER
);

CREATE TABLE cars (
doors INTEGER,
fuel_type VARCHAR(20)
) INHERITS (vehicles);

CREATE TABLE motorcycles (
has_sidecar BOOLEAN DEFAULT FALSE
) INHERITS (vehicles);

INSERT INTO cars (brand, model, year, doors, fuel_type) VALUES
('Toyota', 'Camry', 2023, 4, 'petrol');

INSERT INTO motorcycles (brand, model, year, has_sidecar) VALUES
('Harley', 'Sportster', 2022, FALSE);

-- Запрос к родительской таблице включает и потомков
SELECT * FROM vehicles; -- покажет и cars, и motorcycles

100Как использовать NOTIFY и LISTEN?
-- Создание функции-триггера для уведомлений
CREATE FUNCTION notify_new_employee() RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify(
        'new_employee',
        json_build_object('id', NEW.id, 'name', NEW.name)::TEXT
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_new_employee
AFTER INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION notify_new_employee();

-- В приложении (или psql):
LISTEN new_employee;

-- При вставке:
INSERT INTO employees (name, salary) VALUES ('Новый Сотрудник', 60000);
-- В консоли появится: Asynchronous notification "new_employee" ...


Практические задания

Практическое заданиеКоды
1Создайте таблицу employees с полями id, name, salary, hire_date.
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    salary NUMERIC(10, 2),
    hire_date DATE DEFAULT CURRENT_DATE
);
2Вставьте три записи в таблицу employees.
INSERT INTO employees (name, salary, hire_date) VALUES
('Иван Петров', 75000.00, '2023-01-15'),
('Мария Смирнова', 82000.00, '2023-03-20'),
('Алексей Иванов', 65000.00, '2023-06-10');
3Выберите все строки и столбцы из таблицы employees.
SELECT * FROM employees;
4Выберите сотрудников с зарплатой больше 70000.
SELECT name, salary
FROM employees
WHERE salary > 70000;
5Увеличьте зарплату сотрудника 'Иван Петров' на 10%.
UPDATE employees
SET salary = salary * 1.1
WHERE name = 'Иван Петров';
6Удалите сотрудника с id = 3.
DELETE FROM employees
WHERE id = 3;
7Добавьте столбец department в таблицу employees, затем переименуйте его в dept.
ALTER TABLE employees
ADD COLUMN department VARCHAR(50);

ALTER TABLE employees
RENAME COLUMN department TO dept;

8Удалите таблицу employees (если она существует).
DROP TABLE IF EXISTS employees;
9Удалите все строки из таблицы employees, сохранив её структуру.
TRUNCATE TABLE employees;
10Выберите уникальные отделы из employees, отсортированные по алфавиту, и ограничьте вывод 5 записями.
SELECT DISTINCT dept
FROM employees
ORDER BY dept
LIMIT 5

-- или с OFFSET:
-- LIMIT 5 OFFSET 0;

11Найдите сотрудников с зарплатой от 50000 до 100000 из отделов 'IT' или 'Sales', исключая 'John'.
SELECT *
FROM employees
WHERE salary BETWEEN 50000 AND 100000
  AND (dept = 'IT' OR dept = 'Sales')
  AND NOT name = 'John';
12Найдите сотрудников из отделов 'HR', 'IT' и 'Finance'.
SELECT name, dept, salary
FROM employees
WHERE dept IN ('HR', 'IT', 'Finance');
13Найдите сотрудников, нанятых в первом квартале 2023 года.
SELECT name, hire_date
FROM employees
WHERE hire_date BETWEEN '2023-01-01' AND '2023-03-31';
14Найдите сотрудников, чьи имена начинаются с 'А'.
SELECT name
FROM employees
WHERE name LIKE 'А%';
15Найдите сотрудников, у которых не указан отдел.
SELECT name, dept
FROM employees
WHERE dept IS NULL;
16Выведите сотрудников, отсортированных по зарплате по убыванию, затем по имени по возрастанию.
SELECT name, salary
FROM employees
ORDER BY salary DESC, name ASC;
17Выведите трёх самых высокооплачиваемых сотрудников.
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;

-- или с FETCH:
-- FETCH FIRST 3 ROWS ONLY;

18Выведите сотрудников с 6-й по 10-ю позицию в рейтинге зарплат.
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 5 OFFSET 5;
19Категоризируйте зарплаты: низкая (<50000), средняя (50000-100000), высокая (>100000).
SELECT name, salary,
    CASE
        WHEN salary < 50000 THEN 'Низкая'
        WHEN salary BETWEEN 50000 AND 100000 THEN 'Средняя'
        ELSE 'Высокая'
    END AS category
FROM employees;
20Выведите сотрудников, отсортированных по отделу, поместив NULL в начало списка.
SELECT name, dept
FROM employees
ORDER BY dept NULLS FIRST;

-- или NULLS LAST (по умолчанию для ASC):

21Посчитайте общее количество сотрудников и количество сотрудников с указанным отделом.
SELECT
    COUNT(*) AS total,
    COUNT(dept) AS with_dept
FROM employees;
22Найдите суммарную, среднюю, минимальную и максимальную зарплату.
SELECT
    SUM(salary) AS total_salary,
    ROUND(AVG(salary), 2) AS avg_salary,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary
FROM employees;
23Найдите среднюю зарплату по каждому отделу.
SELECT dept, ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY dept;
24Найдите отделы, где средняя зарплата превышает 80000.
SELECT dept, ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY dept
HAVING AVG(salary) > 80000;
25Вычислите сумму зарплат по отделам и общий итог.
SELECT dept, SUM(salary) AS total
FROM employees
GROUP BY ROLLUP(dept)
ORDER BY dept;
26Вычислите сумму зарплат по всем комбинациям отдела и должности.
SELECT dept, position, SUM(salary) AS total
FROM employees
GROUP BY CUBE(dept, position)
ORDER BY dept, position;
27Получите итоги отдельно по отделам и отдельно по должностям.
SELECT dept, position, SUM(salary) AS total
FROM employees
GROUP BY GROUPING SETS ((dept), (position))
ORDER BY dept, position;
28Посчитайте количество сотрудников с зарплатой выше 70000 и среднюю зарплату в IT-отделе.
SELECT
    COUNT(*) FILTER (WHERE salary > 70000) AS high_earners,
    ROUND(AVG(salary) FILTER (WHERE dept = 'IT'), 2) AS avg_it_salary
FROM employees;
29Сгруппируйте сотрудников по отделу и должности, подсчитав количество в каждой группе.
SELECT dept, position, COUNT(*) AS cnt
FROM employees
GROUP BY dept, position
ORDER BY dept, position;
30Напишите запрос, демонстрирующий порядок: FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT.
SELECT dept, COUNT(*) AS cnt
FROM employees
WHERE salary > 40000
GROUP BY dept
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 5;
31Объедините таблицы employees и departments, чтобы получить название отдела для каждого сотрудника.
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;
32Выведите всех сотрудников и их отделы, включая тех, у кого отдел не указан.
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
33Выведите все отделы и сотрудников в них, включая отделы без сотрудников.
SELECT e.name, d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;
34Выведите всех сотрудников и все отделы, показывая несоответствия с обеих сторон.
SELECT e.name, d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.id;
35Создайте декартово произведение сотрудников и проектов для назначения.
SELECT e.name, p.project_name
FROM employees e
CROSS JOIN projects p;
36Найдите пары сотрудников, работающих в одном отделе (исключая пару с самим собой).
SELECT a.name AS employee1, b.name AS employee2, a.dept
FROM employees a
INNER JOIN employees b ON a.dept = b.dept AND a.id < b.id;
37Объедините таблицы employees и departments по одноимённым столбцам (dept_id).
SELECT *
FROM employees
NATURAL JOIN departments;
38Для каждого отдела найдите трёх самых высокооплачиваемых сотрудников.
SELECT d.dept_name, e.name, e.salary
FROM departments d
CROSS JOIN LATERAL (
    SELECT name, salary
    FROM employees
    WHERE dept_id = d.id
    ORDER BY salary DESC
    LIMIT 3
) e;
39Найдите сотрудников, не привязанных ни к одному отделу.
SELECT e.name
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM departments d WHERE d.id = e.dept_id
);
40Найдите отделы, в которых есть хотя бы один сотрудник.
SELECT d.dept_name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.id
);
41Найдите сотрудников, их отделы, проекты и менеджеров, добавив индексы для ускорения.
CREATE INDEX idx_emp_dept ON employees(dept_id);
CREATE INDEX idx_proj_lead ON projects(lead_id);

SELECT e.name, d.dept_name, p.project_name, m.name AS manager
FROM employees e
JOIN departments d ON e.dept_id = d.id
JOIN projects p ON p.team_id = d.id
LEFT JOIN employees m ON e.manager_id = m.id;

42Найдите сотрудников с зарплатой выше средней по компании.
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
43Найдите сотрудников, чья зарплата выше средней в их отделе.
SELECT e.name, e.salary, e.dept
FROM employees e
WHERE e.salary > (
    SELECT AVG(salary) FROM employees WHERE dept = e.dept
);
44Найдите отделы, где нет сотрудников младше 25 лет.
SELECT d.dept_name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e
    WHERE e.dept_id = d.id AND e.age < 25
);
45Найдите сотрудников, работающих в проектах с бюджетом более 100000.
SELECT name
FROM employees
WHERE id IN (
    SELECT employee_id FROM project_assignments
    WHERE project_id IN (SELECT id FROM projects WHERE budget > 100000)
);
46Найдите сотрудников, чья зарплата больше зарплаты любого (ANY) сотрудника из 'HR', и тех, кто получает больше всех (ALL) в 'Sales'.
SELECT name, salary
FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE dept = 'HR')
  AND salary > ALL (SELECT salary FROM employees WHERE dept = 'Sales');
47Найдите отделы с максимальной зарплатой выше 90000, используя производную таблицу.
SELECT dept, max_sal
FROM (
    SELECT dept, MAX(salary) AS max_sal
    FROM employees
    GROUP BY dept
) AS dept_stats
WHERE max_sal > 90000;
48Выведите имя сотрудника и количество сотрудников в его отделе.
SELECT name,
    (SELECT COUNT(*) FROM employees WHERE dept = e.dept) AS dept_count
FROM employees e;
49Найдите отделы со средней зарплатой выше общей средней, используя CTE.
WITH dept_avg AS (
    SELECT dept, AVG(salary) AS avg_sal
    FROM employees
    GROUP BY dept
)
SELECT dept, avg_sal
FROM dept_avg
WHERE avg_sal > (SELECT AVG(salary) FROM employees);
50Сгенерируйте последовательность чисел от 1 до 10.
WITH RECURSIVE numbers AS (
    SELECT 1 AS n
    UNION ALL
    SELECT n + 1 FROM numbers WHERE n < 10
)
SELECT * FROM numbers;
51Сравните два подхода: найдите сотрудников, работающих в отделе 'IT'.
-- Через JOIN (обычно эффективнее)
SELECT e.name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.dept_name = 'IT';

-- Через подзапрос
SELECT name
FROM employees
WHERE dept_id = (SELECT id FROM departments WHERE dept_name = 'IT');

52Пронумеруйте сотрудников в каждом отделе по убыванию зарплаты.
SELECT name, dept, salary,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees;
53Присвойте ранги сотрудникам по зарплате, показав разницу между RANK и DENSE_RANK.
SELECT name, salary,
    RANK() OVER (ORDER BY salary DESC) AS rnk,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;
54Разделите сотрудников на 4 квартиля по зарплате.
SELECT name, salary,
    NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
55Для каждого сотрудника выведите зарплату предыдущего и следующего по размеру зарплаты.
SELECT name, salary,
    LAG(salary) OVER (ORDER BY salary) AS prev_salary,
    LEAD(salary) OVER (ORDER BY salary) AS next_salary
FROM employees;
56В каждом отделе покажите самого высоко- и низкооплачиваемого сотрудника.
SELECT name, dept, salary,
    FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS highest_paid,
    LAST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest_paid
FROM employees;
57Выведите зарплату сотрудника вместе со средним значением по отделу и общей суммой.
SELECT name, dept, salary,
    AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
    SUM(salary) OVER () AS total_salary
FROM employees;
58Для каждого отдела подсчитайте нарастающий итог зарплат (по возрастанию зарплаты).
SELECT name, dept, salary,
    SUM(salary) OVER (PARTITION BY dept ORDER BY salary
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM employees;
59Выведите сотрудников, отсортированных по дате найма, с порядковым номером внутри отдела.
SELECT name, dept, hire_date,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY hire_date) AS hire_seq
FROM employees;
60Вычислите скользящую среднюю зарплату за 3 соседних записи (текущая + предыдущая + следующая).
SELECT name, salary,
    AVG(salary) OVER (ORDER BY salary
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg
FROM employees;
61Найдите двух самых высокооплачиваемых сотрудников в каждом отделе.
SELECT name, dept, salary
FROM (
    SELECT name, dept, salary,
        ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
    FROM employees
) ranked
WHERE rn <= 2;
62Создайте B-tree индекс на столбце email для ускорения поиска.
CREATE INDEX idx_employees_email ON employees(email);
63Создайте B-tree индекс для поддержки сортировки по дате найма.
-- B-tree используется по умолчанию, подходит для:
-- равенства (=), диапазонов (<, >, BETWEEN), сортировки (ORDER BY)
CREATE INDEX idx_emp_hire_date ON employees(hire_date);

-- Проверка плана выполнения:
EXPLAIN ANALYZE SELECT * FROM employees WHERE hire_date > '2023-01-01';

64Создайте уникальный индекс на email, чтобы гарантировать отсутствие дубликатов.
CREATE UNIQUE INDEX idx_employees_email_unique ON employees(email);

-- Попытка вставить дубликат вызовет ошибку:
-- INSERT INTO employees (email) VALUES ('ivan@example.com'), ('ivan@example.com');

65Создайте индекс для запросов, фильтрующих по отделу и зарплате.
CREATE INDEX idx_emp_dept_salary ON employees(dept, salary);

-- Этот индекс ускорит запрос:
EXPLAIN ANALYZE
SELECT * FROM employees
WHERE dept = 'IT' AND salary > 70000;

66Создайте индекс только для активных сотрудников (status = 'active').
CREATE INDEX idx_emp_active ON employees(hire_date)
WHERE status = 'active';

-- Ускоряет запросы, фильтрующие активных сотрудников:
SELECT * FROM employees
WHERE status = 'active' AND hire_date < '2022-01-01';

67Создайте индекс, покрывающий запрос поиска по email с возвратом имени.
-- Включаем дополнительный столбец в INCLUDE (только для PostgreSQL)
CREATE INDEX idx_emp_email_cover ON employees(email) INCLUDE (name);

-- Запрос будет выполнен только по индексу (Index Only Scan):
EXPLAIN ANALYZE
SELECT name FROM employees WHERE email = 'ivan@example.com';

68Проанализируйте план выполнения запроса с поиском по диапазону зарплат.
EXPLAIN ANALYZE
SELECT name, salary
FROM employees
WHERE salary BETWEEN 60000 AND 80000
ORDER BY salary;
69Сравните планы выполнения для разных объёмов данных.
-- Seq Scan (полное сканирование) — когда выбрано много строк
EXPLAIN ANALYZE SELECT * FROM employees;

-- Index Scan — когда выбрано мало строк
EXPLAIN ANALYZE SELECT * FROM employees WHERE id = 42;

-- Bitmap Index Scan — средний случай
EXPLAIN ANALYZE SELECT * FROM employees WHERE salary > 90000;

70Создайте индексы разных типов для соответствующих задач.
-- BRIN: для больших таблиц с естественно упорядоченными данными
CREATE INDEX idx_emp_hire_brin ON employees USING BRIN(hire_date);

-- GIN: для массивов и полнотекстового поиска
CREATE INDEX idx_emp_skills_gin ON employees USING GIN(skills);

-- GiST: для геоданных и полнотекстового поиска
CREATE INDEX idx_locations_gist ON locations USING GIST(coord);

71Выполните обслуживание индексов и сбор статистики.
-- Сбор статистики для оптимизатора
ANALYZE employees;

-- Перестроение индекса (Pg 12+)
REINDEX INDEX idx_employees_email;

-- Проверка размера индексов
SELECT relname, pg_size_pretty(pg_relation_size(oid))
FROM pg_class
WHERE relkind = 'i'
ORDER BY pg_relation_size(oid) DESC;

72Создайте таблицу с первичным ключом из одного столбца.
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price NUMERIC(10, 2)
);
73Создайте таблицу orders с внешним ключом на customers.
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
order_date DATE DEFAULT CURRENT_DATE,
amount NUMERIC(10, 2)
);

74Добавьте ограничение уникальности на email сотрудника.
ALTER TABLE employees
ADD CONSTRAINT uq_employees_email UNIQUE (email);
75Создайте таблицу, в которой некоторые поля обязательны для заполнения.
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    body TEXT NOT NULL,
    published_at TIMESTAMP
);
76Добавьте проверку, что зарплата положительная и возраст больше 18.
ALTER TABLE employees
ADD CONSTRAINT chk_positive_salary CHECK (salary > 0),
ADD CONSTRAINT chk_adult_age CHECK (age >= 18);
77Создайте таблицу со значениями по умолчанию для нескольких столбцов.
CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    status VARCHAR(20) DEFAULT 'new',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    priority INTEGER DEFAULT 5
);
78Запретите бронирование одного переговорного помещения на пересекающееся время.
CREATE TABLE bookings (
    room_id INTEGER,
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    booked_by VARCHAR(100),
    EXCLUDE USING gist (
        room_id WITH =,
        tstzrange(start_time, end_time) WITH &&
    )
);
79Создайте внешний ключ, проверяемый только в конце транзакции.
CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE transactions (
id SERIAL PRIMARY KEY,
from_account INTEGER REFERENCES accounts(id) DEFERRABLE INITIALLY DEFERRED,
to_account INTEGER REFERENCES accounts(id) DEFERRABLE INITIALLY DEFERRED,
amount NUMERIC(10, 2)
);

-- Вставка без предварительного создания счетов возможна в транзакции:
BEGIN;
INSERT INTO transactions (from_account, to_account, amount) VALUES (1, 2, 100.00);
INSERT INTO accounts (id, name) VALUES (1, 'Alice'), (2, 'Bob');
COMMIT;

80Создайте внешние ключи с каскадным удалением и обнулением при удалении родителя.
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
created_by INTEGER REFERENCES users(id) ON DELETE CASCADE
);

-- При удалении категории: category_id = NULL
-- При удалении пользователя: товары удалятся автоматически

81Временно отключите и снова включите проверку внешних ключей для таблицы.
-- Отключение (требует SUPERUSER)
ALTER TABLE orders DISABLE TRIGGER ALL;

-- Загрузка данных ...
INSERT INTO orders (customer_id) VALUES (999); -- нарушает FK

-- Включение и проверка
ALTER TABLE orders ENABLE TRIGGER ALL;

-- Поиск нарушений
SELECT * FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

82Выполните перевод средств между счетами в одной транзакции.
BEGIN;

UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;

COMMIT;
-- При ошибке: ROLLBACK;

83Вставьте несколько записей с точкой сохранения для частичного отката.
BEGIN;

INSERT INTO logs (message) VALUES ('Шаг 1 выполнен');
SAVEPOINT sp1;

INSERT INTO logs (message) VALUES ('Шаг 2 выполнен');
-- Если шаг 2 ошибочный:
ROLLBACK TO SAVEPOINT sp1;

INSERT INTO logs (message) VALUES ('Шаг 2 (исправленный)');
COMMIT;

84Продемонстрируйте READ COMMITTED vs REPEATABLE READ.
-- Сессия 1 (READ COMMITTED — по умолчанию)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;  -- видит 1000
-- (в сессии 2 обновляют balance = 900 и коммитят)
SELECT balance FROM accounts WHERE id = 1;  -- видит 900 (неповторяющееся чтение)
COMMIT;

-- Сессия 1 (REPEATABLE READ)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- видит 1000
-- (в сессии 2 обновляют balance = 900 и коммитят)
SELECT balance FROM accounts WHERE id = 1; -- всё ещё 1000 (снапшот)
COMMIT;

85Предотвратите фантомное чтение и аномалии сериализации.
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Если две сессии одновременно выполняют этот код,
-- одна из них получит ошибку сериализации:
UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO order_items (order_id, product_id) VALUES (1, 10);

COMMIT;
-- При ошибке: повторить транзакцию

86Создайте ситуацию взаимоблокировки и обработайте её.
-- Сессия 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Сессия 2 делает:
-- UPDATE accounts SET balance = balance - 200 WHERE id = 2;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Если сессии обновляют ресурсы в разном порядке, возникает deadlock,
-- PostgreSQL автоматически завершит одну из транзакций с ошибкой
87Заблокируйте запись при расчёте зарплаты, чтобы избежать гонки.
BEGIN;

-- FOR UPDATE — эксклюзивная блокировка (другие не могут читать/писать)
SELECT * FROM employees WHERE id = 10 FOR UPDATE;

UPDATE employees SET salary = salary * 1.1 WHERE id = 10;

COMMIT;

-- FOR SHARE — разделяемая блокировка (другие могут читать, но не изменять)
SELECT * FROM employees WHERE dept = 'IT' FOR SHARE;

88Пропустите заблокированные строки или завершите запрос с ошибкой при блокировке.
-- SKIP LOCKED: взять только свободные строки (очередь задач)
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY priority DESC
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- NOWAIT: завершиться с ошибкой, если строка заблокирована
SELECT * FROM employees WHERE id = 5 FOR UPDATE NOWAIT;

89Используйте консультативную блокировку для координации между сессиями.
-- Сессия 1
SELECT pg_advisory_lock(12345);

-- Выполнение критической секции
UPDATE employees SET salary = salary * 1.1;

-- Сессия 2 (ждёт)
SELECT pg_advisory_lock(12345); -- блокируется, пока сессия 1 не отпустит

-- Сессия 1
SELECT pg_advisory_unlock(12345);

-- Альтернатива: pg_try_advisory_lock (неблокирующая)

90Проверьте видимость строк через системные столбцы.
-- PostgreSQL не перезаписывает строки, а создаёт новые версии.
-- Проверка mvp (минимальный xmin):
SELECT xmin, xmax, ctid, name, salary
FROM employees
ORDER BY ctid;

-- Количество "мёртвых" кортежей (нуждаются в VACUUM):
SELECT n_dead_tup, n_live_tup, relname
FROM pg_stat_user_tables
WHERE relname = 'employees';

91Вставьте сотрудника или обновите его зарплату, если он уже существует.
INSERT INTO employees (email, name, salary)
VALUES ('ivan@example.com', 'Иван Петров', 80000.00)
ON CONFLICT (email)
DO UPDATE SET
    name = EXCLUDED.name,
    salary = EXCLUDED.salary;
92Синхронизируйте таблицу staging с основной таблицей.
MERGE INTO employees AS target
USING employees_staging AS source
ON target.email = source.email
WHEN MATCHED THEN
    UPDATE SET salary = source.salary, name = source.name
WHEN NOT MATCHED THEN
    INSERT (email, name, salary)
    VALUES (source.email, source.name, source.salary);
93Вставьте запись и сразу получите её id и другие поля.
INSERT INTO employees (name, salary, dept)
VALUES ('Ольга Новикова', 72000.00, 'IT')
RETURNING id, name, salary, created_at;

-- Также работает с UPDATE и DELETE:
UPDATE employees SET salary = salary * 1.1 WHERE dept = 'IT'
RETURNING id, name, salary AS new_salary;

94Создайте таблицу с вычисляемыми столбцами (stored и virtual).
CREATE TABLE invoices (
    id SERIAL PRIMARY KEY,
    quantity INTEGER NOT NULL,
    unit_price NUMERIC(10, 2) NOT NULL,
    total NUMERIC(10, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

INSERT INTO invoices (quantity, unit_price) VALUES (3, 150.00);
SELECT * FROM invoices; -- total = 450.00

95Создайте таблицы с массивом навыков и JSONB-полем, выполните поиск по ним.
-- Массивы
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    skills TEXT[]
);

INSERT INTO employees (name, skills) VALUES
('Иван', ARRAY['Python', 'SQL', 'Docker']),
('Мария', ARRAY['Java', 'SQL', 'Kubernetes']);

SELECT name FROM employees WHERE 'SQL' = ANY(skills);
SELECT name FROM employees WHERE skills @> ARRAY['Python', 'Docker'];

-- JSONB
CREATE TABLE events (
id SERIAL PRIMARY KEY,
payload JSONB
);

INSERT INTO events (payload) VALUES
('{"user_id": 42, "tags": ["premium", "vip"]}');

SELECT payload->>'user_id' AS uid FROM events;
SELECT * FROM events WHERE payload @> '{"tags": ["vip"]}';
CREATE INDEX idx_events_payload ON events USING GIN(payload);

96Выведите иерархию категорий (родитель-потомок) древовидной структуры.
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    parent_id INTEGER REFERENCES categories(id)
);

INSERT INTO categories VALUES
(1, 'Электроника', NULL),
(2, 'Компьютеры', 1),
(3, 'Ноутбуки', 2),
(4, 'Планшеты', 2),
(5, 'Телефоны', 1);

WITH RECURSIVE cat_tree AS (
SELECT id, name, parent_id, 1 AS level, name::TEXT AS path
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1,
ct.path || ' -> ' || c.name
FROM categories c
JOIN cat_tree ct ON c.parent_id = ct.id
)
SELECT * FROM cat_tree ORDER BY path;

97Создайте индекс для полнотекстового поиска по статьям.
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT,
    body TEXT,
    search_vector TSVECTOR
);

-- Заполнение tsvector
UPDATE articles SET search_vector =
to_tsvector('russian', title || ' ' || body);

-- Индекс GIN для ускорения
CREATE INDEX idx_articles_fts ON articles USING GIN(search_vector);

-- Поиск
SELECT title
FROM articles
WHERE search_vector @@ to_tsquery('russian', 'база & данных');

-- Триггер для автоматического обновления
CREATE FUNCTION update_search_vector() RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('russian', NEW.title || ' ' || NEW.body);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_articles_fts
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW
EXECUTE FUNCTION update_search_vector();

98Создайте партиционированную таблицу для логов по месяцам.
CREATE TABLE logs (
    id SERIAL,
    level TEXT,
    message TEXT,
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

CREATE TABLE logs_2024_q1 PARTITION OF logs
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE logs_2024_q2 PARTITION OF logs
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE logs_2024_q3 PARTITION OF logs
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
CREATE TABLE logs_2024_q4 PARTITION OF logs
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');

-- Запрос автоматически использует только нужные партиции
SELECT * FROM logs WHERE created_at BETWEEN '2024-02-01' AND '2024-02-28';

99Создайте базовую таблицу vehicles и наследников cars/motorcycles.
CREATE TABLE vehicles (
    id SERIAL PRIMARY KEY,
    brand VARCHAR(100),
    model VARCHAR(100),
    year INTEGER
);

CREATE TABLE cars (
doors INTEGER,
fuel_type VARCHAR(20)
) INHERITS (vehicles);

CREATE TABLE motorcycles (
has_sidecar BOOLEAN DEFAULT FALSE
) INHERITS (vehicles);

INSERT INTO cars (brand, model, year, doors, fuel_type) VALUES
('Toyota', 'Camry', 2023, 4, 'petrol');

INSERT INTO motorcycles (brand, model, year, has_sidecar) VALUES
('Harley', 'Sportster', 2022, FALSE);

-- Запрос к родительской таблице включает и потомков
SELECT * FROM vehicles; -- покажет и cars, и motorcycles

100Настройте уведомление при добавлении нового сотрудника.
-- Создание функции-триггера для уведомлений
CREATE FUNCTION notify_new_employee() RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify(
        'new_employee',
        json_build_object('id', NEW.id, 'name', NEW.name)::TEXT
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_new_employee
AFTER INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION notify_new_employee();

-- В приложении (или psql):
LISTEN new_employee;

-- При вставке:
INSERT INTO employees (name, salary) VALUES ('Новый Сотрудник', 60000);
-- В консоли появится: Asynchronous notification "new_employee" ...


Лицензия

MIT

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions