Запрос к PostgreSQL через связи:
SELECT item.name item_name, client.name client_name, manager.name manager_name, document_item.quantity FROM document_item , document(document_item) , item(document_item) , client(document) , manager(document)
А вот он же классикой:
SELECT i.name item_name, p.name client_name, m.name manager_name, di.quantity FROM document_item di JOIN document d ON d.id = di.document_id JOIN item i ON i.id = di.item_id JOIN profile p ON p.client_id = d.client_id JOIN profile m ON m.manager_id = d.manager_id
(profile здесь — общая таблица персон с уникальными client_id и manager_id; вся демо-схема — ниже на диаграмме.)
Это ванильный Postgres — без расширений на C, без форка, без нового синтаксиса. Под катом — почему стандарт SQL так и не научился этому за сорок лет (соединения старше самих foreign keys), кто пытался его дожать — от черновиков девяностых до патча, лежащего в pgsql-hackers прямо сейчас, — и как включить такую навигацию в Postgres уже сегодня.
Схема знает связи. SQL заставляет их повторять
FOREIGN KEY — машинно-проверяемое объявление связи: колонки, целевая таблица, кардинальность. Всё это лежит в каталоге, и по этим объявлениям идёт подавляющее большинство реальных соединений — один из практиков в обсуждении на HN оценил эту долю в «95%+ моих джойнов». Но JOIN каждый раз начинается с чистого листа. Отсюда три беды:
Многословие. Пять связей — пять
ON, слово в слово пересказывающих схему.Тихие ошибки.
ONпримет любое равенство:ON d.id = di.item_idвместоdocument_idвыполнится молча — типы совпали. Ошибка всплывёт данными, а не компиляцией.Потеря смысла. По
ON a.x = b.yне видно: связь это или совпадение, кто родитель, размножатся ли строки в агрегате.
Боль не новая: язык начался с SEQUEL в 1974-м — соединения в нём были с первого дня, запятой во FROM. Первый стандарт SQL-86 обошёлся без ссылочной целостности, foreign keys добавила только ревизия SQL-89: синтаксис соединений старше самих связей. А явный JOIN ... ON появился в SQL-92 — комитет проектировал новый синтаксис соединений, уже имея FK перед глазами, и всё равно их не связал. До навигации по связям в запросах стандарт добрался лишь в SQL:2023 — и то графовыми паттернами, а не джойнами.
Пока комитет думал, задачу решили снаружи: любой ORM первым делом даёт навигацию по связям — @relation в Prisma, navigation properties в Entity Framework, select_related в Django. Прикладной код ушёл в ORM во многом именно за этим, стерпев и N+1, и утечки абстракций. Ирония: источник правды о связях всё это время лежал в базе. ORM его дублируют, GraphQL-слои интроспектируют — и только сам SQL делает вид, что ничего не знает.
SQL-сахар, который не помог
Сокращать условия соединения SQL пытался не раз. Все четыре попытки — мимо.
Запятая + WHERE. Досахарная эпоха: таблицы списком, условия в WHERE вперемешку с фильтрами. Забыл условие — получил декартово произведение, молча.
JOIN ... ON. Сегодняшний мейнстрим. Явно, гибко — и ровно с теми тремя бедами, что выше: пересказ, любое равенство, нулевая семантика.
JOIN ... USING (col). Короче, но соединяет по совпадению имён: обе таблицы обязаны называть колонку одинаково. Конвенция supplier_id = supplier_id проходит, самый частый паттерн document_item.document_id → document.id — уже нет. FK-констрейнты в решении не участвуют вовсе.
NATURAL JOIN. Апофеоз: соединить по всем одноимённым колонкам сразу. Добавили во вторую таблицу колонку updated_at, которая была в первой, — запрос молча начал соединять и по ней. Именно NATURAL JOIN дискредитировал саму идею «автоматических» соединений — хотя порок не в автоматике: порок в том, что фундаментом взяли имена колонок (случайность), а не объявленные связи (намерение).
Итог сорока лет стандарта: сказать «соедини по объявленной связи» он так и не разрешил. Либо пересказ схемы руками, либо угадывание по именам.
Кто пытался улучшить синтаксис SQL
Идея «соединять по объявленной связи» витает давно — и у неё богатый некрополь.
Драфты SQL, 1990-е. В черновиках стандарта существовал синтаксис JOIN ... USING PRIMARY KEY | USING FOREIGN KEY | USING CONSTRAINT. По свидетельству Питера Айзентраута, «these ideas just faded away because of other priorities» — идеи просто растворились.
Sybase SQL Anywhere: KEY JOIN. Единственная СУБД, которая это отгрузила — и держит десятилетиями (родословная: Watcom SQL → Sybase → с 2010 года SAP). FROM Products KEY JOIN SalesOrderItems соединяет по объявленному FK; больше того, KEY JOIN там дефолт — голый JOIN без ON означает именно его. Неоднозначность нескольких FK решена конвенцией: role name констрейнта должен совпасть с алиасом. Работает, но за пределы одной нишевой СУБД идея не вышла.
PostgreSQL, волна 2021. Джоэл Якобсон дважды приносил идею в pgsql-hackers: сначала path expressions по FK, затем «Foreign key joins revisited» — синтаксис мутировал девять дней (WITH p->fk = r → ON KEY p.fk → USING KEY) и утонул в байкшеддинге. Приговор Тома Лейна из того же треда стал классикой жанра: «NATURAL JOIN is widely regarded as a foot-gun that the SQL committee should never have invented. Why would we want to create another one?» — NATURAL JOIN повсеместно считают фут-ганом, который комитету не стоило изобретать; зачем создавать ещё один? Параллельный гист JOIN FOREIGN собрал ~200 комментариев на Hacker News — и все возражения, которые будут повторяться потом везде:
несколько FK между теми же таблицами — неоднозначность;
новый FK в схеме молча меняет (или ломает) старые запросы;
имена констрейнтов — плохой интерфейс: ORM генерируют их нечитаемыми;
запрос не должен зависеть от существования констрейнта — FK дропают под bulk load и шардинг.
jOOQ — единственный workaround, добравшийся до массового продакшена. Java-билдер реализовал .onKey() и implicit path joins (BOOK.author().name() сам синтезирует LEFT JOIN по FK). Показательно, что главное возражение jOOQ сформулировал сам, в собственном мануале: «The ON KEY clause can quickly produce ambiguities … queries that have worked in the past … will stop working» — ON KEY быстро порождает неоднозначности, работавшие запросы перестают работать — и увёл пользователей на path joins, где конкретный FK пришпилен кодогенерацией.
PostgreSQL, волна 2026 — прямо сейчас. В мае Якобсон вернулся с командой соавторов, рабочим патчем и change proposal в ISO-комитет: LEFT JOIN order_items oi FOR KEY (order_id) -> o (id) (интерактивные примеры — на keyjoin.org). Акцент сместился с «меньше печатать» на корректность: key join — это объявление, что соединение идёт по ссылочному пути, и база обязана это доказать (главный кейс — тихий fan-out: джойн 1:N незаметно размножает строки, и SUM() честно суммирует дубли). Томаш Вондра встретил патч уважительно, но жёстко: ~5200 строк патча («шанс, что я назову это committable — около нуля»), просадка планирования 30–40%. Тред жив, исход неясен, таймлайн стандартизации — годы.
Каждая волна разбивается об одно и то же: новый синтаксис — это грамматика, парсер, стандарт, обратная совместимость и консенсус комитета.
2026: идея победила везде — кроме SQL
Пока FK-джойны буксуют в комитетах, сама идея «объяви связь один раз — ходи по ней» победила везде вокруг SQL:
Графовые стандарты. SQL:2023 SQL/PGQ: объявляешь property graph поверх таблиц, запрашиваешь паттернами
MATCH (c IS customer)-[IS has_placed]->(o)— уже в Oracle 23ai, закоммичен в PostgreSQL 19 (март 2026, GA ожидается осенью), в DuckDB — расширением DuckPGQ; рядом отдельный ISO-язык GQL (2024) — Neo4j, Spanner Graph. Заметьте: даже стандарт не решился тронуть самJOIN— связи вынесены в отдельный графовый слой со своим синтаксисом.Семантические модели. Snowflake Semantic Views (GA 2025):
RELATIONSHIPS (orders (customer_id) REFERENCES customers (customer_id))объявляется в объекте схемы, запрос называет измерения и метрики — джойны генерирует движок. Malloy ту же мысль довёл до языка:join_one: users with user_idв модели, дальше просто путьusers.name.Слои над базой. Здесь FK-навигация — давно норма. PostgREST собирает вложенные ответы прямо из FK (
?select=title,directors(last_name)). PostGraphile строит из FK целую GraphQL-схему — пара полей на связь (personByAuthorId/postsByAuthorId), 1:1 по UNIQUE — и компилирует запрос в один SQL сjson_agg. А pg_graphql от Supabase исполняет GraphQL одной SQL-функциейgraphql.resolve(...)прямо внутри базы.
Многие независимые системы сошлись на одной и той же модели: FK → пара навигаций в обе стороны, 1:1 — по уникальности. Это конвергентная эволюция: метаданных foreign key достаточно для полной навигационной модели, вопрос лишь в том, на каком языке её отдать. Пока что ответ индустрии — «на любом, кроме самого SQL». pg_graphql доводит иронию до предела: FK-навигация уже живёт внутри вашего Postgres — но говорит на GraphQL.
И контрольный факт: pipe syntax — самая громкая реформа SQL последних лет (BigQuery, Spark 4.0) — переставила всё, кроме джойнов: |> JOIN требует тот же ON.
Решение: связь — это функция
А теперь фокус: для навигации по связям Postgres не нужен ни новый синтаксис, ни патч — не нужна даже свежая версия. Функции от строки таблицы и attribute notation (вызов функции через точку: document.client вместо client(document)) — наследие объектно-реляционных корней Postgres, а инлайн табличных SQL-функций планировщик умеет с 8.4, то есть с 2009 года, — приём должен работать даже на версиях, давно снятых с поддержки. Достаточно превратить каждый FK в пару функций: lookup — перейти по ссылке, и list — собрать ссылающиеся строки. Вот демо-схема, где каждое ребро подписано своей парой:

Эта же схема в SQL — example/init.sql в репозитории, а вместе с готовыми relation-функциями — в песочнице db<>fiddle.
Для одной связи это выглядит так. Lookup — клиент документа:
CREATE FUNCTION client(document) RETURNS SETOF profile LANGUAGE sql STABLE PARALLEL SAFE AS $$ SELECT * FROM public.profile WHERE (client_id) = (($1).client_id) $$;
List — все документы клиента:
CREATE FUNCTION client_document_list(profile) RETURNS SETOF document LANGUAGE sql STABLE PARALLEL SAFE AS $$ SELECT * FROM public.document WHERE (client_id) = (($1).client_id) $$;
Почему функции, а не вьюхи? VIEW фиксирует один готовый джойн, а не связь: вьюха не принимает строку, не компонуется в цепочки вида client(document(di)), не перегружается по типу аргумента — и внутри неё всё равно живёт тот самый рукописный ON. Функция — это связь, её аргумент — модель: ребро графа, по которому можно шагнуть из любого места запроса.
Во FROM такая функция ведёт себя как обычная таблица. LATERAL писать не нужно: функции и так видят соседей по FROM — документация прямо называет его noise word. Обязательная связь (FK-колонка NOT NULL) соединяется запятой. Опциональная (nullable) — через LEFT JOIN ... ON true: условие соединения уже зашито в функцию, а ON true стоит только потому, что LEFT JOIN без ON не бывает:
SELECT d.doc_number, p.name, m.name manager_name FROM document d, client(d) p LEFT JOIN manager(d) m ON true
Как зовут связь
Здесь победила простота: функция называется тем, что связь значит, — а смысл уже записан в имени FK-колонки её автором. client_id даёт client(document); обратная функция зовётся по своей таблице — document_item_list(document). Несколько связей к одной цели расходятся по ролям: client / manager. Никакой теории именования: имена не вспоминают, а угадывают. Полный свод правил — в Naming rules; это дефолты, собранные в одном месте.
Это бесплатно
Ключевой вопрос к любой «магии» — цена. Ответ: неотличимо от ручного джойна. Когда relation-функция стоит во FROM, планировщик полностью инлайнит её: EXPLAIN совпадает с классикой узел в узел — те же Nested Loop / Hash Join, те же индексы, те же параллельные воркеры. На тестовой базе в 300k документов и 1.35M строк тяжёлый агрегат по цепочке из четырёх таблиц: медианы пяти прогонов — 576 мс классикой против 571 мс функциями; по отдельным прогонам «лидер» меняется, при совпадающих планах остаётся только межпрогонный шум. Разборы планов пар «классика vs связи» — в EXPLAIN.md.
И это не возврат к навигационным СУБД, от которых Кодд уводил индустрию в 1970-м: под сахаром та же реляционная алгебра — план идентичен ручному джойну, а навигация свободно смешивается с классикой в одном запросе.
Условия инлайна генератор гарантирует сам: LANGUAGE sql, STABLE, не STRICT, не SECURITY DEFINER и — неочевидное, добытое замером — PARALLEL SAFE: параллельность запроса решается до инлайна, и дефолтный PARALLEL UNSAFE молча отключает воркеров всему запросу (у нас это стоило ~1280 мс против 571 мс). Дрейф ловится штатно: генератор (о нём — ниже) сверяет тело и флаги каждой функции с эталоном, и функция, пересозданная руками или потерявшая PARALLEL SAFE, подсветится в его отчёте.
Генератор: одна функция на всё
Писать такие функции руками — тот же пересказ схемы, только в профиль. Поэтому их пишет сама база. Всё вместе называется pg_relation_sql и представляет собой один самоустанавливающийся SQL-файл relation_sql.sql — без расширений, без доступа к файловой системе сервера, достаточно PostgreSQL 11+. Установка — одна команда:
curl -sf https://raw.githubusercontent.com/asmgit/pg_relation_sql/main/relation_sql.sql | psql postgresql://postgres:postgres@localhost:5432/postgres
Загрузка сама создаёт функции и вешает event trigger, а в ответ печатает дашборд.
Внутри файла — единственная функция relation_sql(mode): она читает pg_constraint и приводит набор relation-функций в соответствие со схемой.
SELECT status, command FROM relation_sql();
pg_relation_sql 0.1.0 — relation functions generated from foreign keys | SELECT status, command FROM relation_sql() event trigger: installed | SELECT status, command FROM relation_sql('uninstall') relation functions: 16 ok, 0 to sync, 0 foreign, 0 duplicate | SELECT status, command FROM relation_sql('drop') details | SELECT * FROM relation_sql('show')
'show' — дифф по каждому FK с готовым SQL; 'sync' — применить разово (шаг миграций); 'install' — event trigger, после которого функции следуют за DDL сами: CREATE TABLE с FK рождает пару функций прямо внутри команды, DROP CONSTRAINT — убирает. Сделано аккуратно: сбой синхронизации никогда не ломает ваш DDL, чужую функцию с тем же именем генератор не тронет, а неразрешимую коллизию имён отдаст человеку вместо тихой перезаписи. Подробности режимов и статусов — в README.
Команде с CI-миграциями больше подходит другой путь — не триггер, а relation_sql('sync') шагом миграции: функции рождаются в том же pipeline, что и весь DDL, попадают в ревью и детерминированно воспроизводятся в любом окружении. Триггер — режим песочниц, соло-проектов и баз, где DDL и так делает один человек. pg_dump/restore при этом проходят чисто в обоих режимах: функции едут как обычные объекты, event trigger восстанавливается последним и посреди рестора не стреляет (проверено).
Что это даёт в запросах
Помимо исчезнувших ON — идиомы, которых у классики нет; вот пять любимых (цена анти-джойна на полнотабличных проходах — в ограничениях):
-- revenue per client: three hops, zero ON SELECT p.name, sum(di.quantity * i.price) revenue FROM profile p, client_document_list(p) d, document_item_list(d) di, item(di) i GROUP BY p.id ; -- deliveries to Berlin: the composite FK stays hidden SELECT d.doc_number, a.street FROM document d, delivery_address(d) a WHERE a.city = 'Berlin' ; -- the whole related row as a field SELECT doc_number, (document.client).* FROM document ; -- profiles without documents SELECT name FROM profile WHERE NOT EXISTS (SELECT FROM client_document_list(profile)) ; -- recursive tree walk: the row travels as a value, the relation makes the step WITH RECURSIVE tree AS ( SELECT item node FROM item WHERE parent_item_id IS NULL UNION ALL SELECT c node FROM tree t, item_list(t.node) c ) SELECT (node).name FROM tree ;
Рекурсивный обход дерева, цепочки, JSON-вложенность, top-N, DML, INTERSECT/EXCEPT — все 19 пар «классика vs связи» в example/query.sql.
Честные ограничения
Инструменту можно верить, только если он знает свои границы. Наши:
NOT EXISTSпо функции не становится anti-join. Планировщик превращаетEXISTS-сублинки в semi/anti-join до инлайна функций, поэтомуNOT EXISTS (SELECT FROM client_document_list(profile))навсегда остаётся коррелированным SubPlan — индексная проба на каждую внешнюю строку. При селективном фильтре разницы нет; полнотабличный анти-джойн пишите классикой (наш замер: ~96 мс против ~40 мс на 100k×300k).Attribute notation в select-list не инлайнится — в плане
ProjectSet, вызов на строку; и семантика INNER: строка с пустой связью исчезает. Это сахар для точечных выборок; тяжёлые запросы — навигацией воFROM. (И это не N+1 из ORM: round-trip один, всё происходит внутри одного запроса — разница только в форме плана.)DROP TABLEтребуетCASCADE— relation-функции зависят от типа строки таблицы; остатки с другой стороны связи приберёт триггер или ближайшийsync. Автогенерённые миграции фреймворков, привыкшие к голомуDROP TABLE, здесь споткнутся — перед сносом таблицы дропните её функции точечно (готовый SQL даёт'show').Новый FK переименовывает соседей. Второй FK на ту же таблицу добавляет ролевые префиксы:
document_listпревращается вclient_document_list, и старые запросы падают ошибкой компиляции — то самое возражение к KEY JOIN из 2021-го, только здесь поломка громкая, а'show'сразу показывает новые имена.Нет констрейнта — нет навигации. Как у всех FK-подходов: дроп FK убирает функцию, и зависящие запросы падают ошибкой компиляции — что честнее, чем тихо изменившийся результат.
Тело функции —
SELECT *, поэтому колоночные привилегии (GRANT SELECT (id, name)) с relation-функциями не уживутся: после инлайна запросу нужна вся строка. RLS, напротив, компонуется штатно — политики применяются к таблице после инлайна.Функций становится вдвое больше, чем FK, и живут они в схемах своих таблиц.
\dfшумнее, автокомплит длиннее; фильтровать и в тулинге, и в schema-diff (migra, atlas) можно поCOMMENT-маркеруpg_relation_sql— а лучше вести их обычнымsync-шагом миграций, тогда для инструментов это управляемый DDL.Event trigger требует суперюзера. Без прав
installне падает: шаг триггера пропускается сWARNING, аsyncвыполняется — функции появятся, автослежения за DDL не будет.Это диалект. Запрос с
client(d)исполним только там, где функции созданы, — как с вьюхами. IDE дополняет их как обычные функции каталога, но семантической подсказки «это связь» не даст; читателю нужна одна новая идея — «функция от строки = связь». Это цена любого сахара; взамен — связь, проверенная компиляцией, а не глазами ревьюера.Это Postgres-only. Зато без патчей: чистый SQL поверх ванильной базы.
Сахар, которого чуть-чуть не хватило
Три мелочи вдогонку к ограничениям: всё уже работает, но могло бы быть приятнее. В отличие от «больших» синтаксических волн из истории выше, это маленькие кандидаты для pgsql-hackers:
LEFT JOIN manager(document)безON true. Условие уже внутри функции,ON true— чистый грамматический шум, но опустить его нельзя.Attribute notation во
FROM. Было бы ещё выразительнее:FROM document_item, document_item.document, document.client— сейчас это syntax error.Инлайнить функции до трансформации
EXISTS. Поменяй планировщик порядок фаз — и первый пункт ограничений исчез бы вместе с разницей ~96/~40 мс.
Есть ирония в том, что статья о «решении без изменений ядра» упирается в список пожеланий к ядру. Но таков честный итог: девяносто процентов задачи закрывается тем, что в Postgres уже есть, — а оставшиеся десять стоят три маленьких патча, а не ~5200 строк key-join-патча.
SQL становится дружелюбнее
Эта история — часть волны побольше. Стандарт не торопится — диалекты не ждут: DuckDB friendly SQL — это FROM tbl перед SELECT, GROUP BY ALL, переиспользуемые алиасы выражений и ещё дюжина упрощений.
Отдельного поклона заслуживает SELECT * EXCLUDE (...). Признание: наш собственный генератор трижды перечисляет двадцать одну колонку — только потому, что в Postgres нельзя взять * и исключить пару лишних колонок. Перечисление колонок — та же болезнь, что перечисление join-условий: пересказ того, что схема уже знает. DuckDB вылечил её для колонок. Мы — для связей. Postgres для колонок пока не лечится.
Краткость как физика: довод для эпохи ИИ
Финальный довод — уже не про людей. SQL сегодня массово пишут и читают языковые модели, и для них краткость — не эстетика, а физика: меньше токенов контекста, меньше поверхности для галлюцинаций, проще ревью человеком. Join-условия — главный источник тихих ошибок генерации: модель напишет ON d.id = di.item_id вместо document_id, запрос выполнится, отчёт сойдётся почти всегда. document(di) исключает этот класс ошибок структурно: либо связь есть и условие верно по построению, либо ошибка компиляции. (Кардинальность — обязательная связь или опциональная — остаётся на авторе, как и в классике: функция проверяет путь, а не NOT NULL.) А словарь навигации агент получает одним запросом — SELECT * FROM relation_sql('show') возвращает все связи схемы с именами и направлениями, вплоть до карты вида document → client с готовым SELECT * FROM document, client(document) на каждую связь.
Десятилетиями SQL сжимали ради людей — диалектами, ORM, обвязкой. Теперь то же сжатие стало инфраструктурой для машин.
Попробовать за минуту
Всё решение — pg_relation_sql: один самоустанавливающийся SQL-файл для PostgreSQL 11+, который превращает каждый foreign key в пару relation-функций и держит их в синхроне со схемой.
Совсем без установки — песочница на db<>fiddle: демо-схема, готовые relation-функции и пять примеров навигации — пощупать можно прямо в браузере.
Хочется по-настоящему — в репозитории есть докер-стенд: демо-схема со всеми видами связей (example/init.sql), те самые 19 пар запросов (example/query.sql) и массовка в 300k документов и 1.35M строк (test/bigdata.sql) для честных планов (разборы — EXPLAIN.md):
git clone https://github.com/asmgit/pg_relation_sql.git cd pg_relation_sql/test && docker compose up -d
Схема знает свои связи. Осталось начать её спрашивать.
Буду рад вопросам, возражениям и кейсам из реальных схем: если на каком-то именование споткнётся — докручу; правила дефолтные, и чем больше схем они переживут, тем лучше станут.
Комментарии (23)

iamkisly
02.08.2026 13:02Если честно не совсем понял чего вам не хватало. Код стал куда более контринтуитивным, потому что часть логики потенциально спрятана от читающего запрос.

mvv-rus
02.08.2026 13:02Мне тоже непонятно. SQL - он вообще в другой парадигме сделан, в реляционной, в которой результат - просто множество, неупорядоченное и какой-то предопределенной навигации просто нет: если не заказывать сортировку записей через ORDER BY то они возвращаются в неопределенном порядке.Так, вообще-то сделано специально: чтобы реализовывать запросы по произвольным путям. А индексы и первичные.внешние ключи - это уже оптимизация поверх реляционной модели. Если же нужно ходить по предопределенным связям, то под это издавна есть другая альтернатива - сетевая модель. Она, столь же древняя (если не древнее), чем реляционная, но зумеры не так давно её открыли заново и теперь она известна под названием графовой NoSQL модели. Если надо ходить по предопределенным путям, то почему бы не пользоваться этой сетевой/графовой моделью, а не насиловать реляционную БД. Ну, а ещё есть средства генерации запросов SQL, знающие про связи через внешний ключ (да хоть те же ORM), и почему бы не использовать их вместо того, чтобы навешивать лишние возможности.

gBear
02.08.2026 13:02… первичные.внешние ключи - это уже оптимизация поверх реляционной модели.
Это, мягко говоря, вообще не так.
Если же нужно ходить по предопределенным связям …
Так “проблема” как раз в том, и заключается, что - в подавляющем большинства случаев - именно ЭТО и нужно. А “запросы по произвольным путям” ™ - это такая узкая ниша… необходимость, которой никто не отрицает, кстати… но синтаксис в SQL есть только для них.
Об чем, собственно, и статья.

asmm Автор
02.08.2026 13:02в статье же указано чего всем не хватило за всю историю развития SQL
Многословие. Пять связей — пять
ON, слово в слово пересказывающих схему.Тихие ошибки.
ONпримет любое равенство:ONd.id= di.item_idвместоdocument_idвыполнится молча — типы совпали. Ошибка всплывёт данными, а не компиляцией.Потеря смысла. По
ON a.x = b.yне видно: связь это или совпадение, кто родитель, размножатся ли строки в агрегате.
и о попытках решения написано и что сейчас до сих пор куча энтузиастов решает проблему и создают спецификации

XelaVopelk
02.08.2026 13:02Пять связей — пять ON, слово в слово пересказывающих схему.
На больших нагруженных базах FK обычно сносят нафиг в угоду производительности.
Есть другие минусы, но как по мне это основной.
iamkisly
02.08.2026 13:02Более того на больших базах их и не думали создавать. У меня сейчас в работе несколько БД.. и они никогда не видели первичных ключей, связей и тд. Просто потому что половина данных ежедневно или ежечасно перегружается с нуля из внешних систем

asmm Автор
02.08.2026 13:02Без первичных ключей это не база данных, это файл, это набор данных. Вся реляционная модель построена на ключах и связях. Реляция - связь!

iamkisly
02.08.2026 13:02Ну значит вы никогда понастоящему не работали с данными. Потому что реляции по ключам реальны только там где они существуют с первого дня, а в жизни все немного иначе. В жизни есть дубли, есть поврежденные и фрагментированные данные. Данные которые вы получаете, но не управляете. У нас основная бд с которой работает подразделение получает данные с оборудования через xml дампы и принтауты консольных команд. Каждый день потенциально новый тип у существующей колонки, и появление новых. Никаких первичных ключей или признаков уникальности не предусмотрено в принципе. Максимум этой базы - синтетические id которые корректны только от аплоада до аплоада.
Тоже самое с картографическими данными коих у нас великое множество

gBear
02.08.2026 13:02Это всё - к сожалению, наверное - никак не отменят того факта, что без PK/FK и прочих ограничений это всё что угодно, но не реляционная модель.
“Дата лейки” не просто так придуманы, наверное… но и строятся они по другим принципам. И “реляционными” их никто не называет.

mvv-rus
02.08.2026 13:02Реляция - связь!
Нет, relation исходно, в реляционной алгебре - это отношение. То, что в промышленных СУБД именуется таблицей (table).

asmm Автор
02.08.2026 13:02Сомнительное утверждение. Тут я обычно цитирую Томаса Кайта с тестами доводами и опытом:
Эффективное проектирование приложений Oracle / Это база данных, а не свалка данных
https://www.rsdn.org/article/db/goodoraapp.xml#EZTAE
потеря 10% производительности при записи, очень редко когда стоят достоверности всего набора. Это исключительные таблицы, которые потом ложатся в реляционную модель.

XelaVopelk
02.08.2026 13:02Это база данных, а не свалка данных
Конечно в серийных продуктах обычно обстоит лучше, но вот 25 летний опыт общения с корпоративными системами "собственной разработки" говорит мне, что уже лет через 5 после создания, первоначальные проектные решения уже начинают плохо биться с реальной жизнью. Хотя бы по банальной причине: бизнес фирмы сильно изменился. Это не говоря о том, что через эти 5 лет те самые проектировщики в фирме уже врят ли работают, а через 10 лет гарантированно не работают.
потеря 10% производительности при записи, очень редко когда стоят достоверности всего набора.
Во первых, 10 процентов это оптимистично, если связей не сильно много у таблицы. Но даже если и так если мы посмотрим на ситуацию, что мы можем выжать быстро целых 10% с RW ноды, которая горизонтально не масштабируется, это уже много и за эти 10% надо биться.
Во вторых, достоверность набора, это не только внешние ключи, это другие ограничения. То что мы одно из ограничений (FK) вынесем на уровень БД это не сильно нас спасёт, если мы начнём глать шлак в прод.

iamkisly
02.08.2026 13:02Проблема в том, что подход из статьи только увеличивает энтропию и количество обьектов.
Ответ: неотличимо от ручного джойна
Что характерно нет никакого этому подтверждения, потому что похоже в статье отсутствует план запроса до-после, или какой-либо бенчмарк. У джентельменов принято верить на слово, и если в pg в все именно так, то это замечательно, но к примеру в sql server использование методов.. смерти подобно потому что код в большинстве случаев не разматывается оптимизатором, как в случае вью или сахарных CTE.
не компонуется в цепочки вида client(document(di))
Лично мое мнение, как человека который последние 10 лет только и делает, что пялится в SQL.. за такое надо немного бить, может быть даже палкой. Во первых за то что это здорово снижает читаемость и понимание кода. Я абсолютно уверен, что пять одинаковых ON не многословность, а явное намерение.
Во вторых, за нейминг в приведенном примере. Табличные и скалярные методы опять же должны выражать намерение в названии.. а тут его нет. В третьих..
CREATEFUNCTION client_document_list(profile) RETURNS SETOF document LANGUAGE sql STABLE PARALLEL SAFEAS$$SELECT*FROMpublic.documentWHERE(client_id) = (($1).client_id) $$;.. я не большой специалист в Pg, но это выглядит так словно мы одной нагой залезли в N+1 проблему. Делать это преимуществом по сравнению с JOIN.. ну такое. В принципе это не проблема для подможества, где использование LATERAL это норма.
В четвертых, у вас появляется единая точка отказа, думаю тут комментировать не надо. Мы надеемся что с
client_document_listбудет все хорошо.. надежда плохой попутчик там где должна быть уверенность.Установка — одна команда
Только вот недавно обсуждали что использование пайплайна в командах из сомнительных источников это плохая практика, и вот опять.

asmm Автор
02.08.2026 13:02Что характерно нет никакого этому подтверждения, потому что похоже в статье отсутствует план запроса до-после, или какой-либо бенчмарк. У джентельменов принято верить на слово, и если в pg в все именно так, то это замечательно, но к примеру в sql server использование методов.. смерти подобно потому что код в большинстве случаев не разматывается оптимизатором, как в случае вью или сахарных CTE.
Ошибаетесь, всё необходимые EXPLAIN'ы выполнены и ссылки приведены и корнер кейсы расписаны. Вы видимо читали статью по-диагонали.
Вот эксплейны:
https://github.com/asmgit/pg_relation_sql/blob/main/EXPLAIN.md
Ссылка есть в статье
Корнер кейсы честно разобраны в разделе "Честные ограничения"
Про нейминг вы видимо тоже не прочитали.
.. я не большой специалист в Pg, но это выглядит так словно мы одной нагой залезли в N+1 проблему. Делать это преимуществом по сравнению с JOIN.. ну такое. В принципе это не проблема для подможества, где использование LATERAL это норма.
Опять же направляю Вас посмотреть EXPLAIN ссылку и почитать правила инлайна в PG (тоже есть в статье). На этой синергии и построен мой удивительный модуль.
Последние ваши изыскания совсем сумбурны. Если хотите донести мысль, поясните.

mvv-rus
02.08.2026 13:02в статье же указано чего всем не хватило за всю историю развития SQL
Вы таки уверены, что всем? Мне вот, к примеру SQL обычно хватало, причем - даже старого, который безо всяких JOIN, а с перечислением таблиц-источников в FROM и условиями их связи в WHERE.
Тихие ошибки. ON примет любое равенство: ON d.id = di.item_id вместо document_id выполнится молча — типы совпали. Ошибка всплывёт данными, а не компиляцией.
Это - не ошибка, это так изначально задумано: реляционная модель данных позволяет использовать любые связи между таблицами, а не только те, которые заданы в структуре БД.
Вообще, ваши претензии к SQL - это претензии к реляционной модели данных. Если вам не подходит по каким-то причинам реляционная модель (например, совсем совсем не нужна гибкость, но очень необхдима производительность), используйте другую модель данных и реализующую ее NoSQL.СУБД. Если вам сложно писать запросы SQL вручную - используйте средства автомтизированной генерации запросов. А PostgreSQL оставьте в покое. Или делайте свою СУБД на основе его хранилища (благо лицензия позволяет), но тогда уж - без претензий, что SQL - какой-то не такой.

asmm Автор
02.08.2026 13:02Статья о том, что вся идеология SQL стремилась получить соединение по связям. Я же процитировал ниже свою же статью:
https://habr.com/ru/articles/1065684/comments/#comment_30295142
Мой подход не ограничивает, а даёт возможность использовать декларацию связей для соединения таблиц

mvv-rus
02.08.2026 13:02Статья о том, что вся идеология SQL стремилась получить соединение по связям.
Вы забыли после слов “идеология SQL” добавить оговорку “как я ее понимаю”. А понимание у вас - довольно специфичное: в исходной концепции реляционной парадигмы (основанной на реляционной алгебре, это такой раздел математики, и Кодд как раз работал в этой области) никаких определенных в структуре БД связей нет вообще, а есть отношения - наборы однородных записей-кротежей, данные из которых выбираются конкретным запросом. Связи - они были в другой парадигме организации БД, сетевой. И там они, кстати, были явные, с физическими указателями на списки связанных записей, а не неявные через сопоставление первичных и внешних ключей, как в современных БД, ориентированных на извлечение из них данных с помощью SQL. И языки запросов там другие, не SQL: например, был в той древности такой CODASYL.
Вы же хотите не просто использовать PostgreSQL как сетвую БД (в этом-то я не вижу ничего плохого), а ещё и сделать SQL языком для работы в сетевой парадигме, по крайней мере отсутствие в нем такой возможности вы считаете недостатком SQL. А это, по моему, плохо, потому что избыток возможностей перегружает язык, делает его чремерно громоздким, неудобным для изучения и работы (если чо, я и PL/1 помню, который страдал от этой самой перегрузки возможностями).
Если же, как у вас в статье, расширение сделано дополнительным пакетом, который в движок БД не встроен и который те, кому это не нужно, могут не использовать - это терпимо. А вот что касается рсширения самого языка (с доработкой его исполняющей системы), влияющее на всех, кому оно нужно и кому не нужно - лично я против. Собственно, только в этом и состоят мои возражения к статье. По поводу же вашего пакета расширения возражений не имею: пусть будет, глядишь, не только вам пригодится.PS А для использования связей (через первичные/внешние ключи) IMHO лучше использовать автогенерацию запросов в той или иной форме, не трогая сам язык SQL.

redfox0
02.08.2026 13:02Не понимаю, что мешает написать так (план запроса такой же, что и с JOIN):
select i.name as item_name, p.name as client_name, m.name as manager_name, di.quantity from document_item di, document d, item i, profile p, profile m where d.id = di.document_id and i.id = di.item_id and p.client_id = d.client_id and m.manager_id = d.manager_id
asmm Автор
02.08.2026 13:02Видимо не внимательно статью читали:
Сокращать условия соединения SQL пытался не раз. Все четыре попытки — мимо.
Запятая +
WHERE. Досахарная эпоха: таблицы списком, условия вWHEREвперемешку с фильтрами. Забыл условие — получил декартово произведение, молча.

XelaVopelk
02.08.2026 13:02Не понимаю, что мешает написать так
Когда я был лидом группы базистов я категорически запретил так писать, т.к. тут не видно какое условие к какому джойну относится и на ревью (или при изменении запроса "потом") кода это читать реально больно. Хорошо тут запросик короткий, а ежели он на весь экран.
Касательно вот такого синтаксиса.
FROM document_item, document(document_item), item(document_item)Всё красиво, когда в запросе только он, но как только ты начинаешь подтаскивать данные не только через FK, начнётся плохочитабельная каша.
В это смысле ИМХО "лучше безобразно, но единообразно"
asmm Автор
02.08.2026 13:02и опять комментатор, который статью не внимательно читал
Всё это лежит в каталоге, и по этим объявлениям идёт подавляющее большинство реальных соединений — один из практиков в обсуждении на HN оценил эту долю в «95%+ моих джойнов»
iamkisly
02.08.2026 13:02Если вы получаете примерно одинаковый негативный фидбек, то со статьей определенно что-то не то
erogov
К слову,
GROUP BY ALLуже почти с нами. А вот сSELECT * EXCLUDEне все так однозначно.