Что это и зачем нужно? Предположим у вас есть крупное новостное издание, у которого много разных типов материалов.  Для каждого типа материала существует своя модель: Topics::Article, Topics::Online, Topics::NewsItem и так далее. У них будут одинаковыми большинство полей, такие как заголовок, обложка, текст, авторы. Различие только в нескольких специфичных полях, уникальных для каждого типа топика. Поэтому вам не хочется раскладывать их по отдельным таблицам. Кроме нежелания создавать почти полностью повторяющиеся таблицы, для этого могут быть и несколько других причин. Необходимость сложных выборок с разными комбинациями этих типов, водопады UNION и полиморфизм подключающихся моделей в том числе. Под катом опыт организации похожих моделей внутри Postgresql, с итогом в виде миграции на наследование таблиц. Стрельба в ногу серебряной пулей тоже присутствует, куда же без нее. Первое что приходит в голову, это классический Single Table Inheritance. В Рельсах он автоматически включается, если наследовать одну модель от другой. Создав одну общую таблицу Topics и добавив в нее служебное поле type можно хранить все классы внутри одной таблицы. Общая логика (например, публикация материала) уходит в Topic. Специфичная в отнаследованные классы.  У этой простой и обкатанной схемы есть проблемы с масштабированием. Что делать, если типов топиков уже больше пятнадцати и у каждого от двух до десяти уникальных полей?  На этом моменте бд пуристы морщатся как будто бы от сильнейшей зубной боли — воображая себе таблицу, строки которой всегда заполнены не более чем на 15-20%.  В Postgresql c 9.4 есть возможность создавать поля типа jsonb. Как это может нам помочь?
Добавив в topics поле data этого типа мы можем хранить все наши дополнительные поля в ключах json.  Подключить в Рельсах это можно так: Теперь можно делать так: Или напрямую обращаться в json: Наслаждение достигнутым успехом быстро омрачается подпорками из костылей. Помимо экзотики вроде массивов и объектов (хешей) jsonb предлагает для всех полей использовать только Number, String и Boolean.
Для других типов полей придется писать дополнительные методы. А если вы предпочитаете с сахаром, то и для этих полей тоже. Предикаты: Более сложный случай для DateTime: Здесь нужно распарсить строку во время и не сломаться. Потому что ломаться нужно на этапе сохранения данных. Валидаций типа на уровне бд нет, можно легко и непринужденно сохранить такой топик: Существующие гемы, например activerecord-typedstore, частично решают проблему парсинга строк, но совсем не справляются с проверкой входных значений. Все нужно закрывать из приложения кастомными валидациями. Булево поле: А теперь даты: Помимо общей монструозности этот запрос еще и будет медленнее работать, из-за вынужденного использования to_date Постгреса. Постгрес пока не умеет делать обычный DISTINCT (.uniq) для записей с jsonb, нужно делать так: Приходится использовать разные конструкции в before_initialize вместо привычных null: false, default: false в миграции. Использовать рельсовые has_many и belongs_to не выйдет. Нужно писать что-то свое. На этом этапе jsonb получил черную метку и до троллейбуса из буханки хлеба дело не дошло. Наследование таблиц появилось в Постгресе достаточно давно (скорее всего обновлять версию не потребуется) и близко концепции наследования классов.
Только не классов, а таблиц, и не в Руби, а в Постгресе. У вас по прежнему есть таблица topics, но расширяете ее вы не через набор дополнительных полей в этой самой таблице, а через дополнительные таблицы, содержащие только уникальные для каждого класса поля. Проще всего показать на примере: Создав topics_onlines мы можем работать с ней как с обычной таблицей, у которой будут все три поля: Это просто, красиво и не требует массивного переписывания кода. Родительская и дочерняя таблицы линкуются внутри Постгреса через tableoid. Мы не увидими никаких джойнов по tableoid в explain, все это работает внутри Постгреса. Из приложения topics_onlines будет выглядеть как самая обычная таблица без наследований, содержащая все поля из topics и специфичные для онлайна поля из topics_onlines. А это значит, что со стороны Рельс нужно только написать миграцию создающую таблицы.
И что наследование таблиц можно использовать с любым фреймворком. Чтобы воспользоваться всей этой радостью нужно написать миграцию. Для начала нам потребуется стандартная обертка для sql миграции в Рельсах: Код дальше вставляется на место многоточия. Чтобы не накидывать простыню кода сразу, я покажу миграцию по порциям. Создаем, но пока нигде не используем триггер в Postgresql: Триггер вызывает ошибку, если топик с таким id уже существует. Это страховка на тот случай, если что-то пошло не так. Самое важное ограничение PGI — для всех дочерних таблиц не действуют индексы и ограничения родительской таблицы. То есть в этом плане все действительно ощущается как разные физические таблицы. Для наших условий у разных типов топиков не может повторяться айдишник, поэтому был добавлен этот триггер. Он опционален и нужен как страховка. Разумеется, миграцию можно посадить на строгую диету, а все проверки вытащить в сами Рельсы. В новые таблицы можно добавлять поля используя стандартные миграции: Если вы не готовы полностью избавиться от STI, то в классах топиков прописывается нужная таблица: Осталось только изменить тип схемы на sql: И все готово.   Заключительным этапом было бы интересно оценить производительность. Так как все это затевалось ради удобства разработки, то тестированию скорости PGI было уделено не так много времени, однако какие-то выводы сделать можно. После миграции были подняты две версии приложения, PGI и старая с jsonb.
Больше 5_000_000 топиков в каждой базе. Самый синтетический пример: PGI: Jsonb: Не удивляйтесь странному числу, топики создавались пока не закончилось место на ssd. PGI: Jsonb: Индекс не используется из-за высокой селективности. PGI: Jsonb: Разница существенная. Хотя бы уже пересекается с реальным миром. PGI: Jsonb: Разница есть. Поиск по индексу, вернет 123 записи из миллиона, Index Scan. PGI: Jsonb: Для задачи полноценного расширения моделей jsonb может оказаться крайне токсичным. Разумеется, PGI тоже имеет свои ограничения, но для наших задач они были преодолены за один раз, во время миграции. И все же jsonb может быть полезен для слабо структурированных данных из внешнего источника. Эти поля даже не нужно определять через store_accessor, их можно просто сохранять как есть, а дальше отдельный класс Builder соберет из них что-то полезное. Для нас такими данными стали спортивные трансляции, забираемые из внешнего апи.