Канал с кабанчиком


Гео и язык канала: Россия, Русский
Категория: Технологии


@djaler пишет про разработку и вот это все

Связанные каналы

Гео и язык канала
Россия, Русский
Категория
Технологии
Статистика
Фильтр публикаций


Пока что я ленивая жопа и всё еще не написал новый пост, поэтому вот почитайте про явление, которое мы будем встречать всё чаще - ИИ-агент, редактирующий свой собственный код.

https://t.me/abstractDL/358

С одной стороны это звучит очень интересно, а с другой - пугающе, но этот прогресс уже не остановить, остается только наблюдать. Ну или бомбить дата-центры, как призывал Юдковский.


Видео недоступно для предпросмотра
Смотреть в Telegram
Не смог пройти мимо тренда, извините


Видео недоступно для предпросмотра
Смотреть в Telegram


В PostgreSQL есть одна интересная фича под названием exclusion constraint. Она позволяет добиться того, чтобы строки в таблице не пересекались по какому-то условию. И чаще всего в примерах описывают сложные кейсы, например защиту от пересечения диапазонов.

Представим, что у нас есть таблица, хранящая информацию о бронировании номеров:
CREATE TABLE room_reservation (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id int NOT NULL, -- идентификатор номера
during tsrange NOT NULL, -- срок бронирования (дата и время с/по)
client_id int NOT NULL -- идентификатор клиента
);
Как нам на уровне БД обеспечить гарантию того, что брони одной и той же комнаты от разных клиентов не пересекутся?

Если упрощать условия и при этом усложнять схему БД, то можно было бы придумать что-то такое:
CREATE TABLE day (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
date date NOT NULL UNIQUE
);

CREATE TABLE room_reservation (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id int NOT NULL,
day_id int NOT NULL REFERENCES day(id),
client_id int NOT NULL,
UNIQUE(room_id, day_id)
);
Во-первых, нам пришлось добавить отдельную таблицу.
Во-вторых, теперь мы оперируем целыми днями.
В-третьих, теперь, если бронь больше чем на день - нам нужно несколько записей в room_reservation.
В общем, минусов хватает.

А exclusion constraint решает её очень просто:
CREATE TABLE room_reservation (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id int NOT NULL,
during tsrange NOT NULL,
client_id int NOT NULL,
EXCLUDE USING GIST (room_id WITH =, during WITH &&)
);
С помощью выражения EXLUDE мы задаем, используя GIST-индекс, следующее условие: записи не должны пересекаться, сравнивая room_id по полному совпадению и during по пересечению. Оператор && - один из операторов у range-типов, к числу которых относится и tsrange. Он позволяет проверить пересечение двух диапазонов.

Но как я ранее сказал, такой пример часто приводят в качестве демонстрации мощи exclusion constraint. Но есть еще один интересный юзкейс, о котором многие не знают, и связан он с более простой проверкой уникальности, без каких-то сложных пересечений диапазонов.
Допустим, у нас есть таблица с большим количеством строковых значений, для которых нам нужно обеспечить уникальность (потому что на эти записи по id ссылаются другие таблицы):
CREATE TABLE quotes (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
quote text NOT NULL
);

Самый очевидный способ это сделать - добавить UNIQUE индекс (или UNIQUE constraint, что фактически одно и то же):
CREATE UNIQUE INDEX quotes_unique_idx ON quotes(quote);

Но вот в чем проблема - теперь наш индекс по размеру примерно совпадает с таблицей, поскольку дефолтный B-Tree индекс содержит в себе все индексируемые значения, а строки у нас достаточно крупные.
Хорошо что мы умные и знаем про Hash индекс, размер которого не зависит от размера индексируемых данных, а зависит только от их количества.
CREATE UNIQUE INDEX quotes_unique_idx ON quotes USING HASH(quote);

ERROR: access method "hash" does not support unique indexes
А, ой. Hash-индексы не умеют в проверку уникальности. А как быть? Хочется и рыбку съесть место сэкономить, и уникальность быстро проверять.

И вот тут на помощь снова приходит exclusion constraint. Мы можем сделать вот так:
ALTER TABLE quotes ADD CONSTRAINT quotes_unique_hash_idx EXCLUDE USING HASH (quote WITH =);

Таким простым маневром мы создаем Hash-индекс, поверх которого работает constraint, проверяющий записи на совпадение по равенству quote. Теперь у нас и уникальность проверяется, и поиск записей по тексту стал чуточку быстрее, но самое главное - индекс занимает в разы меньше места. Все счастливы.


А вот тут https://arxiv.org/pdf/2512.14982 исследователи утверждают, что простое повторение запроса к LLM (Ctrl+C, Ctrl+V) улучшает точность ответа.

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

Ну и интересно, как скоро подобные трюки снова перестанут быть актуальными в новых версиях, как уже произошло с промптами вида Let's think step by step с появлением reasoning-моделей.


А вот тут https://arxiv.org/pdf/2512.14982 исследователи утверждают, что простое повторение запроса к LLM (Ctrl+C, Ctrl+V) улучшает точность ответа.

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

Ну и интересно, как скоро подобные трюки снова перестанут быть актуальными в новых версиях, как уже произошло с промптами вида Let's think step by step с появлением reasoning-моделей.




Всех с Новым Годом! Пока что держите мем


Кстати, это я лично договорился чтоб Йорген Холлер такую фичу добавил.

Вот Йорген стоит, вот я молодой еще.

Не благодарите


У сочетания Kotlin и Spring есть одна старая проблема, на которой часто спотыкаются разработчики.

Спринговая аннотация @Transactional по умолчанию работает следующим образом - любой RuntimeException или Error приводит к роллбеку, но Exception - нет. Такое поведение - прямое наследие EJB (Enterprise JavaBeans), где checked-исключения (наследники Exception) считались ошибками бизнес-логики, а unchecked (наследники RuntimeException) - неожиданными системными/техническими ошибками. Поэтому ожидаемые бизнесовые ошибки предлагалось обрабатывать явно и не отказывать транзакцию автоматически.

В современном мире такое соглашение редко соблюдается, к тому же даже в стандартной библиотеке полно checked-исключений, которые нельзя назвать бизнесовыми (вроде IOException). Разумеется, в Spring это настраивается, у @Transactional есть гибкие способы точечно подтюнить это поведение с помощью параметров rollbackFor/rollbackForClassName/noRollbackFor/noRollbackForClassName. Но даже в Java многие не раз попадались на этот нюанс, или не зная о нем, или забыв.

А в Kotlin всё становится еще интереснее, ведь в Kotlin нет такого понятия как checked-exception. Да, код на Kotlin работает с кодовой базой Java и может взаимодействовать с наследниками Exception, но компилятор не заставит обработать такое исключение, как произошло бы в Java. Как так? А всё потому что checked-исключения - это фича именно языка Java и его компилятора, а не самой JVM. На уровне байткода между этими исключениями нет никакой разницы, и в рантайме они ведут себя одинаково. Только компилятор Java заставляет как-то по-особенному обрабатывать checked-исключения. А вот компилятор Kotlin нет. И в случае @Transactional это большой подвох, потому что разработчик на Kotlin уже понятия не имеет, является ли его исключение наследником Exception или RuntimeException. За исключением работы с @Transactional нет никакой разницы, поэтому и нет смысла задумываться о различиях.

Разумеется, разработчик, который всё-таки знает о таком поведении (а скорее, уже однажды попавшийся на ситуацию с не откатившейся транзакцией), просто напишет @Transactional(rollbackOn = Exception::class.java). Но блин, это же нужно делать везде. Где-то наверняка забудем. К тому же, выглядит уже грязновато.

И до недавних пор адекватного способа решить эту проблему глобально - не было. Но в Spring 6.2 (и Spring Boot 3.4, соответственно) наконец-то появилось решение, закрыв собой ишью аж от 2019 года. Теперь можно задать глобальное поведение по умолчанию вот таким образом - @EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS), а какие-то дополнительные глобальные тюнинги можно выполнять, настраивая бин AnnotationTransactionAttributeSource. Разумеется, команда Spring не может сделать это новым дефолтом, потому что это сломает обратную совместимость, так что фича исключительно opt-in. Но они рекомендуют самостоятельно включить такой режим, и в Java, и, тем более, в Kotlin-приложениях.


Вчера захотел наконец-то потыкать Sora 2. А она сейчас доступна только по инвайтам, которые можно получить от других пользователей.
И дальше произошел большой AI-мем. Я буквально попросил ChatGPT найти мне действующий инвайт и это сработало.


После следующего повышения грейда хочу получить такую дверь с табличкой


А поскольку мне не спится и я пока что еще иногда полезнее LLM: вот запрос, который считает подобный отчет самостоятельно:

WITH table_info AS (SELECT n.nspname AS schema_name,
c.relname AS table_name,
c.reltuples AS est_rows
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = :table_name),
col_stats AS (SELECT s.schemaname,
s.tablename,
s.attname,
s.avg_width,
s.n_distinct
FROM pg_stats s
WHERE s.schemaname = :schema_name
AND s.tablename = :table_name),
table_stats AS (SELECT c.attname AS col_name,
c.avg_width * t.est_rows AS current_size,
4 * t.est_rows AS foreign_key_size,
(4 + c.avg_width) * c.n_distinct AS lookup_table_size
FROM col_stats c
JOIN table_info t ON c.schemaname = t.schema_name AND c.tablename = t.table_name
WHERE c.n_distinct > 1
AND c.avg_width > 4)
SELECT ts.col_name,
pg_size_pretty(round(ts.current_size)::bigint) AS current,
pg_size_pretty(round(ts.foreign_key_size + ts.lookup_table_size)::bigint) AS normalized,
pg_size_pretty(round(ts.current_size - ts.foreign_key_size - ts.lookup_table_size)::bigint) AS gain
FROM table_stats ts
ORDER BY (ts.current_size - ts.foreign_key_size - ts.lookup_table_size) DESC;


Занимаюсь сейчас одной задачей - нужно смигрировать БД размером 1.8 Тб в платформу, где есть ограничение в 1 Тб. Собственно, нужно что-то придумать чтобы вместиться. Параллельно с анализом возможности разбиения этой базы или шардирования решил посмотреть в сторону старой доброй нормализации данных - не повторяются ли одни и те же данные в разных строках. Может быть можно что-то вынести отдельно?

И да, уже даже невооруженным взглядом вижу, что прямо в таблице транзакций упоминаются названия компаний (и они, очевидно, не уникальные). Явный кандидат на вынесение. Но как оценить потенциальную пользу от вынесения компаний в отдельную таблицу? И как отследить другие подобные колонки кандидаты для нормализации?

Идем смотреть статистику по таблице от самой БД:
SELECT attname, null_frac, avg_width, n_distinct
FROM pg_stats
WHERE tablename = 'transaction'
AND n_distinct > 0
ORDER BY avg_width DESC;

Получаем примерно такие результаты:
| attname | avg_width | n_distinct |
| ------------------ | --------- | ---------- |
| details | 114 | 34717 |
| okved_description | 104 | 1392 |
| counter_party_name | 58 | 22992 |
| model_version | 29 | 1 |
| ... |

Тут видно, что значения в колонке details имеют средний размер в 114 байт, при этом уникальных значений 34717. Ах да, а всего строк в этой таблице - примерно 1.2 миллиарда. Кажется, тоже неплохой кандидат на нормализацию - мы здесь явно выиграем в полезном пространстве, но сколько? Тут мне стало уже лень считать самостоятельно, тем более повторять это для других колонок.

Выгружаем эту статистику и других вводные в ChatGPT и получаем отчет:

details
* Avg width: 114 B
* Distinct values: 34 717
* Current size: ~128 GB
* After normalization: ~8 GB
* Gain: ~120 GB

okved_description
* Avg width: 104 B
* Distinct values: 1 392
* Current size: ~116 GB
* After normalization: ~3 GB
* Gain: ~113 GB

counter_party_name
* Avg width: 58 B
* Distinct values: 22 992
* Current size: ~65 GB
* After normalization: ~6 GB
* Gain: ~59 GB

model_version
* Avg width: 29 B
* Distinct values: 1
* Current size: ~33 GB
* After normalization: < 1 GB
* Gain: ~32 GB

Total potential saving: ≈ 320 GB (~55 % of heap).

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




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

Таблица выглядит примерно так:
CREATE TABLE process_instance
(
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
process_instance_id uuid NOT NULL,
env text
);

А вот так обеспечивается уникальность (по нашей задумке):
CREATE UNIQUE INDEX unique_process_instance_id ON process_instance (process_instance_id, env);

Поле env при этом заполняется только в тестовых окружениях, так как они переиспользуют общую базу. Но на проде оно всегда null.

И вот именно на проде мы получили дубликаты:
SELECT * FROM process_instance WHERE process_instance_id = :processInstanceId;

id | process_instance_id | env
---+----------------
1 | 12345678-1234-1234-1234-123456789012 | null
2 | 12345678-1234-1234-1234-123456789012 | null

Как же так вышло? Почему уникальный индекс не сработал? На самом деле, мы пропустили очень логичный, но не очевидный в конкретно этом сценарии момент.

Для проверки уникальности значения сравниваются по равенству. А в SQL в целом и в PostgreSQL в частности, null != null - в любых запросах, где нам нужно сравнивать какое-то значение с null, нужно использовать операторы is null или is not null. Собственно, именно этот факт и подвел нас при проверке уникальности в индексе. Любые строки с совпадающим идентификатором, но env null - считались отличающимися.

Окей, проблему поняли, а как чинить?
Первое что приходит в голову - нужно избавиться от null в индексе. Сделать это можно, например, вот так:
CREATE UNIQUE INDEX unique_process_instance_id ON process_instance (process_instance_id, coalesce(env, ''));

Решение рабочее, но не самое лучшее, поскольку мы подменяем для проверки уникальности null на другое значение. А что если такое значение само по себе может содержаться в поле? Да, для env это, наверное, не реалистичный кейс, но в общей ситуации это может быть проблемой - нужно искать какое-то магическое значение, которое не будет пересекаться с реальными значениями этого поля.
К тому же, для полной утилизации такого индекса при поиске нам пришлось бы использовать coalesce(env, '') и в условии where, что не очень удобно.

Но с PostgreSQL 15 появилось стандартное решение этой проблемы, с помощью модификатора NULLS NOT DISTINCT в индексе:
CREATE UNIQUE INDEX unique_process_instance_id ON process_instance (process_instance_id, env) NULLS NOT DISTINCT;

В таком режиме любые null в индексе считаются равными друг другу, что решает нашу проблему.
Использование такого синтаксиса сразу даёт понять читателю кода чего мы хотели добиться, не заставляя разбираться в замысле coalesce.
Ну и, конечно же, он никак не влияет на механику самого поиска по индексу, поэтому нет необходимости никак модифицировать запросы.




Извините, полезных постов всё ещё нет, но мем прекрасный


Сегодня полезного поста не будет, вместо этого ловите Doom на чистом SQL - https://cedardb.com/blog/doomql/

Больше всего меня тут радует, что все псевдо-3D отображение построено буквально как VIEW вокруг двумерных данных о карте.

А основные данные о происходящем сложены в максимально понятные классические таблицы:
-- Change a setting
update config set ammo_max = 20;

-- Add a player
insert into players values (...);

-- Move forward
update input set action = 'w' where player_id = ;

-- Cheat (pls be smarter about it)
update players set hp = 100000 where player_id = ;

-- Ban cheaters (that weren't smart about it)
delete from players where hp > 100;


Для тех, кто ещё по какой-то причине не купил билет, но собирается - у меня есть промокод на скидку: SpeakerRomanov. https://openconf.today/#price

Показано 20 последних публикаций.

109

подписчиков
Статистика канала