Как стать автором
Обновить

Комментарии 47

Самое обидное - это элементарные вещи, второй курс соответвующего направления в ВУЗе... Но к сожалению многие амбициозные самоучки не обращают внимание на такие "мелочи", и получаются эти монстры. Которые вроде работают(с их тчк зрения), но когда берешься за таким спецом дорабатывать проект, то ужасаешься. И таких людей очень много.
Я сейчас как раз вынужден дорабатывать одно приложение, в котором в БД нет nullable-полей, зато в каждой таблице-справочнике есть специальное значение (0, '_НЕ задано'). Иногда мне хочется придушить автора этого безобразия... ;)

А Вам, конечно же, спасибо и плусик. Надеюсь, кого-то эта статья научит не создавать проблем себе и другим :)
кстати по поводу не nullable полей - такое иногда бывает нужно. Как пример - явно сказать что такой-то аттрибут объекта НЕ задан (или был стерт). Если поместить туда нулл, тогда вы не сможете отловить ошибки непередачи данных, когда из вашего бизнес уровня не приходит в бд значение, и бд пишет туда по умолчанию - т.е. null. В такой ситуации нет возможности отличить запись со стертым идентификатором от записи которая пришла "неполной" из бизнес уровня. Конечно есть много практик проверки ранее и прочее, но часто в бд данные поступают из разных источников, в том числе в виде экспорта из каких нить экселей или других бд. В таком случае редко кто делает дополнительные проверки на корректность данных. А вот FK и неразрешенные NULL сразу укажут на проблему. Может сначала стоит разобраться почему такое решение было сделано, а не огульно критиковать автора (хотя если все остальное в том же духе то скорей всего это от тупости а не от тонкого стратегического решения)?
Стоп. Чем отличается nullable-поле с внешним ключом от не-nullable-поля с внешним ключом и специальным значением?! Только тем, что во втором случае запись не вставится, если будет передан NULL. И если запись все-таки создана, то разделить "непередачу" от "умысла" нереально.

Касательно конкретной системы: данные там поступают вполне из одного источника, на ключевых полях есть дефолты на специальное значение. Так что без вариантов :)
Ну нуллэйбл поля с внешним ключем это defected by design - внешний ключ он для этого и предназначен, чтобы ограничить допустимый перечень значений. А т.к. null не равен никакому значению (даже самому себе, нужна явная проверка - Is Null, а не fieldID=null, последнее выражение всегда false) - вы создаете себе проблемы на этапе проектирования, создавая заведомо кривое решение.
Во втором случае не nullable поле при попытке вставить в него нулл ругнется foreign key, и вы сразу увидете что где-то есть ошибка, или в передаче данных или в их форировании.
Дефолт значения я, если честно, не очень люблю, потому как они в большинстве своем позволяют спрятать ошибки от программиста. Если уж так нужен где то дефолт - выставите его на этапе подготовки к продакшену. Просто с дефолтом то же самое - вы не способны отличить -выбрал ли пользователь в списке значение по умолчанию, или он выбрал другое значение, но оно потерялось по дороге и база вписала default значение. В большинстве своем умолчания это путь ленивого программиста ( хотя местами они нужны) - под девизом - "я не хочу проверять, что там мне передали, я введу значение по умолчанию в бд" прячется в итоге очень много ошибок.
нуллэйбл поля с внешним ключем это defected by design
Почему же сразу заведомо кривое? Просто запись "никуда не ссылается", а не "ссылается на что-то, что нужно считать пустотой". Во-первых, это экономит место и время (отсутствие лишних джойнов/условий), во-вторых, просто кажется мне более правильным.
Во втором случае не nullable поле при попытке вставить в него нулл ругнется foreign key, и вы сразу увидете что где-то есть ошибка, или в передаче данных или в их форировании.
Ну, ругнется-то не FK, а NOT NULL CONSTRAINT, но это не принципиально :)
Тот факт, что NULL может появиться при формировании данных — это я еще могу понять. Ошибки валидации бывают. И их необходимо отлавливать и исправлять, если найдутся. Как, впрочем, и любые другие ошибки.
Но вот про передачу данных — что-то не верится, что сетевой пакет может исказиться так, что ровно одно поле поменяет значение, а в остальном целостность запроса не пострадает. К тому же, наверняка у запроса есть CRC (не говоря уже, что может быть шифрованное соединение, а там любое отклонение от нормы наверняка угробит запрос).
Ну, и проверки входных данных, если таковые нужны, я предпочитаю делать в хранимых процедурах, максимально изолируя пользователя от самих таблиц.
Я говорил о передаче данных не в сетевом пакете, а от слоя к слою в архитектуре.

Никуда не ссылается - это нормально, вопрос в том, как отличить такое "никуда не ссылается" (удалили ссылку) от того, что не передали правильное место "ссылания". Но база это не отследит сама.
Гм, безусловно спасибо за статью, она в чем то полезна. Но в принципе, на мое имхо, такие вещи нужно делать чисто интуитивно. Если вы разбираетесь в данной области - было бы здорово увидеть статью на тему "Как разрабатывать базу данных для большого проекта." - Или разбор архитектуры базы... Навскидку могу задать вопрос : Планируеться (гипотетически) коммунити портал с четко известным количеством юзеров (университет). Хранение информации двух видов. Фото и текст (микропосты). Каким образом учитывая что user,info = const; должна быть архитектура базы с учетом крайне маленькой производительности сервера, а также места выделенного под базу.
Я и сам не понимаю, как можно проектировать по другому. И полностью с вами согласен, что все сказанное интуитивно понятно и очевидно. Однако сейчас передо мной база на аксесе, в которой варварски нарушены все принципы. Все — в буквальном смысле. И мне с этой базой приходится работать. И по первым отзывам видно, что я не одинок.

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

В свою бытность архитектором БД, разрабатывал достаточно серьезные базы. При разработке пользовался Power Designer'ом. Базовые принципы те же самые, что и для «маленьких» баз. Главное сложность — не утонуть в огромном количестве таблиц. На помощь приходит старая как мир техника — разделяй и властвуй. Т.е. разбить базу на составные части (модули), с большой «связаностью» внутри модуля и «слабыми» связями между модулями. Каждый модуль рисовать на отдельном листе, необходимые таблицы из других модулей показывать другим цветом и без перечисления полей (только квадратик с названием таблицы).

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

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

Еще хорошо придерживаться какой-то системы именования. К примеру с префиксами в именах таблиц:
ent_xxxxxx — обычная таблица с данными (от entity)
dct_xxxxxx — таблица-справочник (от dictionary)
ext_xxxxxx — таблица-расширение, из связи один-к-одному (от extention)
col_xxxxxx — связка многие-ко-многим (от collection)

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

Что юзер = конст, надо забыть сразу. Т.к. все-равно они будут добавляться, удалятся и редактироваться. Файлы в базе хранить не стоит — оставьте это промышленным системам, где необходима единая система контроля, репликации и т.п. Проше организовать хранилище на диске. С общим корнем, например /storage. Генерить уникальное имя файлу и путь к нему с учетом ИД владельца. Т.е. путь к файлу будет такой:
/storage/22/7622/qwertyuiop1234asd
где «7622» — ИД пользователя (конечно, если файлы имеют привязку в пользователю)
«22» — хэш-код, что б во-первых избежать огромного числа поддиректорий в stogare, а во-вторых в этом случае проще разносить storage на разные диски.

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

С «микропостами» ничего хитрого придумывать не надо — обычная таблица. Для повышения производительности — кэширование на стороне сервера.
Вопервых спасибо!) Во вторых, учитывая от прискорбный факт, что я повсей видимости отношусь к числу упомянутых выше "амбициозных самоучек" - интересно вот еще что. /*вообще донимать занятых людей - неприлично, сам не люблю... Поэтому не могли бы вы посоветовать что нить почитать на эту тему. Увы и ах потребность в этом уже есть, а курс будут читать только в след году (хорошо если вообще будут) - ничего особо дельного пока не нашел.*/ А в целом интересуют следующие моменты. Вопервых - насколько сильно размер таблицы влияет на скорость выборки? Например берем опять же микроинфо. Пускай 3 тыс пользователей. 2-3 поста в день. Месяц. Итого примерно 500 тыс записей. Если все это в одной таблице будет, Mysql серверу как, хорошо не станет?
Классикой считается «Введение в системы баз данных» Дейта (http://www.williamspublishing.com/Books/…). Правда это глубокая теория. По конкретным вопросам и конкретным СУБД могу порекомендовать только поиск ответов в форумах. 500 тыщ. записей — это мелочи. По крайней мере беспокоится о времени выборки на таких объемах точно не стоит. Правильная индексация и порядок поиска будет log_2(500 тыщ) = 19.
А вообще для сильно больших таблиц существует разбиение на разделы (partition).
А шрифт то, чего такой огромный?
Все делают id числовым.
А чем хуже делать его строковым? (это вопрос, а не утверждение)
Числа сравнивать быстрее. Соответственно, на больших выборках быстрее работают джойны, индексы и прочие фильтры.
Кстати, id не обязательно числовой. Еще довольно распространенная практика делать id guid-ом, чтобы не было конфликтов, например, в merge-репликациях. Опять же, guid-ы все равно быстрее сравнивать, чем строки.
Кстати, строковый ИД хорошо делать в таблицах-справочниках. И в качестве значения использовать кодовое слово. В этом случае результаты запросов на основные таблицы получаются более наглядными.

Guid'ы — достаточно мощный инструмент, но использовать его стоит только по необходимости. Опять же в GET-запросах числовой ИД выглядит куда симпатичнее, чем guid.
Ну да, так и делаем :)
Такие справочники обычно небольшие (типа всяких настроечных таблиц с путями, константами и прочими application-wide данными) и на производительность выборки из них не влияют.
Симпатичнее не значит практичнее. Есть много мест где гуид практичнее. Например возможность генерить полностью запись для бд на клиенте, без БД-запроса следующего ID ( или доставания уже вставленного), вышеупоямнутые мерж репликации.
Опять же защита от атаки на урль методом подстановок (или как он там правильно называется - spoofing) - ваша красота в виде postanswer.php?userId=123 очень часто оборачивается тем, что злоумышленник получает доступ к чужой сессии\данным просто поменяв userId в url. И если такие вещи не предусмотрены сразу то для исправления таких косяков в может потребоваться серьезная переделка ПО. В конце концов - вам не все равно, работает компилятор или БД с гуидом или с интом ?
Вообще не надо думать об удобстве программиста, надо делать корректные решения для машины. А то всякие составные строковые ID и прочие "удобства и красоты для программиста" приводят к ужасным архитектурным решениям. Не надо облегчать себе жизнь на этапе проектирования - эти решения выйдут боком на этапе кодирования или уже использования, когда исправление стоит в сотни а то и тысячи раз дороже.
Надеюсь вы не предлагаете основывать защиту системы на трудности подбора guid'а?
Я достаточно много использовал и guid'ы и числовые id. Реальное преимущество первого — именно при репликации и слиянии баз. Если этого не требуется — лучше использовать числа. С ними значительно быстрее и компактнее.
Основывать на это защиту - конечно нет. Но нету ни одного подхода который один, сам по себе, смог бы лечь в основание защиты. Это всегда комплекс мер. И одна из этих мер - защита от легкости вычисления следующего значения "перечня".
Или вы хотите сказать что для вас трудность подбора гуида - не трудность ???

Насчет быстрее - экономия на семечках, компактнее - да, местами заметная разница, особенно в вебе, но вопрос тоже решаемый.

Насчет еще одного преимущества - как бы вы спроектировали процесс, в котором вам надо вставить бизнес-объект в таблицу, и вернуть "пользователю" идентификатор вставленного объекта в бд. В общем, достаточно популярная задача. Гуид решает такую задачу на клиенте, и вы можете быть увереным, что все у вас хорошо.
С числовым Id, если не пользовать специальные фичи определенных бд ( Например, MSSQL умеет возвращать Id вставленного значения, но это нужно явно писать и обрабатывать), вам потребуется как минимум 1 дополнительный запрос в БД - запросить следующий допустимый Id или запросить ID вставленной записи ( именно своей, а не "последней", при более-менее нагруженной системе последней может оказаться запись уже не ваша). Предложите более простое решение ?
Если уж вдаваться в детали — вот таких «postanswer.php?userId=123» урлов стоит избегать в любом случае. ИД юзеров вообще не стоит светить вне зависимости от способа его генерации. ИД большинства других объектов — безобидны с т.з. безопасности. Что б далеко не ходить за примерами — посмотрите на этот сайт. ИД сообщения — число. А ИД вас, как пользователя, на сайте нигде нет.

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


Вот и википедия говорит, что «Его главная особенность — уникальность, которая позволяет создавать расширяемые сервисы и приложения без опасения конфликтов, вызванных совпадением идентификаторов» (http://ru.wikipedia.org/wiki/GUID)
Если уж брать хабр, то нашествия всяких черных пластилинов и хабравирусов как раз показали - легко подкрутить карму или рейтинг сообщения. Были бы гуиды - было бы не сложнее. Это никак не относится к безопасности и удобству разных типов идентификаторов а говорит о том, что вводу со стороны клиента доверяем бездумно.

"ИД большинства других объектов — безобидны с т.з. безопасности " - ничуть не безобидны! ID заказов, ID любых данных, на которые могут распространяться ограничения. Все алгоритмы генерации нужно явно скрывать (хотя защита через скрытие - порочная практика) - они хоть немного затруднят работу злоумышленника.
Например пользователь в определенной роли может заводить данные типа А и не может - типа Б. Если нету явного sanitize приходящих данных то поменяв ItemTypeId c А на Б, в 99% случаев пользователь обойдет такую "безопасность".
В случае с GUID - перехват URL или "срисовывание" из-за спины - дает всего 1 идентификатор. В случае с Identity - даже срисовывать не надо особо - посмотрел что передается для твоего аккаунта и все - как числи идут друг за другом - всем известно =).

Генерация на клиенте - рассмотрите любой логгер, механизм который вставляет НУ ОЧЕНЬ много данных. Конечно у него не в каждой ситуации нужен обратно ID, и для логгера лучше сделать вставку с пропусканием Identity поля. Но если вставка сложная - то уже есть избыточность. И она-то как раз не даст вам экономить на семечках - много запросов туда-обратно. В общем не так все безоблачно в стане числового Id, как хотелось бы.
А вот насчет забронировать сотню ID - поподробнее ? Что руками полезем менять значение current ? А по рукам за такие вещи еще не давали ?

Википедия много что говорит. Всегда надо относится к ней скептически - думаете что статью про гуиды писал архитектор ? или он знаком с большим спектром применения гуида ? Достаточно обывательская статья, которая дает общие общеизвестные сведения. Я бы не стал полагаться на такой источник как на истину в последней инстанции об основном предназначении гуида. Его уникальность можно использовать по-разному.
PS кстати вы так и не ответили на некоторые мои вопросы:
про трудность подбора гуида для вас и про более простое решение (правда про него вы оправдались что вставка гораздо реже выборки, хотя в системах накопления статистических данных (в период сбора данных)как раз обычно наоборот).
Про «забронировать сотню ИД»
select setval('seq_name', currval('seq_name') + 100) — Postgres

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

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

Безопасность не бывает избыточной, тем более я приводил достаточно простые и распространенные требования к ограничениям - запретить часть классификаторов.
И дело не только в том, что можно или нельзя удалить заказ по гуиду, просто вы говорите примерно так - "в большинстве случаев такая безопасность не нужна, мы ей пренебрежем".
ИМХО пренебрегать недорогим способом усложнить жизнь злоумышленнику почти ничего не потеряв - пренебрегать не стоит.

"Замечательно", вы лезете руками в счетчик и резервируете промежуток (я бы за такое по рукам бы бил, но у вас свои "архитекторы").
Сразу 2 вопроса - как быть если промежуток не понадобился (или транзакция откатилась, т.е. при вставке большого кол-ва данных одна запись оказалась "неуспешной" и вся транзакция failed) - вы не сможете вернуть его в систему не поимев серьезного геммороя с общим счетчиком. И при таком кардинальном подходе представьте у вас резерв не по 100 а хотя бы по 1000 - как быстро закончится ваш Int? Да, и вы забыли в таком подходе запомнить curval, это надо делать в транзакции, иначе кто-то может успеть изменить его на этапе между получением curval и установкой setval. - Вам нравится такое нетрадиционно-ориентированное решение ради какого-то резервирования ? мне - кардинально нет. Мало того что мы руками изменяем системные значения, счетчики, за одно это в нормальных командах разработки можно получить по рукам, так еще создаем микротранзакцию для непонятно чего, имеем гемморой с "возвратом" неиспользованных значений и потенциальный гемморой с исчерпанием счетчиков. И все ради чего ? Ради якобы светлой и великой идеи что инт лучше гуида ? Удачи вам конечно, в ваших разработках, но надеюсь что вы не будете раскладывать такое количество "граблей на будущее" для тех, кому прийдется потом с вашей системой мучаться.
PS может показаться смешным, но в одном проекте, где несколько раз генерились большие объемы тестовых данных мы таки подобрались к пределу стандартного счетчика постгри. После этого все зачесались, сбросили его (благо тестовая бд) и стали куда как более аккуратно его "резервировать". Правда проблем с множественной репликацией это все равно не решило....
Я бы советовал первичный ключ называть как object_id по аналогии с теми внешними, что будут на него ссылаться. Во-первых существенно мене вероятны разночтения, во-вторых часто удобнее JOIN'ить (часто можно обойтись без алиасов и использовать USING). Есть ещё много мелких удобств при дальнейше работе с этими полями в приложениях.
> Я бы советовал первичный ключ называть как object_id
Полностью поддерживаю, кроме того по другому нельзя, если пользоваться CASE-тулзами, там, если поля называются одинаково, то они означают одно и тоже и имеют и один и тот же тип.

> часто можно обойтись без алиасов
Грабли... да, грабли это опыт, но они могут оказаться и детскими.
Где грабли? О чём речь?
Сегодня это работает, а завтра одноименный столбец появляется в другой таблице или сервер меняет свое отношение к подобным записям.
Если вы про SELECT *, то нужно же головой думать. Я про SELECT foo_id, bar_id, bar_field FROM foo, bar USING (foo_id). Это не грабли, а инструмент, который надо уметь использовать. А вот сервер, неожиданно меняющий своё отношение к обработке SQL мне представить сложно.
НЛО прилетело и опубликовало эту надпись здесь
:) кстати да здесь тоже 31-о поле для дней месяца!
Не совсем понял, как устроена связь один-к-одному. Как мы пишем запрос? Допустим, мы получили строчку из таблицы item. Как получить остатки информации? Может стоит добавить поле "тип"?
Согласен, в данном примере действительно стоит указать, сейчас дополню.
Спасибо, исправил.

Просто в большинстве случаев из контекста понятно какие объекты нужно получать. А здесь да — подразумевается, что могут быть «вперемешку».
Спасибо Вам.
"Самое краткое руководство по проектированию Баз Данных" должно обязательно содержать упоминание нормальных форм.
На самом деле тут много чего нет. Я бы ещё бил по голове сковородой за такое описание дерева. Деревья — они же кучей способов реализуются, хранение parent_id самый простой, но не всегда тот способ, который поможет решить проблему. Поэтому это пошаговая инструкция, которая бы очень подошла к книге "Для Чайников". А пошаговая инструкция там, где нужен полёт мысли (как в любой архитектурной работе) _недопустима_. Это всегда приводит к непробиваемому автоматизму и полному отсутствию мышления о применении результата.
Хабралюди, вы вообще предисловие-то прочитали?
Это и есть «пошаговая инструкция для чайников». Ни на что большее эта статья не претендует. Сковородку засуньте себе в… посудомойку. В рассматриваемом примере такая реализация дерева полностью оправдана.
Мил человек, зачем чайникам проектировать базы? Их задача воду на печке кипятить. Мой комментарий не о том, что ваш пост неверно назван. А о том, что такая подача материала в принципе обычно до добра не доводит. Если вы считаете, что прочитав это они начнут читать дальше, вы ошибаетесь. Как минимум нет ссылок.
Незачем им проектировать. Действительно, для проектирования, по-хорошему нужно интуитивно понимать смысл нормальных форм, отношений, целостности и непротиворечивости данных. Но так уж случилось, что я живу в том мире который есть, а не в том каким он должен быть по моим представлениям. И в этом мире чайники проектируют базы, а мне иногда приходится с ними сталкиваться. Причем нельзя сказать, что они мерзавцы и негодяи, эти чайники. Просто вот так сложилась ситуация, что им пришлось сделать то в чем они не компетентны. К счастью иногда, столкнувшись с противоречиями в проектировании, они обращаются к знакомым, читающим хабр, ну или ко мне лично (в случае моих знакомых чайников). Раньше мне приходилось объяснять азы теории в режиме онлайн-трансляции в аське. Теперь я просто дам ссылку на эту статью. Именно по-этому я сделал ее максимально краткой и с минимальных содержанием терминов.

Прочитайте первые отзывы. Иногда нам приходится сталкиваться с базами, содержащими таблицы названные в честь месяцев года и с 31-м полем (по кол-ву дней месяца).
Поэтому я не минусую вам карму и не говорю, что ваша статья сливает. Вы всё верно говорите. Я лишь соглашаюсь с одним из ораторов в том, что в ней не только польза. Я считаю, что её и в простой форме можно было сделать лучше. Но вы всё равно проделали хорошую работу ;].
НЛО прилетело и опубликовало эту надпись здесь
Спасибо! Про «пронумеровать без пропусков» пытался сказать в самом начале — «порядок следования записей не определен». Сейчас перечитывая, вижу, что действительно многие важные моменты упомянуты вскользь. Пока не знаю как их выделить, сохранив краткость… Подумаю.

Кстати, про деревья расскажите пожалуйста, а как их еще-то можно хранить? Просто действительно не знаю :)

В смысле, понимаю, что чисто бинарные деревья можно хранить ссылками на левое и правое поддерево. У произвольных деревьев можно хранить ссылку на «первого потомка» и на «брата». Знаком и с такой экзотикой, как «развертка дерева» (таблица живущая на триггерах и содержащая все пары «узел, предок»), что позволяет делать произвольные запросы по дереву, без использования ораклового «connect by prior». Но за всю практику никогда не доводилось использовать что-то отличное от простого parent_id.
НЛО прилетело и опубликовало эту надпись здесь
спасибо большое, очень познавательно для начинающих, было бы неплохо еще изображение со схемой и всеми связями повесить
а если я разобью книги и диски на две таблицы и присвою им идентификаторы, например, book_id и disk_id - допустимо ли такое решение?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории