Привет, Я DocuDroid!
Оценка ИИ поиска
Спасибо за оценку нашего ИИ поиска!
Мы будем признательны, если вы поделитесь своими впечатлениями, чтобы мы могли улучшить наш ИИ поиск для вас и других читателей.
GitHub

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

Андрей Аксенов

Общее табличное выражение (Common Table Expression, CTE) — это временный набор результатов, определенный в рамках выполнения SQL-запроса. CTE позволяют упростить сложные запросы, разбивая их на более простые и удобные для обработки части. Они также помогают оптимизировать выполнение запроса, когда к одному и тому же набору данных необходимо обращаться несколько раз в рамках этого запроса.

Данная статья описывает, как использовать CTE в SELECT-запросах.

ПРИМЕЧАНИЕ

Для SELECT-запросов можно также использовать команды INSERT, UPDATE и DELETE внутри CTE.

Предварительные требования

Для выполнения команд, описанных в следующих разделах, подключитесь к мастер-хосту Greengage DB с помощью psql, как описано в статье Подключение к Greengage DB с использованием psql. Создайте новую базу данных и подключитесь к ней:

CREATE DATABASE marketplace;
\c marketplace

Нерекурсивные CTE

Синтаксис

Чтобы определить CTE, используйте выражение WITH перед основным запросом:

WITH <cte_name> (<column_list>) AS (
    <cte_subquery>
)
<main_query>;

где:

  • <cte_name> — имя, используемое для обращения к CTE в основном запросе.

  • <column_list> — (опционально) список имен столбцов в результирующем наборе CTE.

  • <cte_subquery> — SQL-запрос, который определяет результирующий набор CTE.

  • <main_query> — основной SQL-запрос, использующий CTE.

Вы также можете определить несколько CTE в одном выражении WITH, разделив их запятыми:

WITH <cte_name1> (<column_list1>) AS (
    <cte_subquery1>
),
<cte_name2> (<column_list2>) AS (
    <cte_subquery2>
),
...
<primary_query>;

Один подзапрос CTE

Чтобы выполнить примеры из этого раздела, создайте таблицу sales:

CREATE TABLE sales
(
    id           INT,
    date         DATE,
    product_name TEXT,
    category     TEXT,
    quantity     INT,
    unit_price   NUMERIC(8, 2)
)
    WITH (appendoptimized = true)
    DISTRIBUTED BY (id);

Добавьте данные:

INSERT INTO sales (id, date, product_name, category, quantity, unit_price)
VALUES (1, '2025-01-03', 'Laptop', 'Electronics', 1, 1200.00),
       (2, '2025-01-04', 'Headphones', 'Electronics', 2, 150.00),
       (3, '2025-01-05', 'Coffee Maker', 'Home', 1, 85.00),
       (4, '2025-01-06', 'T-Shirt', 'Clothing', 3, 25.00),
       (5, '2025-01-07', 'Desk Chair', 'Home', 1, 200.00),
       (6, '2025-01-08', 'Smartphone', 'Electronics', 2, 800.00),
       (7, '2025-01-09', 'Headphones', 'Electronics', 1, 150.00),
       (8, '2025-01-10', 'T-Shirt', 'Clothing', 2, 25.00),
       (9, '2025-01-11', 'Laptop', 'Electronics', 1, 1200.00);

Этот запрос определяет CTE с именем product_sales для агрегирования данных о продажах товаров, а затем выбирает товары, отсортированные по общей выручке в порядке убывания:

WITH product_sales AS (SELECT product_name,
                              SUM(quantity)              AS total_quantity,
                              SUM(quantity * unit_price) AS total_revenue
                       FROM sales
                       GROUP BY product_name)
SELECT product_name,
       total_revenue
FROM product_sales
ORDER BY total_revenue DESC;

Результат выглядит следующим образом:

 product_name | total_revenue
--------------+---------------
 Laptop       |       2400.00
 Smartphone   |       1600.00
 Headphones   |        450.00
 Desk Chair   |        200.00
 T-Shirt      |        125.00
 Coffee Maker |         85.00
(6 rows)

Этот запрос использует подзапрос product_sales в выражении FROM, который возвращает тот же результат, что и CTE выше:

SELECT product_name,
       total_revenue
FROM (SELECT product_name,
             SUM(quantity)              AS total_quantity,
             SUM(quantity * unit_price) AS total_revenue
      FROM sales
      GROUP BY product_name) AS product_sales
ORDER BY total_revenue DESC;

Несколько подзапросов CTE

Этот запрос определяет два CTE: category_sales агрегирует выручку по категориям, а product_sales агрегирует продажи по товарам. Основной запрос объединяет эти CTE, чтобы отобразить выручку для каждого товара вместе с общей выручкой его категории:

WITH category_sales AS (SELECT category,
                                SUM(quantity * unit_price) AS category_revenue
                         FROM sales
                         GROUP BY category),
     product_sales AS (SELECT product_name,
                              category,
                              SUM(quantity)              AS total_quantity,
                              SUM(quantity * unit_price) AS total_revenue
                       FROM sales
                       GROUP BY product_name, category)
SELECT product_sales.product_name,
       product_sales.category,
       product_sales.total_revenue,
       category_sales.category_revenue
FROM product_sales
         JOIN category_sales ON product_sales.category = category_sales.category
ORDER BY product_sales.total_revenue DESC;

Результат должен выглядеть следующим образом:

 product_name |  category   | total_revenue | category_revenue
--------------+-------------+---------------+------------------
 Laptop       | Electronics |       2400.00 |          4450.00
 Smartphone   | Electronics |       1600.00 |          4450.00
 Headphones   | Electronics |        450.00 |          4450.00
 Desk Chair   | Home        |        200.00 |           285.00
 T-Shirt      | Clothing    |        125.00 |           125.00
 Coffee Maker | Home        |         85.00 |           285.00
(6 rows)

Если переписать этот запрос без выражения WITH, получится более сложный запрос с двумя вложенными подзапросами в FROM:

SELECT product_sales.product_name,
       product_sales.category,
       product_sales.total_revenue,
       category_sales.category_revenue
FROM (SELECT sales.product_name,
             sales.category,
             SUM(sales.quantity)                    AS total_quantity,
             SUM(sales.quantity * sales.unit_price) AS total_revenue
      FROM sales
      GROUP BY sales.product_name, sales.category) AS product_sales
         JOIN (SELECT sales.category,
                      SUM(sales.quantity * sales.unit_price) AS category_revenue
               FROM sales
               GROUP BY sales.category) AS category_sales
              ON product_sales.category = category_sales.category
ORDER BY product_sales.total_revenue DESC;

Рекурсивные CTE

Синтаксис

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

WITH RECURSIVE <cte_name> (<column_list>) AS (
    -- Нерекурсивная часть: запускается один раз
    <non_recursive_term>

    UNION [ALL]

    -- Рекурсивная часть: ссылается на <cte_name>, выполняется многократно
    <recursive_term>
)
<main_query>;

где:

  • <cte_name> — имя CTE; используется в рекурсивной части и в основном запросе.

  • <column_list> — (опционально) список имен столбцов в результирующем наборе CTE.

  • <non_recursive_term> — базовый запрос, который формирует начальный набор результатов (выполняется один раз).

  • <recursive_term> — запрос, который ссылается на <cte_name> и выполняется многократно, последовательно добавляя новые строки в результирующий набор, пока не перестанут появляться новые данные. Результат объединяется с базовым запросом с помощью оператора UNION или UNION ALL.

  • <main_query> — основной SQL-запрос, который использует итоговый результат рекурсивного CTE.

Greengage DB обрабатывает рекурсивные CTE следующим образом:

  1. Выполняется нерекурсивная часть для получения начального набора результатов.

  2. Выполняется рекурсивная часть, которая использует результаты предыдущего шага в качестве входных данных для генерации дополнительных строк.

  3. Шаг 2 повторяется до тех пор, пока рекурсивная часть не перестанет возвращать новые строки.

  4. Возвращается итоговый набор результатов — объединение всех строк, полученных в нерекурсивной и рекурсивной частях.

Последовательность чисел

Этот запрос использует рекурсивное CTE для генерации последовательности чисел от 1 до 5:

WITH RECURSIVE numbers AS (
    -- Нерекурсивная часть
    SELECT 1 AS n

    UNION ALL

    -- Рекурсивная часть
    SELECT n + 1 FROM numbers WHERE n < 5
)
SELECT n FROM numbers;

Результат выглядит следующим образом:

 n
---
 1
 2
 3
 4
 5
(5 rows)

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

В этом запросе используется рекурсивное CTE running_sum для вычисления накопительной суммы для чисел от 1 до 5:

WITH RECURSIVE running_sum AS (
    SELECT 1 AS n,
           1 AS running_total

    UNION ALL

    SELECT n + 1,
           running_total + n + 1
    FROM running_sum
    WHERE n < 5
)
SELECT n,
       running_total
FROM running_sum;

Результат выглядит следующим образом:

 n | running_total
---+---------------
 1 |             1
 2 |             3
 3 |             6
 4 |            10
 5 |            15
(5 rows)

Этот запрос вычисляет накопительную сумму количества товаров в таблице sales с помощью рекурсивного CTE. Вычисление начинается с определенной даты и последовательно добавляет количество товаров, проданных в каждый из дней, к накопительному итогу предыдущих дней:

WITH RECURSIVE cumulative_quantity AS (
   SELECT date,
          quantity,
          quantity AS cumulative_total
   FROM sales
   WHERE date = '2025-01-03'

   UNION ALL

   SELECT sales.date,
          sales.quantity,
          cumulative_quantity.cumulative_total + sales.quantity
   FROM sales
   JOIN cumulative_quantity ON sales.date = cumulative_quantity.date + INTERVAL '1 day'
)
SELECT date,
       quantity,
       cumulative_total
FROM cumulative_quantity;

Результат должен выглядеть следующим образом:

    date    | quantity | cumulative_total
------------+----------+------------------
 2025-01-03 |        1 |                1
 2025-01-04 |        2 |                3
 2025-01-05 |        1 |                4
 2025-01-06 |        3 |                7
 2025-01-07 |        1 |                8
 2025-01-08 |        2 |               10
 2025-01-09 |        1 |               11
 2025-01-10 |        2 |               13
 2025-01-11 |        1 |               14
(9 rows)

Иерархические данные

Чтобы выполнить примеры из этого раздела, создайте таблицу customers:

CREATE TABLE customers
(
    customer_id INT,
    name        TEXT,
    referred_by INT,
    region      TEXT
)
    WITH (appendoptimized = true)
    DISTRIBUTED BY (customer_id);

Этот запрос добавляет данные о клиентах с информацией о реферальных связях:

INSERT INTO customers (customer_id, name, referred_by, region)
VALUES (1, 'Alice', NULL, 'North'),
       (2, 'Bob', 1, 'North'),
       (3, 'Charlie', 1, 'North'),
       (4, 'David', 2, 'South'),
       (5, 'Eve', 2, 'South'),
       (6, 'Frank', 4, 'South'),
       (7, 'Grace', 3, 'East');
/*
       Alice
       ├── Bob
       │   ├── David
       │   │   └── Frank
       │   └── Eve
       └── Charlie
           └── Grace
*/

Этот запрос использует рекурсивное CTE для поиска всех клиентов, которые были приглашены клиентом с customer_id, равным 2 — как напрямую, так и через цепочку рекомендаций:

WITH RECURSIVE referrals(customer_id, name, referred_by) AS (
    SELECT customer_id,
           name,
           referred_by
    FROM customers
    WHERE customer_id = 2

    UNION ALL

    SELECT customers.customer_id,
           customers.name,
           customers.referred_by
    FROM customers
    INNER JOIN referrals ON customers.referred_by = referrals.customer_id
)
SELECT * FROM referrals;

Порядок поиска

По умолчанию рекурсивные запросы возвращают результаты в порядке "сначала в ширину" (breadth-first). При применении к таблице customers поиск в порядке "сначала в ширину" обрабатывает всех непосредственных рефералов клиента, прежде чем переходить глубже по цепочке рекомендаций. При порядке "сначала в глубину" (depth-first) запрос проходит по каждой цепочке рекомендаций как можно глубже, прежде чем перейти к следующему рефералу.

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

WITH RECURSIVE referrals(customer_id, name, referred_by, path) AS (
    SELECT
        customer_id,
        name,
        referred_by,
        ARRAY[customer_id] AS path
    FROM customers
    WHERE customer_id = 2

    UNION ALL

    SELECT
        customers.customer_id,
        customers.name,
        customers.referred_by,
        referrals.path || customers.customer_id
    FROM customers
    INNER JOIN referrals ON customers.referred_by = referrals.customer_id
)
SELECT * FROM referrals;

Чтобы вывести результаты в порядке "сначала в глубину", добавьте ORDER BY path во внешнем запросе:

WITH RECURSIVE referrals(customer_id, name, referred_by, path) AS (
    SELECT
        customer_id,
        name,
        referred_by,
        ARRAY[customer_id] AS path
    FROM customers
    WHERE customer_id = 2

    UNION ALL

    SELECT
        customers.customer_id,
        customers.name,
        customers.referred_by,
        referrals.path || customers.customer_id
    FROM customers
    INNER JOIN referrals ON customers.referred_by = referrals.customer_id
)
SELECT * FROM referrals ORDER BY path;

Результаты этих запросов:

 customer_id | name  | referred_by |  path
-------------+-------+-------------+---------
           2 | Bob   |           1 | {2}
           4 | David |           2 | {2,4}
           5 | Eve   |           2 | {2,5}
           6 | Frank |           4 | {2,4,6}
(4 rows)
 customer_id | name  | referred_by |  path
-------------+-------+-------------+---------
           2 | Bob   |           1 | {2}
           4 | David |           2 | {2,4}
           6 | Frank |           4 | {2,4,6}
           5 | Eve   |           2 | {2,5}
(4 rows)

Выявление бесконечных циклов

В рекурсивных SQL-запросах рекурсивная часть должна в конечном итоге возвращать пустой результат, чтобы избежать бесконечного цикла. Чтобы продемонстрировать, как обнаруживать и обрабатывать циклы, создайте цикл, обновив таблицу customers следующим образом:

UPDATE customers
SET referred_by = 6
WHERE customer_id = 1;

Этот SQL-запрос использует рекурсивное CTE для обхода реферальных связей, начиная с клиента с ID, равным 2, и выявляет циклы, отслеживая цепочки рекомендаций. В запрос добавляется флаг cycle, который устанавливается в true, если клиент встречается в цепочке рекомендаций более одного раза:

WITH RECURSIVE referrals(customer_id, name, referred_by, path, cycle) AS (
    SELECT
        customer_id,
        name,
        referred_by,
        ARRAY[customer_id] AS path,
        FALSE AS cycle
    FROM customers
    WHERE customer_id = 2

    UNION ALL

    SELECT
        customers.customer_id,
        customers.name,
        customers.referred_by,
        referrals.path || customers.customer_id,
        customers.customer_id = ANY(referrals.path) AS cycle  -- Выявление цикла
    FROM customers
    INNER JOIN referrals ON customers.referred_by = referrals.customer_id
    WHERE NOT referrals.cycle  -- Остановка рекурсии, если обнаружен цикл
)
SELECT * FROM referrals;
 customer_id |  name   | referred_by |     path      | cycle
-------------+---------+-------------+---------------+-------
           2 | Bob     |           1 | {2}           | f
           5 | Eve     |           2 | {2,5}         | f
           4 | David   |           2 | {2,4}         | f
           6 | Frank   |           4 | {2,4,6}       | f
           1 | Alice   |           6 | {2,4,6,1}     | f
           3 | Charlie |           1 | {2,4,6,1,3}   | f
           2 | Bob     |           1 | {2,4,6,1,2}   | t
           7 | Grace   |           3 | {2,4,6,1,3,7} | f
(8 rows)

Для обнаружения цикла по нескольким полям можно использовать массив строк для отслеживания обработанных комбинаций:

WITH RECURSIVE referrals AS (
    SELECT
        customer_id,
        name,
        referred_by,
        region,
        ARRAY[ROW(customer_id, region)] AS path,
        FALSE AS cycle
    FROM customers
    WHERE customer_id = 2

    UNION ALL

    SELECT
        customers.customer_id,
        customers.name,
        customers.referred_by,
        customers.region,
        referrals.path || ROW(customers.customer_id, customers.region),
        ROW(customers.customer_id, customers.region) = ANY(referrals.path)
    FROM customers
    JOIN referrals ON customers.referred_by = referrals.customer_id
    WHERE NOT referrals.cycle
)
SELECT * FROM referrals;
 customer_id |  name   | referred_by | region |                                   path                                   | cycle
-------------+---------+-------------+--------+--------------------------------------------------------------------------+-------
           2 | Bob     |           1 | North  | {"(2,North)"}                                                            | f
           4 | David   |           2 | South  | {"(2,North)","(4,South)"}                                                | f
           5 | Eve     |           2 | South  | {"(2,North)","(5,South)"}                                                | f
           6 | Frank   |           4 | South  | {"(2,North)","(4,South)","(6,South)"}                                    | f
           1 | Alice   |           6 | North  | {"(2,North)","(4,South)","(6,South)","(1,North)"}                        | f
           2 | Bob     |           1 | North  | {"(2,North)","(4,South)","(6,South)","(1,North)","(2,North)"}            | t
           3 | Charlie |           1 | North  | {"(2,North)","(4,South)","(6,South)","(1,North)","(3,North)"}            | f
           7 | Grace   |           3 | East   | {"(2,North)","(4,South)","(6,South)","(1,North)","(3,North)","(7,East)"} | f
(8 rows)

Полезный прием для предотвращения бесконечных циклов при тестировании рекурсивных SQL-запросов — добавить выражение LIMIT в основной запрос. Например, без LIMIT этот рекурсивный запрос будет выполняться бесконечно:

WITH RECURSIVE numbers AS (
    SELECT 1 AS n

    UNION ALL

    SELECT n + 1 FROM numbers
)
SELECT n FROM numbers LIMIT 10;
 n
----
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
(10 rows)

Отключение рекурсивных CTE

Рекурсивные CTE можно отключить, установив параметр конфигурации сервера gp_recursive_cte в значение false. Эта команда отключает рекурсивные CTE для текущей сессии:

SET gp_recursive_cte = false;

Теперь вы можете проверить, что рекурсивные CTE отключены, например:

WITH RECURSIVE numbers AS (
    SELECT 1 AS n

    UNION ALL

    SELECT n + 1 FROM numbers WHERE n < 5
)
SELECT n FROM numbers;

Запрос должен завершиться с ошибкой:

RECURSIVE clauses in WITH queries are currently disabled

Повторное использование CTE

В Greengage DB GPORCA может автоматически переиспользовать результаты CTE, к которому обращаются несколько раз, в зависимости от оценки стоимости. Планировщик Postgres с помощью параметра конфигурации gp_cte_sharing управляет тем, будет ли CTE, используемый в запросе несколько раз, вычисляться заново для каждого использования:

  • Если gp_cte_sharing установлен в on, планировщик вычисляет CTE один раз и использует результаты для всех обращений.

  • Если gp_cte_sharing установлен в off, каждое обращение к CTE выполняется независимо, то есть вычисление повторяется для каждого использования.

Изменение данных

Для SELECT-запросов можно использовать команды INSERT, UPDATE и DELETE в выражении WITH. Это позволяет выполнять несколько операций в одном запросе.

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

Чтобы выполнить примеры из этого раздела, создайте таблицу stg_sales:

CREATE TABLE stg_sales
(
    id           INT,
    date         DATE,
    product_name TEXT,
    category     TEXT,
    quantity     INT,
    unit_price   NUMERIC(8, 2)
)
    DISTRIBUTED BY (id);

Добавьте данные:

INSERT INTO stg_sales (id, date, product_name, category, quantity, unit_price)
VALUES (1, '2025-01-03', 'Laptop', 'Electronics', 1, 1200.00),
       (2, '2025-01-04', 'Headphones', 'Electronics', 2, 150.00),
       (3, '2025-01-05', 'Coffee Maker', 'Home', 1, 85.00),
       (4, '2025-01-06', 'T-Shirt', 'Clothing', 3, 25.00),
       (5, '2025-01-07', 'Desk Chair', 'Home', 1, 200.00),
       (6, '2025-01-08', 'Smartphone', 'Electronics', 2, 800.00),
       (7, '2025-01-09', 'Headphones', 'Electronics', 1, 150.00),
       (8, '2025-01-10', 'T-Shirt', 'Clothing', 2, 25.00),
       (9, '2025-01-11', 'Laptop', 'Electronics', 1, 1200.00);

Удаление

Этот SQL-запрос удаляет строки из таблицы stg_sales с датами до 6 января 2025 года и возвращает данные об удаленных строках:

WITH deleted_sales AS (
    DELETE FROM stg_sales
        WHERE date < '2025-01-06'
        RETURNING id, product_name, date)
SELECT *
FROM deleted_sales;
 id | product_name |    date
----+--------------+------------
  3 | Coffee Maker | 2025-01-05
  1 | Laptop       | 2025-01-03
  2 | Headphones   | 2025-01-04
(3 rows)

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

ПРИМЕЧАНИЕ

Выражение DELETE …​ RETURNING не поддерживается для оптимизированных для добавления (AO) таблиц.

Обновление

Этот запрос увеличивает цену Laptop на 10% в таблице stg_sales и возвращает обновленные строки:

WITH updated_sales AS (
    UPDATE stg_sales
        SET unit_price = unit_price * 1.10
        WHERE product_name = 'Laptop'
        RETURNING id, product_name, unit_price)
SELECT *
FROM updated_sales;
 id | product_name | unit_price
----+--------------+------------
  9 | Laptop       |    1320.00
(1 row)

Этот запрос увеличивает цену Laptop на 10% в таблице stg_sales, однако основной запрос выбирает все строки из исходной таблицы, а не обновленные строки, возвращенные CTE:

WITH updated_sales AS (
    UPDATE stg_sales
        SET unit_price = unit_price * 1.10
        WHERE product_name = 'Laptop'
        RETURNING id, product_name, unit_price)
SELECT id, product_name, unit_price
FROM stg_sales;
 id | product_name | unit_price
----+--------------+------------
  4 | T-Shirt      |      25.00
  7 | Headphones   |     150.00
  8 | T-Shirt      |      25.00
  5 | Desk Chair   |     200.00
  6 | Smartphone   |     800.00
  9 | Laptop       |    1320.00
(6 rows)

Рекурсивные CTE

Чтобы выполнить примеры из этого раздела, создайте таблицу stg_customers:

CREATE TABLE stg_customers
(
    customer_id INT,
    name        TEXT,
    referred_by INT,
    region      TEXT
)
    DISTRIBUTED BY (customer_id);

Этот запрос добавляет данные о клиентах с информацией о реферальных связях:

INSERT INTO stg_customers (customer_id, name, referred_by, region)
VALUES (1, 'Alice', NULL, 'North'),
       (2, 'Bob', 1, 'North'),
       (3, 'Charlie', 1, 'North'),
       (4, 'David', 2, 'South'),
       (5, 'Eve', 2, 'South'),
       (6, 'Frank', 4, 'South'),
       (7, 'Grace', 3, 'East');
/*
       Alice
       ├── Bob
       │   ├── David
       │   │   └── Frank
       │   └── Eve
       └── Charlie
           └── Grace
*/

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

Этот запрос использует рекурсивное CTE для поиска всех клиентов, напрямую или косвенно приглашенных клиентом с customer_id, равным 2, а затем удаляет их из таблицы stg_customers:

WITH RECURSIVE referrals(customer_id, name, referred_by) AS (
    SELECT customer_id,
           name,
           referred_by
    FROM stg_customers
    WHERE customer_id = 2

    UNION ALL

    SELECT stg_customers.customer_id,
           stg_customers.name,
           stg_customers.referred_by
    FROM stg_customers
    INNER JOIN referrals ON stg_customers.referred_by = referrals.customer_id
)
DELETE FROM stg_customers
WHERE customer_id IN (SELECT customer_id FROM referrals);

Чтобы проверить, что записи о клиентах удалены, выполните запрос к таблице stg_customers:

SELECT *
FROM stg_customers;

Результат выглядит следующим образом:

 customer_id |  name   | referred_by | region
-------------+---------+-------------+--------
           3 | Charlie |           1 | North
           7 | Grace   |           3 | East
           1 | Alice   |             | North
(3 rows)