SELECT и основы запросов
SELECT, WHERE и ORDER BY
SELECT извлекает строки из одной или нескольких таблиц. Всегда указывайте столбцы явно вместо * для производительности и ясности (изменения схемы не сломают ваше приложение). WHERE фильтрует строки перед группировкой. ORDER BY сортирует результаты (ASC по умолчанию, DESC по убыванию). LIMIT/OFFSET реализуют пагинацию — для больших наборов данных предпочитайте пагинацию по ключу (WHERE id > last_id).
-- basic query: select specific columns
SELECT id, name, email
FROM users
WHERE age >= 18 AND status = 'active'
ORDER BY name ASC, created_at DESC
LIMIT 10 OFFSET 0;
-- select all columns (avoid in production)
SELECT * FROM products;
-- column aliases with AS
SELECT name AS product_name, price * 1.1 AS price_with_tax
FROM products;DISTINCT и алиасы
DISTINCT удаляет повторяющиеся строки из набора результатов. Он работает со всей строкой, а не с отдельными столбцами — SELECT DISTINCT city, country возвращает уникальные пары city+country. Алиасы таблиц (u, o) сокращают запросы и требуются при соединении таблицы с самой собой. Алиасы столбцов переименовывают выходные столбцы для читаемости.
-- unique values only
SELECT DISTINCT country FROM users;
SELECT DISTINCT city, country FROM users; -- unique combos
-- table aliases (essential for joins)
SELECT u.name, o.total
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;
-- column alias (AS is optional)
SELECT name product_name, COUNT(*) count
FROM products
GROUP BY name;Фильтрация: BETWEEN, IN, IS NULL
BETWEEN включающий с обоих концов. IN соответствует любому значению в списке или подзапросе. NULL требует IS NULL / IS NOT NULL (нельзя использовать = NULL). Будьте осторожны с NOT IN и подзапросами — если подзапрос возвращает любой NULL, NOT IN не возвращает строк вообще. Используйте NOT EXISTS вместо него, который корректно обрабатывает NULL и часто быстрее.
SELECT * FROM products
WHERE price BETWEEN 10 AND 100 -- inclusive range
AND category IN ('tech', 'home') -- match any value
AND stock IS NOT NULL -- exclude NULLs
AND discount IS NULL; -- only NULLs
-- NOT IN with NULL caveat: returns nothing if subquery has NULL!
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders); -- risky if NULLs
-- safer: NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);LIKE и сопоставление шаблонов
LIKE использует % (ноль или более символов) и _ (ровно один символ) как маски. LIKE чувствителен к регистру в большинстве баз данных, кроме MySQL (без учёта регистра по умолчанию). Используйте ILIKE в PostgreSQL для сопоставления без учёта регистра. Для сложных шаблонов используйте регулярные выражения (~ в PostgreSQL, REGEXP в MySQL). LIKE с ведущим % не может использовать индексы — рассмотрите полнотекстовый поиск для производительности.
-- LIKE: basic pattern matching
SELECT * FROM users WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM users WHERE name LIKE '%son'; -- ends with son
SELECT * FROM users WHERE name LIKE '%a%'; -- contains a
SELECT * FROM users WHERE name LIKE '_a%'; -- second char is a
-- ILIKE (PostgreSQL): case-insensitive
SELECT * FROM users WHERE name ILIKE 'a%';
-- SIMILAR TO (PostgreSQL): regex-like
SELECT * FROM users WHERE name SIMILAR TO '[AB]%';
-- full regex (PostgreSQL)
SELECT * FROM users WHERE name ~ '^A[a-z]+$';Выражения CASE
CASE — это if-then-else в SQL, вычисляется для каждой строки. Может появляться в SELECT, WHERE, ORDER BY и HAVING. Шаблон 'pivot' (SUM с CASE) преобразует строки в столбцы — полезно для отчётности. CASE возвращает NULL, если ни один WHEN не совпал и нет ELSE. Всегда включайте ELSE для предсказуемых результатов.
-- conditional logic in queries
SELECT
name,
price,
CASE
WHEN price < 10 THEN 'cheap'
WHEN price < 50 THEN 'moderate'
WHEN price < 100 THEN 'expensive'
ELSE 'luxury'
END AS price_category
FROM products;
-- CASE in aggregation (pivot table)
SELECT
category,
COUNT(*) AS total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count,
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) AS inactive_count
FROM products
GROUP BY category;JOIN'ы
INNER JOIN
INNER JOIN возвращает только строки, имеющие совпадения в обеих таблицах. JOIN — сокращение для INNER JOIN. Для запросов к нескольким таблицам соединяйте их пошагово. ON указывает условие соединения; USING(column) — сокращение, когда обе таблицы имеют одинаковое имя столбца. Внутренние соединения исключают несовпадающие строки с обеих сторон.
-- only matching rows from both tables
SELECT u.name, o.total, o.created_at
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100
ORDER BY o.total DESC;
-- multiple joins
SELECT u.name, o.total, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- USING (when column names match)
SELECT * FROM users
JOIN profiles ON users.id = profiles.user_id;LEFT JOIN (LEFT OUTER JOIN)
LEFT JOIN возвращает ВСЕ строки из левой таблицы с NULL для несовпадающих правых строк. Это необходимо для запросов 'включить всё'. Шаблон anti-join (WHERE right.id IS NULL) находит строки в левой таблице без совпадения в правой — полезно для 'пользователи, которые не заказывали'. COUNT(right.id) подсчитывает непустые значения, поэтому возвращает 0 для пользователей без заказов.
-- all users, with their orders (NULL if no orders)
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.name;
-- find users with NO orders (anti-join pattern)
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- count orders per user (including zero)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;RIGHT и FULL OUTER JOIN
RIGHT JOIN возвращает все строки правой таблицы; эквивалентно замене таблиц и использованию LEFT JOIN (что более читаемо). FULL OUTER JOIN возвращает все строки из обеих таблиц с NULL там, где нет совпадения — полезно для выверки данных. MySQL не поддерживает FULL OUTER JOIN напрямую; эмулируйте его через LEFT JOIN UNION RIGHT JOIN.
-- RIGHT JOIN: all rows from right table
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- returns all orders, even orphaned ones (user_id = NULL)
-- FULL OUTER JOIN: all rows from both tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- returns all users AND all orders, matching where possible
-- Note: RIGHT JOIN is rarely used (just swap tables and use LEFT)
-- FULL OUTER JOIN is useful for finding mismatches between tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.user_id IS NULL;CROSS JOIN и Self Join
CROSS JOIN создаёт декартово произведение — каждая строка A в паре с каждой строкой B. Используйте для генерации комбинаций (размеры × цвета). Self join (соединение таблицы с самой собой) распространён для иерархических данных (сотрудник-менеджер), поиска дубликатов или сравнения строк в одной таблице. Всегда используйте алиасы таблиц в self join, чтобы различать две 'копии'.
-- CROSS JOIN: Cartesian product (every combination)
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- produces all size+color combinations
-- Self join: join a table to itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- self join for hierarchical data
SELECT c.name AS child, p.name AS parent
FROM categories c
JOIN categories p ON c.parent_id = p.id;
-- self join to find duplicates
SELECT a.id, a.email, b.id AS dup_id
FROM users a
JOIN users b ON a.email = b.email AND a.id < b.id;NATURAL JOIN и сводка типов JOIN
NATURAL JOIN автоматически соединяет по столбцам с одинаковыми именами — удобно, но опасно, так как изменения схемы могут незаметно изменить поведение соединения. Избегайте его в продакшене. LATERAL join позволяют подзапросу ссылаться на столбцы из внешнего запроса — мощно для запросов 'топ N в группе'. Синтаксис с запятой (FROM a, b) эквивалентен CROSS JOIN.
-- NATURAL JOIN: joins on all matching column names
-- (rarely recommended — implicit, fragile)
SELECT * FROM users NATURAL JOIN profiles;
-- joins on any column that exists in BOTH tables
-- Summary of join types:
-- INNER JOIN : matching rows only
-- LEFT JOIN : all left + matching right
-- RIGHT JOIN : all right + matching left
-- FULL JOIN : all from both sides
-- CROSS JOIN : Cartesian product
-- SELF JOIN : table joined to itself
-- LATERAL JOIN (PostgreSQL): subquery can reference outer query
SELECT u.name, recent.*
FROM users u,
LATERAL (
SELECT * FROM orders o
WHERE o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 3
) recent;GROUP BY и агрегация
GROUP BY и HAVING
GROUP BY сворачивает строки в группы, по одной строке на группу. Агрегатные функции (COUNT, SUM, AVG, MIN, MAX) работают с каждой группой. WHERE фильтрует отдельн ые строки ДО группировки; HAVING фильтрует группы ПОСЛЕ агрегации. Неагрегированные столбцы в SELECT должны присутствовать в GROUP BY (стандартный SQL). MySQL лоялен, но непредсказуем — всегда включайте все неагрегированные столбцы в GROUP BY.
-- aggregate per group
SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY cnt DESC;
-- HAVING filters groups (after aggregation)
-- WHERE filters rows (before aggregation)
SELECT dept, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active' -- filter rows first
GROUP BY dept
HAVING AVG(salary) > 50000; -- then filter groupsАгрегатные функции
COUNT(*) подсчитывает все строки, включая NULL; COUNT(column) подсчитывает только непустые значения. COUNT(DISTINCT col) подсчитывает уникальные значения. SUM/AVG игнорируют NULL. AVG = SUM/COUNT(непустые), поэтому NULL влияют на среднее. STRING_AGG (PostgreSQL) / GROUP_CONCAT (MySQL) объединяют строки в группе. BOOL_OR/BOOL_AND возвращают true, если любое/все значения true.
SELECT
COUNT(*) AS total_rows, -- counts all rows
COUNT(email) AS emails_filled, -- counts non-NULL emails
COUNT(DISTINCT country) AS countries,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order,
MIN(amount) AS smallest_order,
MAX(amount) AS largest_order,
-- string aggregation (PostgreSQL)
STRING_AGG(name, ', ') AS all_names,
-- boolean aggregation
BOOL_OR(is_active) AS any_active,
BOOL_AND(is_active) AS all_active
FROM orders;GROUP BY по нескольким столбцам
Группировка по нескольким столбцам создаёт иерархию групп. WITH ROLLUP добавляет строки подытогов и общего итога (NULL в сгруппированном столбце). GROUPING SETS позволяют указать, какие комбинации группировки вы хотите — более гибко, чем ROLLUP. CUBE генерирует все возможные комбинации группировки. Это необходимо для отчётности и OLAP-запросов.
-- multi-level grouping
SELECT
EXTRACT(YEAR FROM created_at) AS yr,
EXTRACT(MONTH FROM created_at) AS mon,
category,
COUNT(*) AS cnt,
SUM(total) AS revenue
FROM orders
GROUP BY yr, mon, category
ORDER BY yr DESC, mon DESC, cnt DESC;
-- GROUP BY with ROLLUP (subtotals + grand total)
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category WITH ROLLUP;
-- last row has NULL category = grand total
-- GROUPING SETS (PostgreSQL): specify multiple groupings
SELECT category, status, COUNT(*)
FROM products
GROUP BY GROUPING SETS ((category, status), (category), ());HAVING vs WHERE
Ключевое различие: WHERE фильтрует отдельные строки до агрегации (не может использовать SUM, COUNT и т.д.), тогда как HAVING фильтрует группы после агрегации (может использовать агрегатные функции). Используйте WHERE для раннего сокращения данных (лучше производительность), затем HAVING для фильтрации агрегированных результатов. Оба могут быть в одном запросе — сначала WHERE, затем GROUP BY, затем HAVING.
-- WHERE: filters rows BEFORE grouping
-- Cannot use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- OK: filter on raw column
GROUP BY category;
-- HAVING: filters groups AFTER grouping
-- Can use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category
HAVING COUNT(*) > 5 -- OK: filter on aggregate
AND AVG(price) > 20; -- OK: multiple aggregate filters
-- combining both
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- filter rows
GROUP BY category -- group
HAVING COUNT(*) > 5; -- filter groupsАгрегация по дате/времени
Усечение дат необходимо для отчётности временных рядов. DATE(col) извлекает только дату; EXTRACT/TIME_PART получает конкретные компоненты (год, месяц, час). TO_CHAR форматирует даты для группировки и отображения. Для анализа временных рядов рассмотрите DATE_TRUNC('month', col), который сохраняет тип timestamp. Индексируйте столбцы дат для производительности на больших таблицах.
-- group by date parts
SELECT
DATE(created_at) AS order_date,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY order_date DESC;
-- group by hour
SELECT
EXTRACT(HOUR FROM created_at) AS hr,
COUNT(*) AS cnt
FROM orders
WHERE created_at >= CURRENT_DATE
GROUP BY hr
ORDER BY hr;
-- monthly revenue trend
SELECT
TO_CHAR(created_at, 'YYYY-MM') AS month,
SUM(total) AS revenue,
COUNT(*) AS orders,
AVG(total) AS avg_order
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY month
ORDER BY month;Подзапросы и CTE
Скалярные и столбцовые подзапросы
Скалярные подзапросы возвращают одно значение и могут использоваться везде, где ожидается значение. Столбцовые подзапросы возвращают один столбец и используются с IN, ANY, ALL. Подзапросы в SELECT (коррелированные) выполняются один раз для каждой внешней строки — могут быть медленными на больших данных. Рассмотрите переписывание как JOIN с GROUP BY для лучшей производительности.
-- scalar subquery (returns single value)
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- column subquery (returns one column, multiple rows)
SELECT name
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 100);
-- subquery in SELECT
SELECT
u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;Коррелированные подзапросы и EXISTS
Коррелированные подзапросы ссылаются на внешний запрос и выполняются один раз для каждой внешней строки — потенциально медленно. EXISTS/NOT EXISTS эффективны, так как они короткозамкнуты (останавливаются при первом совпадении). NOT EXISTS — предпочтительный способ найти 'строки без совпадающих строк' — он корректно обрабатывает NULL и часто быстрее, чем NOT IN. База данных может оптимизировать коррелированные подзапросы в join'ы.
-- correlated: subquery references outer query
SELECT u.name
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.total > 1000
);
-- NOT EXISTS: users without any orders
SELECT u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- correlated subquery in SELECT (runs per row)
SELECT
u.name,
(SELECT MAX(o.total)
FROM orders o
WHERE o.user_id = u.id) AS max_order
FROM users u;Табличные выражения (CTE)
CTE (конструкция WITH) создают именованные временные наборы результатов, делая сложные запросы читаемыми. В отличие от подзапросов, CTE могут ссылаться несколько раз и читаются сверху вниз. В большинстве баз данных CTE инлайнятся (оптимизация происходит на уровне запроса). PostgreSQL 12+ поддерживает подсказки MATERIALIZED/NOT MATERIALIZED. CTE также необходимы для рекурсивных запросов.
-- CTE: named temporary result set
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS cnt, SUM(total) AS revenue
FROM orders
GROUP BY user_id
)
SELECT au.name, uo.cnt, uo.revenue
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id
ORDER BY uo.revenue DESC NULLS LAST;
-- CTEs improve readability for complex queries
-- They are NOT materialized (just syntactic sugar) in most DBs
-- (PostgreSQL 12+ can materialize with MATERIALIZED keyword)Рекурсивные CTE
Рекурсивные CTE ссылаются на самих себя, обеспечивая обход деревьев/графов и генерацию последовательностей. Структура: базовый случай UNION ALL рекурсивный случай. Рекурсивный случай ссылается на CTE и должен завершаться (добавьте WHERE для предотвращения бесконечных циклов). Распространённые применения: организационные диаграммы, деревья категорий, графы зависимостей, последовательности дат. В разных базах данных немного разный синтаксис — проверьте документацию вашей СУБД.
-- hierarchical data: org chart
WITH RECURSIVE org_tree AS (
-- base case: top-level managers
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- recursive case: direct reports
SELECT e.id, e.name, e.manager_id, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT level, name FROM org_tree ORDER BY level, name;
-- generate a series of dates
WITH RECURSIVE dates AS (
SELECT DATE '2024-01-01' AS d
UNION ALL
SELECT d + 1 FROM dates WHERE d < '2024-01-31'
)
SELECT d FROM dates;
-- factorial
WITH RECURSIVE fact(n, result) AS (
SELECT 1, 1
UNION ALL
SELECT n + 1, result * (n + 1) FROM fact WHERE n < 10
)
SELECT * FROM fact;Операторы подзапросов: ANY, ALL
ANY и ALL сравнивают значение с набором результатов подзапроса. > ANY означает 'больше хотя бы одного'. > ALL означает 'больше каждого'. = ANY эквивалентно IN. <> ALL эквивалентно NOT IN, но обрабатывает NULL безопаснее. Эти операторы используются реже, чем IN/EXISTS, но могут выразить некоторые запросы более естественно.
-- ANY: greater than ANY of the values (= at least one)
SELECT * FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds at least one tech product's price
-- ALL: greater than ALL values (= every one)
SELECT * FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds every tech product's price
-- = ANY is equivalent to IN
SELECT * FROM users
WHERE id = ANY (SELECT user_id FROM orders);
-- <> ALL is equivalent to NOT IN (but NULL-safe)
SELECT * FROM users
WHERE id <> ALL (SELECT user_id FROM orders WHERE total < 0);Оконные функции
ROW_NUMBER, RANK, DENSE_RANK
ROW_NUMBER присваивает уникальные последовательные номера (1, 2, 3...). RANK даёт одинаковый ранг связям, но пропускает последующие номера (1, 1, 3). DENSE_RANK даёт одинаковый ранг связям без пропусков (1, 1, 2). PARTITION BY делит строки на группы; функция сбрасывается для каждого раздела. Шаблон 'топ N в группе' (ROW_NUMBER + WHERE rn <= N) крайне распространён в аналитике.
SELECT
name,
salary,
dept,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
-- top 3 earners per department
SELECT * FROM (
SELECT
name,
dept,
salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 3;LAG и LEAD
LAG обращается к значению предыдущей строки; LEAD — к значению будущей строки. Оба принимают опциональное смещение (по умолчанию 1) и значение по умолчанию (по умолчанию NULL). Существенно для анализа временных рядов: изменения день-ко-дню, скользящие сравнения, обнаружение разрывов. NULLIF предотвращает деление на ноль в процентных вычислениях. Всегда указывайте ORDER BY в конструкции OVER для детерминированных результатов.
-- compare each row to previous/next
SELECT
date,
revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_day,
LEAD(revenue) OVER (ORDER BY date) AS next_day,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY date)) * 100.0
/ NULLIF(LAG(revenue) OVER (ORDER BY date), 0),
2
) AS pct_change
FROM daily_sales
ORDER BY date;
-- LAG with offset and default
SELECT
date,
revenue,
LAG(revenue, 7) OVER (ORDER BY date) AS revenue_7_days_ago
FROM daily_sales;Нарастающие итоги и скользящие средние
Оконные фреймы определяют, над какими строками работает функция. ROWS BETWEEN использует физические смещения строк; RANGE использует логические диапазоны значений (лучше для пробелов в датах). UNBOUNDED PRECEDING означает 'с самого начала'. Нарастающие итоги (кумулятивный SUM) и скользящие средние — самые распространённые аналитические шаблоны. Без фрейма агрегатные оконные функции используют значение по умолчанию: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
-- cumulative sum (running total)
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) AS running_total,
SUM(revenue) OVER (ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS last_7_days_sum,
AVG(revenue) OVER (ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM daily_sales
ORDER BY date;
-- window frame options:
-- ROWS BETWEEN n PRECEDING AND n FOLLOWING -- physical rows
-- RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW -- logical range
-- ROWS UNBOUNDED PRECEDING = from start to current
-- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING = all rowsNTILE и PERCENT_RANK
NTILE(n) делит упорядоченные строки на n примерно равных групп (квартили, децили, перцентили). PERCENT_RANK даёт относительный ранг (от 0 до 1). CUME_DIST даёт кумулятивное распределение. FIRST_VALUE/LAST_VALUE возвращают значения из первой/последней строки фрейма — обратите внимание, что LAST_VALUE требует явного фрейма (UNBOUNDED FOLLOWING), так как фрейм по умолчанию заканчивается на текущей строке.
-- divide into quartiles
SELECT
name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS quartile,
PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank,
CUME_DIST() OVER (ORDER BY salary) AS cumulative_dist
FROM employees;
-- first/last value in a partition
SELECT
dept,
name,
salary,
FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS top_earner,
LAST_VALUE(name) OVER (
PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS lowest_earner
FROM employees;Оконные агрегатные функции
Оконные агрегаты (SUM, AVG, COUNT и т.д. с OVER) вычисляют агрегатные значения БЕЗ сворачивания строк — каждая строка получает присоединённый агрегат. Это ключевое отличие от GROUP BY: вы сохраняете все детальные строки, одновременно видя сводку. Идеально для сравнения индивидуальных значений со средними по группе, вычисления процентов и добавления контекстных столбцов к детальным отчётам.
-- aggregates over windows (no row collapse!)
SELECT
name,
dept,
salary,
-- compare to department average
AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY dept) AS diff_from_avg,
-- percentage of department total
salary * 100.0 / SUM(salary) OVER (PARTITION BY dept) AS pct_of_dept,
-- count per department
COUNT(*) OVER (PARTITION BY dept) AS dept_size
FROM employees
ORDER BY dept, salary DESC;
-- key advantage: aggregates without GROUP BY
-- every row is preserved, with the aggregate value attachedDDL: таблицы и схема
CREATE TABLE и типы данных
CREATE TABLE определяет схему. SERIAL (PostgreSQL) / AUTO_INCREMENT (MySQL) автоматически генерирует ID. VARCHAR(n) имеет предел; TEXT не ограничен. DECIMAL(p,s) точный (используйте для денег!), FLOAT приблизительный. CHECK-ограничения обеспечивают бизнес-правила. DEFAULT предоставляет значения, когда не указано. JSONB (PostgreSQL) обеспечивает индексируемые JSON-запросы. Всегда используйте TIMESTAMP WITH TIME ZONE для меток времени, охватывающих часовые пояса.
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- auto-increment (PostgreSQL)
-- MySQL: id INT AUTO_INCREMENT PRIMARY KEY
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
age INT CHECK (age >= 0 AND age <= 150),
salary DECIMAL(10, 2) DEFAULT 0.00, -- precision, scale
bio TEXT, -- unlimited length
avatar BYTEA, -- binary (PostgreSQL)
metadata JSONB, -- JSON (PostgreSQL)
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- common types:
-- INT, BIGINT, SMALLINT, DECIMAL(p,s), NUMERIC
-- VARCHAR(n), CHAR(n), TEXT
-- DATE, TIME, TIMESTAMP, INTERVAL
-- BOOLEAN, UUID, JSON/JSONB, BYTEA/BLOBОграничения: PRIMARY, FOREIGN, UNIQUE, CHECK
Ограничения обеспечивают целостность данных на уровне базы данных. PRIMARY KEY уникально идентифицирует строки (подразумевает NOT NULL + UNIQUE). FOREIGN KEY поддерживает ссылочную целостность — ON DELETE CASCADE удаляет дочерние записи при удалении родителя. UNIQUE предотвращает дубликаты. CHECK обеспечивает пользовательские правила. Определение ограничений в базе данных (не только в коде приложения) обеспечивает целостность независимо от способа доступа к данным.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total DECIMAL(10,2) NOT NULL CHECK (total >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- table-level constraints
CONSTRAINT valid_status CHECK (status IN ('pending','paid','shipped','cancelled')),
CONSTRAINT unique_user_order UNIQUE (user_id, created_at)
);
-- foreign key actions:
-- ON DELETE CASCADE : delete child rows when parent deleted
-- ON DELETE SET NULL : set FK to NULL (column must be nullable)
-- ON DELETE RESTRICT : prevent parent deletion (default)
-- ON UPDATE CASCADE : update FK when parent PK changes
-- add constraint to existing table
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);ALTER TABLE
ALTER TABLE изменяет структуру существующей таблицы. Добавление столбцов со значениями по умолчанию обычно быстро (PostgreSQL 11+ не переписывает таблицу). Удаление столбцов может заблокировать таблицу. Изменение типов столбцов может потребовать полной перезаписи таблицы и может завершиться неудачей, если данные не преобразуются. Всегда тестируйте миграции схемы на копии. Используйте инструменты миграции (Flyway, Alembic, Rails migrations) для версионных изменений схемы.
-- add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN verified BOOLEAN DEFAULT false;
-- drop column
ALTER TABLE users DROP COLUMN avatar;
-- rename column/table
ALTER TABLE users RENAME COLUMN username TO login;
ALTER TABLE users RENAME TO accounts;
-- change column type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- MySQL: ALTER TABLE users MODIFY COLUMN age BIGINT;
-- add/drop constraints
ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);
ALTER TABLE users DROP CONSTRAINT email_unique;
-- set default
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';DROP, TRUNCATE и индексы
DROP TABLE полностью удаляет таблицу; TRUNCATE очищает её, но сохраняет структуру (гораздо быстрее, чем DELETE, сбрасывает идентичность). Индексы ускоряют запросы, но замедляют запись — индексируйте стратегически. Композитные индексы работают слева направо: idx(a,b,c) помогает WHERE a=?, WHERE a=? AND b=?, но НЕ WHERE b=?. GIN-индексы обеспечивают полнотекстовый поиск. Частичные индексы экономят место, индексируя только совпадающие строки.
-- DROP: permanently remove table (structure + data)
DROP TABLE IF EXISTS old_logs CASCADE;
-- CASCADE drops dependent objects (views, FKs)
-- TRUNCATE: remove all data, keep structure (faster than DELETE)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY; -- reset SERIAL counter
TRUNCATE TABLE orders, order_items CASCADE; -- multiple tables
-- CREATE INDEX for query performance
CREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
CREATE INDEX idx_products_name ON products USING gin(to_tsvector('english', name));
-- partial index (PostgreSQL)
CREATE INDEX idx_active_users ON users(last_login)
WHERE status = 'active';
-- DROP INDEX
DROP INDEX IF EXISTS idx_users_email;Представления и материализованные представления
Представления — сохранённые запросы, действующие как виртуальные таблицы — они запускают нижележащий запрос каждый раз. Используйте представления для упрощения сложных запросов, обеспечения безопасности (доступ на уровне столбцов) и предоставления стабильных API. Материализованные представления хранят фактические результаты — быстрее запрашивать, но требуют обновления. Используйте материализованные представления для дорогих агрегаций, не требующих данных в реальном времени. CONCURRENTLY обновляет без блокировки (PostgreSQL).
-- VIEW: stored query (virtual table, runs on access)
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 'active';
SELECT * FROM active_users WHERE name LIKE 'A%';
-- updatable view (simple views can be INSERTed/UPDATEd)
CREATE VIEW user_summary AS
SELECT id, name, email, age FROM users;
-- MATERIALIZED VIEW: stored result (must refresh)
CREATE MATERIALIZED VIEW monthly_stats AS
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY month;
REFRESH MATERIALIZED VIEW monthly_stats;
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_stats; -- no lock
-- drop
DROP VIEW IF EXISTS active_users;
DROP MATERIALIZED VIEW IF EXISTS monthly_stats;DML: Insert, Update, Delete
INSERT
INSERT добавляет строки. Несколько VALUES в одном операторе эффективнее, чем отдельные вставки. INSERT...SELECT копирует данные между таблицами. RETURNING (PostgreSQL/Oracle) извлекает автогенерируемые значения (например, SERIAL ids) за один проход — необходимо для кода приложения. Используйте DEFAULT VALUES для вставки строки со всеми значениями по умолч анию. Всегда указывайте имена столбцов, чтобы сделать код устойчивым к изменениям схемы.
-- single row
INSERT INTO users (name, email, age)
VALUES ('Alice', '[email protected]', 30);
-- multiple rows
INSERT INTO users (name, email) VALUES
('Bob', '[email protected]'),
('Carol', '[email protected]'),
('Dave', '[email protected]');
-- INSERT ... SELECT (copy data between tables)
INSERT INTO archive_users (name, email, deleted_at)
SELECT name, email, NOW()
FROM users
WHERE status = 'deleted';
-- INSERT with RETURNING (PostgreSQL)
INSERT INTO users (name, email)
VALUES ('Eve', '[email protected]')
RETURNING id, created_at; -- returns the generated id
-- DEFAULT values
INSERT INTO users DEFAULT VALUES;UPDATE
UPDATE изменяет существующие строки. ВСЕГДА включайте предложение WHERE, если только не собираетесь обновить каждую строку. Предложение FROM (PostgreSQL) позволяет join'ы в обновлениях. RETURNING показывает, какие строки были изменены. Используйте транзакции для многошаговых обновлений, чтобы можно было выполнить ROLLBACK, если что-то пойдёт не так. Распространённая ошибка — забыть WHERE; рассмотрите запуск SELECT с тем же WHERE сначала для проверки затронутых строк.
-- basic update
UPDATE users
SET age = 31, status = 'verified', updated_at = NOW()
WHERE id = 1;
-- update based on another table
UPDATE products p
SET price = p.price * 1.1
FROM categories c
WHERE p.category_id = c.id AND c.name = 'electronics';
-- update with subquery
UPDATE users
SET status = 'premium'
WHERE id IN (
SELECT user_id FROM orders
GROUP BY user_id HAVING SUM(total) > 1000
);
-- UPDATE with RETURNING (PostgreSQL)
UPDATE users SET status = 'inactive'
WHERE last_login < '2023-01-01'
RETURNING id, name;
-- WARNING: UPDATE without WHERE affects ALL rows!DELETE и TRUNCATE
DELETE удаляет строки по одной (логируется, можно откатить, медленнее). TRUNCATE удаляет все строки сразу (минимальное логирование, намного быстрее, сбрасывает автоинкремент, нельзя откатить в некоторых БД). Для аудиторских следов используйте мягкие удаления (метка времени deleted_at) вместо жёстких удалений. Всегда используйте WHERE с DELETE. Учитывайте ограничения внешнего ключа — ON DELETE CASCADE обрабатывает дочерние строки автоматически.
-- delete specific rows
DELETE FROM users WHERE status = 'inactive';
-- delete with subquery
DELETE FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE status = 'deleted'
);
-- delete with RETURNING (PostgreSQL)
DELETE FROM users
WHERE last_login < '2020-01-01'
RETURNING id, name;
-- delete all rows (slow, logged, can be rolled back)
DELETE FROM logs;
-- TRUNCATE (fast, minimal logging, resets identity)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY CASCADE;
-- soft delete pattern (preferred for audit)
UPDATE users SET deleted_at = NOW() WHERE id = 1;
SELECT * FROM users WHERE deleted_at IS NULL; -- active usersUPSERT (INSERT ... ON CONFLICT)
UPSERT (update or insert) атомарно обрабатывает конфликты дублирующих ключей. PostgreSQL использует ON CONFLICT (column) DO UPDATE/DO NOTHING. MySQL использует ON DUPLICATE KEY UPDATE. EXCLUDED (PostgreSQL) / VALUES() (MySQL) относится к предлагаемым значениям вставки. Это необходимо для идемпотентных операций и избегания состояний гонки. Без upsert потребовался бы SELECT-затем-INSERT/UPDATE, что подвержено состояниям гонки.
-- PostgreSQL: ON CONFLICT (upsert)
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id)
DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()
RETURNING *;
-- DO NOTHING on conflict
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id) DO NOTHING;
-- MySQL: ON DUPLICATE KEY UPDATE
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON DUPLICATE KEY UPDATE email = VALUES(email);
-- SQLite: ON CONFLICT
INSERT INTO users (id, name)
VALUES (1, 'Alice')
ON CONFLICT(id) DO UPDATE SET name = excluded.name;Оператор MERGE
MERGE (он же UPSERT на стероидах) объединяет INSERT, UPDATE и DELETE в одном атомарном операторе на основе совпадения строк. Это самый эффективный способ синхронизации данных между источниками. WHEN MATCHED запускает UPDATE/DELETE для существующих строк; WHEN NOT MATCHED запускает INSERT для новых строк. Доступен в SQL Server, Oracle, PostgreSQL 15+ и DB2. MySQL не поддерживает MERGE — используйте INSERT...ON DUPLICATE KEY.
-- MERGE: conditional insert/update/delete in one statement
-- (SQL Server, Oracle, PostgreSQL 15+)
MERGE INTO products AS target
USING (VALUES
(1, 'Widget', 9.99),
(2, 'Gadget', 19.99),
(3, 'Gizmo', 29.99)
) AS source (id, name, price)
ON target.id = source.id
WHEN MATCHED THEN
UPDATE SET name = source.name, price = source.price
WHEN NOT MATCHED THEN
INSERT (id, name, price) VALUES (source.id, source.name, source.price)
WHEN MATCHED AND source.price < 0 THEN
DELETE;
-- useful for:
-- - syncing data from external sources
-- - bulk upsert with conditional logic
-- - ETL operationsТранзакции и ACID
BEGIN, COMMIT, ROLLBACK
Транзакции группируют операции в атомарную единицу — все успешно (COMMIT) или все неудачно (ROLLBACK). Это 'A' в ACID. BEGIN/START TRANSACTION начинает транзакцию. SAVEPOINT создаёт именованную точку отката в транзакции — можно откатиться к ней без прерывания всей транзакции. Всегда делайте commit или rollback — оставление открытой транзакции удерживает блокировки и может вызвать взаимные блокировки.
-- basic transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- both updates succeed or both fail (atomicity)
-- rollback on error
BEGIN;
INSERT INTO orders (user_id, total) VALUES (1, 50.00);
-- oops, something went wrong
ROLLBACK;
-- the insert is undone
-- transaction with savepoints
BEGIN;
INSERT INTO logs (msg) VALUES ('step 1');
SAVEPOINT my_savepoint;
INSERT INTO logs (msg) VALUES ('step 2');
ROLLBACK TO my_savepoint; -- undo step 2, keep step 1
INSERT INTO logs (msg) VALUES ('step 3');
COMMIT; -- commits step 1 and step 3Уровни изоляции
Уровни изоляции балансируют согласованность и конкурентность. READ COMMITTED (по умолчанию в PostgreSQL/Oracle) предотвращает грязное чтение, но допускает неповторяющееся чтение. REPEATABLE READ предотвращает неповторяющееся чтение, но допускает фантомное чтение. SERIALIZABLE предотвращает все аномалии, но снижает конкурентность. Более высокая изоляция = бо льше блокировок = меньше конкурентности. Выбирайте самый низкий уровень, отвечающий вашим требованиям корректности.
-- set isolation level for transaction
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- can see committed data from other transactions
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
-- isolation levels (from weakest to strongest):
-- READ UNCOMMITTED: can read uncommitted (dirty) data
-- READ COMMITTED: only committed data (PostgreSQL default)
-- REPEATABLE READ: same query returns same results within txn
-- SERIALIZABLE: transactions appear to run sequentially
-- PostgreSQL: set per transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- MySQL: set per session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- check current level
SHOW TRANSACTION ISOLATION LEVEL;Блокировки и SELECT FOR UPDATE
SELECT FOR UPDATE блокирует строки, чтобы другие транзакции не могли их изменять до вашего commit. Это реализует пессимистическое управление конкурентностью. SKIP LOCKED необходим для очередей задач — несколько воркеров могут брать задачи без блокировки друг друга. NOWAIT завершается неудачей быстро вместо ожидания. Используйте блокировки умеренно — они снижают конкурентность и могут вызвать взаимные блокировки. Предпочитайте оптимистическую конкурентность (столбцы версий) для большинства случаев.
-- pessimistic locking: lock rows for update
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- row is locked; other transactions must wait
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; -- lock released
-- NOWAIT: don't wait if locked, error immediately
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- SKIP LOCKED: skip locked rows (useful for job queues)
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- SHARE LOCK: allow reads but prevent updates
SELECT * FROM products WHERE id = 1 FOR SHARE;Взаимные блокировки и обработка ошибок
Взаимные блокировки возникают, когда две транзакции удерживают блокировки, нужные друг другу. База данных обнаруживает взаимные блокировки и прерывает одну транзакцию (жертву). Предотвратите взаимные блокировки, приобретая блокировки в согласованном порядке во всех транзакциях. Всегда будьте готовы повторить транзакции, завершившиеся неудачей из-за взаимных б локировок или ошибок сериализации. Держите транзакции короткими для снижения конкуренции за блокировки. Код приложения должен перехватывать SQLSTATE 40P01 (взаимная блокировка) и повторять попытку.
-- deadlock example:
-- Transaction A:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1; -- locks row 1
UPDATE accounts SET balance = balance + 50 WHERE id = 2; -- waits for row 2
-- Transaction B (concurrent):
BEGIN;
UPDATE accounts SET balance = balance - 30 WHERE id = 2; -- locks row 2
UPDATE accounts SET balance = balance + 30 WHERE id = 1; -- waits for row 1
-- DEADLOCK! Database detects and kills one transaction
-- prevention: always lock in consistent order
-- Transaction A and B both lock id=1 first, then id=2
-- PostgreSQL: error codes for handling
-- 40P01: deadlock_detected
-- 40001: serialization_failure
-- 40P02: transaction_integrity_constraint_violation
-- retry pattern (pseudocode):
-- for attempt in range(3):
-- try:
-- BEGIN; ... COMMIT; break
-- except deadlock:
-- ROLLBACK; continueСвойства ACID
ACID — основа надёжных транзакций базы данных. Атомарность: все операции в транзакции успешны или неудачны вместе. Согласованность: транзакции переводят базу данных из одного корректного состояния в другое (ограничения обеспечиваются). Изолированность: конкурентные транзакции не мешают друг другу (управляется уровнем изоляции). Долговечность: после commit данные переживают сбои (достигается через журналирование с упреждающей записью). Базы данных NoSQL часто жертвуют некото рыми свойствами ACID ради масштабируемости.
-- ACID guarantees for transactions:
-- A: Atomicity (all or nothing)
BEGIN;
INSERT INTO orders (id, total) VALUES (1, 100);
INSERT INTO order_items (order_id, product_id) VALUES (1, 5);
-- if either fails, both are rolled back
COMMIT;
-- C: Consistency (valid state to valid state)
-- constraints are checked at commit
ALTER TABLE accounts ADD CONSTRAINT balance_non_negative
CHECK (balance >= 0);
-- a transaction that would make balance negative fails
-- I: Isolation (concurrent transactions don't interfere)
-- controlled by isolation level (see previous section)
-- D: Durability (committed data survives crashes)
-- achieved via WAL (Write-Ahead Logging) + fsync
-- synchronous_commit = on (default) ensures durabilityИндексы, представления и хранимые процедуры
Типы и стратегии индексов
B-tree индексы (по умолчанию) обрабатывают запросы равенства (=) и диапазона (<, >, BETWEEN). Композитные индексы следуют правилу крайнего левого префикса — индекс (a,b,c) помогает WHERE a=?, WHERE a=? AND b=?, но не WHERE b=?. Частичные индексы экономят место, индексируя только подмножес тво. Индексы выражений обеспечивают индексируемые запросы по функциям (LOWER, вычисляемые столбцы). Используйте EXPLAIN ANALYZE для проверки использования индексов — неиспользуемый индекс тратит место и замедляет запись.
-- B-tree index (default): equality and range queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_date ON orders(created_at);
-- composite index (order matters!)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- helps: WHERE user_id = 1
-- helps: WHERE user_id = 1 AND status = 'paid'
-- does NOT help: WHERE status = 'paid' (leftmost prefix rule)
-- partial index: smaller, faster for common filters
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- expression index
CREATE INDEX idx_lower_email ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = '[email protected]';
-- unique index
CREATE UNIQUE INDEX idx_unique_email ON users(email);
-- EXPLAIN: see if index is used
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';Хранимые процедуры и функции
Функции возвращают значения и могут использоваться в SELECT; процедуры выполняют действия и вызываются с CALL. Хранимые процедуры инкапсулируют бизнес-логику в базе данных — снижая сетевые обращения и централизуя логику. Однако они могут усложнить масштабирование (логика разделена между приложением и БД) и зависят от конкретной БД. Используйте их для операций, интенсивных по данным, которые выигрывают от близости к данным. PostgreSQL использует PL/pgSQL; MySQL использ ует собственный процедурный SQL.
-- PostgreSQL function
CREATE OR REPLACE FUNCTION get_user_orders(p_user_id INT)
RETURNS TABLE(order_id INT, total DECIMAL) AS $$
BEGIN
RETURN QUERY
SELECT id, total FROM orders WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;
-- call function
SELECT * FROM get_user_orders(1);
-- PostgreSQL procedure (can manage transactions, PostgreSQL 11+)
CREATE PROCEDURE transfer_money(
from_id INT, to_id INT, amount DECIMAL
) LANGUAGE plpgsql AS $$
BEGIN
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
COMMIT;
END;
$$;
CALL transfer_money(1, 2, 100.00);
-- MySQL stored procedure
DELIMITER //
CREATE PROCEDURE GetActiveUsers()
BEGIN
SELECT * FROM users WHERE status = 'active';
END //
DELIMITER ;
CALL GetActiveUsers();Триггеры
Триггеры выполняются автоматически при изменениях данных. BEFORE-триггеры могут изменять входящие данные (например, устанавливать метки времени, проверять). AFTER-триггеры выполняют побочные эффекты (например, журналирование аудита, денормализацию). Используйте триггеры умеренно — они скрыты от кода приложения, что усложняет отладку. Распространённые применения: аудиторские следы, вычисляемые столбцы, обеспечение сложных ограничений и синхронизация денормализованных данны х. Всегда чётко документируйте триггеры.
-- PostgreSQL trigger: audit log on update
CREATE OR REPLACE FUNCTION audit_user_change()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_audit (user_id, old_name, new_name, changed_at)
VALUES (OLD.id, OLD.name, NEW.name, NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_user_audit
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_change();
-- trigger timing: BEFORE / AFTER / INSTEAD OF
-- trigger events: INSERT / UPDATE / DELETE / TRUNCATE
-- granularity: FOR EACH ROW / FOR EACH STATEMENT
-- MySQL trigger
CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
SET NEW.created_at = NOW();
-- drop trigger
DROP TRIGGER IF EXISTS trg_user_audit ON users;Операции JSON (PostgreSQL)
JSONB (PostgreSQL) хранит JSON в бинарном формате, обеспечивая индексацию и эффективные запросы. -> возвращает JSON, ->> возвращает текст. @> проверяет вхождение (содержит ли JSON это?). GIN-индексы делают JSON-запросы быстрыми. Используйте JSON-столбцы для гибких/полуструктурированных данных (журналы событий, ответы API, конфигурация), сохраняя реляционные данные в обычных столбцах. JSONB предпочтительнее JSON (быстрее, индексируемый, без дублирующих ключей).
-- JSONB columns (PostgreSQL)
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL
);
INSERT INTO events (data) VALUES
('{"type": "click", "user": {"id": 1, "name": "Alice"}, "tags": ["web", "mobile"]}');
-- extract fields (-> for JSON, ->> for text)
SELECT data->'type' AS type, -- "click" (JSON)
data->'user'->>'name' AS name, -- Alice (text)
data->'tags'->0 AS first_tag -- "web"
FROM events;
-- filter by JSON field
SELECT * FROM events WHERE data->>'type' = 'click';
SELECT * FROM events WHERE data @> '{"type": "click"}'; -- containment
-- GIN index for JSON queries
CREATE INDEX idx_events_data ON events USING gin(data);
SELECT * FROM events WHERE data @> '{"user": {"id": 1}}';
-- modify JSON
UPDATE events SET data = jsonb_set(data, '{user,name}', '"Bob"');Полнотекстовый поиск
Полнотекстовый поиск обеспечивает запросы на естественном языке (стемминг, ранжирование, стоп-слова). to_tsvector преобразует текст в токены для поиска; to_tsquery создаёт поисковый запрос; @@ сопоставляет. ts_rank оценивает результаты; ts_headline подсвечивает совпадения. GIN-индексы делают это быстрым. Для крупномасштабного поиска рассмотрите специализированные движки (Elasticsearch, Solr), но полнотекстовый поиск PostgreSQL превосходен для умеренных наборов данных и избегает сложности инфр аструктуры.
-- PostgreSQL full-text search
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
body TEXT
);
-- create a full-text search index
CREATE INDEX idx_articles_search ON articles
USING gin(to_tsvector('english', title || ' ' || body));
-- search with ranking
SELECT
title,
ts_rank(to_tsvector('english', body), query) AS rank,
ts_headline('english', body, query) AS snippet
FROM articles, to_tsquery('english', 'database & performance') query
WHERE to_tsvector('english', title || ' ' || body) @@ query
ORDER BY rank DESC
LIMIT 10;
-- simplified with generated column
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX idx_articles_search_vec ON articles USING gin(search_vector);
SELECT * FROM articles WHERE search_vector @@ to_tsquery('database');Производительность и оптимизация запросов
EXPLAIN и планы запросов
EXPLAIN показывает план запроса — как база данных выполнит ваш запрос. EXPLAIN ANALYZE фактически запускает его и показывает реальные тайминги. Ищите Sequential Scan на больших таблицах (добавьте индексы), дорогие Sort (добавьте индексы) и несоответствия оценок строк (запустите ANALYZE для обновления статистики). Числа стоимости относительные, а не абсолютные. Понимание планов запросов — навык №1 для настройки производительности SQL.
-- EXPLAIN: show query plan without running
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- EXPLAIN ANALYZE: run the query and show actual timing
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
-- EXPLAIN ANALYZE with buffers (I/O stats)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users
JOIN orders ON users.id = orders.user_id;
-- key things to look for in the plan:
-- Seq Scan: full table scan (bad for large tables — add index)
-- Index Scan: using an index (good)
-- Bitmap Index Scan: index + heap lookup (good for many rows)
-- Hash Join: builds hash table (good for large joins)
-- Nested Loop: good for small result sets
-- Sort: explicit sort (consider index to avoid)
-- cost: estimated cost (first row, all rows)
-- rows: estimated vs actual rows (big mismatch = stale stats)Распространённые ошибки производительности
Sargability (Search Argument Able) означает, что база данных может использовать индексы. Функции на столбцах (DATE(col), UPPER(col)) препятствуют использованию индексов — перепишите как запросы диапазона или используйте индексы выражений. SELECT * тратит I/O и предотвращает покрывающие индексы. OFFSET-пагинация имеет сложность O(n) — используйте пагинацию по ключу (WHERE id > last_id) для O(1). Большие пакетные операции должны быть разбиты на части для избежания долгих блокировок и отставания репликации.
-- 1. Sargability: avoid functions on indexed columns
-- BAD: function prevents index usage
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- GOOD: range query uses index
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- 2. Avoid SELECT * (more I/O, prevents covering indexes)
-- BAD
SELECT * FROM users WHERE status = 'active';
-- GOOD
SELECT id, name, email FROM users WHERE status = 'active';
-- 3. Use LIMIT with ORDER BY for pagination
-- BAD: loads all rows
SELECT * FROM products ORDER BY id;
-- GOOD: keyset pagination
SELECT * FROM products WHERE id > 1000 ORDER BY id LIMIT 20;
-- 4. Batch large updates
-- BAD: one giant transaction
DELETE FROM logs WHERE date < '2023-01-01';
-- GOOD: batch in chunks
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 10000;
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 20000;UNION, INTERSECT и EXCEPT
Операции над множествами объединяют наборы результатов. UNION удаляет дубликаты (дорогая сортировка); UNION ALL сохраняет их (быстрее — предпочитайте, когда знаете, что дубликатов нет или они нужны). INTERSECT возвращает строки в обоих. EXCEPT возвращает строки в первом, но не во втором. Все требуют совместимых типов столбцов. UNION ALL может заменить сложные условия OR и часто работает лучше, так как может использовать разные индексы для каждой ветки.
-- UNION: combine results, remove duplicates
SELECT name FROM customers
UNION
SELECT name FROM suppliers;
-- UNION ALL: faster, keeps duplicates
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers;
-- INTERSECT: rows in BOTH results
SELECT product_id FROM sales_2023
INTERSECT
SELECT product_id FROM sales_2024;
-- EXCEPT (MINUS in Oracle): rows in first but not second
SELECT product_id FROM all_products
EXCEPT
SELECT product_id FROM discontinued_products;
-- rules:
-- - same number of columns
-- - compatible types
-- - column names come from first query
-- - UNION is often faster than OR conditionsСоветы для конкретных баз данных
VACUUM (PostgreSQL) освобождает место от удалённых строк (MVCC оставляет 'мёртвые кортежи'). ANALYZE обновляет статистику таблицы для планировщика запросов — запустите после массовых загрузок. OPTIMIZE TABLE (MySQL) дефрагментирует таблицы. Явно индексируйте внешние ключи (PostgreSQL не делает это автоматически). Мониторьте использование индексов с pg_stat_user_indexes и удаляйте неиспользуемые. Пул соединений (PgBouncer, ProxySQL) необходим для высоконагруженных приложений — открытие соединений дорого.
-- PostgreSQL: VACUUM to reclaim space
VACUUM ANALYZE users; -- update stats, reclaim dead rows
VACUUM FULL users; -- rewrites table (locks, but reclaims all space)
-- PostgreSQL: ANALYZE to update statistics
ANALYZE users; -- helps query planner make better decisions
-- MySQL: OPTIMIZE TABLE
OPTIMIZE TABLE users;
-- Common indexing rules across databases:
-- 1. Index foreign keys (not automatic in all DBs)
-- 2. Index columns used in WHERE, JOIN, ORDER BY, GROUP BY
-- 3. Composite indexes: high selectivity column first
-- 4. Don't over-index (slows writes, uses disk)
-- 5. Drop unused indexes (check with pg_stat_user_indexes)
-- Connection pooling (application level):
-- - Use PgBouncer (PostgreSQL) or ProxySQL (MySQL)
-- - Reuse connections instead of reconnecting per request
-- - Set appropriate pool size (not too high!)Типы данных и обработка NULL
NULL представляет неизвестные/отсутствующие данные, а не ноль или пустоту. Сравнения с NULL всегда дают NULL (unknown), что ложно в WHERE. Используйте IS NULL / IS NOT NULL для проверки. COALESCE предоставляет значения по умолчанию. NULLIF преобразует определённые значения в NULL (полезно для деления на ноль). Агрегаты пропускают NULL — COUNT(col) подсчитывает непустые, COUNT(*) подсчитывает все строки. В LEFT JOIN используйте COUNT(right_table.col) для получения 0 для несовпадающих строк.
-- NULL is not zero or empty string — it's "unknown"
SELECT NULL = NULL; -- NULL (not true!)
SELECT NULL IS NULL; -- true
SELECT NULL <> 1; -- NULL (unknown)
-- COALESCE: first non-NULL value
SELECT COALESCE(nickname, first_name, 'Anonymous') FROM users;
-- NULLIF: return NULL if two values are equal
SELECT NULLIF(score, 0) FROM tests; -- NULL instead of 0
-- useful for avoiding division by zero:
SELECT total / NULLIF(count, 0) FROM stats;
-- aggregate functions ignore NULL
SELECT AVG(score) FROM tests; -- avg of non-NULL scores only
SELECT COUNT(score) FROM tests; -- count of non-NULL
SELECT COUNT(*) FROM tests; -- count of all rows
-- use LEFT JOIN + COUNT carefully
SELECT u.name, COUNT(o.id) AS orders -- COUNT(o.id) = 0 for no orders
FROM users u LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;Рекурсивные CTE
Базовая структура рекурсивного CTE
Рекурсивный CTE ссылается на самого себя для генерации иерархических или последовательных данных. Он состоит из двух частей, соединённых UNION ALL: якорный запрос (базовый случай/отправная точка) и рекурсивный запрос (который ссылается на CTE и добавляет к результату). Рекурсия продолжается, пока рекурсивный запрос не вернёт ни одной строки. Используйте рекурсивные CTE для обхода деревьев (организационные диаграммы, файловые системы), генерации последовательностей и поиска путей в графах. Всегда включайте условие завершения в WHERE для предотвращения бесконечных циклов.
-- Recursive CTE: a CTE that references itself
-- Three parts: anchor, UNION ALL, recursive member
WITH RECURSIVE countdown(n) AS (
-- Anchor: starting point
SELECT 1 AS n
UNION ALL
-- Recursive member: references the CTE itself
SELECT n + 1 FROM countdown WHERE n < 10
)
SELECT n FROM countdown;
-- Result: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
-- PostgreSQL uses WITH RECURSIVE
-- SQL Server/MySQL 8+: WITH (RECURSIVE optional in MySQL)
-- SQLite: WITH RECURSIVEИерархические данные (орг-диаграмма)
Рекурсивные CTE превосходно справляются с обходом иерархических данных, таких как орг-диаграммы, деревья категорий или файловые системы. Якорь выбирает корневой узел; рекурсивный член соединяет таблицу с CTE по родительско-дочернему отношению (manager_id = id). Добавление столбца depth отслеживает, насколько глубоко находится каждая строка, а столбец path (конкатенация строк) показывает полную цепочку предков. Это заменяет необходимость в нескольких self-join или рекурсии на стороне приложения. CAST на path предотвращает ошибки типов во время рекурсии.
-- Employee hierarchy with manager relationships
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT REFERENCES employees(id)
);
-- Find all direct and indirect reports of employee 1 (CEO)
WITH RECURSIVE org_chain AS (
-- Anchor: the starting employee
SELECT id, name, manager_id, 0 AS depth, CAST(name AS VARCHAR(500)) AS path
FROM employees WHERE id = 1
UNION ALL
-- Recursive: find employees whose manager is in the chain
SELECT e.id, e.name, e.manager_id, oc.depth + 1,
CAST(oc.path || ' > ' || e.name AS VARCHAR(500))
FROM employees e
JOIN org_chain oc ON e.manager_id = oc.id
)
SELECT id, name, depth, path FROM org_chain
ORDER BY depth, name;
-- depth shows hierarchy level, path shows the management chainГенерация последовательностей и дат
Рекурсивные CTE могут генерировать последовательности и диапазоны дат — полезно для заполнения пробелов в отчётах временных рядов. Генерируя все даты в диапазоне и LEFT JOIN к вашим данным, вы обеспечиваете появление каждой даты в выводе, даже когда нет записей. Это распространённый шаблон для дашбордов и диаграмм. В PostgreSQL также есть generate_series() как более простая альтернатива. Всегда устанавливайте условие завершения (WHERE n < 100) для предотвращения беск онечной рекурсии. Некоторые базы данных ограничивают глубину рекурсии (например, 100 по умолчанию в MySQL через cte_max_recursion_depth).
-- Generate a series of numbers
WITH RECURSIVE numbers(n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT n FROM numbers;
-- Generate a date range (all days in January 2024)
WITH RECURSIVE dates(d) AS (
SELECT DATE '2024-01-01'
UNION ALL
SELECT d + INTERVAL '1 day' FROM dates
WHERE d < DATE '2024-01-31'
)
SELECT d FROM dates;
-- Generate a time series for gap-filling in reports
-- (ensures every date appears even if no data exists)
SELECT d.date, COALESCE(SUM(o.amount), 0) AS daily_total
FROM dates d
LEFT JOIN orders o ON o.order_date = d.date
GROUP BY d.date ORDER BY d.date;Поиск путей в графе (BFS)
Рекурсивные CTE могут выполнять поиск в ширину (BFS) по графовым структурам. Якорь находит рёбра от начального узла; рекурсивный член расширяет пути, соединяя рёбра с конечной точкой текущего пути. Предотвращение циклов критично в циклических графах — проверяйте, что узел назначения ещё не в пути (используя LIKE или строковый поиск). Ограничение hops — страховка против бесконечной рекурсии. Этот подход работает для поиска маршрутов, разрешения зависимостей и анализа сетей. Для взвешенных кратчайших путей рассмотрите алгоритм Дейкстры в коде приложения.
-- Find all paths in a directed graph
CREATE TABLE edges (src VARCHAR(10), dst VARCHAR(10));
-- Find all reachable nodes from 'A' with the path taken
WITH RECURSIVE paths AS (
-- Anchor: start from node A
SELECT src, dst, CAST(src || '->' || dst AS VARCHAR(1000)) AS path,
1 AS hops
FROM edges WHERE src = 'A'
UNION ALL
-- Recursive: extend the path
SELECT p.src, e.dst,
CAST(p.path || '->' || e.dst AS VARCHAR(1000)),
p.hops + 1
FROM paths p
JOIN edges e ON p.dst = e.src
WHERE p.hops < 10 -- prevent infinite loops in cyclic graphs
AND p.path NOT LIKE '%' || e.dst || '%'
)
SELECT DISTINCT path, hops FROM paths ORDER BY hops;
-- Cycle prevention: check the path doesn't already contain the nodeФакториал и агрегация с рекурсией
Рекурсивные CTE могут выполнять математические вычисления, такие как факториалы, перенося состояние (n, fact) через каждую итерацию. Якорь устанавливает базовый случай (0! = 1), а рекурсивный член вычисляет следующее значение из предыдущего. Нарастающие итоги также можно вычислять так, хотя оконные функции (SUM(amount) OVER (ORDER BY id)) более эффективны и идиоматичны для кумулятивных агрегатов. Рекурсивные CTE для вычислений в основном образовательные — используйте их, когда оконные функции или процедурный код не могут выразить логику. Каждый уровень рекурсии добавляет строку, поэтому набор результатов растёт с глубиной.
-- Compute factorial using recursive CTE
WITH RECURSIVE factorial(n, fact) AS (
-- Anchor: 0! = 1
SELECT 0, 1
UNION ALL
-- Recursive: n! = n * (n-1)!
SELECT n + 1, fact * (n + 1) FROM factorial WHERE n < 10
)
SELECT n, fact FROM factorial;
-- Running accumulation: cumulative sum
WITH RECURSIVE running_total AS (
SELECT id, amount, amount AS cumulative
FROM transactions WHERE id = 1
UNION ALL
SELECT t.id, t.amount, rt.cumulative + t.amount
FROM transactions t
JOIN running_total rt ON t.id = rt.id + 1
)
SELECT * FROM running_total ORDER BY id;
-- Note: window functions (SUM OVER) are usually better for thisPIVOT и UNPIVOT
PIVOT (строки в столбцы)
PIVOT преобразует строки в столбцы — идеально для кросс-таблиц, где нужны категории в качестве заголовков столбцов. Список IN указывает, какие значения становятся столбцами. SQL Server и Oracle имеют собственный синтаксис PIVOT. Внутренний запрос предоставляет исходные данные, а PIVOT применяет агрегат (SUM, AVG, COUNT) для каждой группы столбцов. Это эквивалентно условной агрегации, но более читаемо для широких pivot'ов. Используйте PIVOT, когда у вас есть фиксированный, известный набор значений для pivot'а.
-- Convert rows to columns (cross-tabulation)
-- Source: sales data with rows per quarter
CREATE TABLE sales (quarter VARCHAR(10), region VARCHAR(50), amount DECIMAL(10,2));
-- SQL Server PIVOT syntax:
SELECT region, [Q1], [Q2], [Q3], [Q4]
FROM (
SELECT quarter, region, amount FROM sales
) AS src
PIVOT (
SUM(amount) FOR quarter IN ([Q1], [Q2], [Q3], [Q4])
) AS pvt;
-- Result:
-- region | Q1 | Q2 | Q3 | Q4
-- North | 1000 | 1500 | 1200 | 1800
-- South | 800 | 1100 | 900 | 1300Условная агрегация (универсальный PIVOT)
Условная агрегация (SUM + CASE) — это универсальная техника pivot'а, работающая в любой SQL-базе данных. Каждое выражение CASE фильтрует по одной категории, а SUM агрегирует совпадающие значения. Это часто быстрее, чем PIVOT, и более гибко. ELSE 0 гарантирует, что несовпадающие строки вносят ноль. Функция crosstab() в PostgreSQL (из расширения tablefunc) более лаконична, но требует фиксированных выходных столбцов. Используйте условную агрегацию, когда нужна кросс-БД совместимость или синтаксис PIVOT недоступен.
-- Works in ALL databases (MySQL, PostgreSQL, SQLite, etc.)
SELECT
region,
SUM(CASE WHEN quarter = 'Q1' THEN amount ELSE 0 END) AS Q1,
SUM(CASE WHEN quarter = 'Q2' THEN amount ELSE 0 END) AS Q2,
SUM(CASE WHEN quarter = 'Q3' THEN amount ELSE 0 END) AS Q3,
SUM(CASE WHEN quarter = 'Q4' THEN amount ELSE 0 END) AS Q4,
SUM(amount) AS total
FROM sales
GROUP BY region
ORDER BY region;
-- PostgreSQL-specific: crosstab() from tablefunc extension
-- SELECT * FROM crosstab('SELECT region, quarter, amount FROM sales ORDER BY 1,2')
-- AS ct(region VARCHAR, Q1 DECIMAL, Q2 DECIMAL, Q3 DECIMAL, Q4 DECIMAL);Динамический PIVOT (динамический SQL)
Динамический SQL строит строку запроса во время выполнения, когда столбцы pivot'а неизвестны заранее (например, pivot по месяцам, когда месяцы различаются). Процесс: запрос уникальных значений, построение списка столбцов, конструирование оператора PIVOT и выполнение с sp_executesql (SQL Server) или PREPARE/EXECUTE (MySQL). Всегда очищайте с помощью QUOTENAME() или quote_ident() для предотвращения SQL-инъекций. Динамический SQL мощный, но добавляет сложность и риски безопасности — используйте умеренно и предпочитайте фиксированные pivot'ы, когда возможно. Pivot на стороне приложения часто более безопасная альтернатива.
-- When pivot columns are unknown at write time, use dynamic SQL
-- SQL Server example:
DECLARE @cols AS NVARCHAR(MAX),
@query AS NVARCHAR(MAX);
-- Build the column list dynamically
SELECT @cols = STUFF((
SELECT DISTINCT ',' + QUOTENAME(quarter)
FROM sales FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '');
-- Build and execute the pivot query
SET @query = 'SELECT region, ' + @cols + '
FROM (SELECT quarter, region, amount FROM sales) x
PIVOT (SUM(amount) FOR quarter IN (' + @cols + ')) p';
EXEC sp_executesql @query;
-- WARNING: dynamic SQL is vulnerable to SQL injection
-- — always use QUOTENAME() to sanitize column namesUNPIVOT (столбцы в строки)
UNPIVOT обращает PIVOT вспять — он преобразует столбцы в строки. Это полезно для нормализации денормализованных данных, преобразования широких файлов импорта в длинный формат или подготовки данных для диаграмм. SQL Server имеет собственный синтаксис UNPIVOT. Подход UNION ALL работает везде: каждый SELECT извлекает один столбец и помечает его фиксированным значением. UNION ALL (не UNION) сохраняет дубликаты и быстрее. UNPIVOT распространён в ETL-конвейерах, когда исходные данные приходят в формате таблицы (широкие), но должны храниться нормализованно (длинные).
-- Convert columns back to rows
-- Source: wide table with Q1-Q4 columns
-- Target: narrow table with quarter/amount rows
-- SQL Server UNPIVOT:
SELECT region, quarter, amount
FROM quarterly_sales
UNPIVOT (
amount FOR quarter IN (Q1, Q2, Q3, Q4)
) AS unpvt;
-- Universal UNION ALL approach (all databases):
SELECT region, 'Q1' AS quarter, Q1 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q2' AS quarter, Q2 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q3' AS quarter, Q3 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q4' AS quarter, Q4 AS amount FROM quarterly_sales;
-- Result: one row per region/quarter combinationПрактический пример отчёта с pivot
Этот реальный отчёт с pivot объединяет месячную детализацию со сравнением год-к-году в одном запросе. Условная агрегация (SUM + CASE) создаёт как месячные столбцы, так и годовые итоги. Столбец yoy_change вычисляет разницу инлайн. HAVING отфильтровывает продукты без продаж. Этот шаблон распространён в BI-дашбордах и финансовых отчётах. Функция EXTRACT работает в большинстве баз данных (используйте DATEPART в SQL Server, strftime в SQLite). Для действительно динамических столбцов комбинируйте с динамическим SQL или обрабатывайте pivot на уровне приложения.
-- Monthly sales pivot with year-over-year comparison
SELECT
product_name,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1 THEN amount ELSE 0 END) AS jan,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2 THEN amount ELSE 0 END) AS feb,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3 THEN amount ELSE 0 END) AS mar,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4 THEN amount ELSE 0 END) AS apr,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 5 THEN amount ELSE 0 END) AS may,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 6 THEN amount ELSE 0 END) AS jun,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2024 THEN amount ELSE 0 END) AS total_2024,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2023 THEN amount ELSE 0 END) AS total_2023,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2024 THEN amount ELSE 0 END) -
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2023 THEN amount ELSE 0 END) AS yoy_change
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE order_date BETWEEN '2023-01-01' AND '2024-12-31'
GROUP BY product_name
HAVING SUM(amount) > 0
ORDER BY total_2024 DESC;Полнотекстовый поиск
LIKE vs полнотекстовый поиск
LIKE '%pattern%' выполняет полное сканирование таблицы — не может использовать индексы и имеет O(n) на больших таблицах. Полнотекстовый поиск использует инвертированный индекс (слово → документ), делая поиск O(1) на термин. Полнотекстовый поиск также поддерживает ранжирование релевантн ости, стемминг и логические операторы. Используйте LIKE для простых префиксных совпадений (LIKE 'prefix%' может использовать B-tree индекс) или небольших таблиц. Используйте полнотекстовый поиск для поиска по статьям, описаниям продуктов или любому текстовому контенту. В каждой базе данных своя реализация (MySQL FULLTEXT, PostgreSQL tsvector, SQL Server CONTAINS).
-- LIKE: simple pattern matching (slow on large tables)
SELECT * FROM articles WHERE title LIKE '%database%';
SELECT * FROM articles WHERE title LIKE 'data%'; -- prefix match (can use index)
-- Full-Text Search: indexed, fast, supports ranking
-- MySQL:
CREATE FULLTEXT INDEX ft_title_body ON articles(title, body);
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('database performance' IN NATURAL LANGUAGE MODE);
-- With relevance score:
SELECT id, title,
MATCH(title, body) AGAINST('database performance') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST('database performance')
ORDER BY relevance DESC;
-- LIKE '%word%' scans every row (no index usage)
-- Full-text search uses an inverted index — much fasterПолнотекстовый поиск PostgreSQL (tsvector)
В PostgreSQL самая мощная встроенная полнотекстовая реализация. tsvector — предтокенизированное, нормализованное представление документа (в нижнем регистре, со стеммингом, без стоп-слов). tsquery — поисковое выражение, поддерживающее & (AND), | (OR), ! (NOT). Оператор @@ сопоставляет tsvector с tsquery. GIN-индексы делают поиск чрезвычайно быстрым. ts_rank оценивает результаты по частоте терминов. Используйте to_tsvector для индексации, plainto_tsquery для пользовательского ввода (обрабатывае т обычные слова), phraseto_tsquery для точного фразового совпадения. Это соперничает со специализированными поисковыми движками для многих случаев.
-- PostgreSQL uses tsvector (document) and tsquery (search)
-- Create a search-optimized column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
CREATE INDEX idx_search ON articles USING GIN(search_vector);
-- Populate the search vector (tokenize + normalize)
UPDATE articles SET search_vector =
to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''));
-- Search with tsquery
SELECT id, title,
ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('english', 'database & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- ts_rank_cd gives coverage density ranking
-- plainto_tsquery: plain text → tsquery (user-friendly)
-- phraseto_tsquery: exact phrase matchingПоиск в логическом режиме
Логический режим даёт точный контроль над критериями поиска. В MySQL + требует термин, - исключает его, * — префиксный wildcard, а кавычки сопоставляют точные фразы. В PostgreSQL & | ! — операторы, а <-> сопоставляет соседние слова (фразовый поиск). Логический поиск идеален для расширенных поисковых интерфейсов, где пользователи указывают точные требования. Обратите внимание, что логический режим MySQL не вычисляет оценки релевантности по умолчанию — комбинируйте с NATURAL LANGUAGE MODE для ранжирования. Поиск с wildcards (data*) может быть медленнее, так как сопоставляет множество терминов.
-- MySQL boolean full-text search
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+database +performance' IN BOOLEAN MODE);
-- + = must contain, - = must NOT contain, * = wildcard
-- "" = exact phrase, () = grouping, ~ = fuzzy (negation)
-- Examples:
-- '+MySQL -Oracle' → must have MySQL, must NOT have Oracle
-- '"full text"' → exact phrase "full text"
-- 'data*' → words starting with "data" (database, dataset)
-- '+database >performance' → database required, performance boosts rank
-- PostgreSQL boolean operators in tsquery:
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'database & !oracle');
-- & = AND, | = OR, ! = NOT, <-> = follows (phrase)Поиск с подсветкой и сниппетами
Подсветка результатов поиска показывает пользователям, почему документ совпал, отображая сниппет с выделенными поисковыми терминами. ts_headline в PostgreSQL генерирует контекстно-зависимый отрывок с настраиваемой длиной (MaxWords, MinWords) и тегами подсветки. В MySQL нет встроенной подсветки — используйте SUBSTRING с LOCATE для извлечения контекста вручную. CONTAINSTABLE в SQL Server возвращает таблицу ранжирования, которую вы join'ите обратно к источнику. Подсветка улучшает UX в поиск овых интерфейсах, показывая контекст релевантности. Для продакшн-поиска рассмотрите специализированные инструменты вроде Elasticsearch или Meilisearch для расширенных функций.
-- PostgreSQL: highlight matching terms
SELECT id, title,
ts_headline('english', body, query, 'MaxWords=35, MinWords=15') AS snippet
FROM articles, to_tsquery('english', 'database') query
WHERE search_vector @@ query;
-- ts_headline returns a text snippet with matches highlighted
-- (wrapped in <b> tags by default, customizable)
-- MySQL: no built-in highlighting, but you can use SUBSTRING:
SELECT id,
SUBSTRING(body, GREATEST(1, LOCATE('database', body) - 50), 100) AS snippet
FROM articles
WHERE MATCH(body) AGAINST('database' IN NATURAL LANGUAGE MODE);
-- SQL Server CONTAINSTABLE with ranking:
-- SELECT a.id, a.title, KEY_TBL.RANK
-- FROM articles a
-- INNER JOIN CONTAINSTABLE(articles, body, 'database') AS KEY_TBL
-- ON a.id = KEY_TBL.[KEY]
-- ORDER BY KEY_TBL.RANK DESC;Trigram-поиск (нечёткое сопоставление)
Trigram-поиск разбивает текст на 3-символьные последовательности и сравнивает совпадения — обеспечивая нечёткое, толерантное к опечаткам сопоставление, которое LIKE не может делать. Расширение pg_trgm (PostgreSQL) предоставляет similarity() (оценка 0-1) и оператор % (совпадения выше порога). Это обеспечивает автодополнение, предложения 'вы имели в виду?' и исправление опечаток. GIN-индексы с gin_trgm_ops делают нечёткий поиск быстрым. Trigram'ы обрабатывают опечатки ('databse' → 'database'), которые точный полнотекстовый поиск пропустил бы. similarity_threshold контролирует, насколько свободно сопоставление — более низкие значения совпадают с большим количеством результатов, но с большим количеством ложноположительных.
-- PostgreSQL pg_trgm extension: fuzzy/typo-tolerant search
CREATE EXTENSION pg_trgm;
-- similarity() returns 0-1 based on trigram overlap
SELECT name, similarity(name, 'database') AS sim
FROM products
WHERE name % 'database' -- % operator: trigram similarity threshold
ORDER BY sim DESC;
-- Set similarity threshold (0-1)
SET pg_trgm.similarity_threshold = 0.3;
-- word_similarity: partial word matching
SELECT name, word_similarity('database', name) AS sim
FROM products WHERE name <% 'database';
-- Create a GIN index for fast fuzzy search
CREATE INDEX idx_products_trgm ON products USING GIN(name gin_trgm_ops);
-- Great for: autocomplete, typo correction, "did you mean?" featuresТриггеры
Основы триггеров (AFTER/BEFORE)
Триггеры — код на уровне базы данных, который выполняется автоматически при изменении данных. AFTER-триггеры логируют или распространяют изменения (не могут изменять NEW). BEFORE-триггеры проверяют или преобразуют данные перед записью (могут изменять NEW). FOR EACH ROW срабатывает один раз для каждой затронутой строки; FOR EACH STATEMENT — один раз для оператора. Используйте триггеры для журналирования аудита, обеспечения сложных ограничений и автообновления производных столбцов. Избегайте триггеров для бизнес-логики — они скрыты, трудны для отладки и могут вызывать каскадные эффекты. В разных базах данных разный синтаксис триггеров; PostgreSQL использует функции как тела триггеров.
-- A trigger fires automatically on INSERT/UPDATE/DELETE
-- MySQL syntax:
DELIMITER //
CREATE TRIGGER audit_log
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_table (table_name, action, row_id, changed_at)
VALUES ('employees', 'INSERT', NEW.id, NOW());
END //
DELIMITER ;
-- BEFORE triggers can modify the NEW values:
CREATE TRIGGER validate_email
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.email NOT LIKE '%@%.%' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid email';
END IF;
END //
-- PostgreSQL uses CREATE FUNCTION + CREATE TRIGGER:
-- CREATE TRIGGER audit AFTER INSERT ON employees
-- FOR EACH ROW EXECUTE FUNCTION audit_func();Триггер журналирования аудита
Аудит-триггеры фиксируют каждое изменение данных для соответствия требованиям и отладки. Аудит-таблица хранит тип действия, старые и новые значения, кто внёс изменение (CURRENT_USER) и когда (CURRENT_TIMESTAMP). Нужны отдельные триггеры для INSERT, UPDATE и DELETE. OLD ссылается на значения до изменения (доступно в UPDATE/DELETE), NEW — на значения после изменения (доступно в INSERT/UPDATE). Аудит-таблицы растут бесконечно — разделяйте по датам или архивируйте старые данные. Этот шаблон удовлетворяет требованиям SOX, HIPAA и GDPR по отслеживанию изменений данных.
-- Track all changes to a critical table
CREATE TABLE employee_audit (
audit_id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(10), -- INSERT, UPDATE, DELETE
employee_id INT,
old_name VARCHAR(100),
new_name VARCHAR(100),
old_salary DECIMAL(10,2),
new_salary DECIMAL(10,2),
changed_by VARCHAR(50) DEFAULT CURRENT_USER,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_audit_insert
AFTER INSERT ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, new_name, new_salary)
VALUES ('INSERT', NEW.id, NEW.name, NEW.salary);
CREATE TRIGGER trg_audit_update
AFTER UPDATE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, new_name, old_salary, new_salary)
VALUES ('UPDATE', NEW.id, OLD.name, NEW.name, OLD.salary, NEW.salary);
CREATE TRIGGER trg_audit_delete
AFTER DELETE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, old_salary)
VALUES ('DELETE', OLD.id, OLD.name, OLD.salary);Триггер вычисляемого/производного столбца
Триггеры могут автоматически вычислять производные столбцы, обеспечивая согласованность без кода приложения. BEFORE INSERT/UPDATE-триггеры устанавливают NEW.final_price на основе других столбцов. Однако современные базы данных поддерживают GENERATED (вычисляемые) столбцы нативно — они всегда корректны, не могут быть вручную переопределены и могут быть индексированы. Предпочитайте GENERATED-столбцы триггерам для вычисляемых значений. Используйте триггеры только когда вычисление включает внешние данные, условную логику или межтабличные зависимости, которые GENERATED-столбцы не могут обработать. Помните, что триггеры добавляют накладные расходы к каждой операции записи.
-- Auto-update a derived column when source data changes
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL(10,2),
discount_percent DECIMAL(5,2),
final_price DECIMAL(10,2) -- computed: price * (1 - discount/100)
);
-- BEFORE INSERT: compute final_price
CREATE TRIGGER calc_final_price_insert
BEFORE INSERT ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);
-- BEFORE UPDATE: recompute if price or discount changes
CREATE TRIGGER calc_final_price_update
BEFORE UPDATE ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);
-- Alternative: use GENERATED columns (MySQL 5.7+, PostgreSQL 12+)
-- final_price DECIMAL(10,2) GENERATED ALWAYS AS
-- (price * (1 - discount_percent / 100)) STOREDПредотвращение удалений триггерами
Триггеры могут обеспечивать правила защиты данных, которые CHECK-ограничения не могут выразить. BEFORE DELETE-триггеры могут полностью блокировать удаления (используя SIGNAL/RAISE) или реализовывать мягкие удаления (помечая записи как удалённые вместо их удаления). SIGNAL SQLSTATE '45000' — способ MySQL вызвать пользовательскую ошибку. PostgreSQL использует RAISE EXCEPTION. Это полезно для защиты справочных данных, предотвращения удаления родительских записей с дочерними или реализации неизменяемых аудиторских следов. Будьте осторожны: триггеры, предотвращающие операции, могут удивить разработчиков — чётко документируйте их и рассмотрите проверки на уровне приложения.
-- Prevent deletion of critical records
CREATE TRIGGER prevent_delete_admin
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
IF OLD.role = 'admin' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot delete admin users';
END IF;
END //
-- Soft delete instead of hard delete
CREATE TRIGGER soft_delete
BEFORE DELETE ON articles
FOR EACH ROW
BEGIN
-- Prevent actual deletion, mark as deleted instead
INSERT INTO articles (id, title, body, deleted_at)
VALUES (OLD.id, OLD.title, OLD.body, NOW())
ON DUPLICATE KEY UPDATE deleted_at = NOW();
-- Still need to prevent the DELETE — use SIGNAL or a flag
END //
-- PostgreSQL: raise exception in trigger function
-- IF OLD.role = 'admin' THEN
-- RAISE EXCEPTION 'Cannot delete admin users';
-- END IF;Управление триггерами и отладка
Управление триггерами необходимо для поддержки. SHOW TRIGGERS (MySQL) и представления information_schema перечисляют все триггеры. Удаляйте триггеры с помощью DROP TRIGGER IF EXISTS. Временное отключение триггеров полезно для массовых загрузок данных (которые иначе вызвали бы дорогое журналирование аудита для каждой строки). PostgreSQL использует ALTER TABLE ... DISABLE/ENABLE TRIGGER; SQL Server использует DISABLE/ENABLE TRIGGER. Всегда включайте триггеры обратно после обслуживания. Отладка триггеров сложна — они выполняются молча. Добавьте журналирование в отладочную таблицу или сначала протестируйте логику триггера изолированно. Чрезмерные триггеры создают скрытую сложность и проблемы производительности.
-- View existing triggers
-- MySQL:
SHOW TRIGGERS;
SELECT * FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'mydb';
-- PostgreSQL:
SELECT tgname, tgrelid::regclass, tgtype FROM pg_trigger;
-- SQL Server:
SELECT name, type_desc FROM sys.triggers WHERE parent_id = OBJECT_ID('employees');
-- Drop a trigger
DROP TRIGGER IF EXISTS audit_log; -- MySQL
DROP TRIGGER IF EXISTS audit_log ON employees; -- PostgreSQL
-- Disable/enable triggers (PostgreSQL)
ALTER TABLE employees DISABLE TRIGGER ALL;
ALTER TABLE employees ENABLE TRIGGER ALL;
-- Disable for bulk operations (SQL Server)
DISABLE TRIGGER trg_audit ON employees;
-- ... bulk operations ...
ENABLE TRIGGER trg_audit ON employees;Пользовательские функции (UDF)
Скалярные функции (возвращают одно значение)
Скалярные UDF возвращают одно значение и могут использоваться в SELECT, WHERE и вычисляемых столбцах. DETERMINIC означает, что выход зависит только от входов (включает кэширование). READS SQL DATA объявляет, что функция читает из таблиц. UDF инкапсулируют переиспользуемую логику (скидки, форматирование, вычисления) для согласованности между запросами. Однако скалярные UDF в SQL Server могут вызывать проблемы производительности (построчное выполнение) — используйте встраиваемые табличные функции или вычисляемые столбцы, когда возможно. MySQL 8.0+ лучше оптимизирует детерминированные функции. Всегда документируйте назначение и параметры функции.
-- MySQL: a function that returns one value
CREATE FUNCTION calculate_discount(
price DECIMAL(10,2),
customer_tier VARCHAR(20)
) RETURNS DECIMAL(10,2)
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE discount_rate DECIMAL(5,2);
SET discount_rate = CASE customer_tier
WHEN 'gold' THEN 0.20
WHEN 'silver' THEN 0.10
WHEN 'bronze' THEN 0.05
ELSE 0.00
END;
RETURN price * (1 - discount_rate);
END //
-- Usage in queries:
SELECT name, price, calculate_discount(price, tier) AS final_price
FROM orders;
-- DETERMINISTIC: same inputs always give same output (cacheable)
-- READS SQL DATA: function reads but doesn't modify tablesТабличные функции (возвращают строки)
Табличные функции (TVF) возвращают набор результатов (строки), который можно запросить как таблицу. Встраиваемые TVF (SQL Server) так же быстры, как представления — оптимизатор запросов инлайнит их. Многооператорные TVF материализуют результаты во временную таблицу сначала, что может быть медленнее. Функции PostgreSQL, возвращающие TABLE или SETOF, эквивалентны. TVF — параметризованные представления — используйте их, когда нужно представление с параметрами. Они от лично подходят для инкапсуляции сложных JOIN и фильтров. Предпочитайте встраиваемые TVF многооператорным для производительности. В PostgreSQL также рассмотрите параметризованные представления с предложениями WHERE.
-- SQL Server: Inline table-valued function (fast, like a view)
CREATE FUNCTION fn_OrdersByCustomer(@cust_id INT)
RETURNS TABLE
AS
RETURN (
SELECT o.id, o.order_date, o.total
FROM orders o
WHERE o.customer_id = @cust_id
);
-- Usage: SELECT * FROM fn_OrdersByCustomer(42);
-- PostgreSQL: function returning a table
CREATE OR REPLACE FUNCTION get_orders_by_customer(cust_id INT)
RETURNS TABLE(order_id INT, order_date DATE, total DECIMAL) AS $$
SELECT id, order_date, total
FROM orders
WHERE customer_id = cust_id;
$$ LANGUAGE SQL;
-- Usage:
SELECT * FROM get_orders_by_customer(42);
-- Multi-statement TVF (SQL Server) — slower, materializes result
-- CREATE FUNCTION fn_ComplexReport(@date DATE)
-- RETURNS @result TABLE (...)
-- AS BEGIN ... INSERT INTO @result ... RETURN ENDФункции обработки строк
Пользовательские строковые функции инкапсулируют логику обработки текста, которую встроенные функции не покрывают. Функция get_first_name использует LOCATE и SUBSTRING для извлечения первого слова. Функция make_slug цепочкой LOWER, REPLACE и REGEXP_REPLACE создаёт URL-дружественные слаги. Помечайте их DETERMINISTIC, так как одинаковый вход всегда даёт одинаковый выход. Строковые функции в SQL зависят от базы данных — PostgreSQL имеет split_part(), MySQL имеет SUBSTRING_INDEX(). Создание UDF стандартизирует поведение в п риложении. Учтите, что сложная обработка строк в SQL часто чище в коде приложения.
-- Create a function to split full name into parts
CREATE FUNCTION get_first_name(full_name VARCHAR(200))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
DECLARE space_pos INT;
SET space_pos = LOCATE(' ', full_name);
IF space_pos > 0 THEN
RETURN SUBSTRING(full_name, 1, space_pos - 1);
ELSE
RETURN full_name;
END IF;
END //
-- Function to generate slug from a title
CREATE FUNCTION make_slug(title VARCHAR(500))
RETURNS VARCHAR(500)
DETERMINISTIC
BEGIN
DECLARE slug VARCHAR(500);
SET slug = LOWER(title);
SET slug = REPLACE(slug, ' ', '-');
SET slug = REGEXP_REPLACE(slug, '[^a-z0-9-]', '');
RETURN slug;
END //
-- Usage:
SELECT get_first_name('John Doe Smith') AS first_name; -- John
SELECT make_slug('Hello World! 2024') AS slug; -- hello-world-2024Агрегатные функции (пользовательские)
Пользовательские агрегатные функции позволяют определять новую логику агрегации помимо SUM, AVG, COUNT. CREATE AGGREGATE в PostgreSQL требует функцию перехода состояния (SFUNC, вызывается для каждой строки) и финальную функцию (FINALFUNC, вызывается один раз в конце). Этот пример вычисляет среднее геометрическое (корень n-й степени из произведения). Пользовательские агрегаты мощны для статистических, финансовых или специфичных для предметной области вычислений. Состояние накапливается по строкам; финальная функция вычисляет результат. MySQL и SQL Server не поддерживают пользовательские агрегаты напрямую — используйте хранимые процедуры или вычисления на стороне приложения.
-- PostgreSQL: custom aggregate function
-- Step 1: Define a state transition function
CREATE OR REPLACE FUNCTION geom_mean_state(state numeric[], val numeric)
RETURNS numeric[] AS $$
BEGIN
IF val IS NULL THEN RETURN state; END IF;
IF state IS NULL THEN
RETURN ARRAY[val];
ELSE
RETURN state || val;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Step 2: Define the final function
CREATE OR REPLACE FUNCTION geom_mean_final(vals numeric[])
RETURNS numeric AS $$
BEGIN
IF vals IS NULL OR array_length(vals, 1) IS NULL THEN
RETURN NULL;
END IF;
RETURN exp(avg(ln(v)) FROM unnest(vals) AS v);
END;
$$ LANGUAGE plpgsql;
-- Step 3: Create the aggregate
CREATE AGGREGATE geometric_mean(numeric) (
SFUNC = geom_mean_state,
STYPE = numeric[],
FINALFUNC = geom_mean_final,
INITCOND = '{}'
);
-- Usage:
SELECT category, geometric_mean(price) FROM products GROUP BY category;Функция vs хранимая процедура
Функции и хранимые процедуры служат разным целям. Функции возвращают значение и могут быть встроены в SELECT/WHERE — они должны быть детерминированными (без побочных эффектов в большинстве баз данных). Хранимые процедуры могут изменять данные, управлять транзакциями и возвращать несколько наборов результатов — но не могут использоваться внутри запросов (вызываются через CALL/EXEC). Используйте функции для вычислений и извлечения данных; процедуры для многошаг овых операций (переводы, пакетная обработка, ETL). Функции компонуемы; процедуры императивны. В PostgreSQL функции могут делать почти всё, что могут процедуры (включая изменение данных), размывая различие.
-- FUNCTIONS: return values, usable in queries, no side effects
CREATE FUNCTION get_full_name(fname VARCHAR, lname VARCHAR)
RETURNS VARCHAR(200)
DETERMINISTIC
RETURN CONCAT(fname, ' ', lname);
-- Can be used in SELECT:
SELECT get_full_name(first, last) FROM users; -- OK
SELECT * FROM users WHERE get_full_name(first, last) LIKE 'J%'; -- OK
-- STORED PROCEDURES: can modify data, use transactions, return result sets
CREATE PROCEDURE transfer_funds(
IN from_acct INT, IN to_acct INT, IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount WHERE id = from_acct;
UPDATE accounts SET balance = balance + amount WHERE id = to_acct;
INSERT INTO transfers (from_id, to_id, amount) VALUES (from_acct, to_acct, amount);
COMMIT;
END //
-- Call procedure (can't use in SELECT):
CALL transfer_funds(1, 2, 100.00);Проектирование баз данных и нормализация
Первая нормальная форма (1NF)
Первая нормальная форма требует атомарных значений — каждая ячейка содержит один элемент данных, а не списки или массивы. Значения, разделённые запятыми в столбце, нарушают 1NF, так как нельзя запросить, индексировать или обновить отдельные элементы. Решение: создайте одну строку на элемент (с композитным первичным ключом) или разделите в отдельную детальную таблицу. 1NF также требует первичный ключ для уникальной идентификации каждой строки. Нарушение 1NF делает запросы вроде 'найти все заказы, содержащие мышь' требующими строкового парсинга — медленно и подвержено ошибкам. Всегда начинайте с соответствия 1NF.
-- 1NF: each column contains atomic (indivisible) values
-- no repeating groups, each row is unique
-- VIOLATION: comma-separated values in one column
CREATE TABLE bad_orders (
id INT,
customer_name VARCHAR(100),
products VARCHAR(500) -- "laptop, mouse, keyboard" — BAD!
);
-- 1NF COMPLIANT: separate row per product
CREATE TABLE orders (
order_id INT,
customer_name VARCHAR(100),
product_name VARCHAR(100), -- one product per row
PRIMARY KEY (order_id, product_name) -- composite key for uniqueness
);
-- Better: normalize further with separate tables
CREATE TABLE orders (order_id INT PRIMARY KEY, customer_name VARCHAR(100));
CREATE TABLE order_items (
order_id INT REFERENCES orders(order_id),
product_name VARCHAR(100),
quantity INT,
PRIMARY KEY (order_id, product_name)
);Вторая и третья нормальные формы (2NF, 3NF)
2NF устраняет частичные зависимости — каждый неключевой столбец должен зависеть от ВСЕГО первичного ключа, а не только от его части. Это важно только с композитными ключами. 3NF устраняет транзитивные зависимости — неключевые столбцы должны зависеть только от первичного ключа, а не от других неключевых столбцов. Например, customer_name зависит от customer_id, который зависит от order_id (транзитивно). Нормализация снижает избыточность данных (храните каждый факт один раз) и аномалии (обновляйте имя клиента в одном месте, а не в каждом заказе). Большинство практических баз данных стремятся к 3NF или BCNF.
-- 2NF: 1NF + no partial dependencies (non-key attrs depend on FULL key)
-- Problem: order_items has composite key (order_id, product_id)
-- but product_name depends only on product_id (partial dependency)
-- VIOLATION of 2NF:
CREATE TABLE bad_order_items (
order_id INT,
product_id INT,
product_name VARCHAR(100), -- depends on product_id only!
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- 2NF COMPLIANT: move product_name to products table
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100)
);
CREATE TABLE order_items (
order_id INT,
product_id INT REFERENCES products(product_id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- 3NF: 2NF + no transitive dependencies
-- (non-key attrs don't depend on other non-key attrs)
-- Problem: orders table has customer_name that depends on customer_id
-- Solution: separate customers table (see above)Денормализация (когда нарушать правила)
Денормализация намеренно нарушает нормальные формы для улучшения производительности чтения за счёт сложности записи и хранения. В нормализованных базах данных извлечение полного заказа требует 4 JOIN — дорого для высоконагруженных дашбордов. Денормализованные таблицы предварительно соединяют и вычисляют данные для быстрого чтения. Компромисс: записи должны обновлять несколько мест (риск несогласованности), а хранение увеличивается. Используйте денормализацию для систем с интенсивным чтением (аналитика, отчётность, хранилища данных). Материализованные представления обеспечивают управляемую денормализацию — база данных обрабатывает обновление. OLTP-системы должны оставаться нормализованными; OLAP-системы обычно денормализованы (схемы звезда/снежинка).
-- Denormalization: intentionally adding redundancy for performance
-- Normalized (3NF): requires JOINs to get full order info
SELECT o.order_id, c.name, p.product_name, oi.quantity, p.price
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- Denormalized: store redundant data for read speed
CREATE TABLE order_summary (
order_id INT PRIMARY KEY,
customer_id INT,
customer_name VARCHAR(100), -- redundant (also in customers)
customer_email VARCHAR(200), -- redundant
total_amount DECIMAL(10,2), -- pre-calculated
item_count INT, -- pre-calculated
order_date TIMESTAMP
);
-- Trade-off: faster reads, slower writes, risk of inconsistency
-- Use for: reporting tables, read-heavy dashboards, data warehouses
-- Materialized views are a managed form of denormalization:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT date, region, SUM(amount) AS total FROM sales GROUP BY date, region;
REFRESH MATERIALIZED VIEW sales_summary;Первичные ключи, внешние ключи и ограничения
Ограничения обеспечивают целостность данных на уровне базы данных. PRIMARY KEY уникально идентифицирует строки и создаёт кластерный индекс. UNIQUE предотвращает дубликаты (допускает несколько NULL в большинстве баз данных). CHECK обеспечивает пользовательские правила (salary > 0). FOREIGN KEY поддерживает ссылочную целостность — ON DELETE SET NULL/CASCADE/RESTRICT контролирует, что происходит при удалении родительской строки. ON UPDATE CASCADE распространяет изменения PK на FK. Ограничения — последняя линия защиты от плохих данных — даже если в коде приложения есть ошибки, база данных отклоняет некорректные данные. Всегда определяйте ограничения; они — документация и обеспечение вместе.
CREATE TABLE departments (
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(100) NOT NULL UNIQUE,
budget DECIMAL(12,2) CHECK (budget >= 0),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE,
dept_id INT,
salary DECIMAL(10,2) CHECK (salary > 0 AND salary < 1000000),
hire_date DATE NOT NULL,
manager_id INT REFERENCES employees(emp_id), -- self-reference
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL -- don't delete dept if employees exist
ON UPDATE CASCADE, -- update FK if dept_id changes
INDEX idx_dept (dept_id),
INDEX idx_email (email)
);
-- Constraint types:
-- PRIMARY KEY: unique + not null (clustered index)
-- UNIQUE: no duplicates (allows NULLs)
-- NOT NULL: required field
-- CHECK: custom condition
-- FOREIGN KEY: referential integrity
-- DEFAULT: value when not specifiedСтратегия индексирования
Индексы значительно ускоряют чтение, но замедляют запись (каждый индекс должен обновляться при INSERT/UPDATE/DELETE). B-tree индексы поддерживают равенство, диапазон и сортировку. Композитные индексы следуют правилу крайнего левого префикса — можно использовать (a, b) для запросов по a или a+b, но не по b отдельно. Покрывающие индексы (предложение INCLUDE) хранят дополнительные столбцы, чтобы запрос не касался таблицы — крайне быстро. Частичные индексы индексируют только подмножество строк, экономя место. Мониторьте использование индексов (pg_stat_user_indexes в PostgreSQL) и удаляйте неиспользуемые. Хорошее правило: индексируйте внешние ключи и столбцы в WHERE/JOIN. Чрезмерное индексирование вредит производительности записи и тратит хранилище.
-- B-tree index (default): good for =, <, >, BETWEEN, ORDER BY
CREATE INDEX idx_last_name ON employees(last_name);
-- Composite index: order matters! (leftmost prefix rule)
CREATE INDEX idx_dept_salary ON employees(dept_id, salary);
-- Usable for: WHERE dept_id = 5
-- Usable for: WHERE dept_id = 5 AND salary > 50000
-- NOT usable for: WHERE salary > 50000 (skips dept_id)
-- Covering index: includes all columns a query needs
CREATE INDEX idx_covering ON orders(customer_id, order_date)
INCLUDE (total_amount, status);
-- Query can be satisfied from index alone (no table lookup)
-- Partial/partial index: index only matching rows
CREATE INDEX idx_active_users ON users(last_login)
WHERE active = true; -- PostgreSQL
-- Don't over-index: every index slows writes
-- Index columns used in: WHERE, JOIN, ORDER BY, GROUP BY
-- Drop unused indexes: SELECT * FROM pg_stat_user_indexes;EXPLAIN и оптимизация запросов
Чтение вывода EXPLAIN
EXPLAIN показывает, как база данных выполняет запрос — какие индексы используются, как таблицы соединяются и сколько строк проверяется. EXPLAIN ANALYZE (PostgreSQL) или EXPLAIN с выполнением (MySQL 8.0+) фактически запускает запрос и показывает реальные тайминги. Ищите: Seq Scan / ALL (полное сканирование таблицы — плохо для больших таблиц), Index Scan (хорошо), оценку строк (высокая = дорого). 'Using filesort' или 'Using temporary' в MySQL указывает на дополнительную работу. Если EXPLAIN показывает полное сканирование таблицы на большой таблице, нужен индекс. Всегда выполняйте EXPLAIN перед оптимизацией — не угадывайте.
-- EXPLAIN shows the query execution plan
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
-- EXPLAIN ANALYZE actually runs the query (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
-- PostgreSQL output:
-- Index Scan using idx_customer on orders (cost=0.29..8.31 rows=1 width=74)
-- Index Cond: (customer_id = 42)
-- Execution Time: 0.042 ms
-- Key columns to check:
-- - type: scan method (const > eq_ref > ref > range > index > ALL)
-- - rows: estimated rows examined (lower is better)
-- - key: which index is used (NULL = no index, bad!)
-- - Extra: "Using filesort" or "Using temporary" = warning signs
-- MySQL: EXPLAIN FORMAT=JSON for detailed output
-- SQL Server: SET SHOWPLAN_TEXT ON; or Actual Execution Plan in SSMSРаспространённые проблемы производительности
Несколько распространённых шаблонов препятствуют использованию индексов и вызывают полное сканирование таблицы. Функции на индексированных столбцах (YEAR(date), UPPER(name)) препятствуют использованию индексов — перепишите как условия диапазона. Ведущие wildcards в LIKE ('%pattern') не могут использовать B-tree индексы — используйте полнотекстовый поиск. SELECT * тратит пропускную способность и препятствует оптимизации покрывающего индекса. Неявные преобразования типов (сравнение строкового столбца с целым) могут отключить индексы. Условия OR иногда менее эффективны, чем IN. Всегда проверяйте с EXPLAIN, что ваши индексы фактически используются — неиспользуемый индекс — wasted storage и накладные расходы на запись.
-- 1. Missing index → full table scan
-- Bad: SELECT * FROM orders WHERE customer_id = 42; (no index)
-- Fix: CREATE INDEX idx_customer ON orders(customer_id);
-- 2. Index not used due to function on column
-- Bad: WHERE YEAR(order_date) = 2024 (function prevents index use)
-- Good: WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
-- 3. SELECT * instead of specific columns
-- Bad: SELECT * FROM large_table WHERE id = 1;
-- Good: SELECT id, name, email FROM large_table WHERE id = 1;
-- 4. OR conditions preventing index use
-- Bad: WHERE dept = 'A' OR dept = 'B' OR dept = 'C'
-- Good: WHERE dept IN ('A', 'B', 'C')
-- 5. LIKE with leading wildcard
-- Bad: WHERE name LIKE '%son' (can't use index)
-- OK: WHERE name LIKE 'John%' (can use index)
-- 6. Implicit type conversion
-- Bad: WHERE string_column = 123 (converts to string, skips index)
-- Good: WHERE string_column = '123'Оптимизация JOIN
Оптимизация JOIN критична для запросов к нескольким таблицам. Убедитесь, что столбцы соединения (обычно внешние ключи) индексированы — неиндексированные join'ы вызывают вложенные циклы сканирования (O(n*m)). Оптимизатор запросов обычно выбирает лучший порядок join, но вы можете помочь, фильтруя рано (концептуально WHERE перед JOIN). INNER JOIN быстрее OUTER JOIN, когда не нужны несовпадающие строки. EXISTS часто эффективнее IN для коррелированных подзапросов, так как он короткозамкнут на первом совпадении. Избегайте join'а таблиц, которые не нужны — каждый join умножает работу. Для сложных отчётов рассмотрите материализованные представления или предварительно агрегированные сводные таблицы.
-- Join order matters: smallest table first (optimizer usually handles this)
-- Ensure join columns are indexed (usually foreign keys)
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);
-- Use INNER JOIN when you don't need unmatched rows (faster than OUTER)
SELECT c.name, COUNT(o.order_id) AS order_count
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;
-- Avoid joining unnecessary tables — fetch details lazily if needed
-- Bad: join 5 tables when you only need 2 columns
-- Good: split into simpler queries or use a covering index
-- EXISTS vs IN for subqueries
-- EXISTS is often faster for large subquery results:
SELECT name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- IN is better for small lists:
SELECT * FROM orders WHERE customer_id IN (1, 2, 3);Оптимизация пагинации
Пагинация на основе OFFSET (LIMIT 10 OFFSET 10000) имеет сложность O(n) — база данных должна сканировать и отбросить все пропущенные строки, делая глубокие страницы крайне медленными. Пагинация по ключу (курсором) использует WHERE last_value < cursor для прямого поиска — O(1) независимо от глубины страницы. Это требует индекса на столбце сортировки. Для связей (одинаковая метка времени) используйте композитный курсор (created_at, id). Избегайте COUNT(*) для общего количества на больших таблицах — он сканирует всю таблицу. Используйте приблизительные подсчёты (pg_class.reltuples в PostgreSQL) или вообще не показывайте общее количество (бесконечная прокрутка). Пагинация по ключу — стандарт для высокопроизводительных API.
-- Bad: OFFSET pagination (slow for large offsets)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 10000;
-- Must scan and discard 10000 rows — gets slower as you page deeper
-- Good: Keyset (cursor) pagination using WHERE
SELECT * FROM orders
WHERE created_at < '2024-01-15 10:30:00' -- last seen value
ORDER BY created_at DESC
LIMIT 10;
-- Uses index efficiently — constant time regardless of page depth
-- For composite keyset pagination (handles ties):
SELECT * FROM orders
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 10;
-- Count total (expensive on large tables — avoid if possible)
-- Instead of COUNT(*), use an approximate count:
SELECT reltuples::bigint FROM pg_class WHERE relname = 'orders';Переписывание запросов и чек-лист оптимизации
Оптимизация запросов — итеративный процесс: EXPLAIN, идентификация узких мест, переписывание, повтор. Ключевые техники: заменяйте IN-подзапросы на JOIN (часто быстрее), используйте UNION ALL вместо UNION (пропускает сортировку дедупликации), пакетные INSERT (1 запрос против 1000), используйте подготовленные операторы (кэширует план запроса). CTE улучшают читаемость, но в старых версиях PostgreSQL они материализуются (не могут быть оптимизированы) — PostgreSQL 12+ инлайнит их. Поддерживайте актуальную статистику таблиц (ANALYZE), чтобы планировщик принимал хорошие решения. Золотое правило: измеряйте с EXPLAIN ANALYZE, не угадывайте. Что быстро на одной базе/версии, может быть медленным на другой.
-- 1. Replace subqueries with JOINs when possible
-- Slow: SELECT * FROM orders WHERE customer_id IN
-- (SELECT id FROM customers WHERE active = true);
-- Fast: SELECT o.* FROM orders o
-- JOIN customers c ON o.customer_id = c.id WHERE c.active = true;
-- 2. Use UNION ALL instead of UNION (avoids dedup sort)
SELECT 'A' UNION ALL SELECT 'B'; -- fast
SELECT 'A' UNION SELECT 'B'; -- sorts to remove duplicates
-- 3. Batch operations instead of row-by-row
-- Slow: 1000 individual INSERTs
-- Fast: INSERT INTO t VALUES (1,'a'), (2,'b'), (3,'c'), ...;
-- 4. Use CTEs for readability, but know they may not optimize well
-- (PostgreSQL 12+ inlines CTEs; older versions materialize them)
-- 5. Avoid SELECT DISTINCT when you can use GROUP BY or EXISTS
-- 6. Use prepared statements for repeated queries (plan caching)
PREPARE get_user AS SELECT * FROM users WHERE id = $1;
EXECUTE get_user(42);
-- 7. Analyze tables for up-to-date statistics
ANALYZE orders; -- PostgreSQL: updates planner statisticsСравнение NoSQL и SQL
SQL vs NoSQL: когда что использовать
Выбор SQL vs NoSQL зависит от модели данных, требований к согласованности и масштаба. Базы данных SQL обеспечивают схему, поддерживают ACID-транзакции и превосходны в сложных запросах с JOIN — идеальны для финансовых систем и любых приложений, где целостность данных критична. Базы данных NoSQL торгуют согласованностью ради масштабируемости и гибкости: документные хранилища (MongoDB) для эволюционирующих схем, key-value хранилища (Redis) для кэширования, column-family (Cassandra) для массивной пропускной способности записи и графовые базы данных (Neo4j) для данных с большим количеством связей. Современные базы данных SQL теперь поддерживают JSON, полнотекстовый поиск и масштабирование, снижая потребность в NoSQL во многих случаях.
-- SQL (Relational): structured, consistent, queryable
-- Best for: financial systems, e-commerce, CRM, any ACID-requiring app
-- Examples: PostgreSQL, MySQL, SQL Server, Oracle
-- NoSQL: flexible schema, horizontal scaling, specific data models
-- Document: MongoDB, CouchDB — JSON-like documents, flexible schema
-- Key-Value: Redis, DynamoDB — fast lookups, caching, sessions
-- Column-Family: Cassandra, HBase — wide-column, time-series, write-heavy
-- Graph: Neo4j, ArangoDB — relationships, social networks, recommendations
-- Decision factors:
-- 1. Data structure: fixed → SQL, evolving/varied → NoSQL
-- 2. Consistency: strict ACID → SQL, eventual consistency OK → NoSQL
-- 3. Scale: vertical (bigger server) → SQL, horizontal (more servers) → NoSQL
-- 4. Queries: complex JOINs → SQL, simple lookups → NoSQL
-- 5. Team expertise: SQL is universal, NoSQL varies
-- Many modern databases blur the line:
-- PostgreSQL: JSON columns, full-text search, pub/sub
-- MongoDB: transactions (multi-document ACID since 4.0)Шаблоны документного хранилища (в стиле MongoDB)
Документные хранилища встраивают связанные данные в один документ, а не нормализуют по таблицам. Это устраняет JOIN для шаблонов доступа с интенсивным чтением, но дублирует данные (информация о клиенте в каждом заказе). Встраивание работает, когда данные запрашиваются вместе и имеют ограниченный размер. Для неограниченных отношений (клиент с тысячами заказов) используйте ссылки (храните customer_id, извлекайте отдельно). Столбцы JSONB в PostgreSQL дают гибкость документного хранилища в реляционной базе — вы получаете ACID-транзакции, индексацию (GIN) и SQL-запросы к JSON. Этот гибридный подход становится всё более популярным, снижая потребность в отдельной NoSQL-базе.
-- In SQL, you'd normalize this into 3 tables:
-- customers, orders, order_items
-- In a document store (MongoDB), you might embed everything:
-- (pseudo-code, not SQL)
// db.orders.insertOne({
// _id: 1,
// customer: { name: "Alice", email: "[email protected]" },
// items: [
// { product: "Laptop", price: 999, qty: 1 },
// { product: "Mouse", price: 25, qty: 2 }
// ],
// total: 1049,
// status: "shipped",
// created_at: ISODate("2024-01-15")
// })
-- SQL equivalent with JSON column (PostgreSQL):
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL -- stores the entire document
);
INSERT INTO orders (data) VALUES ('{
"customer": {"name": "Alice", "email": "[email protected]"},
"items": [{"product": "Laptop", "price": 999, "qty": 1}],
"total": 999
}');
-- Query JSON data:
SELECT data->'customer'->>'name' AS name FROM orders
WHERE data->'total' > 500;Шаблоны key-value хранилища (в стиле Redis)
Key-value хранилища вроде Redis превосходны в сверхбыстрых lookup'ах (субмиллисекунда), так как данные живут в памяти. Распространённые применения: кэширование дорогих результатов запросов, хранение сессий (с истечением TTL), счётчики в реальном времени (атомарный INCR) и таблицы лидеров (sorted set'ы). Структуры данных Redis (list'ы, set'ы, sorted set'ы, hash'и) выходят за рамки простого key-value. Компромисс: данные в памяти (ограничено RAM) и персистентность опциональна. Используйте Redis как слой кэша перед SQL — шаблоны write-through или cache-aside. Для данных сессий автоматическое истечение Redis (TTL) идеально. Базы данных SQL могут эмулировать кэширование через таблицу кэша, но не могут сравниться со скоростью Redis для горячих данных.
-- Redis is an in-memory key-value store — not SQL
-- Common patterns (Redis commands, not SQL):
-- Caching: store expensive query results
// SET user:42:profile '{"name":"Alice","age":30}' EX 3600
// GET user:42:profile -- returns cached data, expires in 1 hour
-- Session storage: fast, ephemeral
// SET session:abc123 '{"user_id":42}' EX 1800 -- 30 min TTL
-- Counters & leaderboards:
// INCR page:home:views -- atomic counter
// ZADD leaderboard 1500 "alice" -- sorted set for rankings
// ZREVRANGE leaderboard 0 9 -- top 10 players
-- SQL equivalent for caching (materialized/precomputed):
CREATE TABLE cache (
cache_key VARCHAR(200) PRIMARY KEY,
cache_value TEXT,
expires_at TIMESTAMP,
INDEX idx_expires (expires_at)
);
-- Periodically: DELETE FROM cache WHERE expires_at < NOW();
-- When to use Redis vs SQL cache:
-- Redis: sub-millisecond reads, data structures (sets, sorted sets)
-- SQL: when you need ACID or already have a database connectionПолиглотная персистентность (смешивание баз данных)
Полиглотная персистентность использует разные базы данных для разных потребностей в данных в одном приложении. PostgreSQL обрабатывает транзакции, Redis — кэширование, Elasticsearch — поиск, S3 — файлы. Задача — сохранять данные согласованными между хранилищами — решение — событийно-ориентированная архитектура: пишите в основную базу данных (источник истины), затем асинхронно распространяйте изменения в другие хранилища через Change Data Capture (CDC) или очереди сообщений (Kafka, RabbitMQ). Это обеспечивает eventual consistency — чтение из вторичных хранилищ может немного отставать. Преимущество: каждое хранилище оптимизировано для своей нагрузки. Цена: операционная сложность. Начните с одной SQL-базы; добавляйте специализированные хранилища только при явных ограничениях производительности.
-- Modern applications often use MULTIPLE database types:
-- Each data store handles what it does best
-- Typical architecture:
-- 1. PostgreSQL: core transactional data (users, orders, payments)
-- ACID guarantees, complex queries, foreign keys
CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR UNIQUE);
CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id));
-- 2. Redis: session storage, caching, rate limiting
-- Fast in-memory access, auto-expiring keys
-- 3. Elasticsearch: full-text search, log analytics
-- Inverted index, faceted search, aggregations
-- 4. S3/Object storage: files, images, backups
-- Cheap, unlimited, HTTP-accessible
-- 5. TimescaleDB/InfluxDB: time-series metrics
-- Optimized for timestamped data, downsampling
-- Challenge: data consistency across stores
-- Solution: event-driven architecture (CDC, message queues)
-- 1. Write to PostgreSQL (source of truth)
-- 2. Publish event to Kafka
-- 3. Consumers update Redis cache, Elasticsearch index, etc.
-- 4. eventual consistency — reads may be slightly staleМодели согласованности ACID vs BASE
ACID (Атомарность, Согласованность, Изолированность, Долговечность) гарантирует строгую согласованность — транзакции все-или-ничего, и данные всегда удовлетворяют ограничениям. Это необходимо для финансовых систем, где частичные обновления вызвали бы ошибки. BASE (Basically Available, Soft state, Eventually consistent) торгует немедленной согласованностью за доступность и толерантность к разделению — данные могут быть временно несогласованными, но сходятся со временем. Теорема CAP утверждает, что нельзя иметь все три (Согласованность, Доступность, Толерантность к разделению) одновременно при сетевых разделениях. Базы данных SQL приоритизируют C+A (один узел) или C+P (распределённые). Многие базы данных NoSQL приоритизируют A+P (Cassandra, DynamoDB). Выбирайте ACID, когда критична корректность; BASE, когда доступность и масштаб важнее.
-- ACID (SQL databases):
-- Atomicity: all operations in a transaction succeed or fail together
-- Consistency: data always satisfies constraints (FK, CHECK, etc.)
-- Isolation: concurrent transactions don't interfere
-- Durability: committed data survives crashes
-- SQL transaction example:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If either fails, ROLLBACK undoes both
COMMIT; -- both updates are permanent
-- BASE (NoSQL databases):
-- Basically Available: system responds (may be stale)
-- Soft state: state changes without input (eventual consistency)
-- Eventually consistent: data converges over time
-- NoSQL trade-off: higher availability & partition tolerance
-- at the cost of immediate consistency
-- CAP Theorem: in a network partition, choose:
-- - CP (consistency): refuse writes (some NoSQL: HBase, MongoDB)
-- - AP (availability): accept writes, reconcile later (Cassandra, DynamoDB)
-- SQL databases are typically CA (consistent + available, no partition tolerance
-- in single-node setups; distributed SQL like CockroachDB adds P)CTE и рекурсивные CTE
Базовый CTE
CTE (Common Table Expression) — временный именованный набор результатов. Улучшает читаемость, разбивая сложные запросы. Несколько CTE могут быть объединены запятыми. CTE действительны только для одного оператора.
WITH high_earners AS (
SELECT * FROM employees WHERE salary > 80000
), by_dept AS (
SELECT department, COUNT(*) AS cnt FROM high_earners GROUP BY department
)
SELECT * FROM by_dept ORDER BY cnt DESC;Рекурсивный CTE
Рекурсивные CTE ссылаются на самих себя. Якорь — базовый случай. UNION ALL соединяет с рекурсивной частью. Используется для иерархических данных: орг-диаграммы, файловые системы, обход графов. Должно быть условие завершения.