sqlpostgresqlexceptset-operations

EXCEPT in SQL: Set Difference and the Oracle MINUS Equivalent

How EXCEPT returns rows from the first query that are absent in the second, how it differs from EXCEPT ALL, and when NOT EXISTS wins.

11 min di letturaReferencesql · postgresql · except · set-operations · oracle
Questo articolo è attualmente in russo — la traduzione in inglese è in corso.

В SQL есть операции, которые помогают работать с результатами запросов как с наборами строк.

Например:

UNION

объединяет выборки.

INTERSECT

оставляет только строки, которые есть в обеих выборках.

А:

EXCEPT

делает обратную задачу: показывает строки, которые есть в первой выборке, но которых нет во второй.

То есть EXCEPT отвечает на вопрос:

Что есть здесь, но отсутствует там?

Это очень полезно в реальных задачах:

  • найти пользователей, которые зарегистрировались, но не сделали заказ;
  • найти товары, которые были в старом каталоге, но пропали из нового;
  • найти строки, которые есть в источнике, но не доехали в целевую таблицу;
  • найти клиентов, которых нет в списке заблокированных;
  • сравнить две версии таблицы после миграции.

Если INTERSECT — это «общее», то EXCEPT — это «разница».

Что делает EXCEPT простыми словами

Представим два списка.

Первый список — все пользователи:

user_id
-------
1
2
3
4
5

Второй список — заблокированные пользователи:

user_id
-------
2
4

Если мы хотим получить всех пользователей, кроме заблокированных, нам нужно из первого списка вычесть второй.

Останется:

user_id
-------
1
3
5

В SQL это можно записать через EXCEPT:

SELECT user_id
FROM users

EXCEPT

SELECT user_id
FROM banned_users;

Смысл такой:

Возьми строки из первого SELECT и убери из них строки, которые нашлись во втором SELECT.

Базовый синтаксис EXCEPT

Общий вид запроса:

SELECT column_1, column_2
FROM first_table

EXCEPT

SELECT column_1, column_2
FROM second_table;

Первый SELECT — это основной набор.

Второй SELECT — это набор, который нужно вычесть.

Например:

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users;

Такой запрос вернёт email пользователей, которые есть в users, но отсутствуют в unsubscribed_users.

То есть это пользователи, которые не отписались.

Пример: пользователи без заказов

Допустим, есть таблица users:

id | email
---+----------------
1  | anna@mail.com
2  | bob@mail.com
3  | kate@mail.com
4  | tom@mail.com

И таблица orders:

id | user_id | amount
---+---------+--------
1  | 1       | 1500
2  | 3       | 2300

Нужно найти пользователей, у которых нет заказов.

Первый набор — все пользователи:

SELECT id
FROM users;

Результат:

id
--
1
2
3
4

Второй набор — пользователи, у которых есть заказы:

SELECT user_id
FROM orders;

Результат:

user_id
-------
1
3

Теперь вычитаем второй набор из первого:

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

Результат:

id
--
2
4

Пользователи 2 и 4 есть в таблице users, но их нет среди user_id в заказах.

Значит, они зарегистрировались, но ещё ничего не заказали.

EXCEPT — это не JOIN

Новички иногда пытаются решить все задачи через JOIN, но EXCEPT работает по другой логике.

JOIN соединяет строки из разных таблиц.

А EXCEPT сравнивает два готовых результата SELECT и вычитает один из другого.

Например:

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

Этот запрос не соединяет таблицы. Он просто берёт один список значений и вычитает из него другой список.

Можно запомнить так:

JOIN   — соединить таблицы
EXCEPT — вычесть одну выборку из другой

Если вопрос звучит как «каких строк из первой выборки нет во второй», часто стоит подумать именно про EXCEPT.

EXCEPT сравнивает строки целиком

Очень важный момент: EXCEPT сравнивает всю строку целиком, а не только первую колонку.

Например:

SELECT 'anna@mail.com' AS email, 'Vietnam' AS country

EXCEPT

SELECT 'anna@mail.com' AS email, 'Germany' AS country;

Результат будет:

email          | country
---------------+---------
anna@mail.com  | Vietnam

Почему строка осталась?

Потому что полностью она не совпала со строкой справа.

Слева:

anna@mail.com | Vietnam

Справа:

anna@mail.com | Germany

Email одинаковый, но страна разная. Для EXCEPT это разные строки.

А вот такой запрос вернёт пустой результат:

SELECT 'anna@mail.com' AS email, 'Vietnam' AS country

EXCEPT

SELECT 'anna@mail.com' AS email, 'Vietnam' AS country;

Потому что строка справа полностью совпала со строкой слева и вычла её.

Главное правило:

EXCEPT сравнивает все выбранные колонки.

Если вы хотите вычитать только по email, выбирайте только email:

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users;

Если добавите country, created_at, source или другие колонки, они тоже будут участвовать в сравнении.

Требования к SELECT внутри EXCEPT

У двух запросов должно совпадать количество колонок.

Так можно:

SELECT id, email
FROM users

EXCEPT

SELECT id, email
FROM archived_users;

В обоих запросах по две колонки.

А так нельзя:

SELECT id, email
FROM users

EXCEPT

SELECT id, email, country
FROM archived_users;

Первый запрос возвращает две колонки, а второй — три. База не сможет сравнить такие строки.

Также важен порядок колонок.

Например:

SELECT id, email
FROM users

EXCEPT

SELECT email, id
FROM archived_users;

Даже если запрос технически выполнится, смысл будет неправильным: база будет сравнивать id с email, а email с id.

Поэтому при EXCEPT важно следить за тремя вещами:

  • количество колонок одинаковое;
  • типы колонок совместимы;
  • порядок колонок соответствует смыслу.

Имена колонок берутся из первого SELECT

Названия колонок в итоговом результате берутся из первого SELECT.

Например:

SELECT id, email
FROM users

EXCEPT

SELECT user_id, contact_email
FROM old_users;

Итоговые колонки будут называться:

id | email

Даже если во втором запросе они называются user_id и contact_email.

Поэтому первый SELECT лучше оформлять аккуратно:

SELECT
  id AS user_id,
  email AS user_email
FROM users

EXCEPT

SELECT
  user_id,
  contact_email
FROM old_users;

Так результат будет понятнее.

EXCEPT убирает дубликаты

Обычный EXCEPT работает как операция над множествами. А в множестве одинаковые строки хранятся один раз.

Например:

SELECT 'SQL' AS course

EXCEPT

SELECT 'Python' AS course;

Результат:

course
------
SQL

А если слева строка повторяется несколько раз, обычный EXCEPT всё равно вернёт её максимум один раз.

Представим левую выборку:

user_id
-------
1
1
1
2
3

Правая выборка:

user_id
-------
2

Обычный EXCEPT вернёт:

user_id
-------
1
3

Пользователь 1 был слева три раза, но в результате появился один раз.

Потому что EXCEPT по умолчанию убирает дубликаты.

EXCEPT ALL: разность с учётом повторов

У EXCEPT есть вариант:

EXCEPT ALL

Он учитывает количество повторов.

Логика такая:

сколько раз строка встретилась слева, минус сколько раз она встретилась справа.

Например, слева:

user_id
-------
1
1
1
2
3

Справа:

user_id
-------
1
2

Для user_id = 1:

слева  — 3 раза
справа — 1 раз
остаётся 2 раза

Для user_id = 2:

слева  — 1 раз
справа — 1 раз
остаётся 0 раз

Для user_id = 3:

слева  — 1 раз
справа — 0 раз
остаётся 1 раз

Результат EXCEPT ALL:

user_id
-------
1
1
3

Пример запроса:

SELECT user_id
FROM orders

EXCEPT ALL

SELECT user_id
FROM refunded_orders;

Обычный EXCEPT вернул бы уникальные значения. EXCEPT ALL сохраняет кратность, то есть количество повторов.

На практике обычный EXCEPT используется чаще, потому что во многих задачах важен сам факт отсутствия строки во второй выборке. Но если вы сравниваете фактическое количество строк или ищете расхождения в дублях, EXCEPT ALL может быть полезен.

Пример: товары, которые пропали из нового каталога

Допустим, есть старый каталог товаров:

sku
-------
A-100
A-200
A-300
A-400

И новый каталог:

sku
-------
A-100
A-300
A-500

Нужно найти товары, которые были в старом каталоге, но отсутствуют в новом.

SELECT sku
FROM old_catalog

EXCEPT

SELECT sku
FROM new_catalog;

Результат:

sku
-------
A-200
A-400

Это классический сценарий для EXCEPT.

Мы как будто спрашиваем:

Что было раньше, но исчезло сейчас?

Пример: клиенты, которые не отписались

Допустим, есть таблица users:

email
----------------
anna@mail.com
bob@mail.com
kate@mail.com
tom@mail.com

И таблица unsubscribed_users:

email
----------------
bob@mail.com
tom@mail.com

Нужно получить email пользователей, которым можно отправить рассылку.

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users;

Результат:

email
--------------
anna@mail.com
kate@mail.com

Такой запрос коротко выражает бизнес-логику:

все пользователи минус те, кто отписался.

EXCEPT и NULL

В обычных сравнениях SQL NULL ведёт себя необычно.

Например:

SELECT NULL = NULL;

не возвращает TRUE.

Но в операциях над множествами, таких как EXCEPT, строки с NULL в одинаковых позициях считаются совпадающими.

Пример:

SELECT NULL AS country

EXCEPT

SELECT NULL AS country;

Результат будет пустым.

Почему?

Потому что строка с NULL слева была вычтена строкой с NULL справа.

Для EXCEPT они считаются одинаковыми в рамках сравнения строк.

Это отличается от обычного условия:

a.country = b.country

Такое условие не считает NULL равным NULL.

Поэтому при сравнении данных с NULL EXCEPT иногда оказывается очень удобным: он сравнивает строки как элементы набора, а не через обычное =.

EXCEPT против NOT EXISTS

Разность выборок можно выразить не только через EXCEPT, но и через NOT EXISTS.

Например, пользователи без заказов через EXCEPT:

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

То же самое через NOT EXISTS:

SELECT u.id
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Оба варианта могут вернуть пользователей, у которых нет заказов.

Но смысл записи немного разный.

EXCEPT работает с двумя готовыми наборами:

все id пользователей
минус
user_id из заказов

NOT EXISTS проверяет каждую строку из users:

оставь пользователя, если для него не существует заказа

Когда лучше EXCEPT, а когда NOT EXISTS

EXCEPT хорошо подходит, когда нужно сравнить два набора строк целиком.

Например:

SELECT id, user_id, amount
FROM orders_source

EXCEPT

SELECT id, user_id, amount
FROM orders_target;

Такой запрос сразу показывает строки, которые есть в источнике, но отсутствуют в целевой таблице.

NOT EXISTS часто удобнее, когда:

  • нужно вывести дополнительные колонки из первой таблицы;
  • сравнение идёт по ключу;
  • нужно сохранить дубликаты из левой таблицы;
  • важна производительность на больших таблицах;
  • нужно гибко прописать условия сравнения.

Например:

SELECT u.id, u.email, u.created_at
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Здесь мы выводим не просто id, а любые поля из users.

Через EXCEPT это тоже можно сделать, но если сравнение идёт только по id, NOT EXISTS часто читается естественнее.

EXCEPT против LEFT JOIN ... IS NULL

Ещё один популярный способ найти строки без совпадения — LEFT JOIN.

Например:

SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.user_id IS NULL;

Смысл:

  1. берём всех пользователей;
  2. пытаемся присоединить к ним заказы;
  3. оставляем только тех, у кого заказ не нашёлся.

Это называется anti-join, то есть «соединение наоборот»: найти строки без пары.

Такой подход часто встречается в рабочих запросах.

Но у него есть нюансы:

  • если в правой таблице дубли, JOIN может размножать строки до фильтрации;
  • нужно аккуратно выбирать колонку для проверки IS NULL;
  • если условие соединения сложное, запрос может стать длинным;
  • EXCEPT иногда читается проще, когда нужно сравнить одинаковые наборы колонок.

Пример через EXCEPT:

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

Пример через LEFT JOIN:

SELECT u.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.user_id IS NULL;

Оба варианта имеют право на жизнь. Выбор зависит от задачи, данных и привычек команды.

Почему NOT IN может быть опасен с NULL

Есть ещё один способ, который часто пишут новички:

SELECT id
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
);

На первый взгляд всё логично: найти пользователей, чей id не входит в список user_id из заказов.

Но с NOT IN есть опасная ловушка.

Если подзапрос вернёт хотя бы один NULL, результат может стать неожиданным и даже пустым.

Например, если список справа такой:

user_id
-------
1
3
NULL

условие:

id NOT IN (1, 3, NULL)

становится проблемным, потому что SQL не может уверенно сказать, что id не равен неизвестному значению.

Поэтому для таких задач часто безопаснее использовать NOT EXISTS:

SELECT u.id
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Или использовать EXCEPT:

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

Можно запомнить так:

NOT IN и NULL — опасное сочетание. Если в правой выборке может быть NULL, лучше использовать NOT EXISTS или хорошо понимать последствия.

Сверка данных через EXCEPT

Одна из самых полезных задач для EXCEPT — сверка данных.

Например, вы переносите заказы из старой системы в новую.

Есть таблица-источник:

orders_source

И таблица-приёмник:

orders_target

Нужно проверить, какие строки есть в источнике, но не попали в приёмник.

SELECT id, user_id, amount, status, created_at
FROM orders_source

EXCEPT

SELECT id, user_id, amount, status, created_at
FROM orders_target;

Если запрос вернул строки, значит эти строки есть в источнике, но отсутствуют в целевой таблице.

Если результат пустой, значит все такие строки из источника есть в приёмнике.

Но это только одна сторона проверки.

Двусторонняя сверка

Чтобы убедиться, что две таблицы совпадают полностью, нужно проверить разность в обе стороны.

Первая проверка:

SELECT id, user_id, amount, status, created_at
FROM orders_source

EXCEPT

SELECT id, user_id, amount, status, created_at
FROM orders_target;

Она показывает строки, которые есть в источнике, но отсутствуют в приёмнике.

Вторая проверка:

SELECT id, user_id, amount, status, created_at
FROM orders_target

EXCEPT

SELECT id, user_id, amount, status, created_at
FROM orders_source;

Она показывает строки, которые есть в приёмнике, но отсутствуют в источнике.

Если обе проверки вернули пустой результат, значит таблицы совпадают по выбранным колонкам.

Это гораздо надёжнее, чем просто сравнить количество строк.

Почему?

Потому что одинаковый COUNT(*) ещё не означает, что строки одинаковые.

В одной таблице может быть строка A, в другой — строка B, а количество всё равно совпадает.

Сверка с меткой направления через CTE

Чтобы сразу увидеть расхождения в обе стороны, можно объединить две разности и добавить колонку с направлением. Чтобы не путаться с приоритетом операций, удобнее использовать CTE.

WITH source_minus_target AS (
  SELECT id, user_id, amount, status, created_at
  FROM orders_source

  EXCEPT

  SELECT id, user_id, amount, status, created_at
  FROM orders_target
),
target_minus_source AS (
  SELECT id, user_id, amount, status, created_at
  FROM orders_target

  EXCEPT

  SELECT id, user_id, amount, status, created_at
  FROM orders_source
)
SELECT 'source_minus_target' AS diff_type, *
FROM source_minus_target

UNION ALL

SELECT 'target_minus_source' AS diff_type, *
FROM target_minus_source;

Так результат покажет не только отличающиеся строки, но и направление расхождения.

EXCEPT и ORDER BY

Если нужен порядок строк, ORDER BY пишется в самом конце.

Например:

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users

ORDER BY email;

Без ORDER BY порядок результата не гарантирован.

Даже если база сейчас возвращает строки «в красивом порядке», полагаться на это нельзя. План выполнения может измениться, данных станет больше, и порядок станет другим.

Правило простое:

Нужен порядок — добавьте ORDER BY.

EXCEPT и LIMIT

LIMIT в конце применяется ко всему результату после вычитания.

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users

ORDER BY email
LIMIT 10;

Этот запрос:

  1. возьмёт email из users;
  2. вычтет email из unsubscribed_users;
  3. отсортирует результат;
  4. вернёт первые 10 строк.

Если нужно ограничить только одну ветку до EXCEPT, используйте подзапрос.

Например:

SELECT email
FROM (
  SELECT email
  FROM users
  ORDER BY created_at DESC
  LIMIT 100
) AS recent_users

EXCEPT

SELECT email
FROM unsubscribed_users;

Здесь сначала берутся 100 последних пользователей, а потом из них вычитаются те, кто отписался.

EXCEPT и производительность

EXCEPT очень удобен для чтения, особенно в задачах сверки. Но нужно понимать, что база выполняет реальную работу.

Обычный EXCEPT должен:

  • получить строки из первой выборки;
  • получить строки из второй выборки;
  • сравнить их;
  • убрать строки справа из результата слева;
  • удалить дубликаты.

На небольших таблицах это обычно не проблема.

Но на больших объёмах EXCEPT может быть тяжелее, чем NOT EXISTS, особенно если вы сравниваете большие широкие строки.

Например, такой запрос:

SELECT id, user_id, amount, status, created_at
FROM orders_source

EXCEPT

SELECT id, user_id, amount, status, created_at
FROM orders_target;

может потребовать много памяти, потому что база сравнивает строки целиком.

Если же вам нужно проверить отсутствие только по ключу, иногда быстрее и понятнее использовать NOT EXISTS:

SELECT s.*
FROM orders_source s
WHERE NOT EXISTS (
  SELECT 1
  FROM orders_target t
  WHERE t.id = s.id
);

Такой запрос сравнивает строки по ключу id, а не по всей строке.

Практическое правило:

Для сверки одинаковых наборов колонок EXCEPT очень удобен. Для поиска отсутствующих строк по ключу на больших таблицах часто стоит проверить вариант с NOT EXISTS.

Лучше не гадать, а смотреть план выполнения через EXPLAIN или EXPLAIN ANALYZE.

EXCEPT в PostgreSQL

В PostgreSQL EXCEPT поддерживается полноценно.

Можно использовать:

EXCEPT

и:

EXCEPT ALL

Обычный EXCEPT удаляет дубликаты.

EXCEPT ALL учитывает количество повторов.

PostgreSQL строго проверяет количество колонок и совместимость типов. Если структура двух SELECT не совпадает, запрос завершится ошибкой.

EXCEPT в Oracle

В Oracle аналогичная операция традиционно называется:

MINUS

Пример:

SELECT user_id
FROM users

MINUS

SELECT user_id
FROM banned_users;

По смыслу это то же самое: строки из первой выборки минус строки из второй.

Если вы переходите между PostgreSQL и Oracle, важно помнить это различие в названии:

PostgreSQL: EXCEPT
Oracle:     MINUS

EXCEPT в MySQL

В старых версиях MySQL оператора EXCEPT не было, поэтому разность выборок обычно писали через NOT EXISTS, LEFT JOIN ... IS NULL или NOT IN с осторожностью.

Например:

SELECT u.id
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM banned_users b
  WHERE b.user_id = u.id
);

В современных версиях MySQL поддержка операций множеств стала шире, но при работе с конкретным проектом всё равно лучше проверить версию сервера и поддерживаемый синтаксис.

Если нужно писать максимально переносимый запрос, NOT EXISTS часто оказывается более универсальным вариантом.

EXCEPT в ClickHouse

В ClickHouse операции множеств тоже зависят от версии и настроек. Для аналитических задач часто используют UNION ALL, DISTINCT, JOIN, ANTI JOIN, NOT IN или подзапросы.

Если вы хотите использовать именно EXCEPT, лучше проверить поведение на вашей версии ClickHouse: особенно то, как она работает с дубликатами и поддерживает ли нужную вам форму ALL или DISTINCT.

Для больших аналитических таблиц в ClickHouse часто важнее не только синтаксис, но и то, как данные отсортированы, партиционированы и какие условия попадают в запрос.

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

Все пользователи минус заблокированные

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM banned_users;

Пользователи без заказов

SELECT id
FROM users

EXCEPT

SELECT user_id
FROM orders;

Email для рассылки без отписавшихся

SELECT email
FROM users

EXCEPT

SELECT email
FROM unsubscribed_users;

Товары, которые пропали из нового каталога

SELECT sku
FROM old_catalog

EXCEPT

SELECT sku
FROM new_catalog;

Строки, которые есть в источнике, но отсутствуют в приёмнике

SELECT id, user_id, amount, status, created_at
FROM orders_source

EXCEPT

SELECT id, user_id, amount, status, created_at
FROM orders_target;

Двусторонняя сверка через CTE

WITH source_minus_target AS (
  SELECT id, user_id, amount
  FROM orders_source

  EXCEPT

  SELECT id, user_id, amount
  FROM orders_target
),
target_minus_source AS (
  SELECT id, user_id, amount
  FROM orders_target

  EXCEPT

  SELECT id, user_id, amount
  FROM orders_source
)
SELECT 'source_minus_target' AS diff_type, *
FROM source_minus_target

UNION ALL

SELECT 'target_minus_source' AS diff_type, *
FROM target_minus_source;

Альтернатива через NOT EXISTS

SELECT u.id, u.email
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Альтернатива через LEFT JOIN

SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.user_id IS NULL;

Что важно запомнить

EXCEPT возвращает строки из первой выборки, которых нет во второй.

Пример:

SELECT id FROM users
EXCEPT
SELECT user_id FROM orders;

Такой запрос найдёт пользователей, у которых нет заказов.

Главные правила:

  • первый SELECT — основной набор;
  • второй SELECT — набор, который вычитаем;
  • количество колонок должно совпадать;
  • типы колонок должны быть совместимы;
  • порядок колонок важен;
  • сравнивается вся строка целиком;
  • обычный EXCEPT убирает дубликаты;
  • EXCEPT ALL учитывает количество повторов;
  • строки с NULL в одинаковых позициях считаются совпадающими;
  • порядок результата не гарантирован без ORDER BY.

EXCEPT особенно полезен для сверки данных: найти строки, которые есть в одном источнике, но отсутствуют в другом.

Короткий вывод

EXCEPT — это SQL-оператор для разности выборок.

Он отвечает на вопрос:

Какие строки есть в первом запросе, но отсутствуют во втором?

Например:

SELECT email FROM users
EXCEPT
SELECT email FROM unsubscribed_users;

Так можно получить пользователей, которые не отписались от рассылки.

Можно запомнить три операции вместе:

UNION     — объединить выборки
INTERSECT — найти общее
EXCEPT    — вычесть одну выборку из другой

EXCEPT хорош, когда вы сравниваете два набора строк целиком: например, при миграции, сверке отчётов, проверке каталога или поиске отсутствующих записей.

Если же нужно проверить отсутствие по ключу на больших таблицах, часто стоит рассмотреть NOT EXISTS.

Главное — перед запросом проговорить смысл:

Я вычитаю строки целиком или только ключи? Мне важны дубликаты или нужен уникальный результат? В колонках могут быть NULL?

Ответы на эти вопросы помогают выбрать правильный инструмент и не получить красивый, но неверный отчёт.

Esercitati su esercizi reali

Risolvi esercizi nel trainer SQL con valutazione e suggerimenti istantanei.

Apri il trainer