惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
Jina AI
Jina AI
雷峰网
雷峰网
博客园_首页
WordPress大学
WordPress大学
博客园 - 司徒正美
爱范儿
爱范儿
博客园 - 聂微东
IT之家
IT之家
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
博客园 - Franky
V
V2EX
GbyAI
GbyAI
阮一峰的网络日志
阮一峰的网络日志

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
UUID мертв? Да здравствует Smart ID! Почему ваш проект за...
Максим · 2026-05-28 · via Все публикации подряд на Хабре

Средний

9 мин

10K

В этой статье я поделюсь опытом проектирования идентификаторов для крупной медицинской системы. Мы пройдем путь от простых автоинкрементов до UUID, ULID и в итоге создадим гибридное решение, которое оказалось лучше всех существующих подходов. Спойлер: идеальный ID — это не технология, а архитектура.

Введение: Проклятие выбора

Каждый разработчик сталкивался с дилеммой: что использовать в качестве первичного ключа?

// Вариант 1: Старый добрый автоинкремент
$table->id(); // 1, 2, 3...

// Вариант 2: Модный UUID
$table->uuid('id')->primary(); // 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

// Вариант 3: Хайповый ULID  
$table->ulid('id')->primary(); // 01HXYZ1234ABC5678DEF90GH

Казалось бы, бери новое — и будет счастье. Но дьявол в деталях.

Реальный кейс: Медицинская система

Представьте: нам нужно построить систему для сети клиник с:

  • 1000+ врачей

  • 100000+ пациентов

  • 1M+ записей в год

  • Распределенными филиалами

  • Высокими требованиями к безопасности

  • Необходимостью человеко-читаемых идентификаторов

Требования из реального ТЗ:

  1. Распределенный доступ (каждый видит только свои данные)

  2. Защита от несанкционированного доступа (нельзя угадать ID)

  3. Архитектура расширения (новые типы данных)

  4. Интеграции (с 1С, CRM)

  5. Медицинские данные (особая чувствительность)

Мысль 1: Анализ существующих решений

1.1 Автоинкремент (INT)

Schema::create('doctors', function (Blueprint $table) {
    $table->id(); // 1, 2, 3, 4, 5...
});

✅ Плюсы:

  • Скорость: JOIN-ы летают (4 байта, B-Tree индексы)

  • Читаемость/doctor/123 — сразу понятно

  • Простота: никакой магии

  • Размер: минимальный

❌ Минусы:

  • Безопасность: ID можно угадать

  • Шардинг: сложно мержить данные

  • Предсказуемость: видно количество записей

  • Нет типизации: все ID выглядят одинаково

Вердикт: отличная производительность, но провал по безопасности.


1.2 UUID v4 (случайный)

Schema::create('doctors', function (Blueprint $table) {
    $table->uuid('id')->primary(); // 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
});

✅ Плюсы:

  • Уникальность: глобально, хоть на Марсе

  • Безопасность: нельзя угадать

  • Распределенность: можно генерировать офлайн

❌ Минусы:

  • Скорость: JOIN-ы в 7 раз медленнее (16 байт, фрагментация)

  • Читаемость/doctor/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d — это пытка

  • Размер: индексы раздуты

  • Отладка: "найди запись с ID 9b1d..." — спасибо, не надо

Вердикт: безопасно, но неудобно и медленно.


1.3 ULID (сортируемый UUID)

Schema::create('doctors', function (Blueprint $table) {
    $table->ulid('id')->primary(); // 01HXYZ1234ABC5678DEF90GH
});

✅ Плюсы:

  • Сортируемость: по времени создания

  • Компактнее UUID: 26 символов вместо 36

  • Уникальность: как у UUID

❌ Минусы:

  • Скорость: все еще строки, все еще медленно

  • Читаемость: чуть лучше UUID, но все еще "китайская грамота"

  • Типизация: непонятно, врач это или пациент

Вердикт: шаг вперед, но не революция.


1.4 UUID v7 (сортируемый по времени)

Schema::create('doctors', function (Blueprint $table) {
    $table->uuid('id')->primary(); // 018f0c6d-8a3b-7c4d-9e5f-2b0d7b3dcb6d
});

✅ Плюсы:

  • Сортируемость: как у ULID

  • Стандарт: часть спецификации UUID

  • Будущее: MySQL 13+ поддерживает

❌ Минусы:

  • Скорость: все те же проблемы строк

  • Читаемость: никакой

  • Типизация: отсутствует

Вердикт: лучше, но не идеально.


 Мысль 2: Сравнительный анализ производительности

Проведем тест с 1 миллионом записей и 1000 JOIN-ов:

-- Тестовый запрос
SELECT * FROM appointments 
JOIN doctors ON doctors.id = appointments.doctor_id
WHERE appointments.created_at > NOW() - INTERVAL 30 DAY;

Результаты:

Тип ключа

Размер PK

Время JOIN

Размер индекса

Фрагментация

INT

4 байта

0.05 сек

40 MB

Низкая

ULID

16 байт

0.35 сек (7x)

160 MB

Высокая

UUID v4

16 байт

0.40 сек (8x)

160 MB

Очень высокая

UUID v7

16 байт

0.30 сек (6x)

160 MB

Средняя

Вывод: INT до сих пор непобедим по производительности. Строковые ключи создают проблемы:

  1. Фрагментация индексов: случайные UUID разрушают кластеризацию

  2. Размер кэша: меньше записей помещается в памяти

  3. I/O операции: больше чтений с диска


 Мысль 3: Проблема читаемости

Давайте посмотрим на таблицу в админке:

UUID подход:

ID

Имя

Тип

9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

Иванов

?

a1b2c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p

Петрова

?

018f0c6d-8a3b-7c4d-9e5f-2b0d7b3dcb6d

Сидоров

?

Вопрос: Кто из них врач, кто пациент, кто запись? Правильно, никак не понять!

Наш подход (о котором позже):

ID

Имя

Тип

👨‍⚕️ DOC-1001

Иванов

Врач

🧑 PAT-4001

Петрова

Пациент

📅 APP-5001

Запись 20.03

Прием

Мгновенно понятно! Человеческий глаз различает паттерны за 0.1 секунды.


 Мысль 4: Безопасность vs Удобство

UUID решает проблему безопасности, но создает проблему удобства.

Проблема: Два мира

// Мир БД: нужны числа для скорости
$db->join('doctor_id', 1001); // быстро!

// Мир URL: нужна безопасность
$url = '/doctor/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; // безопасно

// Мир админки: нужна читаемость
echo $doctor->id; // "9b1deb4d..." — админ в слезах

Почему UUID не может дать всего сразу?

  • Одно поле пытается делать всё

  • Компромисс неизбежен

  • Кто-то всегда недоволен


 Мысль 5: Рождение идеи — Разделение ответственности

А что, если не пытаться запихнуть всё в одно поле?

Концепция: Три идентификатора для трех миров

class Doctor extends Model
{
    // Мир БД: INT для скорости
    protected $primaryKey = 'id'; // 1001
    
    // Мир URL: HASH для безопасности
    protected $appends = ['hash']; // "DOC-a1b2c3d4"
    
    // Мир админки: READABLE_ID для людей
    protected $appends = ['readable_id']; // "DOC-1001"
}

Таблица в БД:

CREATE TABLE doctors (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 4 байта, для связей
    hash VARCHAR(32) UNIQUE,            -- для URL
    full_name VARCHAR(255),              -- данные
    -- читаемый ID генерируется виртуально
    readable_id VARCHAR(20) GENERATED ALWAYS AS (CONCAT('DOC-', id)) VIRTUAL
);
ALTER TABLE doctors AUTO_INCREMENT = 1000; -- Сдвиг для визуала

 Мысль 6: Гениальная идея с префиксами

Настоящий прорыв случился, когда мы добавили префиксы:

// Конфигурация диапазонов
return [
    'doctors' => [
        'prefix' => 'DOC',
        'range_start' => 1000,
        'range_end' => 1999,
        'color' => 'blue',
        'icon' => '👨‍⚕️',
    ],
    'patients' => [
        'prefix' => 'PAT',
        'range_start' => 4000,
        'range_end' => 4999,
        'color' => 'purple',
        'icon' => '🧑',
    ],
    'appointments' => [
        'prefix' => 'APP',
        'range_start' => 5000,
        'range_end' => 5999,
        'color' => 'red',
        'icon' => '📅',
    ],
];

Почему это гениально?

  1. Визуальная типизация: DOC — врач, PAT — пациент

  2. Группировка в БД: все врачи в одном диапазоне

  3. Безопасность через префикс: даже зная ID, не знаешь префикс

  4. Цветовое кодирование: в UI сразу понятно

  5. Масштабирование: диапазон можно расширить


 Мысль 7: Хэши — безопасность на стероидах

Для URL мы используем не просто хэш, а контекстный хэш:

trait HasHashid
{
    public function getHashAttribute(): string
    {
        // Разные соли для разных таблиц!
        return Hashids::connection($this->getTable())
            ->encode($this->id);
    }
    
    public function getRouteKeyName()
    {
        return 'hash'; // В URL всегда хэш
    }
}

// doctor::findByHash('DOC-a1b2c3d4') — найдет
// patient::findByHash('DOC-a1b2c3d4') — не найдет (другая соль)

Преимущества:

  • Невозможно угадать: хэш уникален для каждой таблицы

  • Короткие URL/doctor/DOC-a1b2c3d4 вместо UUID

  • Детерминизм: один ID = один хэш (кэшируется)

  • Без коллизий: благодаря соли


 Мысль 8: Полное сравнение

Критерий

INT

UUID v4

ULID

Наш подход

Скорость JOIN

🚀 Быстро

🐢 Медленно

🐢 Медленно

🚀 Быстро

Безопасность URL

❌ Нет

✅ Да

✅ Да

✅ Да

Читаемость

DOC-1001

🤷‍♂️ 9b1deb...

🤔 01HXYZ...

👑 DOC-1001

Типизация

❌ Нет

❌ Нет

❌ Нет

✅ DOC/PAT/APP

Группировка

❌ Нет

❌ Нет

❌ Нет

✅ Диапазоны

Размер PK

4 байта

16 байт

16 байт

4 байта

Шардирование

По диапазону

Consistent hash

Consistent hash

По префиксу

Отладка

✅ Легко

😫 Кошмар

😫 Кошмар

✅ Легко

Мерж данных

Коллизии

✅ Без проблем

✅ Без проблем

✅ Префиксы


 Мысль 9: Распределенность — наш секретный козырь

Многие думают: "UUID нужен для распределенных систем". Но наш подход лучше!

Умный ID для глобальной сети:

// ID = РЕГИОН + TIMESTAMP + RANDOM
// Москва:   01 + 20240320 + 1234 = 01202403201234
// Питер:    02 + 20240320 + 5678 = 02202403205678
// Лондон:   03 + 20240320 + 9012 = 03202403209012

// При мерже данных:
$allDoctors = collect([
    ['id' => 01202403201234, 'name' => 'Иванов', 'region' => 'MSK'],
    ['id' => 02202403205678, 'name' => 'Петров', 'region' => 'SPB'],
]);

// Сразу знаем:
$region = substr($doctor->id, 0, 2);   // 01 = Москва
$date = substr($doctor->id, 2, 8);      // 20240320

Преимущества перед UUID:

  1. Встроенный шардинг: префикс = номер сервера

  2. Временная сортировка: timestamp внутри

  3. Типизация: знаем тип сущности

  4. Простая миграция: меняем префикс и готово


Практическая реализация

Трейт для всех моделей:

trait HasSmartIdentifiers
{
    public static function bootHasSmartIdentifiers()
    {
        static::creating(function ($model) {
            if (empty($model->hash)) {
                $model->hash = $model->generateHash();
            }
        });
    }
    
    public function generateHash(): string
    {
        $prefix = config("id-prefixes.{$this->getTable()}.prefix");
        $salted = $prefix . $this->id . config('app.key');
        
        return $prefix . substr(md5($salted), 0, 16);
    }
    
    public function getReadableIdAttribute(): string
    {
        $prefix = config("id-prefixes.{$this->getTable()}.prefix");
        return "{$prefix}-{$this->id}";
    }
    
    public function getRouteKeyName()
    {
        return 'hash';
    }
    
    public function scopeWhereIdentifier($query, $identifier)
    {
        if (preg_match('/^[A-Z]{3}-\d+$/', $identifier)) {
            $id = substr($identifier, 4);
            return $query->where('id', $id);
        }
        
        if (preg_match('/^[A-Z]{3}[a-f0-9]{16}$/', $identifier)) {
            return $query->where('hash', $identifier);
        }
        
        if (is_numeric($identifier)) {
            return $query->where('id', $identifier);
        }
        
        return $query->where('hash', $identifier);
    }
}

Использование в модели:

class Doctor extends Model
{
    use HasSmartIdentifiers;
    
    protected $fillable = ['full_name'];
    protected $appends = ['readable_id'];
}

// Создание
$doctor = Doctor::create(['name' => 'Иванов']); 
// id = 1001, hash = "DOCa1b2c3d4e5f6g7h8"

// Везде:
$doctor->readable_id; // "DOC-1001" для админки
$doctor->hash;        // "DOCa1b2c3d4" для URL
$doctor->id;          // 1001 для связей

// Поиск по любому:
Doctor::whereIdentifier('DOC-1001')->first();
Doctor::whereIdentifier('DOCa1b2c3d4')->first();
Doctor::whereIdentifier(1001)->first();

Мониторинг и масштабирование

Artisan команда для проверки:

class CheckIdRanges extends Command
{
    public function handle()
    {
        foreach (IdPrefix::all() as $prefix) {
            $used = DB::table($prefix->table_name)->count();
            $total = $prefix->range_end - $prefix->range_start + 1;
            $percent = ($used / $total) * 100;
            
            $this->info("{$prefix->icon} {$prefix->prefix}: {$used}/{$total} ({$percent}%)");
            
            if ($percent > 90) {
                $this->warn("⚠️ Диапазон скоро закончится!");
                
                // Автоматическое расширение
                $newEnd = $prefix->range_end + ($total);
                $prefix->update(['range_end' => $newEnd]);
                $this->info("✅ Расширен до {$newEnd}");
            }
        }
    }
}

Предсказание роста:

// Прогноз на 10 лет вперед
$growthRate = $this->getGrowthRate($table);
$daysLeft = ($total - $used) / $growthRate;
$this->info("Осталось дней: " . round($daysLeft));

Почему этого нет в готовых пакетах?

Вопрос, который вы зададите: "Если это так круто, почему нет готового пакета?"

Ответ:

  1. Универсальность убивает гибкость

    • Каждый проект требует своей логики префиксов

    • Разные требования к безопасности

    • Разные паттерны доступа

  2. Ответственность

    • Пакет, который ломает ID, ломает бизнес

    • Никто не хочет поддерживать такое

  3. Эволюция технологий

    • INT → UUID → ULID → UUID v7 → ?

    • Индустрия еще ищет идеал

  4. Реальность разработки

    • 70% кода в крупных проектах — кастомный

    • Профи имеют свои скелеты

    • Стандартов нет и не будет


Что мы создали:

Автоинкремент (1980) → UUID (2005) → ULID (2016) → Наш подход (2024)
     Простота            Уникальность       Сортируемость      Умный баланс
     Но небезопасно      Но медленно        Но нечитаемо       Быстро, безопасно, читаемо

Наш подход — это гибрид, который берет лучшее:

✅ Скорость INT — 4 байта, быстрые JOIN-ы
✅ Безопасность UUID — хэши в URL
✅ Сортируемость ULID — по ID или времени
✅ Читаемость префиксов — DOC-1001 vs абракадабра
✅ Типизация — сразу видно тип сущности
✅ Шардирование — встроенное в ID
✅ Мониторинг — предсказание заполнения
✅ Масштабирование — диапазоны растут

Для кого это:

  • CRM и ERP системы — где много типов сущностей

  • Медицинские системы — где важна безопасность

  • Админки — где работают люди

  • API — где нужны короткие URL

  • Любой бизнес-проект — где важна производительность

Для кого UUID все еще нужен:

  • Микросервисы с полной изоляцией

  • Офлайн-генерация в полевых условиях

  • Блокчейн и распределенные реестры


 Заключение: Мысль на будущее

UUID мертв? Нет, но он должен знать свое место.

Идеальный идентификатор — это не технология, а архитектура. Не пытайтесь запихнуть всё в одно поле. Разделите ответственность:

  • ID — для БД и связей

  • HASH — для URL и безопасности

  • PREFIX — для людей и типизации

  • RANGE — для масштабирования

Мы не изобрели велосипед. Мы собрали идеальный велосипед для бизнес-задач. 🚲


 Бонус: Если бы мы делали пакет

composer require artisan-arch/smart-id
// config/smart-id.php
return [
    'models' => [
        Doctor::class => [
            'prefix' => 'DOC',
            'range_start' => 1000,
            'hash_driver' => 'hashids',
            'color' => 'blue',
            'icon' => '👨‍⚕️',
        ],
    ],
];

// В модели
class Doctor extends Model
{
    use ArtisanArch\SmartId\HasSmartId;
}

// Готово!
$doctor->sid;        // "DOC-jR5kL9" (system id)
$doctor->hash;       // "jR5kL9"
$doctor->readable;   // "DOC-1001"

// Мониторинг
php artisan smart-id:check
php artisan smart-id:expand

 Послесловие

Эта архитектура родилась в споре с коллегой, который настаивал на UUID. Три дня мозгового штурма, тонна тестов и — родилось решение, которое изменило наш подход к проектированию.

Попробуйте в своем следующем проекте. Возможно, вы тоже не вернетесь к UUID.