Pull to refresh

Comments 727

Полностью поддерживаю.

Не могу не заметить, что примерно половина перечисленных пунктов решается в Ruby on rails самим фреймворком, поэтому я его так люблю. Но, конечно, если человек вместо суммы двух переменных хочет написать звездолёт с машиной времени и вечным двигателем, тут ничего не спасет.

А вообще, кажется, простые и понятные решения всегда предпочтительнее сложных и непонятных, и неважно, Джун ты или помидор.

Простые решения часто плохо масштабируются. Один метод с тремя кейсами можно и без полиморфизма. А вот когда методов уже несколько, а кейсов с десяток, то полиморфизм будет проще. Начинать нежно с простого и потом когда придёт надобность уже переключаться на сложное.

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

Я имею ввиду поддержку и расширение. С полиморфизмом можно посадить пять джунов делать новые классы паралельно. С ифами так не выйдет.

Как раз ифы проще для понимания и там сгодятся джуны.

Джуны уже понимающие ифы но еще не понимающие ООП, называются трейни.

Речь идет о задаче разряда импорт/экспорт различных форматов. Мидл пишет интерфейс, и занимается дальнейшей логикой, джуны пишут свои классы реализаций по каждому формату. А не все реализации через иф - элсиф

Любой человек понимает иф лучше и быстрее. Даже сеньор.

Поэтому лучше писать их. Такой код лаконичней и понятней.

А потом иф вложенный в иф вложенный в иф... Достаешь бумажку, карандаш, и начинаешь разрисовывать все возможные состояния, и внезапно обнаруживаешь, что иф на 5-м уровне вложенности никогда не выполнится, потому что уже был перехвачен на третьем. Больше бумаги богу паутины!

Чем вы ифы замените в таком случае?

В каждом случае индивидуально, золотой пули нет. Часть в методы, часть в методы класса, часть в методы-расширения класса, часть ифок заменить на свитчи, и прочее-прочее-прочее. Главное, что бы оно снова стало читабельно, в идеале бегло.

Золотая пуля - есть! Серебрянной - нет )))

P.S. Некстати говоря, я бы не сказал, что её совсем нет. Когда из Вашего кода уже просто нечего выкинуть, чтоб его улучшить - то вот оно. Лучшее решение.

Когда из Вашего кода уже просто нечего выкинуть, чтоб его улучшить - то вот оно. Лучшее решение.

Есть. Как нас учит ентот самый ТРИЗ, лучшее решение — это когда устройства кода нет, а его функция — выполняется!

вынесением каждого вложенного "ифа" в отдельный метод, функцию или целый класс, с именем обьясняющим что там этот "иф" делает?

на стейт машину

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

Все должно быть в рамках разумного, поэтому не согласен с тем как вы накорню отказываетесь от полиморфизма

Там можно методы бить на части..

.Чем вас спасёт если тот же метод метод voice распадается на десято abstractVoice, AnimalValidatirImpl, AbstractDogVoiceDefiner?

Ну он же написал про контекст. В одном контексте мы проверили логику выбора конретной имлементации. Дальше мы про этот контекст забываем и переходим к контексту имплементации. Таким образом мы снижаем когнитивную нагрузку на мозг.


Там можно методы бить на части..

А можно и на классы с методами. ООП — это и есть метод организации кода.

Тут есть нюанс: вызов полиморфного метод — это абстракция, которая упрощает текущий уровень: нам не надо смотреть на нем реализацию всех возможных вариантов действия, описанного данным методом — мы просто пониамем, что он делает по смыслу, а как он это делает — пока не смотрим (будет надо — посмотрим). А пока нам достаточно знать, что это как-то работает и делает что надо (пусть и по-разному для разных вариантов). Заодно имеем гарантию изоляции — что код, написанный для одного варианта, не вылезет случайно в другом.

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

Полиморфизм небесплатен хотя бы тем, что для него нужен косвенный вызов (по адресу, извлекаемому из таблицы в памяти), а потому — лишнее обращение к памяти.
То, что это не серебрянная пуля — это, как бы, общее место.
Ну, а внутренне присущую задаче сложность все равно никуда не деть. Единственное, что мы можем — разбить задачу на сравнительно слабо связанные куски меньшей сложности. И полиморфизм для этого является одним из приемов для этого — да, имеющий ограниченную область применимости, но — таки имеющий.

Парадокс в том, что разбивая на подзадачи, или применяя полиморфизм, или распиливая на микросервисы, мы увеличиваем суммарную сложность задачи. Упрощение отдельных частей покупается за счет усложнения задачи в целом. До определенного предела это выгодно и упрощает работу, но этот баланс нужно уметь чувствовать и поддерживать, в этом и есть искусство разработчика, ИМХО, то что делает эту работу немного не просто ремеслом.

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

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


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


interface IBinaryOperation<T> {
    T Op(T a, T b);
}

А вот такой интерфейс уже ничего не замедлит:


interface IBinaryOperation<T> {
    void Op(ReadonlySpan<T> a, ReadonlySpan<T> b, Span<T> result);
}
А вот такой интерфейс уже ничего не замедлит:

Ну, не так уж совсем ничего — вызов, да еще и косвенный (т.е. с выбором адреса вызова из памяти) — это небесплатно, особенно — с современной организацией оперативной памяти, кототорая в общем случае работает крайне медленно.
А потому я, к примеру, наблюдал, в коде ASP.NET как MS постаралась для наиболее массового случая — когда некий интерфейс (если кому интересны подробности — могу рассказать, какой именно) реализован стандартным классом ASP.NET — отдельно обработать вызовы методов именно этого класса как невиртуальные — а это ещё и может позволить встроить (inline) эти методы в код, чтобы вообще избавиться от вызовов.

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

Большими кусками не всегда получается.
Например, в том конкретном случае, про который я пишу, так была оптимизирована операция создания контекста обработки запроса HTTP. А запросы HTTP — они по жизни, чаще всего, короткие (особенно если это — CRUD, столь желанный автору статьи ;-) ). Но зато этих запросов может быть много.

А вот когда методов уже несколько, а кейсов с десяток, то полиморфизм будет проще.

Так а с чего вы взяли что полиморфизма там нет? Не забывайте что у полиморфизма есть разные измерения — set of objects vs set of features. Поэтому например существует паттерн "визитор", который часто используется в местах иерархией которая редко меняется, но зато функционал часто разный. А визитор это просто switch in disguise по большому счету.


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


Поэтому тут нет никакого противопоставления.

Важно не запутаться в этих ифах и не накосячить. Полиморфизм через интерфейсы и посетитель помогают в этом. Тесты тоже. Из статьи мне показалось, что автор отвергает все эти способы борьбы со сложностью.

В которое есть проверка на уровне компилятора, что все кейсы по свитчу на энум обрабатываются. То есть добавив новый кейс вы не забудете поправить все свитчи. Вот такой свитч я люблю. Остальные не защищают от ошибок.

В номральных языках есть pattern matching с completeness и redundancy check. Сложно становится накосячить.

С#, Kotlin и даже, внезапно, Python. Но у последнего сложнее с completeness и redundancy check.

Увы, нормального completeness check в C# не завезли:


enum Foo
{
    Bar,
    Baz,
}

var x = foo switch
{
    Foo.Bar => 1,
    Foo.Baz => 2,
};
// warning CS8524: Выражение switch не обрабатывает некоторые типы входных значений, в том числе неименованное значение перечисления (не является исчерпывающим). Например, не охвачен шаблон "(Foo)2".

Ну это если не читать спецификацию языка

А что я там должен прочитать?

Задача-то проверить на этапе компиляции, что все случаи обрабатываются. Чтобы при появлении новых случаев компилятор предупредил если их забыли добавить в switch.


Так вот, эту задачу связка enum+switch в C# не решает. Можно сколько угодно говорить что программист дурак и не понимает как работают enum, а компилятор делает всё правильно, но задача от этого не решится.

Задача-то проверить на этапе компиляции, что все случаи обрабатываются.

Как всегда есть нюанс. В C# используются C_style enums. То есть (Foo)25 это тоже корректное значение и компилятор проверяет что и его тоже покрывает switch-expression.

Чтобы при появлении новых случаев компилятор предупредил если их забыли добавить в switch.

Ну конечно нет.

Так вот, эту задачу связка enum+switch в C# не решает. Можно сколько угодно говорить что программист дурак и не понимает как работают enum, а компилятор делает всё правильно, но задача от этого не решится.

Программист-дурак это тот, кто думает что знает как должно работать, но не читал спецификацию.

С таким подходом можно и Javascript языком со статической типизацией назвать. Ну а что? Типы там есть, аж восемь, а что они в рантайме проверяются — так это программист дурак, раз не читал спецификацию.

UFO just landed and posted this here

В Dart 3.0 добавили patterns, вместе с switch expression, который как и switch statement проверяет exhaustiveness.

В большинстве нормальных языков есть экзошн проверки которые не позволят "забыть" какой-нибудь из вариантов.


я не спорил ни с кем, просто отметил, что противопоставление "свитчи плохо полиморфизм хорошо" это ложная дихотомия, которую впрочем некоторые очень любят.

На какие ифы? Если вы написали else без условий то значит эта ветка должна обрабатывать все что угодно. Если хочется обрабатывать все условия и получать ошибку в случае необрабатываемого нового варианта то иф не подойдет, нужен свитч.


Например?

fn foo(x: Option<i32>) -> i32 {
    match x {
        Some(val) => val   
    }
}

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/lib.rs:2:11
  |
2 |     match x {
  |           ^ pattern `None` not covered
  |
UFO just landed and posted this here

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

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

Согласен, сначала делать просто, потом рефакторить. Главное не пропустить момент.

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

А зачем всю дорогу страдать? Ну упоролись в фундамент, ну ок. Потом строите на нем ларек для шавухи. Не понадобился, снесли - поставили кофейню. Фундамент должен обладать рядом характеристик, по которым определяются ограничения для строительства. Но он не должен диктовать, что должно быть построено. Если на фундаменте для небоскреба вы не можете поставить шаурмяшную - это плохой фундамент.

Был у меня случай, на с++ для UDK делал физическую модель машины(Камаза) на 4 колеса. Пришел техлид и переделал все под шаблоны и 4 колеса превратились в 3(ТРИ!!!). Снес его все правки и вернул как было.

Если смотреть с определённого ракурса, у Камаза будет видно только 3 колеса :)

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

Интересный у вас техлид, однако...

Это база.

Есть такие магические буквы S O L I D. И почему то все забывают что это не ПРАВИЛА. Это вообще-то РЕКОМЕНДАЦИИ. И им необязательно следовать.

На самом деле, мне кажется, ценность синьёра именно в умении БЫСТРО решать задачи. Если он ебать прошаренный, то он точно будет знать когда эти ваши полиморфизмы нужно использовать, а когда, как в примере выше.

У меня на работе была задача сделать миграцию из оракла в постгрес. И значит 2 команды параллельные. В одной я, в другой другие ребята.

Так они начали писать свой инструмент миграции... У них постоянно что-то было не так. Ошибки, неверные типы, типы оракла, аналогов которым нет в постгре...

Они кажись уже пол года пытаются допилить тот инструмент.

Что сделал я: скачал ora2pg и сутки разбирался почему руби не работает на моём компе. А после запустил и всё за пару часов отработало...

Пришел тимлид в мою команду и тоже начал разрабатывать свой инструмент.... За две недели до нг мы всё равно мигрировали через ora2pg.

А можете всеже пояснить, что не так с OneToOne (если я правильно понял о чем речь)?

Если у тебя две таблицы, связанные один к одному - так зачем тебе две таблицы? Сложи все данные в одну.

В основном Иногда такое встречается в легаси. Когда у тебя просто нет возможности добавить новое поле ничего не сломав в системе, которая писалась 20+ лет многими поколениями программистов (да. Тестов нет. От слова - "совсем"). Тем более если ты понимаешь, что это поле востребовано будет раз в год по понедельникам (либо вообще не приживётся и через полгода "бизнес" про него забудет, а тебе с ним жить)

А еще есть Postrgrees который любит при каждом обновлении записи, даже если длина строки не меняется - делать запись новой строки, а не обновляет inline как mysql. итого меняешь одно число - идет запись целой строки в новое место. ну и блокирует всю строку во время транзакции.

ну и другие моменты например User - Profile. первое нужно всегда, второе почти никогда. удобнее делать SELECT *, не указывая конкретный список полей.

Да, про postgres верно подмечено. При обновлении любого, даже самого маленького поля в строке, физически происходит добавление новой записи (со всеми 100500 неизменившимися полями с "картинками")

Это их решение (легаси) многоверсионности и изоляции транзакций. при чем еще на каждую строку выравнивание идет, проставление ид транзакций (создано, удалено) и это все хранится очень долго. Хранится до тех пор, пока vacuum не произойдет.

Вопрос: даже если просто число инкрементировать, то создается новая версия строки (возможно в новой версии это как-то обошли) ? Эксперты, ответьте пожалуйста за что коллегу заминусовали, который упомянул это :)

Поправьте пожалуйста, если я в чем-то не прав

Я думаю, коллеги меня правильно заминусовали, т.к. я слишком обобщил про "картинки". TOAST поля не дублируются при каждом чихе.

Да, TOAST это исключение скорее :)

Ночь. Не заметил, что о картинках речь в вашем комментарии была

Но коллеги могли и не ставить минуса, а пару слов написать хоть :) помочь так сказать

Да не, нормально всё. Т.к. Фактически там "ересь" написана. Так что пусть остаётся заминусованым, раз уж я его исправить не могу. Вдруг кто-нить из поисковика попадёт на тот коммент и решит, что это правда, т.к. Сообщество на это никак не отреагировало.

В смысле "любит"? Он по определению делает новую запись при каждой записи каждой строки. Это не потому, что он любит/не-любит, и не потому, что разработчикам было лень заморачиваться. Так реализована "версионность" для обеспечения всяких разных уровней изоляции транзакций. By design, как говорится.
Вот из "первых рук" https://www.youtube.com/watch?v=SNrv8ZPpPfg&list=PLaFqU3KCWw6LPcuYVymLcXl3muC45mu3e&index=6

UFO just landed and posted this here

Если можно добавить новую функциональность вообще не трогая старый код, это стоит сделать. Чем меньше вы трогаете существующий код, тем меньше шансов сломать то, что сейчас работает.

Контрпример :

Была база с картинками. Служебной инфы минимум, а картинки большие. Вся таблица в память не влезает, поэтому каждый запрос тормозит обращение к диску.
Переделали на 2 таблицы: в одной инфа о картинке (с маленькой превьюхой), в другой сами картинки. Скорость выросла в несколько раз.

Да, хороший пример. Некоторые типы данных плохо уживаются в одной таблице.

Некоторые разработчики не умеют проектировать схему, а уживается все нормально если руки прямые

А вы сами хороший разработчик, верно? Можете спроектировать схему как хранить данные для товарища выше: пяток служебных полей по 32 байта каждая и картинка на 10мб. Как будут хранить это добро в одной таблице разработчики, которые умеют проектировать схему?

Кто сказал что будут использоваться какие-то облачные платформы, а не какой-нибудь дигиталоушон с аргосд и простой постгрей?

ну что вы в самом деле, S3 уже давно имя нарицательно для всего что может блобы хранить и имеет какой никакой интерфейс для этого. S3 просто задал фактически стандарт этого API, а реализаций есть куча не в облаках и думаю при желании и на том же PG найти можно

Для кого стало, а кто амазон только на картинке видел (да, вот такой я динозавр). У других разработчиков другой опыт, и то что вам очевидно для других может быть открытием. Я вот в первый раз про это слышу, но спасибо за информацию)

Мне иногда кажется, что Амазон лучше видеть на картинке.

Я вот потрогал, -NN тысяч нерублей.

Радует, что я стал опытнее не за свой счёт, а за счёт компании, но мне кажется, что некоторые сервисы в интернете прямо-таки сделаны из "темных" паттернов.

Тот API, который задал S3, переусложнён и является первый кандидатом на применение принципа YAGNI. А известная мне клиентская библиотеки для этого API усложняет всё ещё больше...


Главное достоинство S3 — это возможность ограниченно делегировать мониторинг свободного места на диске другой команде. Если вы не в облаке и у вас нет второй команды сисадминов — S3 вам не нужен.

Легко: Укладываем все в одну таблицу.

Я не скажу за все БД, но в MSSQL, MySQL и PostgreSQL блобы размером 10мб будет храниться в отдельных страницах. А если правильные типы подобрать, то оверхед от блобов будет не больше одного указателя в основных страницах таблицы. И почему-то я уверен что в Oracle будет также.

Самое главное качество хорошего разработчика - не считать себя умнее других. Если вдруг мой код, который работает с БД оказывается медленным, то я думаю что это я что-то не знаю или не так сделал, а не база плохо работает.

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

Конечно они её ускорят, но совершенно не по тем причинам, по которым они думают что ускорят.

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

Очевидно это зависит от движка базы. Те которыеы с миллионами часов вложенными в них например будут их иметь, а какой-нибудь SQlite или какая-нибудь очередная революционная рейвендиби может не иметь.


Думаю люди среагировали бы куда мене бурно, если бы эта инфорация была в первом комменте вида "а вы знаели что бд X,Y,Z автоматически это оптимизируют и вам не нужно ничего делать? Вот ссылка на документацию (линк)". Для меня например это была новая информация, спасибо за нее. Впрочем, как я ниже говорил могут быть и другие причины для подобного разделения. В свою очередь считаю что одно из качетв хорошего разработчика — отсутствие категоричности в высказываниях/суждениях.

Очевидно это зависит от движка базы. 

Конечно зависит. Учитывая что движок реляционной БД пилят люди разбирающиеся в теме гораздо лучше меня, то я буду предполагать что там эта проблема решена. А если что-то не получается, то это я не делаю что-то не так.

 какой-нибудь SQlite

Вы удивитесь https://www.sqlite.org/fasterthanfs.html

Но вообще для встраиваемой базы хранить картинки - непонятная затея. У нее варианты использования сильно отличаются от серверов баз данных.

очередная революционная рейвендиби может не иметь

Это вы уже пытаетесь юлить, чтобы ваши утруждения не выглядели столь неубедительно. Но очевидно речь идет о взрослых СУБД, которых в нашей реальности существует 4-5 штук.

Думаю люди среагировали бы куда мене бурно, если бы эта инфорация была в первом комменте вида "а вы знаели что бд X,Y,Z автоматически это оптимизируют и вам не нужно ничего делать?

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

В какой-то момент программист, увидев что программа работает медленно, посчитал что это не его вина, а вина БД, а он такой умный сможет решить. И, к сожалению, решил, что увеличило уверенность в себе.

Закончу тем, с чего начал коммент, на который вы отвечали - хороший программист не считает себя умнее других.

Если вдруг мой код, который работает с БД оказывается медленным, то я думаю что это я что-то не знаю или не так сделал, а не база плохо работает

в основном да. Но иногда бывают нюансы. у меня был случай, когда я в оракле (по-моему 11g) в существующую таблицу решил добавить поле xmltype. Что могло пойти не так? На первый взгляд ничего, тем более что я даже не собирался по этому полю ничего искать. Мне нужно было просто хранить xml. Но внезапно у меня стали тормозить простые запросы вида "select * from my_table". Т.е. Даже без условий (и по-моему, даже с незаполненным этим полем, но это не точно).

Я полдня убил, пытаясь понять кто дурак. В конце концов плюнул и сделал поле обычным "clob".

select *

Это вы уже сделали неправильно.

Это просто пример. За давностью лет подробностей не помню, но по-моему тормозить начинало просто на ровном месте, когда поле xmltype попадало в выборку.

p.s. По поводу неправильности select * я бы не был столь категоричен. Иногда это бывает необходимо.

Картинки в БД - это уже антипаттерн.

ну замени картинки на , поле "комментарий" на 100 килобайт. Суть не меняется, фактически партицирование горизонтальное. Имеет смысл. Иногда можно юзать колумнсторе индексы или ообще всю таблицу колумнстор, если БД позволяет. Но не всегда.

Вроде текстовые поля переменного размера РСУБД и так отдельно хранят. Хотя может от реализации зависит.

В разных базах разные ограничения на типы и размеры, но в целом да

Текстовые поля можно хранить в отдельных страницах и не поднимать при каждом запросе.

но отдельный тэйблспейс им не выдать никак, и не читать их при чтении таблицы тоже не получстся, если это не колумнстор

Вы о какой СУБД? В mssql типы text и image хранятся в отдельных страницах всегда. Не поднимаются если не указаны в select. типы varchar и varbinary сохраняются в отдельных страницах при размере более 2000 байт.

В postgres то же самое происходит типами text и bytea по умолчанию, можно настроить чтобы они всегда сохранялись в TOAST (отдельных страницах)

Я уверен что и в других БД есть подобные механизмы.

С точки зрения клиента БД надо только в select не указывать поле, чтобы картинки даже с диска не считывались.

Речь же выше о тейблспейсе. Вы никак одну таблицу не разложите на разные тейблспейсы. А две в 1-к-1 легко.

Что такое тейблспейс? Зачем на них раскладывать таблицы?

табличные пространства. Например, если есть необходимость раскидать данные А на один физический носитель, а данные Б — на другой, но это довольно специфические случаи, не в каждом круде такая необходимость возникает

Вполне нормальные случаи, когда у нас нижележащий уровень отдает диски по 2 (5, 10, ваш вариант подчеркнуть) ТБ максимум (в силу их физических ограничений). И тут уже начинаются либо всяческие ужимки, либо объединение дисков на уровне ОС (с непредсказуемыми, иногда, последствиями).

ну да, такое вполне может быть и там проще сразу настроить тэйблспейсы на разные диски и партиционирование для самых крупных таблиц, но такой кейс на несколько хранилищ на сервере с базой я не встречал ни разу — обычно были VPS с одим диском, либо сервера с небольшим системным диском и хранилищем большой емкости, на которое основной тэйблспейс и мапится, понятно что если дисковое пространство забьется придется что-то с этим делать — либо масштабировать (но в случае с облачными серверами это в принципе делается просто средствами ОС и при помощи одной крутилки и 3,5 команд) либо добавлять хранилище и мапить на него новый тэйблспейс с настройкой партиционирования

обычно были VPS с одим диском

я однажды упёрся в лимит дисков на VMWare vCenter. Сказали, увы, столько дисков подключить нельзя (к одной виртуалке)

Каждый из них был, ЕМНИП, по 2 ТБ.

ОПСОС, биллинг.

Одну можно партицировать

Партиции во многих базах не совсем корректно работаю, если их на разные диски раскидать.
Вполне нормально реже используемые данные кинуть на hdd.

Бывают разные картинки, разные БД, разные требования.. ИМХО что-нибудь типа скана паспортов весьма разумно сделать в табличке в БД, да еще и зашифровать, а не выкладывать на какую-то файллопомойку, и связь там скорее всего сделают сначала 1к1 к табличке Users (но потом вдруг внезапно окажется что человек может менять паспорта :) ). А вот котиков в телеграмме в БД наверно класть не очень...

Подскажите, пожалуйста, а какие в данном случае паттерны существуют? (вопрос без подтекста, если что, искренне интересно узнать новое)

  1. файлы хранить в даталейке типа S3, в бд можно ссылки на них

  2. таки хранить файлы в базе, часто это ок, как в коменте выше

UFO just landed and posted this here

S3 по мне тоже в топку - это зависит от использования, но деньги тратить за каждый запрос??

S3 давно уже существует в отрыве от AWS, в том числе on-premise. Куча систем хранения умеет его предоставлять.

Только там есть ещё одна проблема. Orphaned objects ... Когда вумники накидали туда всякого, а куда и что забыли. А так, да on-premises просто как грязи.

Это проблема любого объектного хранилища -- хоть ФС, хоть S3.

Файл забыть удалить в ФС ничуть не сложнее, чем объект в S3.

Только в БД хранить файлы может от этого спасти, но, как верно отметили выше, это может создать проблемы производительности и удобства.

S3 это уже много лет не только Amazon.

Во-первых, есть провайдеры, где внутренние запросы к S3 бесплатны внутри локалки (соответственно, простейший nginx проксирующий, либо прокси на уровне приложения делают весь трафик бесплатным) и платишь только за объем.

Во-вторых, есть self-hosted OpenSource решения. Например, minio.

Но при этом S3 масштабируется удобнее, чем простые файлы.

У minio достаточно давно GNU AGPL v3, что существенно ограничивает self-hosted применение.

Во-вторых, есть self-hosted OpenSource решения. Например, minio

Зачем это надо, если развитые способы работы с файлами есть ещё со времён бейсика.

Но при этом S3 масштабируется удобнее, чем простые файлы

С чего это вдруг? Простые файлы прекрасно масштабируются. GlasterFS и Ceph не вчера придумали.

При этом они ещё стандарты POSIX поддерживают. А поддерживает ли S3 стандарты POSIX? Можно ли ,например, залочить файл на S3? Можно ли сделать атомное переименование?

S3 != AWS S3. Есть куча совместимых по API решений. По факту это как раз готовый компонент который управляет файлами как нужно. При этом дает тебе готовый REST API для работы с файлами

С чего это тупость? Опять религиозные рассуждения?

Давайте уж по-инженерному. Мы же технические специалисты, не так ли?

Чем, собственно, БД хуже чем файловая система? Вы прям действительно считаете, что инженеры PostgresSQL не предусмотрели эффективного доступа и хранения Блобов?

Бакап не затрудняется, а облегчается. Ибо вместо бакапов по отдельности мы бакапим один раз с гарантированной консистентностью.

Суммарный размер бэкапа БД и картинок не должен сильно различаться в обоих случаях (в теории).

ценность картинок как правило меньше чем ценность данных

С чего это вдруг??

файлы хранить в даталейке типа S3

S3 - чрезмерное и неоправданное усложнение (в большинстве случаев).

Я, честно говоря, не понимаю в чём преимущество S3 перед обычными файлами, а проблем - выше крыши.

Конечно можно попробовать подмонтировать S3 на файловую систему для упрощения доступа. Но непонятно как хорошо будет работать. Скорее всего и медленнее и менее надёжно.

Суммарный размер бэкапа БД и картинок не должен сильно различаться в обоих случаях

У PG два вида блобов. По умолчанию унитри PG хранит блоб в текстовых полях и делает глобальную экранизацию символов. Поэтому размер там растет. И ещё минус - эти данные не стремятся. Стриминг эмулируется (выкачивается весь блок в память и после стримится).

Зато просто обеспечивается backup/restore через те же текстовые форматы, это да.

s3 я привел как пример потому что у меня и база в облаке. А если база в облаке, то файлы с картинками 100% будут там же.

Но хранить в базе тоже можно, главное просто знать что и зачем мы делаем.

А хранить картинки на диске не позволяет религия?

Ребят. Вы же программисты. Абстрагируйтесь. Картинки - просто пример. Не нужно здесь решать проблему абстрактного примера не зная всех обстоятельств. Возможно, с учётом всех ограничений, кому-то понадобилось хранить именно в таблице.

Да, давайте вместо стейтлесс микросервисов к будем каждому монтировать общий диск, который будет отваливаться, а когда надоест, добавим в систему s3, чтобы картинки хранить. Если хранение картинок не является основным назначением системы и не создает проблем с репликацией и резервированием БД из-за большого объема данных, то нет никакой проблемы сохранить немного изображений в БД.

Т.е. у вас БД - это что-то такое эдакое, витающее в воздухе, а не хранящееся на том же диске? Какая интересная у вас БД

В бд уже "изкаропки" есть транзакционность, поддержка целостности, разделение прав, флэшбэки и репликация, иногда шифрование, настроено резервное копирование.

Если подобных данных (назовём их "картинки") немного и для работы с ними требуются вышеперечисленные фишки, которые уже есть в бд, то - почему нет?

Вы всерьез не видите разницу между кластером из БД, которые пользуются своими своими жесткими дисками в экслюзивном режиме и NFS, которую примонтируют к себе пара десятков микровервисов?

Впрочем, если у вас монолит, который запускается на голой виртуалке, то да, проще прямо в /opt/kartinki сохранять, конечно

Жила была одна база, с 2 организациями и приложенными файлами к документам внутри (точнее файлы таки были на диске). Решили ее разделить по организациям. Сделали копию и получили 2 одинаковых базы. Потом в одной копии удалили документы по одной организации, в другой — по другой. Угадайте, что осталось на диске с приложенными файлами.

Тут возможны варианты от "ничего" до "всё", в зависимости от радиуса кривизны рук))

В данном случае оказалось ничего. Так как копия БД это же копия БД, а не копия еще и связанных файлов с пересчетом ссылок на них. В общем как правильно ниже пишут, копирование превращается в пляски с тем, что движок БД умеет отродясь (транзакционность, целостность, бэкап (в том числе инкрементальный) и тд )+ холивар про 1-1, так как хранение объектов вне БД это именно оно и есть. Одной ссылке в БД соответствует одна картинка на внешнем носителе. А если можно сослаться на одну картинку можно из разных мест без промежуточной таблицы в БД — то привет счетчик ссылок для определения, когда же ее с диска удалять.

UFO just landed and posted this here

Эм, база данных это масштабируемое решение, которое легко кластеризируется, позволяет сделать доступ к одному "полю" от имени множества клинетов одновременно, делать множественные выборки и быстро искать. А файлики, если их пару миллионов - уже нужно писать велосипед для раскладывания по папкам и париться про Name convention

плюс сервисы часто живут в контейнерах, а в этом случае вообще зависимость должна быть минимальна

UFO just landed and posted this here

Ну, не так уж БД и легко кластеризуется, если от кластера требовать суммирование размера хранилища, а не быстродействия.


Но соглашусь что файлы это делают ещё хуже.

Это у вас ФС – это что-то витающее в воздухе, а не БД.

ФС вообще то тоже БД, и функциональность/гарантии этой БД далеко не всегда подходят.

не создает проблем с репликацией и резервированием БД из-за большого объема данных

Тут или картинки нам жизненно важны для бизнеса (и тогда пофиг на сложности с репликацией, она и так, и так будет), или их можно достаточно безболезненно потерять - и тогда пожалуйста храни на shared storage. Хоть файлами, главное чтобы inode не кончились. Потому что добавление +1 решения для хранения картинок потребует железа для этого решения, репликации для этого решения, поддержки этого решения, мониторингу этого решения, и ещё вероятно распределённых транзакций. А, ну ещё чтобы бэкапы не разъехались, когда база у нас в статусе на пять часов утра вчера, а картинки в статусе на cемь тоже вчера, т.е. там есть то, о чем БД не в курсе. Наоборот - веселее...

пофиг на сложности с репликацией, она и так, и так будет

Используйте например CouchDB, и никаких сложностей с репликацией картинок не будет.

бэкапить, переносить, поддерживать целостность вручную?

У меня такой же вопрос возник...

UFO just landed and posted this here

переделать 1-to-1 на 1-to-N вообще нету никаких проблем. Это когда наоборот - проблемы

Хм. По-моему 1к1 это частный случай 1кN. Т.е. В базе с отношениями 1кN запросто могут встречаться случаи 1к1 и это нормально и ничего не ломает. А вот если было 1к1, а стало 1кN, то внезапно многие запросы начнут возвращать больше строк чем ожидалось, что может стать неожиданностью.

Если таблица с реальными картинками ещё и реализация on-disk стораджа (1 запись = 1 файл, без кэширования в памяти, кроме того, что сам файловый массив даёт - FileTables в MSSQL, в других субд вроде как тоже были аналоги, либо бэкэндом реализуемо), я-бы даже похлопал. А то чудесатые многотеррабайтные гидры вроде Sharepoint и прочих традиционных usecase для MongoDB не перестают удивлять, каждый раз когда на них натыкаешься.

Вы в курсе что это все база данных умеет делать под капотом? Вам достаточно правильную проекцию написать.

Написание проекции нарушит правило про круды

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


Это это нарушает правило "всё делать через CRUD ничего не вдумывая", ведь в CRUD только одна операция Read.

Если вы не делаете проекции, то вас на пушечный выстрел нельзя подпускать к базе. Какие вы там правила придумали сами себе - не важно.

Может, и не вы придумали. Но в те древние времена, когда БД были в новинку, никакого такого правила не было — по крайней мере, такого правила не было в той старой книге Мартин Дж. "Организация баз данных в вычислительных системах"(1980 г. издания перевода), по которой я БД изучал. Наоборот, преимуществом БД над плоским файлом как раз считалось то, что приложения могут без проблем работать только с тем набором полей БД(сущностью), который им интересен, полностью игнорируя наличие других полей — которые для других приложений (да-да, в те времена с одной БД могли работать несколько приложений).
Но любители все упростить, чтобы лишний раз не думать (а среди веб-программистов я таких видел много, по крайней мере, лет 20 назад) конечно, могли придумать себе такое правило: "одна сущность = одна таблица" и наоборот. Только не думаю, что это — хорошее правило для любых случаев.

Я тоже так не думаю, а вот автор об этом в разделе "Системность" так и пишет:


Любой процесс вы должны привести к КРУД и запихнуть его в стандартную цепочку классов. РЕАД, АПДЕЙТ, ДЕЛЕТЕ. Любой бизнес-процесс вы обязаны декомпозировать на серию логичных КРУД-операций, будь-то заказ пиццы или перечисление денег.

То есть должен быть строго 1 Read, а два Read это уже антикруд?

Это уже даблкруд. Если Read три, то соответственно триплкруд

В контексте обсуждаемой статьи — да. Иначе вы сеньором станете!

В общем-то колумнарные БД достаточно давно изобрели, кое-где это делается на уровне сервера, и часто нет большой нужды разбивать на таблицы.

Это как, 'SELECT * ' на таблице с картинками?
Я к тому, что все данные по картинке могут находиться в одной таблице, просто SELECT делайте для тех колонок, которые нужны в контексте задачи.

ну если база не колумпнсторная, вычитает ваш select всю таблицу целиком с картинками, не важно сколько колонок вы выбрали

Мы же про 1:1 relation?
Про какую "колумпнсторную" базу речь?

А почему картинки в базе? Ещё скажите что фронту из в base64 отдаете, как и храните.

Таблицы на 30 + полей не так то удобно таскать как кажется.

Процессинговая транзакция на 300+ полей застенчиво улыбается

Бывает и хуже. У нас как то один подобный умелец исходя из подобной логики решил что 600+ полей это нормально. Прошло уже года 3, до сих пор распиливают.

У меня таблица пользователей и вся инфа в них ФИО , активный пользователь и системные поля дата создания итд

И меня бесит когда добавят туда ещё 100500 полей

Почему не создать отдельные таблицы адрес и телефон
Как пример естественно

И создать связь один к одному?

Зачем делать и таблицы склад в 100500 полей, и потом думай,что за что отвечает

И далее такие связи можно легко перевести на один ко многим,в отличии когдв все в одной таблице

Как минимум в два раза больше строк кода? Еще Повышенная сложность.

Для вас связка из двух таблиц это повышенная сложность?

По-моему вопрос на собеседование про join является базовым

Про строк больше в два раза,не понял ничего ((

А если машину поцарапать маленьким гвоздиком, то можно? Только большим нельзя? И ксати да. Там ленивая инициализация или нет, связывается одинаковыми ID, или вторичными ключами, как настроить ленивую, там по умолчанию жадная, НУ и просто я минут 5 матерюсь, когда oneToOne вижу. А потом запросики писать, каждый раз джойны дописывать. Inheritance гуглить-писать. Все что усложняет, пусть хоть на миллисекунду понимания - все плохо.

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

Если у юзера есть 150 уникальных полей, рост, вес, диаметр ноги и так далее, то да в таблице будет 150 полей. Может после 200 полей я подумаю про 1к1.

Это вопрос про дизайн и про размер базы (число юзеров). Если юзеров 100 и будет 1000 ближе к тепловой смерти вселенной, то нормализация не нужна. Хотя я не понимаю такой аллергии по отношению к нормальным таблицам. Пример кстати говорящий - если у юзера 150 полей, то, по вашей же логике, это проектировал синьор.

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

Повторюсь, если у вас в базе 10 связанных таблиц, и как программисту вам приходится делать селект с 33 иннер-аутер-лефт-райт джойнами, обмазываясь констрейнтами и развивая себе абстрактное мышление до визуального представления 11-мерного пространства, синьор в данном случае - это вы. Миддл бы посоветовался с DBA и сделал встроенную функцию. Причем запрос для нее и профайлинг вполне сделает DBA на радостях, что хоть кто-то решил сделать решение не через зад. И да, под каждый запрос - функция. Нужно новое представление в программе - новая функция в базе. Это просто.

p.s. я не DBA и никогда им не был.

Это если вы настолько крупный (в смысле организация), что у вас есть отдельный DBA.

А если его нету, то мидл вместо запроса с лефт-райт-джойнами накодит find()->joinWith()->all() через ORM и будет в коде приложения разгребать то, что получилось.

Нужно новое представление в программе - новая функция в базе. Это просто.

Эх, да. А потом Oracle меняет политику лицензирования, вы очень хотите вот прям завтра съехать на PostgreSQL, но что-то крепко держит под водой за яй... ногу.

Переводил несколько проектов с Оракл на PG. И да, там кол-во функций было от 300 до 5к. Успешно перевели. По времени от 1 месяца до года занимали трансформации (но там нюансы были с тем, что не должно было быть простоя при переключении).

сотнями миллионов

подозреваю в таком случае врядли там 1-к-1

Может после 200 полей я подумаю про 1к1.

Непорядок, что это еще за magic number 200? Надо идти до конца

Чтобы таблица не разрасталась, надо отобрать у дурака фантики.

class User < ApplicationRecord
  ...
  belongs_to :address, optional: true # указали связь, что у пользователя может быть адрес
  delegate :address1, :address2, :city, :state, :zip, to: :address # делегировали поля
end

# пример использования

user = User.last
user.address.city == user.city # это одно и тоже значение из одного и того же поля в БД

Выше пример как это делается в RoR. Не совсем понятно, вот эта одна дополнительная строчка это именно то, что вы имели ввиду под "минимум в два раза больше строк кода"?

Если честно, вы указали связь, что у адреса может быть пользователь.

Ну и в запросах к базе у вас добавится строчка (а ещё интересней будет с выборкой нескольких пользователей с адресами) ;)

Хотя, это не так сложно, и я мнение автора не разделяю. В некоторых случаях связи 1:1 нужны.

Да, вы правы, спасибо что заметили, я действительно неправильно прокомментировал код.

Нет, там не обязательно появится дополнительный запрос, для таких вещей мы используем скоуп с джойном.

Ну тогда джойны и будут теми самыми дополнительными строками кода. Хотя, не понятно, почему их станет минимум в два раза больше. Возможно, особенности java.

Почему не создать отдельные таблицы адрес и телефон

Потому-что потом начнется: "А чего это мы адрес храним одной строкой? Нужна гранулярность! Разбиваем адрес на: Индекс, Регион, Район, Город, населенный пункт + Улица, дом+корпус+квартира. Ещё геокоординаты прикрутим. Зачем? А вот затем! Вдруг нам нужно будет быстренько всех сотрудников объехать, или понять кто с кем близко живёт! Вы мне потом спасибо скажете!"

Как минимум два сотрудника могут жить по одному и тому же адресу.
Если сотрудников хотя бы 1000 и надо найти тех кто живет в одном населенном пункте секс с тентаклями вам обеспечен если адрес одной строкой и вводили несколько разных кадровиков.

и номеров телефона у человека может быть сильно больше одного

И на одном номере телефона неожиданно могут оказаться пять человек (у которых он основной контактный) и две конторы, у которых он рабочий. Вполне себе кейс в кредитном антифроде

Как минимум полезно, если вы что-то клиентам доставляете, и у вас не флетрейт по всему миру.

Может CREATE VIEW решит вашу проблему — и вы эти стопитсот полей никогда и не увидите (ну, разве что когда специально посмотреть)?
А если нужно в таблице ещё и править/добавлять, то и для этого есть средства: например, в MS SQL — Updatable Views, а лично я в стародавние времена в Interbase, в который всяких вкусностей мало завезли, триггеры для этого использовал

А что, юзер и его адрес/телефон — это прямо один-к-одному? Там чистый один-ко-многим. Почти всегда. Ибо адресов мы можем захотеть хранить несколько (прописка/проживание), они могут меняться несколько чаще, чем основная инфа по юзеру (переезды) и тп.

UFO just landed and posted this here

Еще не встречал систем, где адрес бы определсялся независимо. Даже несмотря на КЛАДР и подобные системы ключем адреса обычно бывает "чей адрес", "какой тип адреса", а не собственно адрес. Таким образом для двоих в одной квартире будет 2 адреса в таблице адресов.

UFO just landed and posted this here

Ну, для целей переписи, наверное, нужно :)

по твоей логике и manyToOne не нужен, ну пиши тоже все в 1 таблицу. Понапридумывали каких-то там нормальзаций бд, да еще и по несколько видов. Бред какой-то

Если у тебя две таблицы, связанные один к одному - так зачем тебе две таблицы? Сложи все данные в одну.

Простая задача пользователи и роли. У пользователя может быть только 1 роль, роли можно добавлять динамически.

Это не oneToOne, у ролей может быть много пользователей в общем случае

Ну например пользователь и тариф. У одного пользователя всегда может быть активен только один тариф.

А один тариф может быть у многих пользователей

Ай-яй-яй, и правда, мой косяк.

И это тоже не один к одному - на одном тарифе может быть несколько пользователей.

Ладно, у физлица есть ФИО, телефон и паспорт. При этом телефон доступен всем, а паспорт — только людям с доступом к персднным. В окружающих меня системах настроить ограничения доступа по таблицам сильно проще, чем по реквизитам. А закрывать видимость через интерфейс — злостный антипатерн.

Многие БД умеют разделять права ролей не только "построчно", но и "поколоночно". Стучишься в номер паспорта и получаешь NULL. Или что-то по маске.

многие, но не все.

Повод задуматься о правильности архитектурного решения (понимаю, что никто только по этой причине мигрировать не будет) :)

Да тут можно много примеров на самом деле привести где это реально нужно, автор скорее имеет ввиду случай где создаются manyToMany на пустом месте...

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

Это не oneToOne, это словарь/справочник/реестр. Отдельный паттерн проектирования бд, когда перечислимые общие данные выносятся в отдельную таблицу. По факту это многие-к-одному.

С технической точки зрения документ-родитель связан со своим наследником отношением 1 к 1

Справочник ролей и справочник пользователей - отдельные таблицы. У каждой из этих сущностей свои атрибуты.

А если у меня три и более таблиц? Есть центральная таблица объектов, но в зависимости от типа объекта ему сопоставляется дополнительный набор параметров в одной из таблиц.

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

Была такая идея, но потом на центральную таблицу через многие к одному стали ссылаться совсем другие таблицы, целая иерархия, и разделение по типам породило бы много дублирования и запросов с UNION, когда пользователи в некоторых случаях работают со списками объектов без учёта типа.

Да, в вашем случае получается плюсов у варианта с центральной таблицей больше.

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

Так это тоже не 1-к-1.

Подскажите, а почему решили так разить, а не таких три таблицы: Таблица объектов. Таблица параметров. Таблица с соответствиями ИД объекта и ИД параметра?
и уже на уровне бизнес-логики определять какому типу объекта могут быть сопоставлены какие параметры, модель данных готова ко всем вариантам, в том числе появляется гибкость для кейсов "в некоторых случаях работают со списками объектов без учёта типа".

это называется EAV, может быть очеень серьезным антипаттерном при проектировании БД.

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

О нет. Это как раз строгий антипаттерн. Если вы задумали такое, то надо подумать еще раз. Возможно в сторону не sql БД.

Нужно было поддерживать неограниченное количество атрибутов, не известное заранее, а так же быть консистентными с другими таблицами, например пользователи.

Джойны в транзакциях это всё про не про nosql. Другого решения этой проблемы пока не придумали. Цена поддержки этого очень высока, с этим очень неудобно работать, и приходится следить за многим прямо в коде. Миграции базы это вообще ужас.

Но свою задачу решает. Сотни миллионов атрибутов MySQL ворочал достаточно шустро.

Нужно было поддерживать неограниченное количество атрибутов, не известное заранее, а так же быть консистентными с другими таблицами, например пользователи.

Это не звучит как задача от бизнеса.

Тут надо откатываться еще дальше и идти говорить с бизнесом о том что же он на самом деле хочет. Как правило оказывается что их неограниченное количество атрибутов оказывается равно или меньше 42.

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

Вы серьезно? Это без проблем типизируемая информация которая хорошо ложится в примерно десяток (из них 5-6 это адрес) разных колонок. И рядом текстовая колонка Другое. Экзотики на самом деле не так много.

Не надо оверинжинирить на пустом месте. Хочет клиент указать дорогу на оленях? Да не проблема, вот текстовое поле. Если у нас не система учета оленеводов, то ради него одного делать ничего особо не стоит.

Да в три поля это решается — контрагент, тип контактной информации, собстственно контактная информация. То есть EAV в чистом виде

И даже в одно решается. Если туда джейсон положить. Вопрос что с этим дальше делать. И зачем вообще все это.

Это система которая давно в проде, и атрибутов сильно больше чем 42. Кстати самому бизнесу очень зашло, так как когда надо добавить подтип объекта со своими атрибутами это можно сделать без изменения базы. В комплекте с тем, что можно было написать свой плагин и загрузить его, новые фичи для бизнеса можно сделать реально за часы и даже не нужно перезагружать сервер.

У вас альтеры в БД платные? Я не понимаю этого страха изменения схемы БД. Это абсолютно нормальная операция. Она делается на проде без даунтаймов.

Я правильно понял, что значения параметров в предложенном вами подходе хранятся в одной колонке, схоже с Entity-Attribute-Value?

Я стремился упростить применение средств базы для обеспечения целостности данных, их согласованности в помощь тем проверкам, которые есть на уровне приложения (всякие not null, unique(param1, param3, param17), триггеры) для каких-то наиболее критичных штук + ускорение поиска и тех же проверок уникальности за счёт использования индексов. Приятный бонус - читабельность таблиц.

Поскольку добавление нового типа объекта происходит редко, может раз в год или два (и предполагает новую функциональность), всё это не превращается в огромные switch-case по типам.

Это вы с геопространственными данными не работали, когда внезапно некоторые из геометрий могут оказаться размером в гигабайт, вместо ожидаемых нескольких сотен байтов (например, контур города с вырезанными в нем контурами зданий вместо контура одного здания) и вся ваша база данных окажется просто неработоспособной. И что тогда - будете заводить по отдельной таблице для каждой записи? :)

так тут тем более не вариант оставлять их в основной табалице

Для начала, держать в отдельной таблице. Если такие записи появляются и они необходимы - то по всем уровням зумирования создавать отдельные таблицы с упрощенными геометриями (триггеры помогут). Если и этого мало, то разделять большие геометрии на части, так что получится набор таблиц, в каждой из которых от нуля до множества записей для основной таблицы. Когда и этого мало, то еще и разные типы пространственных индексов использовать, в зависимости от запросов. Ну а дальше все это надо на разные партиции по разным дискам разнести, потом можно и по разным хостам… А кто-то всегда будет громко клясться, что разделять на две таблички это избыточно :)

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

после select вместо * можно перечень только нужных полей сделать

Читать Select 30_названий_полей From table where условие_по_полям_1,2,5,9,17,28 тоже такое себе развлечение.

эти 30 полей будут где-то фигурировать. В энтити, в dto, в скрипте миграции. Без разницы. Вам все равно придется их через запятую перечислить где-то. Я предлагаю в селекте и дто, Вы предлагает в скрипте миграции и энтити.

База все равно грузит записи в память целиком

если таблица не columnstore, с диска всё равно поднимутся все поля

окей, есть ещё два частных случая (хотя на счёт row overflow у меня нет твёрдой уверенности, что это справедливо для всех случаев), когда при использовании списка столбцов в память поднимутся не все поля

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

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

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

прикол в том, что вы сами выдумали тейк про каждый столбец-таблица и сами его осудили

индексы на каждый чих тоже не бесплатное удовольствие

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

В том же общем случае нет смысла делать 1-к-1, так как изменение быстродействия вы не заметите.

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

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

что касается заметности влияния индексов, испытываю жгучее желание тоже броситься в частности и потыкать в очевидно медленные вещи типа gin в pg, но в общем случае, при разумном подходе к индексированию - спорить не буду

ну и я видел красивую таблицу со 170 столбцами и 90 индексами (не в хранилище) - это относительно того, что я увижу, или не увижу

А ещё есть пример, когда мы делаем модульное приложение и модули к нему пишутся отдельно (может быть даже другой командой).
И вот тогда сущность-из-модуля может иметь 1-to-1 связь с сущностью из основного приложения.

Так делают в Django, когда надо добавить к пользователю, который идёт из коробки, новые поля.

А если у меня есть таблица User где есть вся информация о пользователе и таблица UserAuthenticationData с данными для авторизации - их тоже мешать?

А если у меня есть разные по природе сущности - например машина и двигатель, мне их тоже в одну таблицу пихать?

А если я не люблю таблицы по 50 полей?

Возможно есть разница между 1-to-1 и 1-to-0..1

Первое бесполезно чуть менее чем полностью, второе имеет право на жизнь для security, но мало выигрывает просто у одной таблицы.

И когда мы будем работать с одной сущностью предметной области, мы будем блокировать другую, потому что на уровне БД храним это в одной таблице?

Звучит как ерунда какая то.

А с чего вы взяли что это разные сущности? Разделив данные пользователя и логин вы что выиграете?

Что значит "блокировать"? Вы обновляете поля a,b,c в одном методе, и поля x,y,z в другом.

С того что разделение на бизнес сущности - вопрос договорённостей. Если команда\бизнес\архитектор решили, что пользователь и данные для авторизации - это разные сущности, значит так и есть.

Блокировать - rowlock хотя бы.

А если они ошиблись? Массовые заблуждения не такая уж редкость как кажется.

rowlock хотя бы

И в чем проблема?

А зачем лочиться, если можно не лочиться?

И как вы собрались не лочиться? Запрос у вас полюбому повесит rowlock как минимум на одну строку.

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

Я за первый вариант.

Подозреваю что проблема может быть в том, что наложение lock занимает ненулевое время, например. А у нас OLTP, бери больше, кидай дальше, отдыхай, пока летит.

У вас любой запрос лочит строки. Лок двух срок в двух таблицах съест чуть больше ресурсов, чем лок одной строки в одной таблице.

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

Скажем, вам нужно апдейтить баланс, который

id | balance

А основную таблицу

id | passport | reg_address | real_address | contact_phone

вы даже не трогаете.

проблема в том, что это искусственный пример. А реалистичных примеров где уместно 1-к-1 хрен найдешь.

Безусловно, искусственный.

А в реальной жизни у меня табличка "пользователи" содержит 70 полей, одно из которых - json :) еще на сотню+ полей. Весит несколько терабайт.

И да, там есть (точнее, с недавних пор - было) поле balance.

(и да, потребовалось два архитектора, несколько лидов, десяток программистов и два замученных ДБАшника, чтобы утащить это в отдельную таблицу).

Их полно.

Например, заказ с доставкой. Соотносятся они 1к1, заказ в одной табличке - детали доставки в другой. Позвонил клиент, перенёс доставку - обновляете только вторую таблицу.

Контактные данные клиента и клиент. Актуализация контактных данных - это специфический сценарий с точки зрения бизнеса, обновляем только нужные таблички. Никак не трогаем ФИО или ещё-что, что хранится в основной таблице.

Какой-нибудь download manager может держать основную сущность скачивания в одной табличке, а метаинформацию вида скачано\размер - в другой. Добавление ссылки в систему создает запись только в первой таблице, начало скачивания - во второй.

Вопрос же бизнес сценариев работы с этой информацией.

Имхо, есть случаи, когда one-to-one вполне себе уместно использовать. Условно, есть список файлов. Каждый файл можно опубликовать отдельным постом. У поста могут быть теги, превью, комментарии и т. д. Если держать всё в одной таблице, то получим странного виду таблицу для файлов со множеством nullable полей, которые нужно было бы каждый раз исключать при работе с ORM. А если бы понадобился поиск по постам, то пришлось бы в каждый такой запрос добавлять not is_posted. Также, после удаления поста(но не удаления файла), необходимо было бы подчищать таблицы связей(post_tags, post_comments и т.д.).

Пример комплексной таблицы file
CREATE TABLE file (
id INT PRIMARY KEY,
path VARCHAR(256) NOT NULL,
is_posted BOOL NOT NULL DEFAULT false,
thumbnail VARCHAR(256),
posted_by int REFERENCES(user)
)

Конкретно в этом случае, не лучше ли было бы иметь таблицу с постами, которая бы имела ссылку на запись в таблице с файлами?

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

Часто встречаются примеры, когда есть таблица на 100 колонок, большинство из которых null и большая часть из которых нужна для каких процессов, про которые никто не знает. Вставить туда что-то уже подвиг.

Доступы, например. Если у меня есть скажем онлайн-магаз или есть какие-то платные действия в приложении - часть информации о платежах вполне может лежать отдельно чисто для разделения доступа к ней.

Это называется "нормализация/денормализация".

Оба подхода имеют свои применения, но с оговоркой: делая денормализованную таблицу следует ОЧЕНЬ хорошо понимать, зачем это делается. При нормализованных таблицах можно понимать не очень хорошо, цена ошибки околонулевая (дропнуть-добавить очень дешево, с учетом изменения логики).

Сентимент про "сложные запросы" и прочее проистекает от того, что по какой-то неведомой причине программисты пихают бизнес-логику в приложение, а DBA-и им это позволяют. Хотя интерфейсом общения с БД должны быть встроенные в базу функции.

Гхм. А расскажите, пожалуйста, как сделать так, чтобы "дропнуть-добавить" было дёшево? Или, более формально, как "не больно" менять структуру БД. Естественно, в боевых условиях (в таблице сотня миллионов записей, длинный даунтайм делать нельзя, изменения должны быть автоматизированы, поскольку экземпляр приложения далеко не один).
У меня всегда получалась боль с фоновыми миграциями, кодом, который поддерживает обе структуры БД и прочими костылями.
Не говоря уже о том, что в некоторых СУБД (привет, MySQL и его форки) надо плясать с бубном, чтобы без даунтайма структуру менять.

UFO just landed and posted this here

Я для такого использовал pt-online-schema-change из Percona Toolkit
https://www.percona.com/software/database-tools/percona-toolkit

Смысл - делает таблицу с новой структурой, вешает триггеры на все операции со старой таблицей (чтобы дублировались в новую), потом постепенно перетаскивает записи из старой в новую. Потом старая сносится, новая переименовывается.

Но больно, да.

Дропнуть-добавить в нормализованной базе? Путем удаления-добавления таблицы с зависимостью по foreign key. Бизнес-логика при добавлении таблиц не страдает совсем, при удалении - сначала надо убрать зависимости из бизнес-логики. Где там будет даунтайм - не вполне понятно, если выдержать паузу на распространение изменения/устаревание кешей. Если добавление происходит с каким-то ненулевым объемом данных, то есть вероятность какой-то деградации на время добавления - тут, опять же, поможет DBA - как в качественно-количественной оценке рисков, так и в собственно работе.

Дропнуть-добавить в денормализованной - это вы не у того человека спрашиваете, я за такие решения/дизайны не агитирую :)

Я скорее про процедуру миграции (ну вот например, хотим мы, не знаю, про юзера его предпочитаемый напиток, скажем, хранить). Соответственно, мы хотим добавить поле.
Но банальный ALTER TABLE для добавления поля - залочит таблицу на время добавления. Если она большая (например, это не юзеры, а какие-нить эвенты, входы на сайт или клики по баннерам) - это будет долго. И все прочие запросы к этой таблице будут ждать блокировку. Вот и даунтайм.
В противовес этому, если таблица денормализована (например, есть JSON-поле, в которое пихается всё дополнительное) - делать с ней вообще ничего не надо.
Расплачиваемся скоростью работы с таким полем и вознёй с целостностью, но тут уж куда деваться.

Мы, возможно, по-разному понимаем что такое нормализованная-денормализованная.

Нормализованная - это когда 3НФ и вот это все. Когда в таблице минимум информации, который используется для ВСЕХ записей, а все остальное, что может быть опционально (предпочитаемый напиток из примера выше), выносится в дополнительные таблицы с foreign key (на юзера из примера выше).

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

Повторяю, я _не_ топлю за добавление полей. Я против добавления полей просто потому, что так кому-то проще. И не стал бы его делать (кроме случая, когда изначально кривой дизайн с пропущенными ключевыми ОБЯЗАТЕЛЬНЫМИ ВЕЗДЕ полями, но это уже другая история).

Надеюсь, теперь мы на одной волне.

В случае с опциональными полями - да, тут абсолютно согласен.

Я тут наверное больше прикопался к тезису "дропнуть-добавить таблицу / поле в нормализованной БД несложно". Если нам надо добавить обязательное поле (например, мы теперь в таблице с событиями входа юзеров хотим всегда запоминать, с какого IP юзер зашёл) - нормализация не то чтобы поможет. Ну либо у нас будет хренова гора таблиц вида user_id, yet_another_field с зависимостями 1-к-1 на юзера и сборка всех данных по юзеру из этих таблиц превратится в многоуровневый join.
Хотя согласен, alter-ить таблицу юзеров в таком варианте не надо. Но выглядит как костыль, если честно.

Не было в моем исходном сообщении про "добавить поле" :)

Дропнуть-добавить разве не о том? Если таблицы дропать и добавлять, тогда конечно согласен, с этим проблем нет.

Hive? Но он, правда, не реляционный. И, конечно, не OLTP.

Ну да, делать OLTP на Hive - это что-то совсем странное.

Как вариант, чтобы разделить сущности (или, вернее, scope эти сущностей)?

Смотрите. Вот, к примеру, человек. Антон Дропбазаданко. У меня есть какой-то уникальный айди (предположим, uuid_v4 или, скажем, номер СНИЛСа (но насчет второго это не точно)).

uuid | name | date_of_birth (я-я)

uuid | worker_table_number | grade | title | salary (я-сотрудник)

uuid | size_of_feet | waist_size | height | army_specialization_id (я-военнообязанный)

ну и вишенкой на торте - гораздо удобнее выдавать GRANTs на такую БД для разных потребителей.

Это типичное один-ко-многим. Потому что чаще всего надо хранить историю "меня": сегодня у меня один грейд / зарплата, а завтра другой. И повляется "дата начала" и "дата окончания" действия записи, причем одна из них войдет в PK дочерней таблицы.
А если и в родительской нам нужны даты (имя поменялось) — то многие-ко-многим.

Для истории можно сделать отдельную таблицу.

Всё-таки исторические запросы нужны реже, чем актуальные данные.

у вас в одной табличке 300 миллионов записей, а теперь добавьте колонку к этой табличке без даунтайма, при условии того, что это таблица из которой постоянно читаются и пишутся данные.
И даже не хайлоад - всего то под сотню рпс.
Я вам скажу - это нереально. Буквально неделю назад словил такой кейс.

А дальше ещё интереснее - у вас там могут быть не 1:1, а 1:0...1 и тут вы ещё и в объёме можете выиграть, потому что у вас основная таблица не будет лишнее место занимать.

А если вам ещё и не для всех запросов нужны данные из дополнительной таблицы, то это вообще вам на руку.

Мягко говоря спорный пунктик... И уверен на хайлоаде с хотя бы 1к рпс там добавится своих аргументов

Для MySQL и его друзей есть Percona Toolkit, при помощи которого можно такой финт ушами (без даунтайма чтоб) проворачивать (в комменте выше отписывал). Не серебряная пуля, но может быть полезно.

Пример: реестр документов. Одна таблица - общая шапка. Другие - содержат переменную часть, зависящую от конкретного типа документа

И ещё один банальный пример технического ограничения: мне нужно прикрыть optimistic concurrency только одно поле в сущности. Или даже просто разделить поля с разным паттерном доступа (чтение/запись), чтобы это всё лучше чувствовало себя под нагрузкой.

Как насчёт разделения доступа? У вас таблица с данными о работниках. Там ФИО, рост, любимое блюдо, другая неконфиденциальная инфа, а также паспортные данные, адрес, телефон и прочее, уже чувствительное. Базой пользуются как отдел кадров, так и столовая. Если всё хранить в одной таблице, столовая будет, кроме любимого блюда работника, знать и его паспортные данные. Разделим таблицу на две, во вторую вынесем чувствительную инфу и ключи. Доступ ко второй таблице столовой не дадим. Таблицы по ключам свяжем 1 к 1. Профит. Чем плох такой сценарий?

GRANT SELECT (col1), UPDATE (col1) ON mytable TO miriam_rw;

можно так, джойны будут не нужны

Добавление колонки с nonnull значением по умолчанию в таблицу с миллиардами записей на pg до не помню какой версии вызывало остановку работы на полдня.

Еще раньше в отношениях 1-к-1 в хибернейте была специфическая проблема с невозможностью ленивой инициализации этого поля (можно запроксировать коллекцию, а потом лениво инициализировать ее, добавив элементы, но нельзя проставить null, а потом превратить его в объект)

Для меня 1-1 можно применять только если вам нужно хранить в базе тяжелые картинки и лень прикручивать файлохранилище.

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

Для Вас, вернее ваших задач - не вопрос, но ведь не все только на "картинках" заканчивается.

Простенький пример: таблица клиентов с рисковыми коэфициентами. Для юр.лиц этих коэфициентов 100500, а для физиков - 100. Учитываем, что физиков ЗНАЧИТЕЛЬНО больше.

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

Это про кошку и собаку простенький пример. А что такое рисковый коэф, я слабо представляю.

Ну, про собаку допустим. Есть собаки и есть будки. В каждую будку влазит максимум одна собака, у каждой собаки может быть максимум одна будка. Вы предлагаете эапихать это в одну таблицу СобакоБудка?

А если у человека есть адрес и рост, то вы называете сущность человекоАдресРост?

Нет, сущность будет просто собака, еще странные вопросы будут?

А это и не важно. Ваш оппонент о том, что может быть так: миллион юзеров, из них 999000 - физики, 1000 - юрики. При этом для физика заполняется 10 полей в таблице, для юрика - 100.
Дак вот в таком случае "поля-для-юрика" имеет смысл выпинать в отдельную таблицу и сделать 1-to-1 связь.
Экономия потому что.

тут он видимо просто 2 таблицы сделает, совсем отдельных