Как хранить ФИО в базе данных

ФИОбаза данныхархитектура

Две крайности

При проектировании БД для хранения ФИО обычно выбирают между:

  1. Одно полеfull_name VARCHAR(255)
  2. Три поляsurname, first_name, patronymic

Оба подхода имеют проблемы. Разберём их и выберем лучший вариант.

Подход 1: Одно поле

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  full_name VARCHAR(255) NOT NULL
);

INSERT INTO users (full_name) VALUES ('Иванов Сергей Петрович');

Плюсы

  • Простота: один столбец, один индекс
  • Гибкость: любой формат («Иванов С.П.», «Sergey Ivanov»)
  • Быстрый ввод: пользователь заполняет одно поле

Минусы

  • Поиск по фамилии требует LIKE или полнотекстовый индекс
  • Сортировка по имени невозможна без парсинга
  • Персонализация («Уважаемый Сергей») требует парсинга
  • Дубликаты: «Иванов С.П.» и «Сергей Иванов» — разные записи

Подход 2: Три поля

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  surname VARCHAR(100) NOT NULL,
  first_name VARCHAR(100) NOT NULL,
  patronymic VARCHAR(100)
);

INSERT INTO users (surname, first_name, patronymic)
VALUES ('Иванов', 'Сергей', 'Петрович');

Плюсы

  • Точный поиск по любому компоненту
  • Сортировка по фамилии, имени
  • Персонализация без парсинга
  • Дедупликация проще

Минусы

  • Форма ввода сложнее (3 поля вместо 1)
  • Импорт данных требует парсинга
  • Иностранные имена не всегда укладываются в схему

Рекомендуемый подход: 4 поля

Храните и исходные данные, и распарсенные:

CREATE TABLE users (
  id SERIAL PRIMARY KEY,

  -- Исходные данные (как ввёл пользователь)
  full_name_original VARCHAR(255) NOT NULL,

  -- Распарсенные компоненты
  surname VARCHAR(100),
  first_name VARCHAR(100),
  patronymic VARCHAR(100),

  -- Метаданные
  gender CHAR(1), -- 'M', 'F', NULL
  confidence DECIMAL(3,2), -- 0.00-1.00

  -- Индексы
  CONSTRAINT gender_check CHECK (gender IN ('M', 'F') OR gender IS NULL)
);

-- Индексы для поиска
CREATE INDEX idx_users_surname ON users(surname);
CREATE INDEX idx_users_full_name ON users USING gin(to_tsvector('russian', full_name_original));

Почему это работает

  1. full_name_original — сохраняем как ввёл пользователь. Полезно для аудита и отладки.

  2. surname, first_name, patronymic — заполняем автоматически через NER-парсинг. Используем для поиска и персонализации.

  3. gender — определяем автоматически по имени. Используем для обращений.

  4. confidence — насколько уверены в парсинге. При низкой уверенности — ручная проверка.

Пример заполнения через API

import requests

def parse_and_store_user(db, full_name: str):
    # Парсим ФИО через API
    response = requests.post(
        "https://api.humandata.ru/v1/clean",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
        json={"query": full_name, "type": "name"}
    )
    data = response.json()

    # Сохраняем в БД
    db.execute("""
        INSERT INTO users (full_name_original, surname, first_name, patronymic, gender, confidence)
        VALUES (%s, %s, %s, %s, %s, %s)
    """, (
        full_name,
        data.get("surname"),
        data.get("name"),
        data.get("patronymic"),
        data.get("gender"),
        data.get("confidence")
    ))

# Использование
parse_and_store_user(db, "иванов сергей петрович")
# Результат: surname='Иванов', first_name='Сергей', patronymic='Петрович', gender='M', confidence=0.98

Нормализация при сохранении

Перед записью в БД приводите данные к единому формату:

def normalize_name_component(value: str | None) -> str | None:
    if not value:
        return None

    # Удаляем лишние пробелы
    value = " ".join(value.split())

    # Первая буква заглавная, остальные строчные
    # Исключение: двойные фамилии (Салтыков-Щедрин)
    parts = value.split("-")
    normalized = "-".join(p.capitalize() for p in parts)

    return normalized

# Примеры
normalize_name_component("ИВАНОВ")      # "Иванов"
normalize_name_component("салтыков-щедрин")  # "Салтыков-Щедрин"
normalize_name_component("  сергей  ")  # "Сергей"

Поиск по ФИО

С 4-польной схемой поиск работает быстро:

-- Точный поиск по фамилии
SELECT * FROM users WHERE surname = 'Иванов';

-- Поиск по части имени
SELECT * FROM users
WHERE surname ILIKE 'иван%'
   OR first_name ILIKE 'иван%';

-- Полнотекстовый поиск по оригиналу
SELECT * FROM users
WHERE to_tsvector('russian', full_name_original) @@ to_tsquery('russian', 'Иванов & Сергей');

Миграция с одного поля на четыре

Если у вас уже есть таблица с full_name:

-- 1. Добавляем новые столбцы
ALTER TABLE users
ADD COLUMN surname VARCHAR(100),
ADD COLUMN first_name VARCHAR(100),
ADD COLUMN patronymic VARCHAR(100),
ADD COLUMN gender CHAR(1),
ADD COLUMN confidence DECIMAL(3,2);

-- 2. Переименовываем старый столбец
ALTER TABLE users RENAME COLUMN full_name TO full_name_original;

-- 3. Заполняем новые столбцы через batch-обработку (Python скрипт)
-- 4. Создаём индексы
CREATE INDEX idx_users_surname ON users(surname);

Итого

ПодходКогда использовать
Одно полеMVP, прототип, если ФИО только отображается
Три поляЕсли ФИО вводится структурированно (госсистемы)
4 поляРекомендуется: гибкость + точный поиск

Рекомендация: используйте 4-польную схему с автоматическим парсингом через API. Это даёт максимум гибкости при минимуме усилий пользователя.