惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
IT之家
IT之家
F
Fortinet All Blogs
腾讯CDC
酷 壳 – CoolShell
酷 壳 – CoolShell
月光博客
月光博客
GbyAI
GbyAI
Recent Announcements
Recent Announcements
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
Y
Y Combinator Blog
人人都是产品经理
人人都是产品经理
N
Netflix TechBlog - Medium
博客园_首页
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
爱范儿
爱范儿
F
Full Disclosure
博客园 - 【当耐特】
V
V2EX
M
MIT News - Artificial intelligence
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
大猫的无限游戏
大猫的无限游戏
T
The Blog of Author Tim Ferriss
S
Schneier on Security
Latest news
Latest news
S
Secure Thoughts
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
Blog — PlanetScale
Blog — PlanetScale
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cyberwarzone
Cyberwarzone
Spread Privacy
Spread Privacy
K
Kaspersky official blog
L
Lohrmann on Cybersecurity
宝玉的分享
宝玉的分享
A
About on SuperTechFans
Jina AI
Jina AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Palo Alto Networks Blog
Microsoft Azure Blog
Microsoft Azure Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Невидимый syntax error в PostgreSQL: как одна строка SQL съела полтора дня дебага
viktdo · 2026-05-07 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение7 мин

Охват и читатели139

Туториал

Хочу рассказать про баг, который я искал полтора дня и который оказался опечаткой длиной в одну инструкцию. Если коротко: PostgreSQL (16.4 в моём случае, но воспроизводится начиная с 12-й) не разрешает UNION ALL сразу после ORDER BY ... LIMIT N без круглых скобок. И самое неприятное - ошибку об этом я узнал последним, потому что между моим SQL-запросом и логами стояло пять промежуточных слоёв, каждый из которых её по-своему проглотил.

Расскажу как нашёл, как фиксил, и что в итоге добавил в pre-deploy чек-лист.

Симптом: тихий пустой ответ от webhook

У меня есть n8n-воркфлоу, который раз в день проверяю - там GET /webhook/result отдаёт финальную карточку клиента в JSON: профиль, статус, прикреплённые объекты, лид-инфа. Простой read-only эндпоинт. Цепочка из шести нод: webhook trigger -> Postgres Load Session -> Postgres Load Lead -> Postgres Load Properties -> Code Build Card -> Respond to Webhook. Ничего хитрого.

В пятницу вечером фронтенд начал стабильно падать на странице результата с пустым ответом. Открыл DevTools - 200 OK, content-type application/json, body пустой. То есть никакой ошибки на сервере, просто 0 байт.

Самое раздражающее, что у меня в голове сразу выстроился неправильный причинно-следственный ряд: «фронт раньше работал, я последний раз правил рендер карточки, наверное где-то null-pointer». Полез смотреть фронт. Конечно ничего не нашёл (потому что нечего смотреть, body пустой). Откатил последний релиз - продолжает падать. Так что нет, дело не во фронте.

Перешёл смотреть n8n. Открываю executions - workflow выполняется, отдаёт ok: true, никаких error-выходов. То есть ноды формально пройдены. Но что-то по дороге становится пустым.

В понедельник утром, после кофе и с ясной головой, наконец сел открывать каждую ноду по очереди и смотреть Output руками. На второй ноде - Postgres: Load Lead - увидел что она отдаёт пустой массив []. Ну ок, лида для этой сессии может и не быть. Дальше код-нода Build Card ждёт минимум 1 строку (берёт [0]) и при [0] === undefined молча возвращает пустой объект. Который и улетает в Respond. Вот и весь пустой ответ.

Дальше начинается интересное.

Что было в SQL

Запрос в Load Lead был написан так (я писал его несколько недель назад, копируя свой же паттерн откуда-то с прошлого проекта):

SELECT summary, escalation_reason, created_at FROM leads
WHERE session_id = '...'
ORDER BY created_at DESC
LIMIT 1
UNION ALL
(SELECT NULL, NULL, NULL)
LIMIT 1

Идея логичная: «возьми последний лид для сессии, а если его нет - верни строку с NULL-полями, чтобы downstream-нода не споткнулась о пустой массив». Без UNION ALL n8n получал бы 0 items и обрывал цепочку.

Запрос вроде читается. Лид есть - вернёт его. Лида нет - первая часть пустая, UNION ALL приклеит NULL-fallback, итого LIMIT 1 ограничит до одной строки. Всё.

Берёшь этот SQL, скармливаешь psql:

psql ... -c "
SELECT 1 AS x WHERE 1=2 ORDER BY x DESC LIMIT 1 UNION ALL (SELECT NULL) LIMIT 1
"

И получаешь:

ERROR:  syntax error at or near "UNION"
LINE 2: SELECT 1 AS x WHERE 1=2 ORDER BY x DESC LIMIT 1 UNION ALL (S...

Сюрприз. PostgreSQL такое не понимает. Хотя по идее всё разумно.

Почему PostgreSQL так строг

Если копнуть в SQL:2011 спеку (или в документацию PostgreSQL про UNION) - грамматика <query expression> устроена так, что ORDER BY и LIMIT относятся к внешнему запросу, к результату всех UNION/INTERSECT/EXCEPT. То есть когда вы пишете:

SELECT a FROM t1 ORDER BY a LIMIT 1
UNION ALL
SELECT b FROM t2

Парсер не может решить, к чему относится ORDER BY a LIMIT 1 - к первому SELECT (что вы хотели) или вообще странная конструкция, потому что после LIMIT должен идти конец query, а не UNION.

Чтобы сказать парсеру «ORDER BY/LIMIT относится только к первому SELECT» - его надо обернуть в скобки. Тогда это становится <query primary>, который имеет право иметь свои собственные сортировки и лимиты.

Это не баг постгреса. Это так и задумано стандартом, чтобы не было неоднозначности. Но узнаёшь об этом только когда упрёшься.

Честно - я и до этого случая знал это правило теоретически, но никогда не задумывался об edge-кейсах с LIMIT ровно на той же строке что и UNION. В обычной практике ORDER BY всё-таки чаще ставят в самом конце, в <query expression>, и проблемы не возникает.

Три варианта фикса

После того как я понял в чём дело, у меня было три рабочих способа починить.

Вариант 1: круглые скобки

Самое прямолинейное решение - то самое, на которое прямо намекает спека:

(SELECT summary, escalation_reason, created_at FROM leads
 WHERE session_id = '...'
 ORDER BY created_at DESC LIMIT 1)
UNION ALL
SELECT NULL::text, NULL::text, NULL::timestamptz
LIMIT 1

Скобки превращают первую часть в <query primary> со своими ORDER BY/LIMIT. Парсер счастлив, запрос работает.

Подвох: если первая часть нашла запись, то после UNION ALL получится 2 строки (найденная + NULL-fallback), а внешний LIMIT 1 обрежет до первой. Но порядок строк в UNION ALL без явной сортировки не гарантирован. То есть теоретически вы можете получить NULL-строку даже когда лид существует. На практике PostgreSQL обычно отдаёт в порядке выполнения подзапросов и этого не происходит, но полагаться я бы не стал.

Чтобы гарантированно получать «реальную» строку первой - надо добавить ещё один уровень с ORDER BY по флагу:

SELECT summary, escalation_reason, created_at
FROM (
  (SELECT 0 AS prio, summary, escalation_reason, created_at FROM leads
   WHERE session_id = '...' ORDER BY created_at DESC LIMIT 1)
  UNION ALL
  SELECT 1 AS prio, NULL::text, NULL::text, NULL::timestamptz
) t
ORDER BY prio
LIMIT 1

Уже не так красиво. Поэтому я этот вариант отбросил.

Вариант 2: RIGHT JOIN с константной таблицей

Это то, что я в итоге выкатил. Идея простая: сделать join с гарантированно непустой константной строкой, чтобы downstream всегда получал ровно одну строку - либо с данными, либо с NULL-полями.

SELECT summary, escalation_reason, created_at FROM (
  SELECT summary, escalation_reason, created_at FROM leads
  WHERE session_id = '...'
  ORDER BY created_at DESC LIMIT 1
) t
RIGHT JOIN (SELECT 1 AS d) s ON true

Что тут происходит:

  • Внутренний SELECT ... LIMIT 1 живёт в скобках без проблем (это derived table)

  • (SELECT 1 AS d) - константа, гарантированно одна строка

  • RIGHT JOIN ... ON true означает: «возьми все строки правой части (всегда одна) и соедини с левой если есть пара». При ON true пара есть всегда. Если левая пустая - правая возвращает себя с NULL во всех полях левой

  • Внешний SELECT берёт только колонки из t (поле d из правой нам не нужно)

Итог: ВСЕГДА одна строка. Если в leads есть запись - получаем её значения. Если нет - все NULL.

Прелесть в том, что я не описываю NULL-fallback явно (нет SELECT NULL, NULL, NULL) - он автоматически возникает из левого join'а. Если завтра я добавлю в SELECT ещё одну колонку, мне не нужно править fallback в двух местах. Только в одном внутреннем запросе. А типы - PostgreSQL вычислит сам.

Главный минус решения - оно непривычное. Когда коллега видит такой запрос впервые, ему нужно секунд тридцать чтобы понять что происходит. Я добавил коммент над запросом «// Trick: RIGHT JOIN with constant guarantees exactly 1 row, NULL when empty» - этого хватает.

Performance? На таблице leads с 50k записей и индексом по session_id запрос отрабатывает за 1.2 ms. RIGHT JOIN с однострочной константой - бесплатный по сути. Ничего не оптимизируется хуже чем оригинал.

Вариант 3: scalar subqueries

Кратко - можно вообще обойтись без UNION и JOIN, если просто разнести каждую колонку в отдельный scalar subquery:

SELECT
  (SELECT summary           FROM leads WHERE session_id='...' ORDER BY created_at DESC LIMIT 1) AS summary,
  (SELECT escalation_reason FROM leads WHERE session_id='...' ORDER BY created_at DESC LIMIT 1) AS escalation_reason,
  (SELECT created_at        FROM leads WHERE session_id='...' ORDER BY created_at DESC LIMIT 1) AS created_at

Каждый subquery возвращает скаляр или NULL. Итого получаем строку из трёх колонок, каждая либо со значением, либо с NULL.

Минус очевидный - один и тот же WHERE/ORDER BY/LIMIT повторяется три раза. Не страшно для маленькой таблицы (планировщик может закешировать), но на большой может стать заметно медленнее. Плюс если завтра нужно добавить колонку - править в каждом subquery.

В моём случае таблица leads не размером с Кострому, но я всё равно выбрал RIGHT JOIN-вариант - он чище читается и компактнее.

Почему n8n проглотил ошибку

Самое обидное в этой истории не сам SQL-баг, а то, что я не увидел error-сообщения вовремя. Postgres-нода в n8n, когда executeQuery падает с syntax error, ведёт себя по-разному в зависимости от настроек:

  • On Error: Stop Workflow - workflow обрывается, error-сообщение видно в executions

  • On Error: Continue - workflow идёт дальше с пустым output, ошибки не видно

  • On Error: Continue (using error output) - workflow идёт дальше, но создаётся отдельный error-выход; ошибка доступна, если этот выход подключить к downstream-ноде

В моём workflow Load Lead был выставлен в Continue (using error output), но error-выход никуда не был подключен (просто потому что я не предусмотрел такой сценарий). Получилось: ошибка возникла, нода поставила 0 items на main-выход, error-выход остался обрывком. Workflow «успешно» завершился пустой картой.

Урок: если в Postgres-ноде SQL динамический (через expressions, через билдер, через template) - либо используйте Stop Workflow, чтобы любой syntax error падал громко, либо подключайте error-выход к Telegram-нотификации или к Sentry. Тихий error в продакшене - худшее что бывает.

Для моих воркфлоу я в итоге сделал гибрид: для read-only нод оставил Continue (потому что иногда легитимно ничего не находится), но добавил отдельную ноду-проверку перед Build Card: если предыдущий output пустой - сразу логирую event в Telegram. Это поймало бы мою же проблему ещё в пятницу.

Что добавил в pre-deploy чек-лист

Этот случай заставил меня формализовать одну вещь:

  • Любой SQL с UNION/INTERSECT/EXCEPT, у которого части содержат ORDER BY/LIMIT - обязательно прогоняется руками в psql ПЕРЕД сохранением в n8n. Не «потом проверим в браузере», не «всё равно же тестим воркфлоу через webhook» - именно через psql. Парсер n8n не равен парсеру psql, и иногда ошибка маскируется.

Заодно добавил себе универсальную проверку для любого нового SQL: запускаю с заведомо невыполнимым WHERE 1=2. Если получаю ожидаемый результат (пустая выдача либо корректный fallback) и структуру колонок такую же как при WHERE 1=1 - запрос можно деплоить. Это banalно но я как-то про это забывал, пока не получил полтора дня дебага.

Полезные ссылки

Если есть свой любимый паттерн «всегда вернуть 1 строку, даже когда таблица пустая» - был бы рад увидеть в комментариях. У меня их получилось три, но наверняка есть и четвёртый.