[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-26-sharding-vs-partitsionirovanie-ogromnye-tablitsy":3},{"article":4,"author":55},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":24,"related_articles":35},"d0000000-0000-0000-0000-000000000226","a0000000-0000-0000-0000-000000000015","Deep EVM #26: Шардинг vs партиционирование — архитектура для огромных таблиц","deep-evm-26-sharding-vs-partitsionirovanie-ogromnye-tablitsy","Разница между шардингом и партиционированием, когда использовать каждый подход, и архитектурные паттерны для горизонтального масштабирования PostgreSQL.","## Партиционирование vs шардинг: ключевые различия\n\nПартиционирование и шардинг часто путают, но это принципиально разные подходы к масштабированию. Партиционирование делит таблицу на секции внутри одного сервера. Шардинг распределяет данные между несколькими серверами. Первое масштабирует производительность запросов, второе — объём данных и пропускную способность.\n\nКогда одного сервера PostgreSQL перестаёт хватать — по объёму данных, по количеству записей в секунду или по количеству параллельных соединений — наступает время шардинга.\n\n## Когда партиционирования достаточно\n\nПартиционирование решает проблемы:\n- Медленный VACUUM на больших таблицах\n- Неэффективные запросы с фильтром по дате\n- Необходимость удалять старые данные (DROP PARTITION вместо DELETE)\n- Раздутые индексы\n\nПартиционирование НЕ решает:\n- Ограничение дискового пространства одного сервера\n- Ограничение write throughput одного сервера\n- Ограничение количества соединений\n\n## Стратегии шардинга\n\n### Application-level шардинг\n\nПриложение само определяет, на какой сервер направить запрос:\n\n```rust\nstruct ShardRouter {\n    shards: Vec\u003CPgPool>,\n}\n\nimpl ShardRouter {\n    fn shard_for_address(&self, address: &str) -> &PgPool {\n        let hash = xxhash_rust::xxh64::xxh64(address.as_bytes(), 0);\n        let shard_id = (hash % self.shards.len() as u64) as usize;\n        &self.shards[shard_id]\n    }\n\n    async fn get_balance(\n        &self, address: &str, token: &str\n    ) -> Result\u003CBalance> {\n        let pool = self.shard_for_address(address);\n        sqlx::query_as!(Balance,\n            \"SELECT * FROM balances WHERE address = $1 AND token = $2\",\n            address, token\n        ).fetch_one(pool).await.map_err(Into::into)\n    }\n}\n```\n\nПлюсы: полный контроль, нет дополнительных зависимостей.\nМинусы: сложность управления, ребалансировка при добавлении шардов.\n\n### Consistent hashing\n\nДля минимизации перемещения данных при добавлении шардов:\n\n```rust\nuse std::collections::BTreeMap;\n\nstruct ConsistentHash {\n    ring: BTreeMap\u003Cu64, usize>,  \u002F\u002F hash -> shard_id\n    replicas: usize,\n}\n\nimpl ConsistentHash {\n    fn new(shard_count: usize, replicas: usize) -> Self {\n        let mut ring = BTreeMap::new();\n        for shard_id in 0..shard_count {\n            for replica in 0..replicas {\n                let key = format!(\"shard-{}-replica-{}\", shard_id, replica);\n                let hash = xxhash_rust::xxh64::xxh64(key.as_bytes(), 0);\n                ring.insert(hash, shard_id);\n            }\n        }\n        Self { ring, replicas }\n    }\n\n    fn get_shard(&self, key: &str) -> usize {\n        let hash = xxhash_rust::xxh64::xxh64(key.as_bytes(), 0);\n        *self.ring.range(hash..).next()\n            .unwrap_or_else(|| self.ring.iter().next().unwrap())\n            .1\n    }\n}\n```\n\nПри добавлении нового шарда перемещается только ~1\u002FN данных.\n\n### Citus — расширение для распределённого PostgreSQL\n\nCitus превращает PostgreSQL в распределённую базу данных:\n\n```sql\n-- На координаторе\nCREATE EXTENSION citus;\n\n-- Добавить рабочие узлы\nSELECT citus_add_node('worker-1', 5432);\nSELECT citus_add_node('worker-2', 5432);\n\n-- Распределить таблицу\nSELECT create_distributed_table('transactions', 'from_address');\n```\n\nCitus прозрачно маршрутизирует запросы и поддерживает распределённые JOIN'ы.\n\n## Выбор ключа шардинга\n\nВыбор ключа шардинга — самое важное архитектурное решение:\n\n| Ключ | Плюсы | Минусы |\n|------|-------|--------|\n| Адрес пользователя | Все данные пользователя на одном шарде | Hotspots для крупных аккаунтов |\n| Hash транзакции | Равномерное распределение | Cross-shard запросы для аналитики |\n| Дата | Простая архивация | Горячая партиция для текущих данных |\n| Chain ID | Изоляция сетей | Неравномерное распределение |\n\nЗолотое правило: шардируйте по ключу, который чаще всего используется в WHERE.\n\n## Cross-shard запросы\n\nГлавная проблема шардинга — запросы, затрагивающие несколько шардов:\n\n```rust\nasync fn global_total_volume(\n    router: &ShardRouter,\n) -> Result\u003Cf64> {\n    let mut handles = Vec::new();\n\n    for pool in &router.shards {\n        let pool = pool.clone();\n        handles.push(tokio::spawn(async move {\n            sqlx::query_scalar!(\n                \"SELECT COALESCE(SUM(value), 0) FROM transactions\"\n            ).fetch_one(&pool).await\n        }));\n    }\n\n    let mut total = 0.0;\n    for handle in handles {\n        total += handle.await??;\n    }\n    Ok(total)\n}\n```\n\nCross-shard запросы медленнее и сложнее. Минимизируйте их через правильный выбор ключа шардинга.\n\n## Read replicas как альтернатива\n\nПрежде чем шардировать, рассмотрите read replicas:\n\n```rust\nstruct DbRouting {\n    writer: PgPool,      \u002F\u002F Primary\n    readers: Vec\u003CPgPool>, \u002F\u002F Replicas\n    counter: AtomicUsize,\n}\n\nimpl DbRouting {\n    fn read_pool(&self) -> &PgPool {\n        let idx = self.counter.fetch_add(1, Ordering::Relaxed)\n            % self.readers.len();\n        &self.readers[idx]\n    }\n\n    fn write_pool(&self) -> &PgPool {\n        &self.writer\n    }\n}\n```\n\nRead replicas масштабируют чтение без сложности шардинга. Для многих приложений этого достаточно.\n\n## Архитектурная матрица решений\n\n| Проблема | Решение |\n|----------|--------|\n| Медленные запросы по дате | Партиционирование по range |\n| Медленный VACUUM | Партиционирование |\n| Много чтений, мало записей | Read replicas |\n| Много записей | Шардинг |\n| Огромный объём данных | Шардинг + партиционирование |\n| Аналитические запросы | Отдельная OLAP-база (ClickHouse) |\n\n## Комбинирование подходов\n\nВ реальных системах подходы комбинируются:\n\n1. **Шардинг** по chain_id — каждая сеть на своём сервере\n2. **Партиционирование** по дате внутри каждого шарда\n3. **Read replicas** для аналитических запросов\n4. **Materialized views** для агрегированных метрик\n\nЭта архитектура масштабируется до сотен миллиардов записей.\n\n## Заключение\n\nШардинг — это архитектурное решение, которое нельзя легко отменить. Начинайте с партиционирования и read replicas. Переходите к шардингу только когда исчерпаны возможности вертикального масштабирования. И всегда выбирайте ключ шардинга, исходя из паттернов доступа к данным.","\u003Ch2 id=\"vs\">Партиционирование vs шардинг: ключевые различия\u003C\u002Fh2>\n\u003Cp>Партиционирование и шардинг часто путают, но это принципиально разные подходы к масштабированию. Партиционирование делит таблицу на секции внутри одного сервера. Шардинг распределяет данные между несколькими серверами. Первое масштабирует производительность запросов, второе — объём данных и пропускную способность.\u003C\u002Fp>\n\u003Cp>Когда одного сервера PostgreSQL перестаёт хватать — по объёму данных, по количеству записей в секунду или по количеству параллельных соединений — наступает время шардинга.\u003C\u002Fp>\n\u003Ch2 id=\"\">Когда партиционирования достаточно\u003C\u002Fh2>\n\u003Cp>Партиционирование решает проблемы:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Медленный VACUUM на больших таблицах\u003C\u002Fli>\n\u003Cli>Неэффективные запросы с фильтром по дате\u003C\u002Fli>\n\u003Cli>Необходимость удалять старые данные (DROP PARTITION вместо DELETE)\u003C\u002Fli>\n\u003Cli>Раздутые индексы\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Партиционирование НЕ решает:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Ограничение дискового пространства одного сервера\u003C\u002Fli>\n\u003Cli>Ограничение write throughput одного сервера\u003C\u002Fli>\n\u003Cli>Ограничение количества соединений\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"\">Стратегии шардинга\u003C\u002Fh2>\n\u003Ch3>Application-level шардинг\u003C\u002Fh3>\n\u003Cp>Приложение само определяет, на какой сервер направить запрос:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct ShardRouter {\n    shards: Vec&lt;PgPool&gt;,\n}\n\nimpl ShardRouter {\n    fn shard_for_address(&amp;self, address: &amp;str) -&gt; &amp;PgPool {\n        let hash = xxhash_rust::xxh64::xxh64(address.as_bytes(), 0);\n        let shard_id = (hash % self.shards.len() as u64) as usize;\n        &amp;self.shards[shard_id]\n    }\n\n    async fn get_balance(\n        &amp;self, address: &amp;str, token: &amp;str\n    ) -&gt; Result&lt;Balance&gt; {\n        let pool = self.shard_for_address(address);\n        sqlx::query_as!(Balance,\n            \"SELECT * FROM balances WHERE address = $1 AND token = $2\",\n            address, token\n        ).fetch_one(pool).await.map_err(Into::into)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Плюсы: полный контроль, нет дополнительных зависимостей.\nМинусы: сложность управления, ребалансировка при добавлении шардов.\u003C\u002Fp>\n\u003Ch3>Consistent hashing\u003C\u002Fh3>\n\u003Cp>Для минимизации перемещения данных при добавлении шардов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::collections::BTreeMap;\n\nstruct ConsistentHash {\n    ring: BTreeMap&lt;u64, usize&gt;,  \u002F\u002F hash -&gt; shard_id\n    replicas: usize,\n}\n\nimpl ConsistentHash {\n    fn new(shard_count: usize, replicas: usize) -&gt; Self {\n        let mut ring = BTreeMap::new();\n        for shard_id in 0..shard_count {\n            for replica in 0..replicas {\n                let key = format!(\"shard-{}-replica-{}\", shard_id, replica);\n                let hash = xxhash_rust::xxh64::xxh64(key.as_bytes(), 0);\n                ring.insert(hash, shard_id);\n            }\n        }\n        Self { ring, replicas }\n    }\n\n    fn get_shard(&amp;self, key: &amp;str) -&gt; usize {\n        let hash = xxhash_rust::xxh64::xxh64(key.as_bytes(), 0);\n        *self.ring.range(hash..).next()\n            .unwrap_or_else(|| self.ring.iter().next().unwrap())\n            .1\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>При добавлении нового шарда перемещается только ~1\u002FN данных.\u003C\u002Fp>\n\u003Ch3>Citus — расширение для распределённого PostgreSQL\u003C\u002Fh3>\n\u003Cp>Citus превращает PostgreSQL в распределённую базу данных:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- На координаторе\nCREATE EXTENSION citus;\n\n-- Добавить рабочие узлы\nSELECT citus_add_node('worker-1', 5432);\nSELECT citus_add_node('worker-2', 5432);\n\n-- Распределить таблицу\nSELECT create_distributed_table('transactions', 'from_address');\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Citus прозрачно маршрутизирует запросы и поддерживает распределённые JOIN’ы.\u003C\u002Fp>\n\u003Ch2 id=\"\">Выбор ключа шардинга\u003C\u002Fh2>\n\u003Cp>Выбор ключа шардинга — самое важное архитектурное решение:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Ключ\u003C\u002Fth>\u003Cth>Плюсы\u003C\u002Fth>\u003Cth>Минусы\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Адрес пользователя\u003C\u002Ftd>\u003Ctd>Все данные пользователя на одном шарде\u003C\u002Ftd>\u003Ctd>Hotspots для крупных аккаунтов\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Hash транзакции\u003C\u002Ftd>\u003Ctd>Равномерное распределение\u003C\u002Ftd>\u003Ctd>Cross-shard запросы для аналитики\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Дата\u003C\u002Ftd>\u003Ctd>Простая архивация\u003C\u002Ftd>\u003Ctd>Горячая партиция для текущих данных\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Chain ID\u003C\u002Ftd>\u003Ctd>Изоляция сетей\u003C\u002Ftd>\u003Ctd>Неравномерное распределение\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Золотое правило: шардируйте по ключу, который чаще всего используется в WHERE.\u003C\u002Fp>\n\u003Ch2 id=\"cross-shard\">Cross-shard запросы\u003C\u002Fh2>\n\u003Cp>Главная проблема шардинга — запросы, затрагивающие несколько шардов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">async fn global_total_volume(\n    router: &amp;ShardRouter,\n) -&gt; Result&lt;f64&gt; {\n    let mut handles = Vec::new();\n\n    for pool in &amp;router.shards {\n        let pool = pool.clone();\n        handles.push(tokio::spawn(async move {\n            sqlx::query_scalar!(\n                \"SELECT COALESCE(SUM(value), 0) FROM transactions\"\n            ).fetch_one(&amp;pool).await\n        }));\n    }\n\n    let mut total = 0.0;\n    for handle in handles {\n        total += handle.await??;\n    }\n    Ok(total)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Cross-shard запросы медленнее и сложнее. Минимизируйте их через правильный выбор ключа шардинга.\u003C\u002Fp>\n\u003Ch2 id=\"read-replicas\">Read replicas как альтернатива\u003C\u002Fh2>\n\u003Cp>Прежде чем шардировать, рассмотрите read replicas:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct DbRouting {\n    writer: PgPool,      \u002F\u002F Primary\n    readers: Vec&lt;PgPool&gt;, \u002F\u002F Replicas\n    counter: AtomicUsize,\n}\n\nimpl DbRouting {\n    fn read_pool(&amp;self) -&gt; &amp;PgPool {\n        let idx = self.counter.fetch_add(1, Ordering::Relaxed)\n            % self.readers.len();\n        &amp;self.readers[idx]\n    }\n\n    fn write_pool(&amp;self) -&gt; &amp;PgPool {\n        &amp;self.writer\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Read replicas масштабируют чтение без сложности шардинга. Для многих приложений этого достаточно.\u003C\u002Fp>\n\u003Ch2 id=\"\">Архитектурная матрица решений\u003C\u002Fh2>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Проблема\u003C\u002Fth>\u003Cth>Решение\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Медленные запросы по дате\u003C\u002Ftd>\u003Ctd>Партиционирование по range\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Медленный VACUUM\u003C\u002Ftd>\u003Ctd>Партиционирование\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Много чтений, мало записей\u003C\u002Ftd>\u003Ctd>Read replicas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Много записей\u003C\u002Ftd>\u003Ctd>Шардинг\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Огромный объём данных\u003C\u002Ftd>\u003Ctd>Шардинг + партиционирование\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Аналитические запросы\u003C\u002Ftd>\u003Ctd>Отдельная OLAP-база (ClickHouse)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Ch2 id=\"\">Комбинирование подходов\u003C\u002Fh2>\n\u003Cp>В реальных системах подходы комбинируются:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Шардинг\u003C\u002Fstrong> по chain_id — каждая сеть на своём сервере\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Партиционирование\u003C\u002Fstrong> по дате внутри каждого шарда\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Read replicas\u003C\u002Fstrong> для аналитических запросов\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Materialized views\u003C\u002Fstrong> для агрегированных метрик\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Эта архитектура масштабируется до сотен миллиардов записей.\u003C\u002Fp>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Шардинг — это архитектурное решение, которое нельзя легко отменить. Начинайте с партиционирования и read replicas. Переходите к шардингу только когда исчерпаны возможности вертикального масштабирования. И всегда выбирайте ключ шардинга, исходя из паттернов доступа к данным.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.976165Z","Шардинг vs партиционирование — архитектура для огромных таблиц","Разница между шардингом и партиционированием PostgreSQL: consistent hashing, Citus, read replicas и выбор ключа шардинга.","шардинг партиционирование postgresql",null,"index, follow",[22,27,31],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000012","DevOps","devops","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000005","PostgreSQL","postgresql",[36,43,49],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":41,"published_at":42},"d0200000-0000-0000-0000-000000000013","Почему Бали становится хабом импакт-технологий Юго-Восточной Азии в 2026 году","pochemu-bali-stanovitsya-khabom-impakt-tekhnologiy-2026","Бали занимает 16-е место среди стартап-экосистем Юго-Восточной Азии. Растущая концентрация Web3-разработчиков, ИИ-стартапов в области устойчивого развития и компаний в сфере эко-тревел-технологий формирует нишу столицы импакт-технологий региона.","Инженерия","2026-03-28T10:44:37.953039Z",{"id":44,"title":45,"slug":46,"excerpt":47,"locale":12,"category_name":41,"published_at":48},"d0200000-0000-0000-0000-000000000012","Защита данных в ASEAN: чек-лист разработчика для мультистранового комплаенса","zashchita-dannykh-asean-chek-list-razrabotchika-komplaens","Семь стран ASEAN имеют собственные законы о защите данных с разными моделями согласия, требованиями к локализации и штрафами. Практический чек-лист для разработчиков мультистрановых приложений.","2026-03-28T10:44:37.944001Z",{"id":50,"title":51,"slug":52,"excerpt":53,"locale":12,"category_name":41,"published_at":54},"d0200000-0000-0000-0000-000000000011","Цифровая трансформация Индонезии на $29 миллиардов: возможности для софтверных компаний","tsifrovaya-transformatsiya-indonezii-29-milliardov-vozmozhnosti-dlya-kompaniy","Рынок IT-услуг Индонезии вырастет с $24,37 млрд в 2025 году до $29,03 млрд в 2026 году. Облачная инфраструктура, искусственный интеллект, электронная коммерция и дата-центры обеспечивают самый быстрый рост в Юго-Восточной Азии.","2026-03-28T10:44:37.917095Z",{"id":13,"name":56,"slug":57,"bio":58,"photo_url":19,"linkedin":19,"role":59,"created_at":60,"updated_at":60},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]