﻿<rss version="2.0">
    <channel>    
        <title>Влад Костянецкий</title>
        <description>Привет! Меня зовут Влад, я — разработчик приложений для бизнеса.</description>
        <language>ru</language>
        <link>https://kostyanetsky.ru</link>
        <lastBuildDate>Sat, 28 Mar 2026 19:42:53 +0700</lastBuildDate>
        
        <item>
            <title>Номер года в литерале типа дата превышает 3999</title>
            <link>https://kostyanetsky.ru/notes/date-literal-exceeds-3999-once-again</link>
            <guid isPermaLink="false">note-date-literal-exceeds-3999-once-again</guid>
            <pubDate>Sat, 28 Mar 2026 19:42:53 +0700</pubDate>
            <description><p>Про ошибку в названии заметки я уже как-то <a href="https://kostyanetsky.ru/notes/date-literal-exceeds-3999" target="_blank">писал</a>. Напомню суть: платформа не переваривает даты позже 3999 года (на практике начинает стрелять с 3999-12-01). Теоретически такие даты вообще нельзя записать, но <s>если очень хочется</s> из-за ошибок в прикладном коде (и в коде самой платформы) это всё-таки возможно и время от времени случается. </p>
<p>Обычные симптомы — перестают работать какие-то отчёты, не проводятся какие-то документы, падает пересчёт итогов. В общем, страдает любой код, который трогает записи с проблемными датами.</p>
<p>Способ лечения, который я рамочно описал в заметке по ссылке выше — рабочий, но сравнительно медленный: нужно настроить ТЖ, собрать и распарсить результаты. Между тем, платформа падает на первом же обращении к битой дате, а их может быть много и валяться они могут в разных таблицах. То есть может потребоваться несколько подходов к снаряду: проверили, получили ошибку, исправили, проверили, получили следующую...</p>
<p><img alt="Thank You, Mario!" src="https://kostyanetsky.ru/notes/date-literal-exceeds-3999-once-again/thanks.jpg"/></p>
<p>Желая решать проблему как-нибудь побыстрее, несколько лет назад я написал <a href="https://gist.github.com/vkostyanetsky/a58ff201d2a87a35e70c4c8f4112ad4c" target="_blank">запрос для PostgreSQL</a>. Общая идея:</p>
<ol>
<li>Ищем в базе данных все поля с датами.</li>
<li>Строим мегазапрос к таким полям (ищем даты, которые выходят за лимит 1С).</li>
</ol>
<p>То есть этот запрос генерирует другой запрос, да. Выполняем его и получаем полную картинку проблемы: список таблиц с некорректными датами. Дальше уже дело техники — смотрим, что за таблицы и решаем, как быть. В нашем случае проблемные даты иногда возникают в итогах и оборотах, так что их можно просто удалять и пересчитывать итоги через штатные инструменты.</p>
<p>Почему действуем на уровне СУБД? Ну, средствами 1С такой номер не отколоть — напомню, платформа падает при попытке потрогать проблемные записи. Это касается любого взаимодействия, включая чтение. Кроме того, в данном случае работать напрямую быстрее и удобнее.</p>
<p>На днях я переписал этот <a href="https://gist.github.com/vkostyanetsky/5990a16caacc4a9057b577c6a5694512" target="_blank">запрос для MS SQL</a>. Вышло длиннее (в силу особенностей этой СУБД), но идея та же.</p>
<p>Если будете использовать, имейте в виду, что:</p>
<ul>
<li>Поле <code>_Fld626</code> в тексте запроса — разделитель для фреша. В вашей базе может называться по-другому или вообще отсутствовать.</li>
<li>Запрос написан для базы со сдвигом в 2000 лет. Если в вашей базе сдвига нет, нужно скорректировать условие (см. функцию <code>DATEADD()</code>).</li>
<li>Трюк с выводом в XML (см. <code>FOR XML</code>) я добавил, чтобы не дать SSMS обрезать текст мегазапроса (показалось быстрее, чем возиться с приведением типов). Побочный эффект: в получившемся мегазапросе нужно заменить <code>&amp;gt;</code> на <code>&gt;</code>, прежде чем выполнять его.</li>
</ul></description>
        </item>
        
        <item>
            <title>С побочным эффектом</title>
            <link>https://kostyanetsky.ru/notes/with-side-effect</link>
            <guid isPermaLink="false">note-with-side-effect</guid>
            <pubDate>Sun, 15 Mar 2026 12:24:12 +0700</pubDate>
            <description><p>В последнем релизе <a href="https://firstbit.ae" target="_blank">нашей ERP</a> мы оптимизировали несколько тяжелых динамических списков (заказы, инвойсы, проформа инвойсы и так далее). Там накопился приличный техдолг, в основном — горы вспомогательных таблиц, прикрученных к основным запросам (контактная информация, технические реквизиты, остатки, обороты и так далее). В итоге даже в сравнительно небольших приложениях оптимизатор СУБД начинал выдавать бредни вместо планов запроса и мало-помалу платить за это ресурсами стало совсем неприятно.</p>
<p>В общем, запатчили через несколько разных подходов, один из них — подгрузка дополнительных данных в обработчике <code>ПриПолученииДанныхНаСервере()</code> (я про него недавно <a href="https://kostyanetsky.ru/notes/desire-paths" target="_blank">вспоминал</a>, кстати). Написали удобный фреймворк вокруг фичи, внедрили, потестировали и... В общем, сидим с кислыми минами. </p>
<p>Нет, с производительностью и правда стало сильно лучше. Запрос к тяжелым виртуальным таблицам можно затюнить просто идеально. Проблема в другом: значения полей, которые заполняет этот обработчик, не пробрасываются в стандартные механики динамического списка. То есть для них не работает поиск, сортировка и группировка.</p>
<p>Например, вводим значение, которое видим в колонке, а строка не находится. Или находятся не все строки. Или находятся строки, которые не должны были найтись. Для пользователя это выглядит отвратительно, ну баг и баг же: визуально-то такие поля никак не отличаются. Как ты ему объяснишь, что это особенность работы механизма платформы™? </p>
<p>Исключишь такое поле из механизмов, которые с ними работать не могут — будет ещё чище. Например, попытаешься отсортировать — получишь здоровенную ошибку. Опять же, объяснить, почему на этой колонке с суммой вылетает ошибка, а на соседней сортировка прекрасно работает — задача со звездочкой.</p>
<p>Блин, вот как можно было разработать такой замечательный концепт обработчика и настолько заруинить реализацию на уровне платформы?</p>
<p>Неожиданно вспомнил Реддит. В некоторых сообществах популярны треды «придумай суперсилу, но с побочным эффектом». Типа, кто-то пишет в комментариях: я могу бегать со скоростью ветра! Ему отвечают: да, но не можешь тормозить. И так далее. Местами получается забавно.</p>
<p><img alt="Superpower" src="https://kostyanetsky.ru/notes/with-side-effect/reddit.png"/></p>
<p>Вот и мы с разработчиками платформы в таком же диалоге. Вы можете разогнать динамические списки (но UI будет вызывать у пользователей бешенство). Вы можете автоматом слать двоичные данные в бакет S3 (но потеряете их в один неосторожный клик). Вы можете телепортироваться куда угодно (но тратите столько времени, словно шли пешком). Вы можете превращаться (но только в пожилого мопса). У вас пышные, шелковистые волосы (но на заднице). </p></description>
        </item>
        
        <item>
            <title>Икигай</title>
            <link>https://kostyanetsky.ru/notes/ikigai</link>
            <guid isPermaLink="false">note-ikigai</guid>
            <pubDate>Mon, 09 Mar 2026 22:01:43 +0700</pubDate>
            <description><p>В японском языке есть замечательное слово «икигай». Это что-то вроде смысла жизни, но не в большом экзистенциальном понимании, а в более приземлённом и конкретном: источник внутренней энергии, источник радости. Ради чего хочется вставать по утрам, короче.</p>
<p>Таких опор у человека может быть несколько, и у всех они свои. Кофе, кот, любимое ремесло, забота о близких, сад, прогулки, видеоигры...</p>
<p>Я это к чему. Наткнулся на канал «<a href="https://t.me/aleshkino_svoe" target="_blank">Алёшкино своё</a>». Его автор, насколько я понял, питерский юрист, который разводит породистых курочек и регулярно записывает видео то про конкретные породы, то про нюансы инкубации, то про кормление.</p>
<p>Я вообще не в теме и птицами не интересуюсь, но поймал себя на том, что минут двадцать просто сидел и заворожённо слушал, не отрываясь. Работает почти как терапия, ей-богу. И, кажется, это вполне такой солидный икигай: дело, которое будешь любить так, как этот мужик любит своих курочек.</p></description>
        </item>
        
        <item>
            <title>Автосаммари</title>
            <link>https://kostyanetsky.ru/notes/autosummary</link>
            <guid isPermaLink="false">note-autosummary</guid>
            <pubDate>Sun, 08 Feb 2026 09:53:48 +0700</pubDate>
            <description><p>Записывать все встречи я, судя по моим же <a href="https://kostyanetsky.ru/notes/video-recording" target="_blank">заметкам</a>, начал ещё в 2020-м. Видео всегда точнее памяти, а ткнуть на «Start Recording» в <a href="https://obsproject.com" target="_blank">OBS</a> — самый дешёвый способ не потерять какую-нибудь ценную инфу.</p>
<p>Минусы, впрочем, очевидны — с видео нельзя быстро ухватить суть встречи, по нему невозможен быстрый поиск, оно много весит, в нём легко засветить что-то личное и так далее. Чтобы частично компенсировать это, я в течение встреч тезисно помечал ключевые моменты, а потом либо перекидывал в задачник, либо делал себе что-то вроде саммари: с кем встречался, что обсуждали, какие решения подобрали. Если что-то забыл или упустил детали — сверялся с видео.</p>
<p>Однако этот метод тоже не идеален. Создание конспекта (даже тезисного) отъедает фокус от самой встречи. Кроме того, про какие-нибудь забытые по ходу дела подробности можно узнать слишком поздно.</p>
<p><img alt="Забыл" src="https://kostyanetsky.ru/notes/autosummary/forgot.jpg"/></p>
<p>Короче, пришёл к тому, что хороший вариант — просто извлекать из видео аудио разговора, превращать его в текст (нейронкой), а текст разговора — в детальное саммари по встрече (опять нейронка).</p>
<p>Аудио проще всего выдрать через <a href="https://www.ffmpeg.org" target="_blank">ffmpeg</a> (консольная утилита для работы с аудио и видео). Ниже пример вызова (один аудиоканал, дискретизация 16 кГц + нормализация громкости):</p>
<pre>
ffmpeg.exe -y -i "D:\video.mkv" -vn -ac 1 -ar 16000 -af loudnorm -c:a pcm_s16le "D:\audio.wav"
</pre>
<p>Что до извлечения текста — я экспериментировал с <a href="https://alphacephei.com/vosk/" target="_blank">Vosk</a> в связке с <a href="https://github.com/benob/recasepunc" target="_blank">recasepunc</a>, но про это без слёз не вспомнить. Даже комментировать не хочу. А вот <s>Боромир</s> <a href="https://openai.com/index/whisper" target="_blank">Whisper</a> (нейросеть от OpenAI, умеющая распознавать голос) ставится в фоне за 10 минут:</p>
<pre>
py -3.10 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install setuptools wheel
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install openai-whisper
</pre>
<p>Пример вызова:</p>
<pre>
whisper "D:\audio.wav" --model medium --language Russian --output_format txt
</pre>
<p>На выходе получится текстовый файл с расшифровкой, который можно запихать в любой чатбот и получить вполне связное саммари. Его, конечно, всё ещё нужно вычитать — убрать ошибки и фантазии, что-то переформулировать — но это всё ещё сильно лучше создания конспекта на ходу.</p>
<p>Вот, в общем-то, и весь метод. Остаётся написать простой скрипт, чтобы не дергать две команды вручную. Если вы тоже на Windows и вам лень вайбкодить, можете взять <a href="https://gist.github.com/vkostyanetsky/4f4760097f1b417cc85d71d11662a642" target="_blank">мой скрипт</a> и подогнать под себя.</p>
<p>Скрипт ищет в своей папке первый попавшийся .mkv, прогоняет через ffmpeg + Whisper и сохраняет результат в ту же папку. Если будете использовать — обратите внимание, что он работает через <a href="https://developer.nvidia.com/cuda" target="_blank">CUDA</a> (на CPU тоже можно, но будет сильно медленнее) + скачанные модели Whisper он сохраняет не в дефолтный кэш, а в папку с текущим Python-окружением.</p>
<p>(при желании можно прикрутить к нему вызов API или обращение к какой-нибудь модели в локальной <a href="https://lmstudio.ai" target="_blank">LM Studio</a>, но персонально для себя я решил — не, уже перебор)</p></description>
        </item>
        
        <item>
            <title>Новый UI в блоге</title>
            <link>https://kostyanetsky.ru/notes/new-ui</link>
            <guid isPermaLink="false">note-new-ui</guid>
            <pubDate>Sat, 17 Jan 2026 18:18:43 +0700</pubDate>
            <description><p>На новогодних праздниках внезапно закусился и переписал UI блога. Хотел всего-то прикрутить поиск по заметкам: их уже довольно много и время от времени нужно что-то быстро выудить из кучи написанного (например, ссылку коллеге скинуть).</p>
<p>Блог живёт на <a href="https://pages.github.com" target="_blank">Github Pages</a>, так что выбор решений небогат: либо слать запрос в гугл, либо делать свой статический индекс и отдавать в браузер пользователя (пусть сам в нём роется). Я пошёл по второму пути: быстрее, управляемее <s>и можно самому покодить</s>. При первом поиске <a href="https://kostyanetsky.ru/notes.json" target="_blank">файл индекса</a>, правда, нужно скачать, но что такое 200 Кб в современном интернете? Смешно.</p>
<p>Ну а там как-то, знаете ли, пошло-поехало... Сначала не удавалось прикрутить к полю поиска <a href="https://tachyons.io" target="_blank">Tachyons</a> — разозлился и переделал всё на <a href="https://tailwindcss.com" target="_blank">Tailwind</a> (всё равно хотел попробовать, а случая всё никак не представлялось). Пока писал код для поиска — подумал, что логично сразу вкрутить в него теги, чтобы два раза не вставать. Очнулся с облаком тегов над заметками и сообразил, что тогда уж надо и с проектником то же самое сделать, только там не теги нужны, а стеки...</p>
<p>В общем, получилось как в том меме из «Страха и отвращения в Лас-Вегасе». Сложно было остановиться. Осталось заставить себя писать в новый проектник: работы всегда дофига, и работа интересная, но если не писать о ней — кофе, всё тонет в кофе.</p></description>
        </item>
        
        <item>
            <title>Управление бэкапами</title>
            <link>https://kostyanetsky.ru/notes/backup-ui</link>
            <guid isPermaLink="false">note-backup-ui</guid>
            <pubDate>Sat, 06 Dec 2025 20:57:48 +0700</pubDate>
            <description><p>В конце года выкатили для нашего внутреннего инструмента (я уже вскользь <a href="https://kostyanetsky.ru/notes/easter-eggs" target="_blank">писал</a> о нём) большой апдейт, дающий коллегам адекватный доступ к бэкапам пользовательских приложений. Бэкапы в SaaS-компании нужны всем и всегда — для разработки, для тестирования, для расследования проблем, да много для чего. Без адекватного учёта процесс превращается в зоопарк, когда три человека в один момент времени создают три запроса на практически одинаковые копии одной и той же базы. Задача, конечно, решается, но ресурсов прожрано в три раза больше, чем хотелось бы.</p>
<p>У нас уже было решение на базе UI Битрикса, но в силу, э-э, особенностей развития этого продукта оно приносило больше боли, чем пользы. Поэтому мы переосмыслили процесс и всё переписали. На фронте — 1C, на бэке — PostgREST, PostgreSQL, PowerShell и много чего ещё. Логика довольно сложная, но у пользователя — простой и дружелюбный UI, через который можно заказать бэкап буквально в два нажатия.</p>
<p>Выбрать можно один из трёх видов бэкапов:</p>
<ul>
<li>облачный (копия реального приложения, развёрнутая в облаке и доступная, в том числе, через браузер);</li>
<li>файловый бэкап (обычный .dt-файл, который можно скачать и развернуть на локальной машине);</li>
<li>бэкап конфигурации и расширений (.cf + .cfe).</li>
</ul>
<p>Кроме того, новое решение отслеживает попытки заказать бэкап приложения, если он уже делается прямо сейчас. А ещё — не даёт пользователям бэкапить одно и то же приложение чаще, чем раз в час. </p>
<p>Ну и продолжаем хохмить в интерфейсе, конечно.</p>
<p><img alt="But Still!" src="https://kostyanetsky.ru/notes/backup-ui/but-still.png"/></p>
<p><img alt="Coffee First!" src="https://kostyanetsky.ru/notes/backup-ui/coffee-first.png"/></p></description>
        </item>
        
        <item>
            <title>Протоптанные дорожки</title>
            <link>https://kostyanetsky.ru/notes/desire-paths</link>
            <guid isPermaLink="false">note-desire-paths</guid>
            <pubDate>Sat, 29 Nov 2025 21:00:48 +0700</pubDate>
            <description><p>Ладно, загадка Жана Фреско. У вас есть таблица, скажем, на 50 тысяч строк. Как прочитать из неё полмиллиарда?</p>
<p>Раз плюнуть, Nested Loops + Clustered Index Seek:</p>
<p><img alt="Полмиллиарда" src="https://kostyanetsky.ru/notes/desire-paths/500_million.png"/></p>
<p>От Clustered Index Seek тут одно название, конечно. Фактически оператор при каждом исполнении пробегает по всей таблице (всему кластерному индексу) и сверяет каждую запись с Predicates. И так — 10 730 раз для 51 391 записей. В итоге 551 425 430 строк прочитали, 13 343 вернули.</p>
<p><img alt="Ох" src="https://kostyanetsky.ru/notes/desire-paths/ouch.gif"/></p>
<p>Короче, идеальный пример плохого плана запроса в вакууме, хоть сейчас тащи в палату мер и весов. Nested Loops, если кто позабыл, работает примерно так:</p>
<pre>
For Each Table1Row In Table1 Do
    For Each Table2Row In Table2 Do
        ...
</pre>
<p>Это ОК для мелких таблиц, но СУБД может его применить и для таблиц поболбше — например, если ей не хватает времени на построение плана.</p>
<p>Это и произошло в нашем случае. Прыгнем повыше, на уровень платформы: тут у нас динамический список с запросом по таблице документа, к которой разработчики прицепили с десяток виртуальных таблиц регистров накопления.</p>
<p>Некоторые регистры и сами по себе были здоровенными, а виртуальные таблицы дополнительно поддали жару (каждая превращается в 2+ вложенных запроса). СУБД честно пыталась придумать эффективный алгоритм, но в какой-то момент решала, что хреновый план запроса всё же лучше, чем вообще никакого.</p>
<p>В итоге пользователь что? Пытался поискать документ по номеру и клиентское приложение просто-напросто зависало.</p>
<p>Короче, по поводу виртуальных таблиц в динамических списках. В английском есть выражение «desire path», «протоптанная дорожка». Часто прицепить виртуальную таблицу к основной — и в самом деле самый простой, быстрый и привычный способ решить задачу. Но он <strong>не эффективен</strong>.</p>
<p>Есть, например, обработчик <code>ПриПолученииДанныхНаСервере()</code>. Он дольше в реализации, но позволяет хорошо затюнить виртуальную таблицу и избежать сценария выше. На каждую прокрутку списка получится больше запросов, но они будут быстрее и эффективнее, чем один, но гигантский.</p></description>
        </item>
        
        <item>
            <title>Дневник питания в Obsidian Bases</title>
            <link>https://kostyanetsky.ru/notes/obsidian-foodiary-bases</link>
            <guid isPermaLink="false">note-obsidian-foodiary-bases</guid>
            <pubDate>Sun, 23 Nov 2025 00:01:58 +0700</pubDate>
            <description><p>Переписал с помощью <a href="https://help.obsidian.md/bases" target="_blank">Obsidian Bases</a> свой прошлогодний <a href="https://kostyanetsky.ru/notes/obsidian-foodiary" target="_blank">плагин</a>, считающий калории, белки, жиры и углеводы в пище. Получилось сильно более гибкая и настраиваемая штука, чем в виде плагина — не нужно ничего переписывать, собирать и релизить, если вдруг решил посчитать клетчатку в еде или просто подвигать колонки в отчёте.</p>
<p>Ну и симпатичная, да:</p>
<p><img alt="UI" src="https://kostyanetsky.ru/notes/obsidian-foodiary-bases/base.png"/></p>
<p>Все необходимые настройки и скрипты — в <a href="https://github.com/vkostyanetsky/ObsidianFoodiaryBases" target="_blank">репозитории</a> на Github'е; инструкция <a href="https://github.com/vkostyanetsky/ObsidianFoodiaryBases/blob/main/README.ru.md" target="_blank">переведена</a> на русский язык.</p></description>
        </item>
        
        <item>
            <title>Ну, есть кое-какие</title>
            <link>https://kostyanetsky.ru/notes/well-there-are-some</link>
            <guid isPermaLink="false">note-well-there-are-some</guid>
            <pubDate>Mon, 17 Nov 2025 13:26:00 +0700</pubDate>
            <description><p>Гуляю вечером, сзади идёт какая-то мама и её мелкий — лет пяти, наверное. Я их не вижу, просто слышу разговор. Мама объясняет ребенку про университет: мол, туда надо поступить, учиться, будут экзамены и всё такое.</p>
<p>Мальчик молчит, потом расстроенно выдает:</p>
<p>— Я думал, есть только школа, а оказывается есть ещё сложности...</p></description>
        </item>
        
        <item>
            <title>Безвредный вред</title>
            <link>https://kostyanetsky.ru/notes/harmless-harm</link>
            <guid isPermaLink="false">note-harmless-harm</guid>
            <pubDate>Sun, 16 Nov 2025 10:36:00 +0700</pubDate>
            <description><p>Разбирали на днях с коллегой проблему. Ничего особенно серьёзного, очередное расследование вида «какого черта этот запрос ведет себя странно?».</p>
<p>Упрощая, суть: читаем таблицу базы данных и кладем результат во временную таблицу. Если срабатывает определённое условие, нужно, чтобы временная таблица всё равно создавалась, но была пустой (независимо от того, есть строки в исходной таблице или нет).</p>
<p>Запрос был примерно такой:</p>
<pre>
SELECT
    Table.Field1 AS Field1
FROM
    Table AS Table
WHERE 
    &amp;Parameter
</pre>
<p>Если нужно было отбирать записи из исходной таблицы во временную, в параметр передавался TRUE; если временную таблицу нужно было получить пустой — передавался FALSE.</p>
<p>Несмотря на кажущуюся простоту, такой трюк — проблема для производительности, если таблица, которую читает запрос — большая.</p>
<p>Причина в том, как СУБД работают с параметризованными запросами. И MS SQL, и PostgreSQL строят план выполнения запроса на основе его текста, и в примере выше значение параметра <strong>не</strong> повлияет на принятие решения, нужно читать таблицу или нет.</p>
<p>Таким образом, при выполнении такого запроса обе СУБД педантично прочитают всю таблицу (ну, или её индекс), даже если параметр равен FALSE. В последнем случае каждая прочитанная запись будет отброшена и алгоритм будет работать корректно, однако мы будем тратить ресурсы на бессмысленное чтение данных и забивать буферный кэш, замедляя систему в целом и активно работая на глобальное потепление :)</p>
<p>Решение тут простое — вставлять TRUE/FALSE в тело запроса как константу, не используя параметр. Либо использовать оператор TOP, так текст запроса будет даже проще:</p>
<pre>
SELECT TOP 0
    Table.Field1 AS Field1
FROM
    Table AS Table
</pre>
<p>Тут на уровне SQL мы получим что-то вроде «SELECT TOP 0 ... FROM Table» (для MS SQL) и «SELECT ... FROM Table LIMIT 0» (для PostgreSQL). В итоговом плане будет оператор чтения, но исполнитель фактически не запросит ни одной строки, так что реального сканирования данных не случится (ура).</p>
<p>P.S. Если не критично получать во временной таблице корректные типы колонок, можно вообще вот так:</p>
<pre>
SELECT TOP 0
    UNDEFINED AS Field1
</pre>
<p>Выигрыш в производительности, впрочем, будет таким копеечным, что можно не упарываться. </p></description>
        </item>
        
    </channel>
</rss>