[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-27-proizvoditelnost-postgresql-indeksy-vacuum":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-000000000227","a0000000-0000-0000-0000-000000000015","Deep EVM #27: Производительность PostgreSQL — индексы, VACUUM и оптимизация запросов","deep-evm-27-proizvoditelnost-postgresql-indeksy-vacuum","Глубокое погружение в производительность PostgreSQL: типы индексов, настройка VACUUM, оптимизация запросов и мониторинг для высоконагруженных систем.","## PostgreSQL под нагрузкой: что ломается первым\n\nPostgreSQL — это надёжная, зрелая СУБД, способная обрабатывать тысячи транзакций в секунду. Но без правильной настройки производительность деградирует по мере роста данных. В блокчейн-инфраструктуре, где таблицы растут на миллионы строк в день, правильная настройка — это разница между системой, отвечающей за 5мс, и системой, отвечающей за 5 секунд.\n\n## Типы индексов и когда их использовать\n\n### B-tree (по умолчанию)\n\nУниверсальный индекс для операторов `=`, `\u003C`, `>`, `BETWEEN`, `IN`, `LIKE 'prefix%'`:\n\n```sql\nCREATE INDEX idx_tx_hash ON transactions (tx_hash);\nCREATE INDEX idx_tx_block_time ON transactions (block_number, created_at DESC);\n```\n\n### GIN (Generalized Inverted Index)\n\nДля массивов, JSONB и полнотекстового поиска:\n\n```sql\n-- JSONB\nCREATE INDEX idx_events_data ON events USING GIN (data jsonb_path_ops);\n\n-- Полнотекстовый поиск\nCREATE INDEX idx_articles_fts ON articles\nUSING GIN (to_tsvector('russian', title || ' ' || content_md));\n\n-- Массивы\nCREATE INDEX idx_tags ON articles USING GIN (tags);\n```\n\n### GiST (Generalized Search Tree)\n\nДля геоданных, диапазонов и полнотекстового поиска:\n\n```sql\n-- IP-диапазоны\nCREATE INDEX idx_ip_ranges ON access_log USING GIST (ip_range);\n\n-- Диапазоны блоков\nCREATE INDEX idx_block_ranges ON sync_jobs\nUSING GIST (int8range(start_block, end_block));\n```\n\n### BRIN (Block Range Index)\n\nДля колоночно-коррелированных данных (монотонно растущие значения):\n\n```sql\n-- Для таблицы, где строки вставляются хронологически\nCREATE INDEX idx_tx_created_brin ON transactions\nUSING BRIN (created_at) WITH (pages_per_range = 32);\n```\n\nBRIN-индекс занимает в 100-1000 раз меньше места, чем B-tree, но эффективен только когда физический порядок строк коррелирует с индексированным столбцом.\n\n### Partial и Expression индексы\n\n```sql\n-- Индексировать только актуальные записи\nCREATE INDEX idx_pending_tx ON transactions (created_at)\nWHERE status = 'pending';\n\n-- Индекс на выражении\nCREATE INDEX idx_tx_lower_hash ON transactions (LOWER(tx_hash));\n```\n\n## VACUUM: жизненный цикл строк в PostgreSQL\n\nPostgreSQL использует MVCC (Multi-Version Concurrency Control). Каждый UPDATE и DELETE создаёт новую версию строки, а старая помечается как мёртвая. VACUUM удаляет мёртвые строки и освобождает место.\n\n### Настройка autovacuum\n\n```sql\nALTER TABLE transactions SET (\n    autovacuum_vacuum_threshold = 10000,\n    autovacuum_vacuum_scale_factor = 0.01,  -- 1% вместо 20% по умолчанию\n    autovacuum_analyze_threshold = 5000,\n    autovacuum_analyze_scale_factor = 0.005,\n    autovacuum_vacuum_cost_delay = 2  -- мс, агрессивнее\n);\n```\n\nДля таблиц с высокой частотой обновлений (> 1000 UPDATE\u002Fs) стандартные настройки autovacuum слишком консервативны.\n\n### Мониторинг мёртвых строк\n\n```sql\nSELECT\n    relname,\n    n_live_tup,\n    n_dead_tup,\n    ROUND(n_dead_tup::numeric \u002F NULLIF(n_live_tup, 0) * 100, 2) AS dead_pct,\n    last_vacuum,\n    last_autovacuum\nFROM pg_stat_user_tables\nWHERE n_dead_tup > 10000\nORDER BY n_dead_tup DESC;\n```\n\nЕсли `dead_pct` превышает 10-20%, autovacuum не справляется.\n\n## Оптимизация запросов\n\n### EXPLAIN ANALYZE — ваш главный инструмент\n\n```sql\nEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT a.*, c.name as category_name\nFROM articles a\nJOIN categories c ON a.category_id = c.id\nWHERE a.published = true AND a.locale = 'ru'\nORDER BY a.published_at DESC\nLIMIT 20;\n```\n\nНа что обращать внимание:\n- **Seq Scan** на большой таблице — нужен индекс\n- **Nested Loop** с большим outer — рассмотрите Hash Join\n- **Sort** с `external merge Disk` — не хватает work_mem\n- **Buffers: shared hit** vs **shared read** — кэш-хит рейт\n\n### Типичные оптимизации\n\n```sql\n-- ПЛОХО: SELECT *\nSELECT * FROM articles WHERE category_id = $1;\n\n-- ХОРОШО: только нужные колонки\nSELECT id, title, slug, excerpt, published_at\nFROM articles WHERE category_id = $1;\n\n-- ПЛОХО: OFFSET для пагинации\nSELECT * FROM articles ORDER BY created_at DESC\nLIMIT 20 OFFSET 10000;\n\n-- ХОРОШО: keyset пагинация\nSELECT * FROM articles\nWHERE created_at \u003C $1\nORDER BY created_at DESC\nLIMIT 20;\n```\n\nOFFSET сканирует и отбрасывает строки. Keyset pagination использует индекс.\n\n## Настройка PostgreSQL для производительности\n\nКлючевые параметры:\n\n```ini\n# Память\nshared_buffers = 4GB            # 25% от RAM\neffective_cache_size = 12GB     # 75% от RAM\nwork_mem = 64MB                 # Для сортировок и хешей\nmaintenance_work_mem = 1GB      # Для VACUUM, CREATE INDEX\n\n# WAL\nwal_buffers = 64MB\ncheckpoint_completion_target = 0.9\nmax_wal_size = 4GB\n\n# Планировщик\nrandom_page_cost = 1.1          # Для SSD (по умолчанию 4.0)\neffective_io_concurrency = 200  # Для SSD\n\n# Параллелизм\nmax_parallel_workers_per_gather = 4\nmax_parallel_workers = 8\n```\n\n## Мониторинг в реальном времени\n\n### pg_stat_statements\n\n```sql\nSELECT\n    LEFT(query, 80) AS query,\n    calls,\n    ROUND(mean_exec_time::numeric, 2) AS mean_ms,\n    ROUND(total_exec_time::numeric, 2) AS total_ms,\n    rows\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 20;\n```\n\n### Активные запросы\n\n```sql\nSELECT\n    pid,\n    now() - pg_stat_activity.query_start AS duration,\n    state,\n    LEFT(query, 80) AS query\nFROM pg_stat_activity\nWHERE state != 'idle'\nORDER BY duration DESC;\n```\n\n### Блокировки\n\n```sql\nSELECT\n    blocked.pid AS blocked_pid,\n    blocking.pid AS blocking_pid,\n    blocked.query AS blocked_query\nFROM pg_locks blocked\nJOIN pg_stat_activity blocked_act ON blocked.pid = blocked_act.pid\nJOIN pg_locks blocking ON blocked.locktype = blocking.locktype\nJOIN pg_stat_activity blocking_act ON blocking.pid = blocking_act.pid\nWHERE NOT blocked.granted;\n```\n\n## Connection pooling с PgBouncer\n\nPostgreSQL создаёт отдельный процесс для каждого соединения (~5MB RAM). Для 500 соединений — 2.5GB только на соединения. PgBouncer решает эту проблему:\n\n```ini\n[databases]\nmydb = host=127.0.0.1 port=5432 dbname=mydb\n\n[pgbouncer]\nlisten_port = 6432\npool_mode = transaction    # Переиспользовать после транзакции\nmax_client_conn = 1000     # Макс. клиентских соединений\ndefault_pool_size = 50     # Размер пула к PostgreSQL\n```\n\nВ режиме `transaction` 1000 клиентов мультиплексируются через 50 реальных соединений.\n\n## Заключение\n\nПроизводительность PostgreSQL — это не одна настройка, а комбинация правильных индексов, настроенного VACUUM, оптимизированных запросов и мониторинга. Начните с pg_stat_statements для поиска проблемных запросов, используйте EXPLAIN ANALYZE для диагностики, и помните: 90% проблем производительности решаются добавлением правильного индекса или устранением N+1 запросов.","\u003Ch2 id=\"postgresql\">PostgreSQL под нагрузкой: что ломается первым\u003C\u002Fh2>\n\u003Cp>PostgreSQL — это надёжная, зрелая СУБД, способная обрабатывать тысячи транзакций в секунду. Но без правильной настройки производительность деградирует по мере роста данных. В блокчейн-инфраструктуре, где таблицы растут на миллионы строк в день, правильная настройка — это разница между системой, отвечающей за 5мс, и системой, отвечающей за 5 секунд.\u003C\u002Fp>\n\u003Ch2 id=\"\">Типы индексов и когда их использовать\u003C\u002Fh2>\n\u003Ch3>B-tree (по умолчанию)\u003C\u002Fh3>\n\u003Cp>Универсальный индекс для операторов \u003Ccode>=\u003C\u002Fcode>, \u003Ccode>&lt;\u003C\u002Fcode>, \u003Ccode>&gt;\u003C\u002Fcode>, \u003Ccode>BETWEEN\u003C\u002Fcode>, \u003Ccode>IN\u003C\u002Fcode>, \u003Ccode>LIKE 'prefix%'\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE INDEX idx_tx_hash ON transactions (tx_hash);\nCREATE INDEX idx_tx_block_time ON transactions (block_number, created_at DESC);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>GIN (Generalized Inverted Index)\u003C\u002Fh3>\n\u003Cp>Для массивов, JSONB и полнотекстового поиска:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- JSONB\nCREATE INDEX idx_events_data ON events USING GIN (data jsonb_path_ops);\n\n-- Полнотекстовый поиск\nCREATE INDEX idx_articles_fts ON articles\nUSING GIN (to_tsvector('russian', title || ' ' || content_md));\n\n-- Массивы\nCREATE INDEX idx_tags ON articles USING GIN (tags);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>GiST (Generalized Search Tree)\u003C\u002Fh3>\n\u003Cp>Для геоданных, диапазонов и полнотекстового поиска:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- IP-диапазоны\nCREATE INDEX idx_ip_ranges ON access_log USING GIST (ip_range);\n\n-- Диапазоны блоков\nCREATE INDEX idx_block_ranges ON sync_jobs\nUSING GIST (int8range(start_block, end_block));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>BRIN (Block Range Index)\u003C\u002Fh3>\n\u003Cp>Для колоночно-коррелированных данных (монотонно растущие значения):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Для таблицы, где строки вставляются хронологически\nCREATE INDEX idx_tx_created_brin ON transactions\nUSING BRIN (created_at) WITH (pages_per_range = 32);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>BRIN-индекс занимает в 100-1000 раз меньше места, чем B-tree, но эффективен только когда физический порядок строк коррелирует с индексированным столбцом.\u003C\u002Fp>\n\u003Ch3>Partial и Expression индексы\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Индексировать только актуальные записи\nCREATE INDEX idx_pending_tx ON transactions (created_at)\nWHERE status = 'pending';\n\n-- Индекс на выражении\nCREATE INDEX idx_tx_lower_hash ON transactions (LOWER(tx_hash));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"vacuum-postgresql\">VACUUM: жизненный цикл строк в PostgreSQL\u003C\u002Fh2>\n\u003Cp>PostgreSQL использует MVCC (Multi-Version Concurrency Control). Каждый UPDATE и DELETE создаёт новую версию строки, а старая помечается как мёртвая. VACUUM удаляет мёртвые строки и освобождает место.\u003C\u002Fp>\n\u003Ch3>Настройка autovacuum\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">ALTER TABLE transactions SET (\n    autovacuum_vacuum_threshold = 10000,\n    autovacuum_vacuum_scale_factor = 0.01,  -- 1% вместо 20% по умолчанию\n    autovacuum_analyze_threshold = 5000,\n    autovacuum_analyze_scale_factor = 0.005,\n    autovacuum_vacuum_cost_delay = 2  -- мс, агрессивнее\n);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Для таблиц с высокой частотой обновлений (&gt; 1000 UPDATE\u002Fs) стандартные настройки autovacuum слишком консервативны.\u003C\u002Fp>\n\u003Ch3>Мониторинг мёртвых строк\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT\n    relname,\n    n_live_tup,\n    n_dead_tup,\n    ROUND(n_dead_tup::numeric \u002F NULLIF(n_live_tup, 0) * 100, 2) AS dead_pct,\n    last_vacuum,\n    last_autovacuum\nFROM pg_stat_user_tables\nWHERE n_dead_tup &gt; 10000\nORDER BY n_dead_tup DESC;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Если \u003Ccode>dead_pct\u003C\u002Fcode> превышает 10-20%, autovacuum не справляется.\u003C\u002Fp>\n\u003Ch2 id=\"\">Оптимизация запросов\u003C\u002Fh2>\n\u003Ch3>EXPLAIN ANALYZE — ваш главный инструмент\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT a.*, c.name as category_name\nFROM articles a\nJOIN categories c ON a.category_id = c.id\nWHERE a.published = true AND a.locale = 'ru'\nORDER BY a.published_at DESC\nLIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>На что обращать внимание:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Seq Scan\u003C\u002Fstrong> на большой таблице — нужен индекс\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Nested Loop\u003C\u002Fstrong> с большим outer — рассмотрите Hash Join\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Sort\u003C\u002Fstrong> с \u003Ccode>external merge Disk\u003C\u002Fcode> — не хватает work_mem\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Buffers: shared hit\u003C\u002Fstrong> vs \u003Cstrong>shared read\u003C\u002Fstrong> — кэш-хит рейт\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Типичные оптимизации\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- ПЛОХО: SELECT *\nSELECT * FROM articles WHERE category_id = $1;\n\n-- ХОРОШО: только нужные колонки\nSELECT id, title, slug, excerpt, published_at\nFROM articles WHERE category_id = $1;\n\n-- ПЛОХО: OFFSET для пагинации\nSELECT * FROM articles ORDER BY created_at DESC\nLIMIT 20 OFFSET 10000;\n\n-- ХОРОШО: keyset пагинация\nSELECT * FROM articles\nWHERE created_at &lt; $1\nORDER BY created_at DESC\nLIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>OFFSET сканирует и отбрасывает строки. Keyset pagination использует индекс.\u003C\u002Fp>\n\u003Ch2 id=\"postgresql\">Настройка PostgreSQL для производительности\u003C\u002Fh2>\n\u003Cp>Ключевые параметры:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ini\"># Память\nshared_buffers = 4GB            # 25% от RAM\neffective_cache_size = 12GB     # 75% от RAM\nwork_mem = 64MB                 # Для сортировок и хешей\nmaintenance_work_mem = 1GB      # Для VACUUM, CREATE INDEX\n\n# WAL\nwal_buffers = 64MB\ncheckpoint_completion_target = 0.9\nmax_wal_size = 4GB\n\n# Планировщик\nrandom_page_cost = 1.1          # Для SSD (по умолчанию 4.0)\neffective_io_concurrency = 200  # Для SSD\n\n# Параллелизм\nmax_parallel_workers_per_gather = 4\nmax_parallel_workers = 8\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Мониторинг в реальном времени\u003C\u002Fh2>\n\u003Ch3>pg_stat_statements\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT\n    LEFT(query, 80) AS query,\n    calls,\n    ROUND(mean_exec_time::numeric, 2) AS mean_ms,\n    ROUND(total_exec_time::numeric, 2) AS total_ms,\n    rows\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 20;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Активные запросы\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT\n    pid,\n    now() - pg_stat_activity.query_start AS duration,\n    state,\n    LEFT(query, 80) AS query\nFROM pg_stat_activity\nWHERE state != 'idle'\nORDER BY duration DESC;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Блокировки\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT\n    blocked.pid AS blocked_pid,\n    blocking.pid AS blocking_pid,\n    blocked.query AS blocked_query\nFROM pg_locks blocked\nJOIN pg_stat_activity blocked_act ON blocked.pid = blocked_act.pid\nJOIN pg_locks blocking ON blocked.locktype = blocking.locktype\nJOIN pg_stat_activity blocking_act ON blocking.pid = blocking_act.pid\nWHERE NOT blocked.granted;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"connection-pooling-pgbouncer\">Connection pooling с PgBouncer\u003C\u002Fh2>\n\u003Cp>PostgreSQL создаёт отдельный процесс для каждого соединения (~5MB RAM). Для 500 соединений — 2.5GB только на соединения. PgBouncer решает эту проблему:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ini\">[databases]\nmydb = host=127.0.0.1 port=5432 dbname=mydb\n\n[pgbouncer]\nlisten_port = 6432\npool_mode = transaction    # Переиспользовать после транзакции\nmax_client_conn = 1000     # Макс. клиентских соединений\ndefault_pool_size = 50     # Размер пула к PostgreSQL\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>В режиме \u003Ccode>transaction\u003C\u002Fcode> 1000 клиентов мультиплексируются через 50 реальных соединений.\u003C\u002Fp>\n\u003Ch2 id=\"\">Заключение\u003C\u002Fh2>\n\u003Cp>Производительность PostgreSQL — это не одна настройка, а комбинация правильных индексов, настроенного VACUUM, оптимизированных запросов и мониторинга. Начните с pg_stat_statements для поиска проблемных запросов, используйте EXPLAIN ANALYZE для диагностики, и помните: 90% проблем производительности решаются добавлением правильного индекса или устранением N+1 запросов.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.983319Z","Производительность PostgreSQL — индексы, VACUUM и оптимизация запросов","Глубокое погружение в производительность PostgreSQL: B-tree, GIN, BRIN индексы, autovacuum и мониторинг.","производительность 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"]