vyvchy
    Теми розділу

    05 · Бази даних і SQL для QA

    JOIN: типи з'єднань таблиць

    Зміст

    У реляційній базі дані навмисно розкидані по таблицях: замовлення не зберігає імʼя клієнта, воно тримає лише customer_id — посилання на рядок в іншій таблиці (див. Реляційна модель). Це економить місце й уникає розсинхрону, але означає, що жодна окрема таблиця не показує повної картини. Щоб побачити «замовлення № 101 клієнта Ігоря з Києва на 3 позиції», ці шматки треба зібрати назад. Саме це й робить JOIN — зʼєднує рядки двох чи більше таблиць за умовою відповідності.

    Для QA JOIN — це не академічна вправа, а щоденний інструмент. Ним звіряють, що UI показує правильно зшиті дані; шукають «сирітські» (orphan) записи, які посилаються в нікуди; ловлять роздуті звіти, де сума подвоїлась через невдалий JOIN. Помилка в одному ключовому слові — LEFT замість INNER чи умова не в тому місці — тихо змінює результат, і зіпсований звіт виглядає правдоподібно. Тому механіку треба розуміти, а не завчати назви.

    INNER JOIN: лишаються тільки збіги

    INNER JOIN (або просто JOIN — це синоніми) бере рядок лівої таблиці, шукає для нього відповідні рядки правої за умовою ON, і повертає лише ті пари, де умова істинна. Рядок, для якого пари немає з жодного боку, у результат не потрапляє взагалі.

    SELECT o.id, o.total, c.name
    FROM orders AS o
    INNER JOIN customers AS c ON c.id = o.customer_id;

    Умова після ON — це серце зʼєднання. Найчастіше вона звіряє (foreign key) однієї таблиці з (primary key) іншої: o.customer_id = c.id. Але ON — звичайний логічний вираз: там можуть бути кілька умов через AND, нерівності, порівняння діапазонів. (o, c) не обовʼязкові, та коли таблиць кілька або їхні стовпці однойменні, без них запит стає нечитабельним і неоднозначним.

    Ключовий наслідок, який часто забувають: INNER JOIN симетричний і водночас відсіює. Якщо в orders є замовлення з customer_id, якого немає в customers, — таке замовлення зникне з результату мовчки. Якщо клієнт не зробив жодного замовлення — його теж не буде. INNER JOIN показує лише перетин.

    Спробуй сам — зʼєднай дві таблиці в :

    Завантаження SQL-пісочниці…

    LEFT JOIN: зберегти все зліва, доповнити NULL

    Часто перетину замало. «Покажи всіх клієнтів разом із їхніми замовленнями, а хто нічого не купив — теж покажи» — це вже про LEFT JOIN (повна назва LEFT OUTER JOIN). Він гарантує: кожен рядок лівої таблиці буде в результаті. Якщо для нього знайшлась пара справа — стовпці правої таблиці заповнені; якщо ні — вони заповнені NULL.

    SELECT c.name, o.id AS order_id
    FROM customers AS c
    LEFT JOIN orders AS o ON o.customer_id = c.id;

    Клієнт без замовлень тут зʼявиться одним рядком, де order_id дорівнює NULL. Це прямий місток до теми пасток NULL: NULL після outer-джойна означає не «нуль» і не «порожньо», а «пари не знайшлося».

    RIGHT JOIN — дзеркальна версія: зберігає всі рядки правої таблиці. На практиці його пишуть рідко, бо будь-який RIGHT JOIN переписується як LEFT JOIN простою перестановкою таблиць, а читати запит, де всі джойни ліві, легше. Знати RIGHT треба, застосовувати — здебільшого ні.

    Сирітські записи: анти-джойн

    LEFT JOIN дає елегантний спосіб знайти записи без пари — те, що на співбесідах формулюють як «знайди клієнтів, які нічого не замовляли». Роблять outer-джойн, а потім лишають тільки ті рядки, де права сторона виявилась NULL:

    -- клієнти без жодного замовлення
    SELECT c.id, c.name
    FROM customers AS c
    LEFT JOIN orders AS o ON o.customer_id = c.id
    WHERE o.id IS NULL;

    Цей патерн називають (anti-join). Він же ловить биту цілісність — замовлення, що посилається на неіснуючого клієнта:

    -- сирітські замовлення: customer_id вказує в нікуди
    SELECT o.id, o.customer_id
    FROM orders AS o
    LEFT JOIN customers AS c ON c.id = o.customer_id
    WHERE c.id IS NULL;

    Для QA це готова перевірка посилальної цілісності: у здоровій базі з правильними зовнішніми ключами такий запит має повертати нуль рядків. Якщо повертає — маєте баг даних, і це канонічна тема глави Тестування БД.

    Класична перевірка через LEFT JOIN + IS NULL — знайди клієнтів без замовлень:

    Завантаження SQL-пісочниці…

    FULL OUTER і CROSS

    FULL OUTER JOIN — обʼєднання обох боків: зберігає і рядки лівої таблиці без пари, і рядки правої без пари, доповнюючи відсутню сторону NULL. Він відповідає на питання «покажи все з обох таблиць, а де немає відповідності — познач». Зручно для звірки двох наборів: які записи є лише зліва, які лише справа, які збіглися.

    SELECT c.name, o.id
    FROM customers AS c
    FULL OUTER JOIN orders AS o ON o.customer_id = c.id;

    Пастка діалектів: FULL OUTER JOIN є не скрізь

    PostgreSQL FULL OUTER JOIN підтримує: він описаний у довідці з табличних виразів нарівні з INNER, LEFT і RIGHT, зі своєю семантикою й синтаксисом.

    MySQL його не має — але сказати це чесно складніше, ніж здається, тому варто знати, звідки береться сама теза. У довіднику MySQL сторінка JOIN Clause про FULL OUTER JOIN мовчить: вона не називає його ані підтримуваним, ані непідтримуваним, тож посилатися на неї як на «підтверджену відсутність» не можна — відсутність згадки не є доказом. Відсутність самої можливості зафіксована на боці розробки MySQL:

    • офіційний worklog Oracle WL#1604 «Support FULL [OUTER] JOIN by rewriting with UNION» — проєктна специфікація, як цю підтримку зробити, — досі має Affects: Prototype Only і Status: Un-Assigned;
    • привʼязаний до неї запит у баг-трекері #18003 «FULL OUTER JOIN (no syntax to cover)» команда MySQL Verification Team прийняла й перекваліфікувала у feature request: Status: Verified, Severity: S4 (Feature request). Подано 2006 року, виконавця не призначено.

    Тобто підтримка FULL OUTER JOIN у MySQL існує як невиконана задача, а не як можливість. Якщо запит із FULL OUTER JOIN падає із синтаксичною помилкою — перше, що варто перевірити, це яка саме СУБД під запитом.

    Емуляцію для MySQL дає той самий worklog, і вона теж не безкоштовна:

    -- Формула з WL#1604: FULL OUTER JOIN ≡ LEFT ∪ RIGHT
    (SELECT c.name, o.id FROM customers AS c LEFT  JOIN orders AS o ON o.customer_id = c.id)
    UNION
    (SELECT c.name, o.id FROM customers AS c RIGHT JOIN orders AS o ON o.customer_id = c.id);

    LEFT JOIN

    клієнти без замовлень

    збіги

    RIGHT JOIN

    сирітські замовлення

    UNION без ALL

    емуляція FULL OUTER JOIN

    LEFT JOIN

    клієнти без замовлень

    збіги

    RIGHT JOIN

    сирітські замовлення

    UNION без ALL

    емуляція FULL OUTER JOIN

    Ключова думка діаграми: FULL OUTER JOIN тут не вмикається однією командою, а складається з двох половин — LEFT JOIN дає збіги і клієнтів без замовлень, RIGHT JOIN доносить сирітські замовлення, а UNION зводить обидві половини в один набір.

    UNION без ALL тут потрібен, щоб прибрати дублікати від рядків, які реально збіглися за умовою. Але він не розрізняє, які дублікати «зайві», а які були в даних із самого початку: якщо в лівій таблиці є два однакові рядки, UNION схлопне їх в один. У прикладі самої специфікації LEFT JOIN повертає два рядки, а емуляція через UNION — один; висновок там сформульований дослівно: «full join» повертає менше рядків, ніж лівий джойн. Для QA це і є головна пастка: FULL OUTER JOIN беруть саме для звірки двох наборів, а емуляція на даних із повторюваними рядками мовчки з'їдає частину розбіжностей, які ви й шукали.

    CROSS JOIN стоїть окремо: у нього немає ON. Він дає декартів добуток (Cartesian product) — кожен рядок лівої таблиці склеюється з кожним рядком правої. Дві таблиці по 10 рядків дадуть 100. Іноді це саме те, що треба — згенерувати всі комбінації розмірів і кольорів:

    SELECT s.size, cl.color
    FROM sizes AS s
    CROSS JOIN colors AS cl;

    Але значно частіше декартів добуток трапляється випадково — коли таблиці перелічені через кому, а умову зʼєднання забули у WHERE. FROM orders o, customers c без WHERE — це той самий CROSS JOIN, тільки замаскований під старий синтаксис. Результат роздувається, запит гальмує, а звіт бреше. Явний JOIN ... ON тим і кращий за кому, що умова зʼєднання видима: PostgreSQL INNER JOIN без ON узагалі не пропустить, а от MySQL пропустить і мовчки зробить декартів добуток — там JOIN, INNER JOIN і CROSS JOIN синтаксично взаємозамінні.

    Self-join: таблиця зʼєднана сама із собою

    Іноді звʼязок веде всередину тієї самої таблиці. Класика — співробітники, де кожен має manager_id, що посилається на id іншого співробітника в тій самій таблиці employees. Щоб поряд із іменем працівника показати імʼя його керівника, таблицю зʼєднують саму із собою — це self-join. Технічно нічого нового: та сама таблиця згадується двічі під різними аліасами, і без аліасів тут ніяк — інакше СУБД не зрозуміє, про який із двох екземплярів ідеться.

    SELECT e.name AS employee, m.name AS manager
    FROM employees AS e
    LEFT JOIN employees AS m ON m.id = e.manager_id;

    LEFT JOIN тут не випадковий: у найвищого керівника manager_id дорівнює NULL, і INNER JOIN викинув би його з результату. Self-join застосовують і для ієрархій категорій, і щоб знайти пари рядків з одним значенням (наприклад, двох користувачів з однаковим email) — усюди, де рядки таблиці посилаються одне на одного.

    Дублікати після JOIN: пастка 1:N

    Найпідступніший ефект джойна виникає на звʼязку «один до багатьох» (1:N). Один клієнт має багато замовлень; одне замовлення — багато позицій (order_items). Коли ви зʼєднуєте «одиничну» таблицю з «множинною», рядок з боку «один» повторюється стільки разів, скільки в нього пар з боку «багато».

    клієнт 7 · Ігор

    JOIN orders

    Ігор · замовлення 101

    Ігор · замовлення 102

    Ігор · замовлення 103

    клієнт 7 · Ігор

    JOIN orders

    Ігор · замовлення 101

    Ігор · замовлення 102

    Ігор · замовлення 103

    Сам по собі це не баг — так і має бути, ми ж просили рядок на кожну пару. Баг зʼявляється, коли поверх такого джойна рахують агрегати. Якщо після зʼєднання orders з order_items порахувати SUM(o.total), сума кожного замовлення додасться стільки разів, скільки в ньому позицій, — і виторг магічно виросте. Так само COUNT(*) рахуватиме не клієнтів і не замовлення, а рядки роздутого набору.

    -- НЕБЕЗПЕЧНО: total подвоюється на кожну позицію замовлення
    SELECT c.name, SUM(o.total)
    FROM customers AS c
    JOIN orders AS o      ON o.customer_id = c.id
    JOIN order_items AS oi ON oi.order_id = o.id
    GROUP BY c.name;

    Це класичний сюжет «чому цифра неправильна». Ознака — результат більший за очікуваний рівно в кілька разів. Лікування залежить від задачі: агрегувати «множинну» таблицю окремо (у чи CTE — див. Підзапити і CTE) до зʼєднання, або застосувати COUNT(DISTINCT ...) замість COUNT(*). Детальніше про правила агрегації поверх джойнів — у главі Агрегація: GROUP BY і HAVING. Головне — памʼятати, що JOIN за звʼязком 1:N розмножує рядки, і кожен агрегат після нього треба перевіряти окремо.

    Пастка ON vs WHERE у LEFT JOIN

    Ось де ламається інтуїція навіть у тих, хто впевнено знає різницю INNER/LEFT. Питання: куди покласти умову-фільтр на праву таблицю — в ON чи у WHERE? Для INNER JOIN різниці в результаті немає. Для LEFT JOIN вона фундаментальна.

    Щоб зрозуміти чому, треба знати логічний порядок обробки: спершу виконується зʼєднання за умовою ON (і саме тут outer-джойн додає рядки без пари, доповнені NULL), і лише потім по всьому зʼєднаному набору проходить WHERE.

    FROM customers

    LEFT JOIN orders

    ON: умова зʼєднання
    рядки без пари → доповнені NULL

    WHERE: фільтр по зʼєднаному набору
    рядки з NULL можуть відсіятись

    SELECT

    FROM customers

    LEFT JOIN orders

    ON: умова зʼєднання
    рядки без пари → доповнені NULL

    WHERE: фільтр по зʼєднаному набору
    рядки з NULL можуть відсіятись

    SELECT

    Тепер порівняйте два майже однакові запити:

    -- НЕ те, що очікуєш: фактично INNER JOIN
    SELECT c.name, o.id
    FROM customers AS c
    LEFT JOIN orders AS o ON o.customer_id = c.id
    WHERE o.status = 'paid';
    
    -- Правильно: фільтр — усередині ON
    SELECT c.name, o.id
    FROM customers AS c
    LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid';

    У першому запиті LEFT JOIN чесно зберіг клієнтів без замовлень, доповнивши їхній o.status значенням NULL. Але потім WHERE o.status = 'paid' перевіряє цей NULL — і NULL = 'paid' дає не «істина» і не «хиба», а «невідомо», тож рядок відсіюється. У результаті всі клієнти без оплачених замовлень зникли, і LEFT JOIN тихо перетворився на INNER JOIN. У другому запиті умова стоїть в ON: вона впливає на те, які замовлення прикріпляться під час зʼєднання, але не викидає клієнтів — хто без оплачених замовлень, той лишається з NULL у стовпці замовлення.

    Правило: фільтр на праву (nullable) таблицю LEFT JOIN кладуть в ON, а не у WHERE — інакше зникає сам сенс outer-джойна. Виняток свідомий: якщо WHERE right.key IS NULL — це вже навмисний анти-джойн, і саме там WHERE доречний.

    JOIN трьох і більше таблиць

    Реальні запити рідко обмежуються двома таблицями. Щоб зібрати «клієнт, місто, замовлення, товар», джойни ставлять ланцюгом — кожен наступний приєднує ще одну таблицю до вже зʼєднаного набору:

    SELECT c.name, ci.name AS city, o.id AS order_id, p.name AS product
    FROM orders AS o
    JOIN customers   AS c  ON c.id = o.customer_id
    JOIN cities      AS ci ON ci.id = c.city_id
    JOIN order_items AS oi ON oi.order_id = o.id
    JOIN products    AS p  ON p.id = oi.product_id;

    СУБД обчислює це послідовно, але оптимізатор вільний змінювати фізичний порядок зʼєднань — на результат це не впливає, лише на швидкодію (тема глави Індекси і швидкодія поки що попереду). Що справді впливає на результат — це тип кожного джойна в ланцюгу. Варто памʼятати одну річ: щойно ви зробили LEFT JOIN, а наступним кроком INNER JOIN до тієї самої, потенційно NULL-ової таблиці, — INNER викине рядки з NULL, і ваш LEFT знеціниться. Якщо треба зберегти рядки лівого боку до кінця ланцюга, усі наступні джойни по цій гілці теж мають бути LEFT.

    Побіжно про синоніми: USING (customer_id) — це скорочення для ON a.customer_id = b.customer_id, коли стовпці однойменні (об'єднує їх в один у виводі). А от NATURAL JOIN, який сам зʼєднує за всіма однойменними стовпцями, у продакшн-запитах краще не застосовувати — досить комусь додати таблиці ще один випадково однойменний стовпець (наприклад, created_at), і умова зʼєднання тихо зміниться. Явний ON завжди надійніший.

    Типові помилки

    • LEFT JOIN, а рядків стало менше, ніж у лівій таблиці. Виглядає як загублені дані, а насправді умову на праву таблицю поклали у WHERE замість ON — і outer-джойн виродився в inner.
    • SUM/COUNT після джойна завищені в кілька разів. Виглядає як дублікати в даних, а насправді звʼязок 1:N розмножив рядки «одиничної» таблиці, і агрегат порахував їх повторно.
    • Рядки з NULL у ключі зникли після INNER JOIN. Виглядає як втрата записів, а насправді NULL не дорівнює нічому, навіть іншому NULL, тож пари для нього не знаходиться ніколи.
    • FULL OUTER JOIN падає з синтаксичною помилкою. Виглядає як помилка у вашому SQL, а насправді під запитом MySQL, у якому цієї можливості немає (WL#1604 не призначений, feature request #18003 відкритий) — потрібна емуляція через UNION.
    • Емуляція FULL OUTER JOIN через UNION показала менше розбіжностей, ніж очікували. Виглядає як «дані збіглися», а насправді UNION без ALL схлопнув і справжні дублікати рядків: на таких даних емуляція повертає менше рядків, ніж навіть звичайний LEFT JOIN.
    • Таблиці через кому — і рядків раптом десятки тисяч. Виглядає як звичайний джойн, а насправді забута умова зʼєднання перетворила його на CROSS JOIN з декартовим добутком.
    • NATURAL JOIN повертає не те. Виглядає як зручне скорочення, а насправді він зʼєднав за випадково однойменним стовпцем, про який ви не подумали.

    Підсумок

    • INNER JOIN лишає тільки збіги; LEFT/RIGHT/FULL OUTER зберігають рядки без пари, доповнюючи відсутню сторону NULL.
    • FULL OUTER JOIN є в PostgreSQL і немає в MySQL: там це досі невиконана задача (WL#1604, feature request #18003), а емуляція LEFT UNION RIGHT на даних із дублікатами повертає менше рядків, ніж мала б.
    • Фільтр на праву таблицю в LEFT JOIN кладуть у ON; той самий фільтр у WHERE мовчки перетворює його на INNER JOIN.
    • Зʼєднання за звʼязком 1:N розмножує рядки «одиничного» боку — будь-який COUNT/SUM після такого джойна перевіряй окремо.
    • й записи без пари шукають анти-джойном: LEFT JOIN ... WHERE right_table.key IS NULL.
    • У ланцюгу кількох таблиць тип кожного джойна важить: INNER після LEFT по nullable-гілці знецінює LEFT.

    Можливі питання

    • «Чим INNER JOIN відрізняється від LEFT JOIN — базове питання-фільтр. Інтервʼюер хоче почути не лише «LEFT зберігає ліву таблицю», а й звідки беруться NULL у результаті та які рядки зникають при INNER.
    • «Знайдіть клієнтів, які не зробили жодного замовлення». Очікують анти-джойн (LEFT JOIN ... WHERE o.id IS NULL) або еквівалент через NOT EXISTS; слабка відповідь — спроба через NOT IN, який ламається на NULL.
    • «Після JOIN сума виторгу подвоїлась — чому?» — перевірка розуміння 1:N. Дивляться, чи згадаєте і чи запропонуєте агрегувати до джойна або COUNT(DISTINCT).
    • «У чому різниця між умовою в ON і у WHERE для LEFT JOIN — сеньйорський сигнал. Сильний кандидат пояснює логічний порядок обробки: ON діє під час зʼєднання, WHERE — після, коли NULL-рядки вже додані.
    • «Що таке CROSS JOIN і коли він трапляється випадково?» — перевіряють, чи розумієте декартів добуток і звідки беруться роздуті результати.
    • «FULL OUTER JOIN є в MySQL?» — питання на діалекти. Достатньо «у PostgreSQL є, у MySQL немає, там емулюють через UNION лівого й правого». Сильна відповідь додає, чому емуляція не точний еквівалент: UNION без ALL схлопує й справжні дублікати рядків.

    Головне, на що дивиться інтервʼюер, — чи міркуєте ви про які рядки лишаються і звідки береться NULL, а не переказуєте завчені означення типів джойна. Уміння накидати анти-джойн і пояснити пастку 1:N цінується вище за знання, що RIGHT JOIN існує.

    Джерела

    INNER JOIN: лишаються тільки збіги

    • PostgreSQL 18 — 7.2. Table ExpressionsINNER як дефолт, слова INNER/OUTER необов'язкові, і семантика внутрішнього зʼєднання: рядок на кожну пару, що задовольняє ON.
    • MySQL 8.0 — JOIN Clause — той самий синтаксис у MySQL: JOIN та INNER JOIN як синоніми і роль умови ON.
    • PostgreSQL 18 — SELECT — аліаси таблиць у FROM і навіщо вони потрібні, коли стовпці однойменні.

    LEFT JOIN: зберегти все зліва, доповнити NULL

    • PostgreSQL 18 — 7.2. Table ExpressionsLEFT OUTER JOIN: спершу внутрішнє зʼєднання, потім рядки лівої таблиці без пари доповнюються null, тож результат має щонайменше один рядок на кожен лівий.
    • MySQL 8.0 — JOIN Clause — анти-джойн LEFT JOIN … WHERE right.key IS NULL як офіційний патерн доки, і пряма рекомендація писати LEFT JOIN замість RIGHT JOIN заради переносності.
    • PostgreSQL 18 — 5.5. Constraints — чому в базі з живими зовнішніми ключами такий запит зобовʼязаний повертати нуль рядків.

    FULL OUTER і CROSS

    • PostgreSQL 18 — 7.2. Table ExpressionsFULL OUTER JOIN у PostgreSQL нарівні з рештою типів; CROSS JOIN як N * M рядків і застереження, що кома звʼязується слабше за JOIN.
    • MySQL Worklog WL#1604 — Support FULL [OUTER] JOIN by rewriting with UNION — бік MySQL: Affects: Prototype Only, Status: Un-Assigned; звідти ж формула емуляції LEFT ∪ RIGHT і висновок джерела, що така «full join» повертає менше рядків, ніж лівий джойн.
    • MySQL Bugs #18003 — FULL OUTER JOIN (no syntax to cover) — друге, незалежне підтвердження того самого стану: Status: Verified, Severity: S4 (Feature request), виконавця не призначено.
    • MySQL 8.0 — JOIN Clause — у MySQL JOIN, INNER JOIN і CROSS JOIN синтаксично взаємозамінні, а кома без умови дає той самий декартів добуток. Про FULL OUTER JOIN ця сторінка не пише нічого — ані що він є, ані що його немає (dec-0343, dec-0460).
    • PostgreSQL 18 — 7.4. Combining Queries (UNION, INTERSECT, EXCEPT) — чому UNION без ALL прибирає дублікати, і що саме він при цьому схлопує в емуляції.

    Self-join: таблиця зʼєднана сама із собою

    • PostgreSQL 18 — 7.2. Table Expressions — аліас потрібен саме для self-join, «де ту саму таблицю скановано кілька разів», разом із дослівним прикладом my_table AS a … my_table AS b.
    • PostgreSQL 18 — SELECT — аліаси в FROM як спосіб розрізнити два екземпляри однієї таблиці в одному запиті.

    Дублікати після JOIN: пастка 1:N

    • PostgreSQL 18 — 7.2. Table Expressions — джерело самої механіки: INNER JOIN дає рядок на кожну пару, що задовольняє умову, тож бік «один» повторюється стільки разів, скільки має пар.
    • PostgreSQL 18 — SELECT — чому агрегат після такого зʼєднання рахує рядки роздутого набору, а не вихідні сутності.
    • PostgreSQL 18 — 9.21. Aggregate FunctionsCOUNT(DISTINCT col) як рахунок унікальних значень замість рядків.

    Пастка ON vs WHERE у LEFT JOIN

    • PostgreSQL 18 — 7.2. Table Expressions — дослівна межа: для зовнішніх зʼєднань вибору немає, умова ON не еквівалентна умові WHERE, бо вона додає рядки, а не лише прибирає.
    • PostgreSQL 18 — SELECT — порядок обробки, з якого це випливає: ON діє під час зʼєднання, WHERE — уже по зʼєднаному набору.
    • PostgreSQL 18 — 9.2. Comparison Functions and Operators — чому доданий null не переживає WHERE: результат порівняння з null не істинний, а рядок лишається лише при істині.
    • MySQL 8.0 — JOIN Clause — розподіл ролей ON і WHERE у MySQL і той самий анти-джойн як виняток, де WHERE … IS NULL доречний.

    JOIN трьох і більше таблиць

    • PostgreSQL 18 — 7.2. Table Expressions — ланцюг зʼєднань без дужок вкладається зліва направо; USING названо «reasonably safe», а NATURAL — «considerably more risky», бо без спільних імен він вироджується в CROSS JOIN.
    • PostgreSQL 18 — 14.1. Using EXPLAIN — чому фізичний порядок зʼєднань обирає планувальник, і на результат це не впливає.
    • MySQL 8.0 — JOIN Clause — межа 61 таблиці в одному зʼєднанні й STRAIGHT_JOIN як спосіб зафіксувати порядок читання.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.