- PVSM.RU - https://www.pvsm.ru -
Простой вопрос, который разделяет инженеров и «операторов фреймворков»

Я провёл больше ста технических собеседований. Разных: на джуна, мидла, сеньора, системных аналитиков, бэкендеров, фуллстеков.
И у меня есть один вопрос. Простой до неприличия. Без алгоритмов, без паттернов проектирования, без «реализуй красно-чёрное дерево на доске».
Один вопрос, который с точностью хирургического скальпеля отделяет людей, которые думают о данных, от тех, кто просто заполняет формочки.
Вот он:
Вы проектируете базу данных. В ней нужно хранить серию и номер российского паспорта. Какой тип данных выберете? База – SQL.
Всё. Больше никаких условий. Можно уточнять, можно думать вслух.
Прежде чем читать дальше – ответьте себе честно. Прямо сейчас. Какой тип?
Большинство кандидатов с ожиданиями от 150к рублей отвечают примерно так:
«Ну... это же цифры. Значит, INTEGER. Или BIGINT, если много данных».
Иногда добавляют:
«Можно NUMBER(10), чтобы ограничить длину».
Редкий кандидат скажет NUMERIC. Это звучит умнее, но суть та же.
Один PHP-разработчик с опытом больше пяти лет тоже уверенно ответил NUMBER. А когда я намекнул, что есть подвох, начал выкручиваться: «Ну, можно при чтении проверять длину числа, и если она меньше четырёх – дописывать ноль спереди». Костыль в бизнес-логике вместо правильного типа данных – и он настаивал, что «зато так база работает быстрее». Человек готов жонглировать строками в коде, лишь бы не пересмотреть выбор типа.
И все они ошибаются. Причём ошибка не в синтаксисе и не в незнании конкретной СУБД. Ошибка – в способе .
Давайте разберём, почему.
Да, многие из вас уже догадались, в чём подвох. Задача элементарная – и тем обиднее, что на ней спотыкаются снова и снова. Не джуны на первом собеседовании, а опытные разработчики с годами коммерческой практики.
Серия российского паспорта – это четыре цифры. Например: 0306.
Что произойдёт, если вы сохраните 0306 в поле типа INTEGER?
INSERT INTO persons (passport_series) VALUES (0306);
SELECT passport_series FROM persons; -- вернёт 306
Ведущий ноль исчезнет. Молча и без ошибки. База данных просто сделает своё дело: сохранит число триста шесть.
Паспорт серии 0306 и паспорт серии 306 – это юридически разные документы. Но в вашей базе они станут неотличимы.
Вы только что сломали идентификацию человека. Тихо, незаметно, без единого исключения в логах.
Масштабируем: в России живёт 146 миллионов человек. У скольких серия паспорта начинается с нуля? У миллионов. Поздравляю, вы только что помножили миллионы людей на тот самый ноль – в буквальном смысле. Их паспорта перестали существовать в корректном виде.
Но даже если бы ведущих нулей не существовало – тип INTEGER всё равно был бы неправильным ответом.
Спросите себя: что вы делаете с числами?
Вы складываете их. Вычитаете. Находите среднее. Делите. Сортируете по возрастанию, чтобы найти «наибольший» или «наименьший».
Теперь вопрос: когда вы в последний раз складывали серию паспорта с номером дома? Вычисляли среднее арифметическое паспортов в базе? Искали паспорт с «наибольшим» значением серии?
Никогда. Потому что это бессмысленно.
Паспорт – это идентификатор. Это метка, уникально указывающая на документ. Над идентификаторами не производят математических операций. Их сравнивают на равенство, ищут по ним, передают – и всё.
Это принципиально другая семантика. И тип данных должен эту семантику отражать.
Правильный ответ: VARCHAR (или CHAR, если длина фиксирована, что здесь как раз так).
-- Серия: ровно 4 символа
passport_series CHAR(4) NOT NULL,
-- Номер: ровно 6 символов
passport_number CHAR(6) NOT NULL
Иногда серию и номер хранят в одном поле – тогда CHAR(10) или VARCHAR(10):
-- Серия + номер: 10 символов без пробела
passport_full CHAR(10) NOT NULL -- '0306123456'
Смысл не меняется: это строка. Не число. Точка.
Это возражение я слышу часто. И оно технически верное, но практически бессмысленное.
CHAR(4) для цифр занимает те же 4 байта, что и INTEGER (для однобайтовых символов, а цифры в любой кодировке – один байт). В данном случае разницы нет вообще.
Но даже если бы VARCHAR занимал в два раза больше – это не имело бы значения. Вы храните паспорт человека, а не оптимизируете хранилище для задачи уровня FAANG. Экономия нескольких байт на строке не стоит сломанных данных.
Поставьте индекс. Индекс на VARCHAR работает отлично. Поиск по точному совпадению строки через B-tree индекс – это O(log n), ровно как и для числа. (Да, hash-индекс даёт O(1) – но B-tree в большинстве СУБД стоит по умолчанию, и для наших объёмов разница неощутима.)
Если кто-то говорит, что «числовые индексы быстрее строковых» – пусть покажет бенчмарк на реальных данных. Для CHAR фиксированной длины разница практически нулевая.
Вы можете подумать: «Ну, это академический вопрос. На практике такое не встречается».
Встречается и постоянно.
Пару лет назад моя команда разрабатывала сервис для оформления заявок на парковочные абонементы. Один начинающий специалист спроектировал таблицу, в которой поле паспорта имело тип NUMBER. Форма прошла код-ревью, прошла тестирование, прошла приёмку – и благополучно ушла в прод. Работала без нареканий. Ровно до того момента, пока заявку не подал я сам.
У меня серия паспорта – 0306. Система проглотила ведущий ноль, сохранила 306, и заявка тихо сломалась. Не с ошибкой, не с исключением – просто данные стали невалидными. Паспорт в базе и паспорт на бумаге перестали совпадать.
С тех пор этот вопрос – один из моих любимых на собеседованиях. Не потому что он сложный, а потому что он настоящий. За ним стоит конкретный баг, конкретная сломанная заявка и конкретный урок: типы данных – это не синтаксис, это проектное решение.
Эту ситуацию можно было исключить одним правильным решением в начале.
Раз уж мы разобрались с паспортом, давайте добьём тему.
СНИЛС – 11 цифр, формат XXX-XXX-XXX XX. Тип? VARCHAR(14) или CHAR(11) после очистки от дефисов. Не BIGINT. Никогда.
ИНН физлица – 12 цифр. Может начинаться с нуля? Да: коды регионов 01–09 (Адыгея, Башкортостан, Бурятия и т.д.) дают ИНН, начинающийся с нуля. Так что аргумент «ведущий ноль» здесь работает в полную силу. Но даже без него хранить в VARCHAR(12) – это правильная привычка, потому что ИНН – идентификатор, а не число.
Номер телефона – +7 (903) 123-45-67. Тип? Только VARCHAR. Да, + можно отрезать и хранить чистые цифры 79031234567 – но даже в этом случае вы не будете складывать телефоны между собой. Телефон – идентификатор: его ищут, сравнивают и передают. Арифметика над ним бессмысленна. А если завтра потребуется хранить международный формат с + – придётся менять схему. VARCHAR(20) и никаких компромиссов.
Почтовый индекс – 6 цифр. Российские индексы начинаются с цифр от 1 до 6 (минимальный – 101000, Москва), с нуля не начинается ни один. Но для международных индексов (Великобритания, Канада) формат буквенно-цифровой, и главное – индекс всё равно не число. CHAR(6).
Паттерн везде один: если вы никогда не будете делать арифметику с этим полем – это не число.
Этот вопрос – не ловушка. Это не попытка поймать вас на незнании какой-то синтаксической особенности.
Он проверяет одну вещь: думаете ли вы о природе данных, прежде чем выбрать инструмент для их хранения.
Плохой инженер смотрит на данные и думает: «Там цифры – значит число».
Хороший инженер задаёт три вопроса:
Какова семантика этих данных? (Что они означают?)
Какие операции над ними будут производиться?
Какие ограничения накладывает предметная область?
Только ответив на них, он выбирает тип.
Это и есть разница между человеком, который пишет код, и человеком, который решает задачи.
|
Данные |
Формат |
Правильный тип |
Частая ошибка |
Что случится, если ошибётесь |
|---|---|---|---|---|
|
Серия паспорта |
4 цифры |
|
|
Серия |
|
Номер паспорта |
6 цифр |
|
|
Потеря ведущего нуля, сломанная идентификация |
|
Паспорт (серия + номер) |
10 цифр |
|
|
Потеря до двух ведущих нулей |
|
СНИЛС |
11 цифр |
|
|
ИНН/СНИЛС регионов 01–09 обрезаются |
|
ИНН физлица |
12 цифр |
|
|
ИНН с кодом 01–09 теряет разряд |
|
ИНН юрлица |
10 цифр |
|
|
Аналогично: ведущий ноль → невалидный ИНН |
|
Телефон |
|
|
|
Невозможно хранить |
|
Почтовый индекс |
6 цифр |
|
|
Международный индекс не влезет |
|
Банковский счёт |
20 цифр |
|
Нет подходящего INT |
Переполнение, потеря денег |
|
БИК |
9 цифр |
|
|
Обрезка при нестандартном коде |
Если вы начинающий специалист – запомните эту таблицу. Она сэкономит вам часы отладки и сохранит репутацию.
Если вы сеньор – задайте этот вопрос на следующем собеседовании. Ответ скажет вам о кандидате больше, чем час разговора про паттерны.
Если вы тимлид – проверьте свою текущую схему БД. Прямо сегодня. Найдите поле с серией паспорта или СНИЛС. Посмотрите на его тип.
Иногда самые дорогие баги прячутся в самых скучных местах.
Эта статья – адаптированный фрагмент из моей книги «Поколение JSON: хроники Client-Side Testing». Книга о том, как индустрия свернула не туда. Бесплатная глава – на json-book.ru [2]
Автор: MrTheFirst
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bazy-danny-h/447394
Ссылки в тексте:
[1] мышления: http://www.braintools.ru
[2] json-book.ru: https://json-book.ru
[3] Источник: https://habr.com/ru/articles/1012950/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1012950
Нажмите здесь для печати.