[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-23-otladka-proizvoditelnosti-baza-dannykh-latentnost":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":35,"related_articles":36},"d0000000-0000-0000-0000-000000000223","a0000000-0000-0000-0000-000000000016","Deep EVM #23: Отладка производительности — когда чтения из БД убивают латентность","deep-evm-23-otladka-proizvoditelnosti-baza-dannykh-latentnost","Диагностика и решение проблем производительности, вызванных неоптимальными запросами к базе данных: N+1 проблема, отсутствие индексов и пул соединений.","## Почему база данных — главный источник латентности\n\nВ типичном веб-приложении на Rust\u002FAxum 80-95% времени ответа приходится на операции с базой данных. Ваш Rust-код исполняется за микросекунды, но один неоптимальный SQL-запрос может добавить сотни миллисекунд. Когда пользователи жалуются на медленность — ищите проблему в базе данных.\n\nВ этой статье мы разберём типичные проблемы производительности, связанные с PostgreSQL, и инструменты для их диагностики.\n\n## Проблема N+1 запросов\n\nКлассическая проблема: загружаем список статей, затем для каждой делаем отдельный запрос за автором:\n\n```rust\n\u002F\u002F ПЛОХО: N+1 запросов\nlet articles = sqlx::query_as!(Article, \"SELECT * FROM articles LIMIT 20\")\n    .fetch_all(&pool).await?;\n\nfor article in &articles {\n    let author = sqlx::query_as!(Author,\n        \"SELECT * FROM authors WHERE id = $1\", article.author_id\n    ).fetch_one(&pool).await?;\n    \u002F\u002F ... используем author\n}\n```\n\nРезультат: 1 + 20 = 21 запрос вместо одного. На латентности сети 1мс каждый — это 21мс только на round-trip'ы.\n\n### Решение: JOIN\n\n```rust\n\u002F\u002F ХОРОШО: 1 запрос\nlet articles_with_authors = sqlx::query_as!(\n    ArticleWithAuthor,\n    r#\"SELECT a.*, au.name as author_name\n       FROM articles a\n       JOIN authors au ON a.author_id = au.id\n       LIMIT 20\"#\n).fetch_all(&pool).await?;\n```\n\n### Решение: batch-загрузка\n\nКогда JOIN неуместен (например, разные таблицы для разных типов):\n\n```rust\nlet author_ids: Vec\u003CUuid> = articles.iter()\n    .map(|a| a.author_id).collect();\n\nlet authors = sqlx::query_as!(Author,\n    \"SELECT * FROM authors WHERE id = ANY($1)\",\n    &author_ids\n).fetch_all(&pool).await?;\n\nlet author_map: HashMap\u003CUuid, Author> = authors\n    .into_iter()\n    .map(|a| (a.id, a))\n    .collect();\n```\n\n2 запроса вместо 21.\n\n## Отсутствие индексов\n\nВторая по частоте проблема — отсутствие нужных индексов. PostgreSQL выполняет sequential scan по всей таблице вместо index scan.\n\n### Диагностика с EXPLAIN ANALYZE\n\n```sql\nEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT * FROM articles WHERE slug = 'my-article';\n```\n\nЕсли видите `Seq Scan` на большой таблице — нужен индекс:\n\n```sql\nCREATE INDEX CONCURRENTLY idx_articles_slug ON articles (slug);\n```\n\n### Составные индексы\n\nДля запросов с несколькими условиями:\n\n```sql\n-- Запрос\nSELECT * FROM articles\nWHERE category_id = $1 AND published = true\nORDER BY published_at DESC;\n\n-- Индекс\nCREATE INDEX idx_articles_cat_pub\nON articles (category_id, published_at DESC)\nWHERE published = true;\n```\n\nЧастичный индекс (`WHERE published = true`) ещё эффективнее — индексирует только опубликованные статьи.\n\n## Пул соединений: настройка и подводные камни\n\nsqlx по умолчанию создаёт пул с 10 соединениями. Для нагруженных приложений этого может быть мало:\n\n```rust\nlet pool = PgPoolOptions::new()\n    .max_connections(50)\n    .min_connections(5)\n    .acquire_timeout(Duration::from_secs(3))\n    .idle_timeout(Duration::from_secs(600))\n    .max_lifetime(Duration::from_secs(1800))\n    .connect(&database_url)\n    .await?;\n```\n\nКлючевые параметры:\n- **max_connections** — не больше, чем `max_connections` PostgreSQL минус резерв\n- **acquire_timeout** — сколько ждать свободного соединения\n- **idle_timeout** — когда закрывать неиспользуемые соединения\n\n### Формула для max_connections\n\nОбщее правило: `max_connections = (количество ядер CPU) * 2 + эффективное количество дисков`. Для SSD-систем с 4 ядрами — 10-20 соединений обычно оптимально.\n\n## Медленные запросы: pg_stat_statements\n\nВключите расширение `pg_stat_statements` для отслеживания медленных запросов:\n\n```sql\nCREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n\n-- Топ-10 медленных запросов по общему времени\nSELECT query, calls, mean_exec_time, total_exec_time\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 10;\n```\n\n## Кэширование результатов\n\nДля часто запрашиваемых и редко меняющихся данных используйте in-memory кэш:\n\n```rust\nuse moka::future::Cache;\n\nlet cache: Cache\u003CString, Article> = Cache::builder()\n    .max_capacity(1000)\n    .time_to_live(Duration::from_secs(300))\n    .build();\n\nasync fn get_article(\n    pool: &PgPool,\n    cache: &Cache\u003CString, Article>,\n    slug: &str,\n) -> Result\u003CArticle> {\n    if let Some(article) = cache.get(slug) {\n        return Ok(article);\n    }\n    let article = sqlx::query_as!(Article,\n        \"SELECT * FROM articles WHERE slug = $1\", slug\n    ).fetch_one(pool).await?;\n    cache.insert(slug.to_string(), article.clone()).await;\n    Ok(article)\n}\n```\n\nmoka — это высокопроизводительный конкурентный кэш для Rust, вдохновлённый Caffeine из Java.\n\n## Инструментирование с tracing\n\nДобавьте tracing для всех SQL-запросов:\n\n```rust\nlet pool = PgPoolOptions::new()\n    .after_connect(|conn, _meta| {\n        Box::pin(async move {\n            conn.execute(\"SET application_name = 'my-app'\")\n                .await?;\n            Ok(())\n        })\n    })\n    .connect(&database_url)\n    .await?;\n```\n\nИспользуйте `sqlx::query!` с `tracing` для автоматического логирования времени выполнения каждого запроса.\n\n## Чеклист производительности БД\n\n1. Нет ли N+1 запросов? Используйте JOIN или batch-загрузку.\n2. Есть ли индексы для всех WHERE\u002FORDER BY? Проверьте с EXPLAIN ANALYZE.\n3. Правильно ли настроен пул соединений? Не больше, чем PostgreSQL может обработать.\n4. Включён ли pg_stat_statements? Отслеживайте медленные запросы.\n5. Нужно ли кэширование? moka для in-process, Redis для распределённого.\n6. Есть ли tracing? Измеряйте каждый запрос.\n\n## Заключение\n\nОптимизация производительности базы данных — это не одноразовое мероприятие, а непрерывный процесс. Инструментируйте запросы, мониторьте латентность, анализируйте explain-планы. В 90% случаев проблема — это отсутствие индекса или N+1 запросов. Эти проблемы легко обнаружить и легко исправить, если у вас есть правильные инструменты.","\u003Ch2 id=\"\">Почему база данных — главный источник латентности\u003C\u002Fh2>\n\u003Cp>В типичном веб-приложении на Rust\u002FAxum 80-95% времени ответа приходится на операции с базой данных. Ваш Rust-код исполняется за микросекунды, но один неоптимальный SQL-запрос может добавить сотни миллисекунд. Когда пользователи жалуются на медленность — ищите проблему в базе данных.\u003C\u002Fp>\n\u003Cp>В этой статье мы разберём типичные проблемы производительности, связанные с PostgreSQL, и инструменты для их диагностики.\u003C\u002Fp>\n\u003Ch2 id=\"n-1\">Проблема N+1 запросов\u003C\u002Fh2>\n\u003Cp>Классическая проблема: загружаем список статей, затем для каждой делаем отдельный запрос за автором:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F ПЛОХО: N+1 запросов\nlet articles = sqlx::query_as!(Article, \"SELECT * FROM articles LIMIT 20\")\n    .fetch_all(&amp;pool).await?;\n\nfor article in &amp;articles {\n    let author = sqlx::query_as!(Author,\n        \"SELECT * FROM authors WHERE id = $1\", article.author_id\n    ).fetch_one(&amp;pool).await?;\n    \u002F\u002F ... используем author\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Результат: 1 + 20 = 21 запрос вместо одного. На латентности сети 1мс каждый — это 21мс только на round-trip’ы.\u003C\u002Fp>\n\u003Ch3>Решение: JOIN\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F ХОРОШО: 1 запрос\nlet articles_with_authors = sqlx::query_as!(\n    ArticleWithAuthor,\n    r#\"SELECT a.*, au.name as author_name\n       FROM articles a\n       JOIN authors au ON a.author_id = au.id\n       LIMIT 20\"#\n).fetch_all(&amp;pool).await?;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Решение: batch-загрузка\u003C\u002Fh3>\n\u003Cp>Когда JOIN неуместен (например, разные таблицы для разных типов):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let author_ids: Vec&lt;Uuid&gt; = articles.iter()\n    .map(|a| a.author_id).collect();\n\nlet authors = sqlx::query_as!(Author,\n    \"SELECT * FROM authors WHERE id = ANY($1)\",\n    &amp;author_ids\n).fetch_all(&amp;pool).await?;\n\nlet author_map: HashMap&lt;Uuid, Author&gt; = authors\n    .into_iter()\n    .map(|a| (a.id, a))\n    .collect();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>2 запроса вместо 21.\u003C\u002Fp>\n\u003Ch2 id=\"\">Отсутствие индексов\u003C\u002Fh2>\n\u003Cp>Вторая по частоте проблема — отсутствие нужных индексов. PostgreSQL выполняет sequential scan по всей таблице вместо index scan.\u003C\u002Fp>\n\u003Ch3>Диагностика с EXPLAIN ANALYZE\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT * FROM articles WHERE slug = 'my-article';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Если видите \u003Ccode>Seq Scan\u003C\u002Fcode> на большой таблице — нужен индекс:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE INDEX CONCURRENTLY idx_articles_slug ON articles (slug);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Составные индексы\u003C\u002Fh3>\n\u003Cp>Для запросов с несколькими условиями:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Запрос\nSELECT * FROM articles\nWHERE category_id = $1 AND published = true\nORDER BY published_at DESC;\n\n-- Индекс\nCREATE INDEX idx_articles_cat_pub\nON articles (category_id, published_at DESC)\nWHERE published = true;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Частичный индекс (\u003Ccode>WHERE published = true\u003C\u002Fcode>) ещё эффективнее — индексирует только опубликованные статьи.\u003C\u002Fp>\n\u003Ch2 id=\"\">Пул соединений: настройка и подводные камни\u003C\u002Fh2>\n\u003Cp>sqlx по умолчанию создаёт пул с 10 соединениями. Для нагруженных приложений этого может быть мало:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let pool = PgPoolOptions::new()\n    .max_connections(50)\n    .min_connections(5)\n    .acquire_timeout(Duration::from_secs(3))\n    .idle_timeout(Duration::from_secs(600))\n    .max_lifetime(Duration::from_secs(1800))\n    .connect(&amp;database_url)\n    .await?;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ключевые параметры:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>max_connections\u003C\u002Fstrong> — не больше, чем \u003Ccode>max_connections\u003C\u002Fcode> PostgreSQL минус резерв\u003C\u002Fli>\n\u003Cli>\u003Cstrong>acquire_timeout\u003C\u002Fstrong> — сколько ждать свободного соединения\u003C\u002Fli>\n\u003Cli>\u003Cstrong>idle_timeout\u003C\u002Fstrong> — когда закрывать неиспользуемые соединения\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Формула для max_connections\u003C\u002Fh3>\n\u003Cp>Общее правило: \u003Ccode>max_connections = (количество ядер CPU) * 2 + эффективное количество дисков\u003C\u002Fcode>. Для SSD-систем с 4 ядрами — 10-20 соединений обычно оптимально.\u003C\u002Fp>\n\u003Ch2 id=\"pg-stat-statements\">Медленные запросы: pg_stat_statements\u003C\u002Fh2>\n\u003Cp>Включите расширение \u003Ccode>pg_stat_statements\u003C\u002Fcode> для отслеживания медленных запросов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n\n-- Топ-10 медленных запросов по общему времени\nSELECT query, calls, mean_exec_time, total_exec_time\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 10;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Кэширование результатов\u003C\u002Fh2>\n\u003Cp>Для часто запрашиваемых и редко меняющихся данных используйте in-memory кэш:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use moka::future::Cache;\n\nlet cache: Cache&lt;String, Article&gt; = Cache::builder()\n    .max_capacity(1000)\n    .time_to_live(Duration::from_secs(300))\n    .build();\n\nasync fn get_article(\n    pool: &amp;PgPool,\n    cache: &amp;Cache&lt;String, Article&gt;,\n    slug: &amp;str,\n) -&gt; Result&lt;Article&gt; {\n    if let Some(article) = cache.get(slug) {\n        return Ok(article);\n    }\n    let article = sqlx::query_as!(Article,\n        \"SELECT * FROM articles WHERE slug = $1\", slug\n    ).fetch_one(pool).await?;\n    cache.insert(slug.to_string(), article.clone()).await;\n    Ok(article)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>moka — это высокопроизводительный конкурентный кэш для Rust, вдохновлённый Caffeine из Java.\u003C\u002Fp>\n\u003Ch2 id=\"tracing\">Инструментирование с tracing\u003C\u002Fh2>\n\u003Cp>Добавьте tracing для всех SQL-запросов:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">let pool = PgPoolOptions::new()\n    .after_connect(|conn, _meta| {\n        Box::pin(async move {\n            conn.execute(\"SET application_name = 'my-app'\")\n                .await?;\n            Ok(())\n        })\n    })\n    .connect(&amp;database_url)\n    .await?;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Используйте \u003Ccode>sqlx::query!\u003C\u002Fcode> с \u003Ccode>tracing\u003C\u002Fcode> для автоматического логирования времени выполнения каждого запроса.\u003C\u002Fp>\n\u003Ch2 id=\"\">Чеклист производительности БД\u003C\u002Fh2>\n\u003Col>\n\u003Cli>Нет ли N+1 запросов? Используйте JOIN или batch-загрузку.\u003C\u002Fli>\n\u003Cli>Есть ли индексы для всех WHERE\u002FORDER BY? Проверьте с EXPLAIN ANALYZE.\u003C\u002Fli>\n\u003Cli>Правильно ли настроен пул соединений? Не больше, чем PostgreSQL может обработать.\u003C\u002Fli>\n\u003Cli>Включён ли pg_stat_statements? Отслеживайте медленные запросы.\u003C\u002Fli>\n\u003Cli>Нужно ли кэширование? moka для in-process, Redis для распределённого.\u003C\u002Fli>\n\u003Cli>Есть ли tracing? Измеряйте каждый запрос.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Оптимизация производительности базы данных — это не одноразовое мероприятие, а непрерывный процесс. Инструментируйте запросы, мониторьте латентность, анализируйте explain-планы. В 90% случаев проблема — это отсутствие индекса или N+1 запросов. Эти проблемы легко обнаружить и легко исправить, если у вас есть правильные инструменты.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.946911Z","Отладка производительности — когда чтения из БД убивают латентность","Диагностика проблем производительности PostgreSQL: N+1 запросы, индексы, пул соединений и кэширование.","производительность postgresql rust",null,"index, follow",[22,27,31],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000005","PostgreSQL","postgresql",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust","Инженерия",[37,43,49],{"id":38,"title":39,"slug":40,"excerpt":41,"locale":12,"category_name":35,"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":35,"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":35,"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"]