[{"data":1,"prerenderedAt":2314},["ShallowReactive",2],{"article-alternates":3,"article-\u002Fru\u002Fdata\u002Fcohort-table-architecture-production-scaling":11},{"i18nKey":4,"paths":5},"data-007-2026-06",{"de":6,"es":7,"fr":8,"it":9,"ru":10},"\u002Fde\u002Fdata\u002Fcohort-table-architecture-retention-at-scale","\u002Fes\u002Fdata\u002Farquitectura-tabla-cohortes","\u002Ffr\u002Fdata\u002Fcohort-table-architecture-production-scaling","\u002Fit\u002Fdata\u002Farchitettura-tabella-cohort","\u002Fru\u002Fdata\u002Fcohort-tablitsa-arkhitektura-masshtabirovanie-analiza-uderzhaniia-v-produkcii",{"_path":12,"_dir":13,"_draft":14,"_partial":14,"_locale":15,"title":16,"description":17,"publishedAt":18,"modifiedAt":18,"category":19,"i18nKey":4,"tags":20,"readingTime":26,"author":27,"body":28,"_type":2308,"_id":2309,"_source":2310,"_file":2311,"_stem":2312,"_extension":2313},"\u002Fru\u002Fdata\u002Fcohort-table-architecture-production-scaling","data",false,"","Архитектура таблиц когорт: масштабирование анализа retention в Production","Запускайте анализ когорт на 10М+ событиях в день с latency в миллисекунды через materialized view, partitioning и query cost optimization.","2026-06-26","verianalizi",[21,22,23,24,25],"cohort-analysis","bigquery-optimization","materialized-views","retention-engineering","data-partitioning",9,"Roibase",{"type":29,"children":30,"toc":2295},"root",[31,39,46,51,416,421,467,472,478,491,876,888,908,915,948,960,966,976,1515,1541,1547,1560,1579,1585,1590,1608,1618,1628,1634,1646,1699,1949,1968,1974,1979,1987,2012,2017,2089,2110,2116,2121,2274,2279,2284,2289],{"type":32,"tag":33,"props":34,"children":35},"element","p",{},[36],{"type":37,"value":38},"text","Если ваш dashboard retention'а загружается 45 секунд на каждый клик, проблема не в определении когорты — проблема в архитектуре таблиц. Расчёт D1, D7, D30 retention'а на 10 миллионах ежедневных событий в BigQuery может обойтись в 2TB scan'а и $10 в день. Или же, при правильной partition стратегии, incremental materialized view и pre-aggregation'е, это 200MB и 50 миллисекунд. Разница между production-ready системой и \"работает, но никто не может это использовать\" лежит именно тут.",{"type":32,"tag":40,"props":41,"children":43},"h2",{"id":42},"почему-анализ-когорт-взрывается-в-production",[44],{"type":37,"value":45},"Почему анализ когорт \"взрывается\" в Production",{"type":32,"tag":33,"props":47,"children":48},{},[49],{"type":37,"value":50},"Расчёт retention по своей природе — это full-scan операция. Найти первое событие каждого юзера, посчитать его активность в следующие дни, сгруппировать по когорте, вычислить проценты. Наивный SQL выглядит так:",{"type":32,"tag":52,"props":53,"children":57},"pre",{"className":54,"code":55,"language":56,"meta":15,"style":15},"language-sql shiki shiki-themes github-dark","WITH first_events AS (\n  SELECT user_id, MIN(event_date) AS cohort_date\n  FROM events\n  GROUP BY user_id\n),\nretention_raw AS (\n  SELECT \n    f.cohort_date,\n    DATE_DIFF(e.event_date, f.cohort_date, DAY) AS day_offset,\n    COUNT(DISTINCT e.user_id) AS active_users\n  FROM events e\n  JOIN first_events f USING(user_id)\n  GROUP BY 1, 2\n)\nSELECT * FROM retention_raw;\n","sql",[58],{"type":32,"tag":59,"props":60,"children":61},"code",{"__ignoreMap":15},[62,90,124,138,152,161,178,191,215,278,324,337,361,383,392],{"type":32,"tag":63,"props":64,"children":67},"span",{"class":65,"line":66},"line",1,[68,74,80,85],{"type":32,"tag":63,"props":69,"children":71},{"style":70},"--shiki-default:#F97583",[72],{"type":37,"value":73},"WITH",{"type":32,"tag":63,"props":75,"children":77},{"style":76},"--shiki-default:#E1E4E8",[78],{"type":37,"value":79}," first_events ",{"type":32,"tag":63,"props":81,"children":82},{"style":70},[83],{"type":37,"value":84},"AS",{"type":32,"tag":63,"props":86,"children":87},{"style":76},[88],{"type":37,"value":89}," (\n",{"type":32,"tag":63,"props":91,"children":93},{"class":65,"line":92},2,[94,99,104,110,115,119],{"type":32,"tag":63,"props":95,"children":96},{"style":70},[97],{"type":37,"value":98},"  SELECT",{"type":32,"tag":63,"props":100,"children":101},{"style":76},[102],{"type":37,"value":103}," user_id, ",{"type":32,"tag":63,"props":105,"children":107},{"style":106},"--shiki-default:#79B8FF",[108],{"type":37,"value":109},"MIN",{"type":32,"tag":63,"props":111,"children":112},{"style":76},[113],{"type":37,"value":114},"(event_date) ",{"type":32,"tag":63,"props":116,"children":117},{"style":70},[118],{"type":37,"value":84},{"type":32,"tag":63,"props":120,"children":121},{"style":76},[122],{"type":37,"value":123}," cohort_date\n",{"type":32,"tag":63,"props":125,"children":127},{"class":65,"line":126},3,[128,133],{"type":32,"tag":63,"props":129,"children":130},{"style":70},[131],{"type":37,"value":132},"  FROM",{"type":32,"tag":63,"props":134,"children":135},{"style":76},[136],{"type":37,"value":137}," events\n",{"type":32,"tag":63,"props":139,"children":141},{"class":65,"line":140},4,[142,147],{"type":32,"tag":63,"props":143,"children":144},{"style":70},[145],{"type":37,"value":146},"  GROUP BY",{"type":32,"tag":63,"props":148,"children":149},{"style":76},[150],{"type":37,"value":151}," user_id\n",{"type":32,"tag":63,"props":153,"children":155},{"class":65,"line":154},5,[156],{"type":32,"tag":63,"props":157,"children":158},{"style":76},[159],{"type":37,"value":160},"),\n",{"type":32,"tag":63,"props":162,"children":164},{"class":65,"line":163},6,[165,170,174],{"type":32,"tag":63,"props":166,"children":167},{"style":76},[168],{"type":37,"value":169},"retention_raw ",{"type":32,"tag":63,"props":171,"children":172},{"style":70},[173],{"type":37,"value":84},{"type":32,"tag":63,"props":175,"children":176},{"style":76},[177],{"type":37,"value":89},{"type":32,"tag":63,"props":179,"children":181},{"class":65,"line":180},7,[182,186],{"type":32,"tag":63,"props":183,"children":184},{"style":70},[185],{"type":37,"value":98},{"type":32,"tag":63,"props":187,"children":188},{"style":76},[189],{"type":37,"value":190}," \n",{"type":32,"tag":63,"props":192,"children":194},{"class":65,"line":193},8,[195,200,205,210],{"type":32,"tag":63,"props":196,"children":197},{"style":106},[198],{"type":37,"value":199},"    f",{"type":32,"tag":63,"props":201,"children":202},{"style":76},[203],{"type":37,"value":204},".",{"type":32,"tag":63,"props":206,"children":207},{"style":106},[208],{"type":37,"value":209},"cohort_date",{"type":32,"tag":63,"props":211,"children":212},{"style":76},[213],{"type":37,"value":214},",\n",{"type":32,"tag":63,"props":216,"children":217},{"class":65,"line":26},[218,223,228,232,237,242,247,251,255,259,264,269,273],{"type":32,"tag":63,"props":219,"children":220},{"style":76},[221],{"type":37,"value":222},"    DATE_DIFF(",{"type":32,"tag":63,"props":224,"children":225},{"style":106},[226],{"type":37,"value":227},"e",{"type":32,"tag":63,"props":229,"children":230},{"style":76},[231],{"type":37,"value":204},{"type":32,"tag":63,"props":233,"children":234},{"style":106},[235],{"type":37,"value":236},"event_date",{"type":32,"tag":63,"props":238,"children":239},{"style":76},[240],{"type":37,"value":241},", ",{"type":32,"tag":63,"props":243,"children":244},{"style":106},[245],{"type":37,"value":246},"f",{"type":32,"tag":63,"props":248,"children":249},{"style":76},[250],{"type":37,"value":204},{"type":32,"tag":63,"props":252,"children":253},{"style":106},[254],{"type":37,"value":209},{"type":32,"tag":63,"props":256,"children":257},{"style":76},[258],{"type":37,"value":241},{"type":32,"tag":63,"props":260,"children":261},{"style":70},[262],{"type":37,"value":263},"DAY",{"type":32,"tag":63,"props":265,"children":266},{"style":76},[267],{"type":37,"value":268},") ",{"type":32,"tag":63,"props":270,"children":271},{"style":70},[272],{"type":37,"value":84},{"type":32,"tag":63,"props":274,"children":275},{"style":76},[276],{"type":37,"value":277}," day_offset,\n",{"type":32,"tag":63,"props":279,"children":281},{"class":65,"line":280},10,[282,287,292,297,302,306,311,315,319],{"type":32,"tag":63,"props":283,"children":284},{"style":106},[285],{"type":37,"value":286},"    COUNT",{"type":32,"tag":63,"props":288,"children":289},{"style":76},[290],{"type":37,"value":291},"(",{"type":32,"tag":63,"props":293,"children":294},{"style":70},[295],{"type":37,"value":296},"DISTINCT",{"type":32,"tag":63,"props":298,"children":299},{"style":106},[300],{"type":37,"value":301}," e",{"type":32,"tag":63,"props":303,"children":304},{"style":76},[305],{"type":37,"value":204},{"type":32,"tag":63,"props":307,"children":308},{"style":106},[309],{"type":37,"value":310},"user_id",{"type":32,"tag":63,"props":312,"children":313},{"style":76},[314],{"type":37,"value":268},{"type":32,"tag":63,"props":316,"children":317},{"style":70},[318],{"type":37,"value":84},{"type":32,"tag":63,"props":320,"children":321},{"style":76},[322],{"type":37,"value":323}," active_users\n",{"type":32,"tag":63,"props":325,"children":327},{"class":65,"line":326},11,[328,332],{"type":32,"tag":63,"props":329,"children":330},{"style":70},[331],{"type":37,"value":132},{"type":32,"tag":63,"props":333,"children":334},{"style":76},[335],{"type":37,"value":336}," events e\n",{"type":32,"tag":63,"props":338,"children":340},{"class":65,"line":339},12,[341,346,351,356],{"type":32,"tag":63,"props":342,"children":343},{"style":70},[344],{"type":37,"value":345},"  JOIN",{"type":32,"tag":63,"props":347,"children":348},{"style":76},[349],{"type":37,"value":350}," first_events f ",{"type":32,"tag":63,"props":352,"children":353},{"style":70},[354],{"type":37,"value":355},"USING",{"type":32,"tag":63,"props":357,"children":358},{"style":76},[359],{"type":37,"value":360},"(user_id)\n",{"type":32,"tag":63,"props":362,"children":364},{"class":65,"line":363},13,[365,369,374,378],{"type":32,"tag":63,"props":366,"children":367},{"style":70},[368],{"type":37,"value":146},{"type":32,"tag":63,"props":370,"children":371},{"style":106},[372],{"type":37,"value":373}," 1",{"type":32,"tag":63,"props":375,"children":376},{"style":76},[377],{"type":37,"value":241},{"type":32,"tag":63,"props":379,"children":380},{"style":106},[381],{"type":37,"value":382},"2\n",{"type":32,"tag":63,"props":384,"children":386},{"class":65,"line":385},14,[387],{"type":32,"tag":63,"props":388,"children":389},{"style":76},[390],{"type":37,"value":391},")\n",{"type":32,"tag":63,"props":393,"children":395},{"class":65,"line":394},15,[396,401,406,411],{"type":32,"tag":63,"props":397,"children":398},{"style":70},[399],{"type":37,"value":400},"SELECT",{"type":32,"tag":63,"props":402,"children":403},{"style":70},[404],{"type":37,"value":405}," *",{"type":32,"tag":63,"props":407,"children":408},{"style":70},[409],{"type":37,"value":410}," FROM",{"type":32,"tag":63,"props":412,"children":413},{"style":76},[414],{"type":37,"value":415}," retention_raw;\n",{"type":32,"tag":33,"props":417,"children":418},{},[419],{"type":37,"value":420},"Этот запрос при каждом выполнении читает всю таблицу events с начала. 500 дней истории × 10M событий в день = 5 миллиардов строк. BigQuery начинает потреблять все слоты, dashboard зависает на 40 секунд, BI tool'ы выбивают timeout'ы. Проблема сконцентрирована в трёх местах:",{"type":32,"tag":33,"props":422,"children":423},{},[424,430,432,437,439,443,448,450,455,457,460,465],{"type":32,"tag":425,"props":426,"children":427},"strong",{},[428],{"type":37,"value":429},"1. Full table scan:",{"type":37,"value":431}," Partition pruning не работает, потому что JOIN по ",{"type":32,"tag":59,"props":433,"children":435},{"className":434},[],[436],{"type":37,"value":310},{"type":37,"value":438}," разрушает границы partition'а.",{"type":32,"tag":440,"props":441,"children":442},"br",{},[],{"type":32,"tag":425,"props":444,"children":445},{},[446],{"type":37,"value":447},"2. Повторяющиеся вычисления:",{"type":37,"value":449}," ",{"type":32,"tag":59,"props":451,"children":453},{"className":452},[],[454],{"type":37,"value":209},{"type":37,"value":456}," уже известны, но пересчитываются на каждый запрос.",{"type":32,"tag":440,"props":458,"children":459},{},[],{"type":32,"tag":425,"props":461,"children":462},{},[463],{"type":37,"value":464},"3. Overhead агрегации:",{"type":37,"value":466}," Из 5 миллиардов строк получаешь 500 когорт × 90 дней = 45.000 строк — ratio compute\u002Foutput = 100.000:1.",{"type":32,"tag":33,"props":468,"children":469},{},[470],{"type":37,"value":471},"В production такой подход невозможен. Решение — переделать архитектуру таблиц.",{"type":32,"tag":40,"props":473,"children":475},{"id":474},"materialized-cohort-base-первый-шаг-инкрементальный-snapshot",[476],{"type":37,"value":477},"Materialized Cohort Base: первый шаг — инкрементальный snapshot",{"type":32,"tag":33,"props":479,"children":480},{},[481,483,489],{"type":37,"value":482},"Самая дорогая часть анализа когорт — это вычисление ",{"type":32,"tag":59,"props":484,"children":486},{"className":485},[],[487],{"type":37,"value":488},"MIN(event_date)",{"type":37,"value":490},". Посчитаешь один раз, запишешь результат в snapshot table, каждый день добавляешь только новых юзеров. В BigQuery вместо materialized view используем dbt incremental model:",{"type":32,"tag":52,"props":492,"children":494},{"className":54,"code":493,"language":56,"meta":15,"style":15},"-- models\u002Fcohorts\u002Fuser_cohort_base.sql\n{{ config(\n  materialized='incremental',\n  unique_key='user_id',\n  partition_by={'field': 'cohort_date', 'data_type': 'date'},\n  cluster_by=['cohort_date', 'user_id']\n) }}\n\nSELECT\n  user_id,\n  MIN(event_date) AS cohort_date,\n  COUNT(*) AS first_day_events\nFROM {{ source('raw', 'events') }}\n{% if is_incremental() %}\nWHERE event_date >= (SELECT MAX(cohort_date) FROM {{ this }})\n  AND user_id NOT IN (SELECT user_id FROM {{ this }})\n{% endif %}\nGROUP BY user_id\n",[495],{"type":32,"tag":59,"props":496,"children":497},{"__ignoreMap":15},[498,507,515,538,559,614,631,639,648,656,664,685,715,746,764,810,854,863],{"type":32,"tag":63,"props":499,"children":500},{"class":65,"line":66},[501],{"type":32,"tag":63,"props":502,"children":504},{"style":503},"--shiki-default:#6A737D",[505],{"type":37,"value":506},"-- models\u002Fcohorts\u002Fuser_cohort_base.sql\n",{"type":32,"tag":63,"props":508,"children":509},{"class":65,"line":92},[510],{"type":32,"tag":63,"props":511,"children":512},{"style":76},[513],{"type":37,"value":514},"{{ config(\n",{"type":32,"tag":63,"props":516,"children":517},{"class":65,"line":126},[518,523,528,534],{"type":32,"tag":63,"props":519,"children":520},{"style":76},[521],{"type":37,"value":522},"  materialized",{"type":32,"tag":63,"props":524,"children":525},{"style":70},[526],{"type":37,"value":527},"=",{"type":32,"tag":63,"props":529,"children":531},{"style":530},"--shiki-default:#9ECBFF",[532],{"type":37,"value":533},"'incremental'",{"type":32,"tag":63,"props":535,"children":536},{"style":76},[537],{"type":37,"value":214},{"type":32,"tag":63,"props":539,"children":540},{"class":65,"line":140},[541,546,550,555],{"type":32,"tag":63,"props":542,"children":543},{"style":76},[544],{"type":37,"value":545},"  unique_key",{"type":32,"tag":63,"props":547,"children":548},{"style":70},[549],{"type":37,"value":527},{"type":32,"tag":63,"props":551,"children":552},{"style":530},[553],{"type":37,"value":554},"'user_id'",{"type":32,"tag":63,"props":556,"children":557},{"style":76},[558],{"type":37,"value":214},{"type":32,"tag":63,"props":560,"children":561},{"class":65,"line":154},[562,567,571,576,581,586,591,595,600,604,609],{"type":32,"tag":63,"props":563,"children":564},{"style":76},[565],{"type":37,"value":566},"  partition_by",{"type":32,"tag":63,"props":568,"children":569},{"style":70},[570],{"type":37,"value":527},{"type":32,"tag":63,"props":572,"children":573},{"style":76},[574],{"type":37,"value":575},"{",{"type":32,"tag":63,"props":577,"children":578},{"style":530},[579],{"type":37,"value":580},"'field'",{"type":32,"tag":63,"props":582,"children":583},{"style":76},[584],{"type":37,"value":585},": ",{"type":32,"tag":63,"props":587,"children":588},{"style":530},[589],{"type":37,"value":590},"'cohort_date'",{"type":32,"tag":63,"props":592,"children":593},{"style":76},[594],{"type":37,"value":241},{"type":32,"tag":63,"props":596,"children":597},{"style":530},[598],{"type":37,"value":599},"'data_type'",{"type":32,"tag":63,"props":601,"children":602},{"style":76},[603],{"type":37,"value":585},{"type":32,"tag":63,"props":605,"children":606},{"style":530},[607],{"type":37,"value":608},"'date'",{"type":32,"tag":63,"props":610,"children":611},{"style":76},[612],{"type":37,"value":613},"},\n",{"type":32,"tag":63,"props":615,"children":616},{"class":65,"line":163},[617,622,626],{"type":32,"tag":63,"props":618,"children":619},{"style":76},[620],{"type":37,"value":621},"  cluster_by",{"type":32,"tag":63,"props":623,"children":624},{"style":70},[625],{"type":37,"value":527},{"type":32,"tag":63,"props":627,"children":628},{"style":76},[629],{"type":37,"value":630},"['cohort_date', 'user_id']\n",{"type":32,"tag":63,"props":632,"children":633},{"class":65,"line":180},[634],{"type":32,"tag":63,"props":635,"children":636},{"style":76},[637],{"type":37,"value":638},") }}\n",{"type":32,"tag":63,"props":640,"children":641},{"class":65,"line":193},[642],{"type":32,"tag":63,"props":643,"children":645},{"emptyLinePlaceholder":644},true,[646],{"type":37,"value":647},"\n",{"type":32,"tag":63,"props":649,"children":650},{"class":65,"line":26},[651],{"type":32,"tag":63,"props":652,"children":653},{"style":70},[654],{"type":37,"value":655},"SELECT\n",{"type":32,"tag":63,"props":657,"children":658},{"class":65,"line":280},[659],{"type":32,"tag":63,"props":660,"children":661},{"style":76},[662],{"type":37,"value":663},"  user_id,\n",{"type":32,"tag":63,"props":665,"children":666},{"class":65,"line":326},[667,672,676,680],{"type":32,"tag":63,"props":668,"children":669},{"style":106},[670],{"type":37,"value":671},"  MIN",{"type":32,"tag":63,"props":673,"children":674},{"style":76},[675],{"type":37,"value":114},{"type":32,"tag":63,"props":677,"children":678},{"style":70},[679],{"type":37,"value":84},{"type":32,"tag":63,"props":681,"children":682},{"style":76},[683],{"type":37,"value":684}," cohort_date,\n",{"type":32,"tag":63,"props":686,"children":687},{"class":65,"line":339},[688,693,697,702,706,710],{"type":32,"tag":63,"props":689,"children":690},{"style":106},[691],{"type":37,"value":692},"  COUNT",{"type":32,"tag":63,"props":694,"children":695},{"style":76},[696],{"type":37,"value":291},{"type":32,"tag":63,"props":698,"children":699},{"style":70},[700],{"type":37,"value":701},"*",{"type":32,"tag":63,"props":703,"children":704},{"style":76},[705],{"type":37,"value":268},{"type":32,"tag":63,"props":707,"children":708},{"style":70},[709],{"type":37,"value":84},{"type":32,"tag":63,"props":711,"children":712},{"style":76},[713],{"type":37,"value":714}," first_day_events\n",{"type":32,"tag":63,"props":716,"children":717},{"class":65,"line":363},[718,723,728,733,737,742],{"type":32,"tag":63,"props":719,"children":720},{"style":70},[721],{"type":37,"value":722},"FROM",{"type":32,"tag":63,"props":724,"children":725},{"style":76},[726],{"type":37,"value":727}," {{ source(",{"type":32,"tag":63,"props":729,"children":730},{"style":530},[731],{"type":37,"value":732},"'raw'",{"type":32,"tag":63,"props":734,"children":735},{"style":76},[736],{"type":37,"value":241},{"type":32,"tag":63,"props":738,"children":739},{"style":530},[740],{"type":37,"value":741},"'events'",{"type":32,"tag":63,"props":743,"children":744},{"style":76},[745],{"type":37,"value":638},{"type":32,"tag":63,"props":747,"children":748},{"class":65,"line":385},[749,754,759],{"type":32,"tag":63,"props":750,"children":751},{"style":76},[752],{"type":37,"value":753},"{% ",{"type":32,"tag":63,"props":755,"children":756},{"style":70},[757],{"type":37,"value":758},"if",{"type":32,"tag":63,"props":760,"children":761},{"style":76},[762],{"type":37,"value":763}," is_incremental() %}\n",{"type":32,"tag":63,"props":765,"children":766},{"class":65,"line":394},[767,772,777,782,787,791,796,801,805],{"type":32,"tag":63,"props":768,"children":769},{"style":70},[770],{"type":37,"value":771},"WHERE",{"type":32,"tag":63,"props":773,"children":774},{"style":76},[775],{"type":37,"value":776}," event_date ",{"type":32,"tag":63,"props":778,"children":779},{"style":70},[780],{"type":37,"value":781},">=",{"type":32,"tag":63,"props":783,"children":784},{"style":76},[785],{"type":37,"value":786}," (",{"type":32,"tag":63,"props":788,"children":789},{"style":70},[790],{"type":37,"value":400},{"type":32,"tag":63,"props":792,"children":793},{"style":106},[794],{"type":37,"value":795}," MAX",{"type":32,"tag":63,"props":797,"children":798},{"style":76},[799],{"type":37,"value":800},"(cohort_date) ",{"type":32,"tag":63,"props":802,"children":803},{"style":70},[804],{"type":37,"value":722},{"type":32,"tag":63,"props":806,"children":807},{"style":76},[808],{"type":37,"value":809}," {{ this }})\n",{"type":32,"tag":63,"props":811,"children":813},{"class":65,"line":812},16,[814,819,824,829,834,838,842,846,850],{"type":32,"tag":63,"props":815,"children":816},{"style":70},[817],{"type":37,"value":818},"  AND",{"type":32,"tag":63,"props":820,"children":821},{"style":76},[822],{"type":37,"value":823}," user_id ",{"type":32,"tag":63,"props":825,"children":826},{"style":70},[827],{"type":37,"value":828},"NOT",{"type":32,"tag":63,"props":830,"children":831},{"style":70},[832],{"type":37,"value":833}," IN",{"type":32,"tag":63,"props":835,"children":836},{"style":76},[837],{"type":37,"value":786},{"type":32,"tag":63,"props":839,"children":840},{"style":70},[841],{"type":37,"value":400},{"type":32,"tag":63,"props":843,"children":844},{"style":76},[845],{"type":37,"value":823},{"type":32,"tag":63,"props":847,"children":848},{"style":70},[849],{"type":37,"value":722},{"type":32,"tag":63,"props":851,"children":852},{"style":76},[853],{"type":37,"value":809},{"type":32,"tag":63,"props":855,"children":857},{"class":65,"line":856},17,[858],{"type":32,"tag":63,"props":859,"children":860},{"style":76},[861],{"type":37,"value":862},"{% endif %}\n",{"type":32,"tag":63,"props":864,"children":866},{"class":65,"line":865},18,[867,872],{"type":32,"tag":63,"props":868,"children":869},{"style":70},[870],{"type":37,"value":871},"GROUP BY",{"type":32,"tag":63,"props":873,"children":874},{"style":76},[875],{"type":37,"value":151},{"type":32,"tag":33,"props":877,"children":878},{},[879,881,886],{"type":37,"value":880},"На первом run'е модель читает всю историю (one-time cost), на последующих дневных запусках добавляет только новых юзеров вчерашнего дня. Partition по ",{"type":32,"tag":59,"props":882,"children":884},{"className":883},[],[885],{"type":37,"value":209},{"type":37,"value":887}," гарантирует, что BigQuery не трогает старые partition'ы — стоимость запроса остаётся пропорциональна ежедневному объёму событий (10M новых → ~50MB scan).",{"type":32,"tag":33,"props":889,"children":890},{},[891,893,898,900,906],{"type":37,"value":892},"Cluster по ",{"type":32,"tag":59,"props":894,"children":896},{"className":895},[],[897],{"type":37,"value":310},{"type":37,"value":899}," ускоряет JOIN'ы: когда downstream retention запросы джойнят с ",{"type":32,"tag":59,"props":901,"children":903},{"className":902},[],[904],{"type":37,"value":905},"user_cohort_base",{"type":37,"value":907},", BigQuery выполняет binary search внутри micro-partition'ов — вместо 5 миллиардов строк читает только релевантные блоки.",{"type":32,"tag":909,"props":910,"children":912},"h3",{"id":911},"стратегия-partitionа-дата-события-или-дата-когорты",[913],{"type":37,"value":914},"Стратегия partition'а: дата события или дата когорты?",{"type":32,"tag":33,"props":916,"children":917},{},[918,920,925,927,932,934,939,941,946],{"type":37,"value":919},"Если ваша таблица events partition'ирована по ",{"type":32,"tag":59,"props":921,"children":923},{"className":922},[],[924],{"type":37,"value":236},{"type":37,"value":926},", то cohort base должна быть partition'ирована по ",{"type":32,"tag":59,"props":928,"children":930},{"className":929},[],[931],{"type":37,"value":209},{"type":37,"value":933},". Причина: запросы retention'а часто звучат как \"retention январской когорты в феврале\". Partition по ",{"type":32,"tag":59,"props":935,"children":937},{"className":936},[],[938],{"type":37,"value":236},{"type":37,"value":940}," не поможет — он не может припрезировать данные по дате первого события. Partition по ",{"type":32,"tag":59,"props":942,"children":944},{"className":943},[],[945],{"type":37,"value":209},{"type":37,"value":947}," — может: запрос \"январская когорта\" прочитает только январские partition'ы.",{"type":32,"tag":33,"props":949,"children":950},{},[951,953,959],{"type":37,"value":952},"Но будьте осторожны с количеством partition'ов: лимит — 4000. Для 10 лет данных это 3650 partition'ов — на краю лимита. Если когорта по неделям или месяцам достаточна, используйте ",{"type":32,"tag":59,"props":954,"children":956},{"className":955},[],[957],{"type":37,"value":958},"DATE_TRUNC(cohort_date, WEEK)",{"type":37,"value":204},{"type":32,"tag":40,"props":961,"children":963},{"id":962},"pre-aggregated-retention-cube-снижение-cost-в-100-раз",[964],{"type":37,"value":965},"Pre-Aggregated Retention Cube: снижение cost в 100 раз",{"type":32,"tag":33,"props":967,"children":968},{},[969,974],{"type":32,"tag":59,"props":970,"children":972},{"className":971},[],[973],{"type":37,"value":905},{"type":37,"value":975}," готова, но каждый retention запрос всё ещё джойнит с events. Следующий шаг — предварительно вычислить метрики retention и запишем их в materialized table:",{"type":32,"tag":52,"props":977,"children":979},{"className":54,"code":978,"language":56,"meta":15,"style":15},"-- models\u002Fcohorts\u002Fdaily_retention_cube.sql\n{{ config(\n  materialized='incremental',\n  unique_key=['cohort_date', 'day_offset'],\n  partition_by={'field': 'cohort_date', 'data_type': 'date'}\n) }}\n\nWITH cohort_activity AS (\n  SELECT\n    c.cohort_date,\n    DATE_DIFF(e.event_date, c.cohort_date, DAY) AS day_offset,\n    COUNT(DISTINCT e.user_id) AS active_users\n  FROM {{ ref('user_cohort_base') }} c\n  JOIN {{ source('raw', 'events') }} e USING(user_id)\n  {% if is_incremental() %}\n  WHERE e.event_date >= CURRENT_DATE() - 1\n  {% endif %}\n  GROUP BY 1, 2\n)\nSELECT\n  cohort_date,\n  day_offset,\n  active_users,\n  active_users \u002F FIRST_VALUE(active_users) OVER (\n    PARTITION BY cohort_date ORDER BY day_offset\n  ) AS retention_rate\nFROM cohort_activity\n",[980],{"type":32,"tag":59,"props":981,"children":982},{"__ignoreMap":15},[983,991,998,1017,1033,1081,1088,1095,1115,1123,1143,1199,1238,1260,1296,1312,1352,1360,1379,1387,1395,1404,1413,1422,1455,1484,1502],{"type":32,"tag":63,"props":984,"children":985},{"class":65,"line":66},[986],{"type":32,"tag":63,"props":987,"children":988},{"style":503},[989],{"type":37,"value":990},"-- models\u002Fcohorts\u002Fdaily_retention_cube.sql\n",{"type":32,"tag":63,"props":992,"children":993},{"class":65,"line":92},[994],{"type":32,"tag":63,"props":995,"children":996},{"style":76},[997],{"type":37,"value":514},{"type":32,"tag":63,"props":999,"children":1000},{"class":65,"line":126},[1001,1005,1009,1013],{"type":32,"tag":63,"props":1002,"children":1003},{"style":76},[1004],{"type":37,"value":522},{"type":32,"tag":63,"props":1006,"children":1007},{"style":70},[1008],{"type":37,"value":527},{"type":32,"tag":63,"props":1010,"children":1011},{"style":530},[1012],{"type":37,"value":533},{"type":32,"tag":63,"props":1014,"children":1015},{"style":76},[1016],{"type":37,"value":214},{"type":32,"tag":63,"props":1018,"children":1019},{"class":65,"line":140},[1020,1024,1028],{"type":32,"tag":63,"props":1021,"children":1022},{"style":76},[1023],{"type":37,"value":545},{"type":32,"tag":63,"props":1025,"children":1026},{"style":70},[1027],{"type":37,"value":527},{"type":32,"tag":63,"props":1029,"children":1030},{"style":76},[1031],{"type":37,"value":1032},"['cohort_date', 'day_offset'],\n",{"type":32,"tag":63,"props":1034,"children":1035},{"class":65,"line":154},[1036,1040,1044,1048,1052,1056,1060,1064,1068,1072,1076],{"type":32,"tag":63,"props":1037,"children":1038},{"style":76},[1039],{"type":37,"value":566},{"type":32,"tag":63,"props":1041,"children":1042},{"style":70},[1043],{"type":37,"value":527},{"type":32,"tag":63,"props":1045,"children":1046},{"style":76},[1047],{"type":37,"value":575},{"type":32,"tag":63,"props":1049,"children":1050},{"style":530},[1051],{"type":37,"value":580},{"type":32,"tag":63,"props":1053,"children":1054},{"style":76},[1055],{"type":37,"value":585},{"type":32,"tag":63,"props":1057,"children":1058},{"style":530},[1059],{"type":37,"value":590},{"type":32,"tag":63,"props":1061,"children":1062},{"style":76},[1063],{"type":37,"value":241},{"type":32,"tag":63,"props":1065,"children":1066},{"style":530},[1067],{"type":37,"value":599},{"type":32,"tag":63,"props":1069,"children":1070},{"style":76},[1071],{"type":37,"value":585},{"type":32,"tag":63,"props":1073,"children":1074},{"style":530},[1075],{"type":37,"value":608},{"type":32,"tag":63,"props":1077,"children":1078},{"style":76},[1079],{"type":37,"value":1080},"}\n",{"type":32,"tag":63,"props":1082,"children":1083},{"class":65,"line":163},[1084],{"type":32,"tag":63,"props":1085,"children":1086},{"style":76},[1087],{"type":37,"value":638},{"type":32,"tag":63,"props":1089,"children":1090},{"class":65,"line":180},[1091],{"type":32,"tag":63,"props":1092,"children":1093},{"emptyLinePlaceholder":644},[1094],{"type":37,"value":647},{"type":32,"tag":63,"props":1096,"children":1097},{"class":65,"line":193},[1098,1102,1107,1111],{"type":32,"tag":63,"props":1099,"children":1100},{"style":70},[1101],{"type":37,"value":73},{"type":32,"tag":63,"props":1103,"children":1104},{"style":76},[1105],{"type":37,"value":1106}," cohort_activity ",{"type":32,"tag":63,"props":1108,"children":1109},{"style":70},[1110],{"type":37,"value":84},{"type":32,"tag":63,"props":1112,"children":1113},{"style":76},[1114],{"type":37,"value":89},{"type":32,"tag":63,"props":1116,"children":1117},{"class":65,"line":26},[1118],{"type":32,"tag":63,"props":1119,"children":1120},{"style":70},[1121],{"type":37,"value":1122},"  SELECT\n",{"type":32,"tag":63,"props":1124,"children":1125},{"class":65,"line":280},[1126,1131,1135,1139],{"type":32,"tag":63,"props":1127,"children":1128},{"style":106},[1129],{"type":37,"value":1130},"    c",{"type":32,"tag":63,"props":1132,"children":1133},{"style":76},[1134],{"type":37,"value":204},{"type":32,"tag":63,"props":1136,"children":1137},{"style":106},[1138],{"type":37,"value":209},{"type":32,"tag":63,"props":1140,"children":1141},{"style":76},[1142],{"type":37,"value":214},{"type":32,"tag":63,"props":1144,"children":1145},{"class":65,"line":326},[1146,1150,1154,1158,1162,1166,1171,1175,1179,1183,1187,1191,1195],{"type":32,"tag":63,"props":1147,"children":1148},{"style":76},[1149],{"type":37,"value":222},{"type":32,"tag":63,"props":1151,"children":1152},{"style":106},[1153],{"type":37,"value":227},{"type":32,"tag":63,"props":1155,"children":1156},{"style":76},[1157],{"type":37,"value":204},{"type":32,"tag":63,"props":1159,"children":1160},{"style":106},[1161],{"type":37,"value":236},{"type":32,"tag":63,"props":1163,"children":1164},{"style":76},[1165],{"type":37,"value":241},{"type":32,"tag":63,"props":1167,"children":1168},{"style":106},[1169],{"type":37,"value":1170},"c",{"type":32,"tag":63,"props":1172,"children":1173},{"style":76},[1174],{"type":37,"value":204},{"type":32,"tag":63,"props":1176,"children":1177},{"style":106},[1178],{"type":37,"value":209},{"type":32,"tag":63,"props":1180,"children":1181},{"style":76},[1182],{"type":37,"value":241},{"type":32,"tag":63,"props":1184,"children":1185},{"style":70},[1186],{"type":37,"value":263},{"type":32,"tag":63,"props":1188,"children":1189},{"style":76},[1190],{"type":37,"value":268},{"type":32,"tag":63,"props":1192,"children":1193},{"style":70},[1194],{"type":37,"value":84},{"type":32,"tag":63,"props":1196,"children":1197},{"style":76},[1198],{"type":37,"value":277},{"type":32,"tag":63,"props":1200,"children":1201},{"class":65,"line":339},[1202,1206,1210,1214,1218,1222,1226,1230,1234],{"type":32,"tag":63,"props":1203,"children":1204},{"style":106},[1205],{"type":37,"value":286},{"type":32,"tag":63,"props":1207,"children":1208},{"style":76},[1209],{"type":37,"value":291},{"type":32,"tag":63,"props":1211,"children":1212},{"style":70},[1213],{"type":37,"value":296},{"type":32,"tag":63,"props":1215,"children":1216},{"style":106},[1217],{"type":37,"value":301},{"type":32,"tag":63,"props":1219,"children":1220},{"style":76},[1221],{"type":37,"value":204},{"type":32,"tag":63,"props":1223,"children":1224},{"style":106},[1225],{"type":37,"value":310},{"type":32,"tag":63,"props":1227,"children":1228},{"style":76},[1229],{"type":37,"value":268},{"type":32,"tag":63,"props":1231,"children":1232},{"style":70},[1233],{"type":37,"value":84},{"type":32,"tag":63,"props":1235,"children":1236},{"style":76},[1237],{"type":37,"value":323},{"type":32,"tag":63,"props":1239,"children":1240},{"class":65,"line":363},[1241,1245,1250,1255],{"type":32,"tag":63,"props":1242,"children":1243},{"style":70},[1244],{"type":37,"value":132},{"type":32,"tag":63,"props":1246,"children":1247},{"style":76},[1248],{"type":37,"value":1249}," {{ ref(",{"type":32,"tag":63,"props":1251,"children":1252},{"style":530},[1253],{"type":37,"value":1254},"'user_cohort_base'",{"type":32,"tag":63,"props":1256,"children":1257},{"style":76},[1258],{"type":37,"value":1259},") }} c\n",{"type":32,"tag":63,"props":1261,"children":1262},{"class":65,"line":385},[1263,1267,1271,1275,1279,1283,1288,1292],{"type":32,"tag":63,"props":1264,"children":1265},{"style":70},[1266],{"type":37,"value":345},{"type":32,"tag":63,"props":1268,"children":1269},{"style":76},[1270],{"type":37,"value":727},{"type":32,"tag":63,"props":1272,"children":1273},{"style":530},[1274],{"type":37,"value":732},{"type":32,"tag":63,"props":1276,"children":1277},{"style":76},[1278],{"type":37,"value":241},{"type":32,"tag":63,"props":1280,"children":1281},{"style":530},[1282],{"type":37,"value":741},{"type":32,"tag":63,"props":1284,"children":1285},{"style":76},[1286],{"type":37,"value":1287},") }} e ",{"type":32,"tag":63,"props":1289,"children":1290},{"style":70},[1291],{"type":37,"value":355},{"type":32,"tag":63,"props":1293,"children":1294},{"style":76},[1295],{"type":37,"value":360},{"type":32,"tag":63,"props":1297,"children":1298},{"class":65,"line":394},[1299,1304,1308],{"type":32,"tag":63,"props":1300,"children":1301},{"style":76},[1302],{"type":37,"value":1303},"  {% ",{"type":32,"tag":63,"props":1305,"children":1306},{"style":70},[1307],{"type":37,"value":758},{"type":32,"tag":63,"props":1309,"children":1310},{"style":76},[1311],{"type":37,"value":763},{"type":32,"tag":63,"props":1313,"children":1314},{"class":65,"line":812},[1315,1320,1324,1328,1332,1337,1342,1347],{"type":32,"tag":63,"props":1316,"children":1317},{"style":70},[1318],{"type":37,"value":1319},"  WHERE",{"type":32,"tag":63,"props":1321,"children":1322},{"style":106},[1323],{"type":37,"value":301},{"type":32,"tag":63,"props":1325,"children":1326},{"style":76},[1327],{"type":37,"value":204},{"type":32,"tag":63,"props":1329,"children":1330},{"style":106},[1331],{"type":37,"value":236},{"type":32,"tag":63,"props":1333,"children":1334},{"style":70},[1335],{"type":37,"value":1336}," >=",{"type":32,"tag":63,"props":1338,"children":1339},{"style":76},[1340],{"type":37,"value":1341}," CURRENT_DATE() ",{"type":32,"tag":63,"props":1343,"children":1344},{"style":70},[1345],{"type":37,"value":1346},"-",{"type":32,"tag":63,"props":1348,"children":1349},{"style":106},[1350],{"type":37,"value":1351}," 1\n",{"type":32,"tag":63,"props":1353,"children":1354},{"class":65,"line":856},[1355],{"type":32,"tag":63,"props":1356,"children":1357},{"style":76},[1358],{"type":37,"value":1359},"  {% endif %}\n",{"type":32,"tag":63,"props":1361,"children":1362},{"class":65,"line":865},[1363,1367,1371,1375],{"type":32,"tag":63,"props":1364,"children":1365},{"style":70},[1366],{"type":37,"value":146},{"type":32,"tag":63,"props":1368,"children":1369},{"style":106},[1370],{"type":37,"value":373},{"type":32,"tag":63,"props":1372,"children":1373},{"style":76},[1374],{"type":37,"value":241},{"type":32,"tag":63,"props":1376,"children":1377},{"style":106},[1378],{"type":37,"value":382},{"type":32,"tag":63,"props":1380,"children":1382},{"class":65,"line":1381},19,[1383],{"type":32,"tag":63,"props":1384,"children":1385},{"style":76},[1386],{"type":37,"value":391},{"type":32,"tag":63,"props":1388,"children":1390},{"class":65,"line":1389},20,[1391],{"type":32,"tag":63,"props":1392,"children":1393},{"style":70},[1394],{"type":37,"value":655},{"type":32,"tag":63,"props":1396,"children":1398},{"class":65,"line":1397},21,[1399],{"type":32,"tag":63,"props":1400,"children":1401},{"style":76},[1402],{"type":37,"value":1403},"  cohort_date,\n",{"type":32,"tag":63,"props":1405,"children":1407},{"class":65,"line":1406},22,[1408],{"type":32,"tag":63,"props":1409,"children":1410},{"style":76},[1411],{"type":37,"value":1412},"  day_offset,\n",{"type":32,"tag":63,"props":1414,"children":1416},{"class":65,"line":1415},23,[1417],{"type":32,"tag":63,"props":1418,"children":1419},{"style":76},[1420],{"type":37,"value":1421},"  active_users,\n",{"type":32,"tag":63,"props":1423,"children":1425},{"class":65,"line":1424},24,[1426,1431,1436,1441,1446,1451],{"type":32,"tag":63,"props":1427,"children":1428},{"style":76},[1429],{"type":37,"value":1430},"  active_users ",{"type":32,"tag":63,"props":1432,"children":1433},{"style":70},[1434],{"type":37,"value":1435},"\u002F",{"type":32,"tag":63,"props":1437,"children":1438},{"style":106},[1439],{"type":37,"value":1440}," FIRST_VALUE",{"type":32,"tag":63,"props":1442,"children":1443},{"style":76},[1444],{"type":37,"value":1445},"(active_users) ",{"type":32,"tag":63,"props":1447,"children":1448},{"style":70},[1449],{"type":37,"value":1450},"OVER",{"type":32,"tag":63,"props":1452,"children":1453},{"style":76},[1454],{"type":37,"value":89},{"type":32,"tag":63,"props":1456,"children":1458},{"class":65,"line":1457},25,[1459,1464,1469,1474,1479],{"type":32,"tag":63,"props":1460,"children":1461},{"style":70},[1462],{"type":37,"value":1463},"    PARTITION",{"type":32,"tag":63,"props":1465,"children":1466},{"style":70},[1467],{"type":37,"value":1468}," BY",{"type":32,"tag":63,"props":1470,"children":1471},{"style":76},[1472],{"type":37,"value":1473}," cohort_date ",{"type":32,"tag":63,"props":1475,"children":1476},{"style":70},[1477],{"type":37,"value":1478},"ORDER BY",{"type":32,"tag":63,"props":1480,"children":1481},{"style":76},[1482],{"type":37,"value":1483}," day_offset\n",{"type":32,"tag":63,"props":1485,"children":1487},{"class":65,"line":1486},26,[1488,1493,1497],{"type":32,"tag":63,"props":1489,"children":1490},{"style":76},[1491],{"type":37,"value":1492},"  ) ",{"type":32,"tag":63,"props":1494,"children":1495},{"style":70},[1496],{"type":37,"value":84},{"type":32,"tag":63,"props":1498,"children":1499},{"style":76},[1500],{"type":37,"value":1501}," retention_rate\n",{"type":32,"tag":63,"props":1503,"children":1505},{"class":65,"line":1504},27,[1506,1510],{"type":32,"tag":63,"props":1507,"children":1508},{"style":70},[1509],{"type":37,"value":722},{"type":32,"tag":63,"props":1511,"children":1512},{"style":76},[1513],{"type":37,"value":1514}," cohort_activity\n",{"type":32,"tag":33,"props":1516,"children":1517},{},[1518,1520,1525,1527,1532,1534,1539],{"type":37,"value":1519},"Эта таблица обновляется каждый день, добавляя только вчерашнюю активность. Partition по ",{"type":32,"tag":59,"props":1521,"children":1523},{"className":1522},[],[1524],{"type":37,"value":209},{"type":37,"value":1526}," — старые когорты не перепересчитываются. Результат: вместо чтения ",{"type":32,"tag":425,"props":1528,"children":1529},{},[1530],{"type":37,"value":1531},"5 миллиардов строк events",{"type":37,"value":1533}," читаем ",{"type":32,"tag":425,"props":1535,"children":1536},{},[1537],{"type":37,"value":1538},"500 когорт × 90 дней = 45.000 строк cube",{"type":37,"value":1540},". Dashboard запросы теперь идут прямо в cube — объём scan'а падает в 100.000 раз, latency с 45 секунд до 50 миллисекунд.",{"type":32,"tag":909,"props":1542,"children":1544},{"id":1543},"стратегия-window-functionов-расчёт-retention-rate",[1545],{"type":37,"value":1546},"Стратегия window function'ов: расчёт retention rate",{"type":32,"tag":33,"props":1548,"children":1549},{},[1550,1552,1558],{"type":37,"value":1551},"Выражение ",{"type":32,"tag":59,"props":1553,"children":1555},{"className":1554},[],[1556],{"type":37,"value":1557},"FIRST_VALUE(active_users) OVER (PARTITION BY cohort_date ORDER BY day_offset)",{"type":37,"value":1559}," \"переносит\" количество D0 юзеров в каждую строку. Благодаря этому retention rate вычисляется при записи, а не при чтении. Альтернатива — отдельный JOIN с D0, но window function в BigQuery оптимизируется под efficient slot usage (упорядоченное чтение внутри partition'а).",{"type":32,"tag":33,"props":1561,"children":1562},{},[1563,1565,1570,1572,1577],{"type":37,"value":1564},"Важно: ",{"type":32,"tag":59,"props":1566,"children":1568},{"className":1567},[],[1569],{"type":37,"value":1450},{"type":37,"value":1571}," clause не ломает partition pruning, потому что физический partition (",{"type":32,"tag":59,"props":1573,"children":1575},{"className":1574},[],[1576],{"type":37,"value":209},{"type":37,"value":1578},") совпадает с window partition'ом. BigQuery обрабатывает каждый partition независимо — cross-partition shuffle не происходит.",{"type":32,"tag":40,"props":1580,"children":1582},{"id":1581},"query-cost-optimization-slot-usage-и-caching",[1583],{"type":37,"value":1584},"Query Cost Optimization: slot usage и caching",{"type":32,"tag":33,"props":1586,"children":1587},{},[1588],{"type":37,"value":1589},"BigQuery считает деньги по объёму scan'а ($5 за TB), но для production latency критичнее — slot usage. Materialized view стратегия снижает cost, но slot contention всё ещё возможен — особенно когда 10 пользователей одновременно дёргают разные cohort фильтры на dashboard'е.",{"type":32,"tag":33,"props":1591,"children":1592},{},[1593,1598,1600,1606],{"type":32,"tag":425,"props":1594,"children":1595},{},[1596],{"type":37,"value":1597},"BI-engine caching:",{"type":37,"value":1599}," BigQuery BI Engine кэширует до 100GB горячих данных в RAM. Таблица ",{"type":32,"tag":59,"props":1601,"children":1603},{"className":1602},[],[1604],{"type":37,"value":1605},"daily_retention_cube",{"type":37,"value":1607}," из 45.000 строк × 200 байт ≈ 9MB полностью кэшируется. Следующие запросы используют 0 слотов, возвращаются за 10 миллисекунд. BI Engine резервация открывается вручную (BigQuery console → Capacity Management → 100GB tier = $300\u002Fмесяц). ROI высокий: 1000 запросов в день × $0.01 slot cost = $10\u002Fдень, в резервации $10\u002Fдень flat.",{"type":32,"tag":33,"props":1609,"children":1610},{},[1611,1616],{"type":32,"tag":425,"props":1612,"children":1613},{},[1614],{"type":37,"value":1615},"Query result caching:",{"type":37,"value":1617}," BigQuery кэширует результаты запросов на 24 часа. На dashboard'е, если все пользователи запрашивают \"когорты за последние 7 дней\", первый запрос полный, остальные из cache. Но при смене параметров (date range, segment filter) cache miss'ится. Тут снова спасает pre-aggregated cube.",{"type":32,"tag":33,"props":1619,"children":1620},{},[1621,1626],{"type":32,"tag":425,"props":1622,"children":1623},{},[1624],{"type":37,"value":1625},"Slot allocation:",{"type":37,"value":1627}," Если переходите на flat-rate pricing (500 slot = $10.000\u002Fмесяц), выделите dedicated pool для retention pipeline'а. Peak hour'ы пусть BI запросы и retention вычисления не конкурируют за слоты. В production Roibase setup'е scheduled query'и крутятся в off-peak (03:00-05:00), user-facing dashboard'ы на flex slot'ах (100-500 с autoscale).",{"type":32,"tag":40,"props":1629,"children":1631},{"id":1630},"identity-resolution-интеграция-кросс-девайс-когорты",[1632],{"type":37,"value":1633},"Identity Resolution интеграция: кросс-девайс когорты",{"type":32,"tag":33,"props":1635,"children":1636},{},[1637,1639,1644],{"type":37,"value":1638},"Классический когорт анализ работает по ",{"type":32,"tag":59,"props":1640,"children":1642},{"className":1641},[],[1643],{"type":37,"value":310},{"type":37,"value":1645},", но в cross-device journey один человек может иметь 3+ ID (web anonymous, app logged-in, CRM). Если retention = 15%, реальный retention может быть 22% — из-за фрагментации ID.",{"type":32,"tag":33,"props":1647,"children":1648},{},[1649,1651,1660,1662,1668,1670,1676,1677,1682,1683,1689,1691,1697],{"type":37,"value":1650},"В рамках ",{"type":32,"tag":1652,"props":1653,"children":1657},"a",{"href":1654,"rel":1655},"https:\u002F\u002Fwww.roibase.com.tr\u002Fru\u002Ffirstparty",[1656],"nofollow",[1658],{"type":37,"value":1659},"First-Party Вероятностный и CDP",{"type":37,"value":1661}," строится identity graph: таблица ",{"type":32,"tag":59,"props":1663,"children":1665},{"className":1664},[],[1666],{"type":37,"value":1667},"identity_map",{"type":37,"value":1669}," связывает каждый ",{"type":32,"tag":59,"props":1671,"children":1673},{"className":1672},[],[1674],{"type":37,"value":1675},"anonymous_id",{"type":37,"value":241},{"type":32,"tag":59,"props":1678,"children":1680},{"className":1679},[],[1681],{"type":37,"value":310},{"type":37,"value":241},{"type":32,"tag":59,"props":1684,"children":1686},{"className":1685},[],[1687],{"type":37,"value":1688},"crm_id",{"type":37,"value":1690}," с canonical ",{"type":32,"tag":59,"props":1692,"children":1694},{"className":1693},[],[1695],{"type":37,"value":1696},"person_id",{"type":37,"value":1698},". Обогатите cohort base этим graph'ом:",{"type":32,"tag":52,"props":1700,"children":1702},{"className":54,"code":1701,"language":56,"meta":15,"style":15},"WITH resolved_events AS (\n  SELECT\n    COALESCE(i.person_id, e.user_id) AS person_id,\n    e.event_date\n  FROM {{ source('raw', 'events') }} e\n  LEFT JOIN {{ ref('identity_map') }} i ON e.user_id = i.user_id\n)\nSELECT person_id, MIN(event_date) AS cohort_date\nFROM resolved_events\nGROUP BY person_id\n",[1703],{"type":32,"tag":59,"props":1704,"children":1705},{"__ignoreMap":15},[1706,1726,1733,1787,1804,1832,1890,1897,1925,1937],{"type":32,"tag":63,"props":1707,"children":1708},{"class":65,"line":66},[1709,1713,1718,1722],{"type":32,"tag":63,"props":1710,"children":1711},{"style":70},[1712],{"type":37,"value":73},{"type":32,"tag":63,"props":1714,"children":1715},{"style":76},[1716],{"type":37,"value":1717}," resolved_events ",{"type":32,"tag":63,"props":1719,"children":1720},{"style":70},[1721],{"type":37,"value":84},{"type":32,"tag":63,"props":1723,"children":1724},{"style":76},[1725],{"type":37,"value":89},{"type":32,"tag":63,"props":1727,"children":1728},{"class":65,"line":92},[1729],{"type":32,"tag":63,"props":1730,"children":1731},{"style":70},[1732],{"type":37,"value":1122},{"type":32,"tag":63,"props":1734,"children":1735},{"class":65,"line":126},[1736,1741,1745,1750,1754,1758,1762,1766,1770,1774,1778,1782],{"type":32,"tag":63,"props":1737,"children":1738},{"style":106},[1739],{"type":37,"value":1740},"    COALESCE",{"type":32,"tag":63,"props":1742,"children":1743},{"style":76},[1744],{"type":37,"value":291},{"type":32,"tag":63,"props":1746,"children":1747},{"style":106},[1748],{"type":37,"value":1749},"i",{"type":32,"tag":63,"props":1751,"children":1752},{"style":76},[1753],{"type":37,"value":204},{"type":32,"tag":63,"props":1755,"children":1756},{"style":106},[1757],{"type":37,"value":1696},{"type":32,"tag":63,"props":1759,"children":1760},{"style":76},[1761],{"type":37,"value":241},{"type":32,"tag":63,"props":1763,"children":1764},{"style":106},[1765],{"type":37,"value":227},{"type":32,"tag":63,"props":1767,"children":1768},{"style":76},[1769],{"type":37,"value":204},{"type":32,"tag":63,"props":1771,"children":1772},{"style":106},[1773],{"type":37,"value":310},{"type":32,"tag":63,"props":1775,"children":1776},{"style":76},[1777],{"type":37,"value":268},{"type":32,"tag":63,"props":1779,"children":1780},{"style":70},[1781],{"type":37,"value":84},{"type":32,"tag":63,"props":1783,"children":1784},{"style":76},[1785],{"type":37,"value":1786}," person_id,\n",{"type":32,"tag":63,"props":1788,"children":1789},{"class":65,"line":140},[1790,1795,1799],{"type":32,"tag":63,"props":1791,"children":1792},{"style":106},[1793],{"type":37,"value":1794},"    e",{"type":32,"tag":63,"props":1796,"children":1797},{"style":76},[1798],{"type":37,"value":204},{"type":32,"tag":63,"props":1800,"children":1801},{"style":106},[1802],{"type":37,"value":1803},"event_date\n",{"type":32,"tag":63,"props":1805,"children":1806},{"class":65,"line":154},[1807,1811,1815,1819,1823,1827],{"type":32,"tag":63,"props":1808,"children":1809},{"style":70},[1810],{"type":37,"value":132},{"type":32,"tag":63,"props":1812,"children":1813},{"style":76},[1814],{"type":37,"value":727},{"type":32,"tag":63,"props":1816,"children":1817},{"style":530},[1818],{"type":37,"value":732},{"type":32,"tag":63,"props":1820,"children":1821},{"style":76},[1822],{"type":37,"value":241},{"type":32,"tag":63,"props":1824,"children":1825},{"style":530},[1826],{"type":37,"value":741},{"type":32,"tag":63,"props":1828,"children":1829},{"style":76},[1830],{"type":37,"value":1831},") }} e\n",{"type":32,"tag":63,"props":1833,"children":1834},{"class":65,"line":163},[1835,1840,1844,1849,1854,1859,1863,1867,1871,1876,1881,1885],{"type":32,"tag":63,"props":1836,"children":1837},{"style":70},[1838],{"type":37,"value":1839},"  LEFT JOIN",{"type":32,"tag":63,"props":1841,"children":1842},{"style":76},[1843],{"type":37,"value":1249},{"type":32,"tag":63,"props":1845,"children":1846},{"style":530},[1847],{"type":37,"value":1848},"'identity_map'",{"type":32,"tag":63,"props":1850,"children":1851},{"style":76},[1852],{"type":37,"value":1853},") }} i ",{"type":32,"tag":63,"props":1855,"children":1856},{"style":70},[1857],{"type":37,"value":1858},"ON",{"type":32,"tag":63,"props":1860,"children":1861},{"style":106},[1862],{"type":37,"value":301},{"type":32,"tag":63,"props":1864,"children":1865},{"style":76},[1866],{"type":37,"value":204},{"type":32,"tag":63,"props":1868,"children":1869},{"style":106},[1870],{"type":37,"value":310},{"type":32,"tag":63,"props":1872,"children":1873},{"style":70},[1874],{"type":37,"value":1875}," =",{"type":32,"tag":63,"props":1877,"children":1878},{"style":106},[1879],{"type":37,"value":1880}," i",{"type":32,"tag":63,"props":1882,"children":1883},{"style":76},[1884],{"type":37,"value":204},{"type":32,"tag":63,"props":1886,"children":1887},{"style":106},[1888],{"type":37,"value":1889},"user_id\n",{"type":32,"tag":63,"props":1891,"children":1892},{"class":65,"line":180},[1893],{"type":32,"tag":63,"props":1894,"children":1895},{"style":76},[1896],{"type":37,"value":391},{"type":32,"tag":63,"props":1898,"children":1899},{"class":65,"line":193},[1900,1904,1909,1913,1917,1921],{"type":32,"tag":63,"props":1901,"children":1902},{"style":70},[1903],{"type":37,"value":400},{"type":32,"tag":63,"props":1905,"children":1906},{"style":76},[1907],{"type":37,"value":1908}," person_id, ",{"type":32,"tag":63,"props":1910,"children":1911},{"style":106},[1912],{"type":37,"value":109},{"type":32,"tag":63,"props":1914,"children":1915},{"style":76},[1916],{"type":37,"value":114},{"type":32,"tag":63,"props":1918,"children":1919},{"style":70},[1920],{"type":37,"value":84},{"type":32,"tag":63,"props":1922,"children":1923},{"style":76},[1924],{"type":37,"value":123},{"type":32,"tag":63,"props":1926,"children":1927},{"class":65,"line":26},[1928,1932],{"type":32,"tag":63,"props":1929,"children":1930},{"style":70},[1931],{"type":37,"value":722},{"type":32,"tag":63,"props":1933,"children":1934},{"style":76},[1935],{"type":37,"value":1936}," resolved_events\n",{"type":32,"tag":63,"props":1938,"children":1939},{"class":65,"line":280},[1940,1944],{"type":32,"tag":63,"props":1941,"children":1942},{"style":70},[1943],{"type":37,"value":871},{"type":32,"tag":63,"props":1945,"children":1946},{"style":76},[1947],{"type":37,"value":1948}," person_id\n",{"type":32,"tag":33,"props":1950,"children":1951},{},[1952,1954,1959,1961,1966],{"type":37,"value":1953},"Такой JOIN дорогой, но ",{"type":32,"tag":59,"props":1955,"children":1957},{"className":1956},[],[1958],{"type":37,"value":1667},{"type":37,"value":1960}," получает дневной incremental update и cluster'ирована по ",{"type":32,"tag":59,"props":1962,"children":1964},{"className":1963},[],[1965],{"type":37,"value":310},{"type":37,"value":1967}," — BigQuery делает hash join с минимальным overhead. В итоговых когортах D7 retention показывает реальное значение, и маркетинг принимает решения на правильных данных (переперераспределение бюджета, LTV forecast).",{"type":32,"tag":40,"props":1969,"children":1971},{"id":1970},"incremental-refresh-стратегия-backfill-vs-daily-delta",[1972],{"type":37,"value":1973},"Incremental refresh стратегия: backfill vs daily delta",{"type":32,"tag":33,"props":1975,"children":1976},{},[1977],{"type":37,"value":1978},"Критический риск materialized view'ов: если upstream data поправится (late-arriving event, GDPR deletion), view'у будет неправильна. BigQuery materialized view'ы не обновляются автоматически — вы управляете.",{"type":32,"tag":33,"props":1980,"children":1981},{},[1982],{"type":32,"tag":425,"props":1983,"children":1984},{},[1985],{"type":37,"value":1986},"Две стратегии:",{"type":32,"tag":1988,"props":1989,"children":1990},"ol",{},[1991,2002],{"type":32,"tag":1992,"props":1993,"children":1994},"li",{},[1995,2000],{"type":32,"tag":425,"props":1996,"children":1997},{},[1998],{"type":37,"value":1999},"Daily delta:",{"type":37,"value":2001}," Каждый день пересчитываем только новый partition. Быстро, но пропускает ретроспективные исправления.",{"type":32,"tag":1992,"props":2003,"children":2004},{},[2005,2010],{"type":32,"tag":425,"props":2006,"children":2007},{},[2008],{"type":37,"value":2009},"Rolling backfill:",{"type":37,"value":2011}," Каждый день пересчитываем последние 7 дней. Ловим late events, но используем 7x compute.",{"type":32,"tag":33,"props":2013,"children":2014},{},[2015],{"type":37,"value":2016},"В production Roibase — гибридный подход: daily delta + еженедельный full refresh. В dbt:",{"type":32,"tag":52,"props":2018,"children":2022},{"className":2019,"code":2020,"language":2021,"meta":15,"style":15},"language-yaml shiki shiki-themes github-dark","# dbt_project.yml\nmodels:\n  cohorts:\n    daily_retention_cube:\n      +full_refresh: \"{{ var('force_backfill', false) }}\"\n","yaml",[2023],{"type":32,"tag":59,"props":2024,"children":2025},{"__ignoreMap":15},[2026,2034,2048,2060,2072],{"type":32,"tag":63,"props":2027,"children":2028},{"class":65,"line":66},[2029],{"type":32,"tag":63,"props":2030,"children":2031},{"style":503},[2032],{"type":37,"value":2033},"# dbt_project.yml\n",{"type":32,"tag":63,"props":2035,"children":2036},{"class":65,"line":92},[2037,2043],{"type":32,"tag":63,"props":2038,"children":2040},{"style":2039},"--shiki-default:#85E89D",[2041],{"type":37,"value":2042},"models",{"type":32,"tag":63,"props":2044,"children":2045},{"style":76},[2046],{"type":37,"value":2047},":\n",{"type":32,"tag":63,"props":2049,"children":2050},{"class":65,"line":126},[2051,2056],{"type":32,"tag":63,"props":2052,"children":2053},{"style":2039},[2054],{"type":37,"value":2055},"  cohorts",{"type":32,"tag":63,"props":2057,"children":2058},{"style":76},[2059],{"type":37,"value":2047},{"type":32,"tag":63,"props":2061,"children":2062},{"class":65,"line":140},[2063,2068],{"type":32,"tag":63,"props":2064,"children":2065},{"style":2039},[2066],{"type":37,"value":2067},"    daily_retention_cube",{"type":32,"tag":63,"props":2069,"children":2070},{"style":76},[2071],{"type":37,"value":2047},{"type":32,"tag":63,"props":2073,"children":2074},{"class":65,"line":154},[2075,2080,2084],{"type":32,"tag":63,"props":2076,"children":2077},{"style":2039},[2078],{"type":37,"value":2079},"      +full_refresh",{"type":32,"tag":63,"props":2081,"children":2082},{"style":76},[2083],{"type":37,"value":585},{"type":32,"tag":63,"props":2085,"children":2086},{"style":530},[2087],{"type":37,"value":2088},"\"{{ var('force_backfill', false) }}\"\n",{"type":32,"tag":33,"props":2090,"children":2091},{},[2092,2094,2100,2102,2108],{"type":37,"value":2093},"Обычный запуск ",{"type":32,"tag":59,"props":2095,"children":2097},{"className":2096},[],[2098],{"type":37,"value":2099},"dbt run --select daily_retention_cube",{"type":37,"value":2101}," (incremental). В выходной ",{"type":32,"tag":59,"props":2103,"children":2105},{"className":2104},[],[2106],{"type":37,"value":2107},"dbt run --select daily_retention_cube --vars '{force_backfill: true}'",{"type":37,"value":2109}," (full refresh). Так контролируется trade-off между cost и accuracy.",{"type":32,"tag":40,"props":2111,"children":2113},{"id":2112},"бенчмарк-производительности-naïve-vs-оптимизированный",[2114],{"type":37,"value":2115},"Бенчмарк производительности: naïve vs оптимизированный",{"type":32,"tag":33,"props":2117,"children":2118},{},[2119],{"type":37,"value":2120},"Production dataset: 10M событий\u002Fдень, 18 месяцев истории, 5.4 миллиарда строк.",{"type":32,"tag":2122,"props":2123,"children":2124},"table",{},[2125,2154],{"type":32,"tag":2126,"props":2127,"children":2128},"thead",{},[2129],{"type":32,"tag":2130,"props":2131,"children":2132},"tr",{},[2133,2139,2144,2149],{"type":32,"tag":2134,"props":2135,"children":2136},"th",{},[2137],{"type":37,"value":2138},"Метрика",{"type":32,"tag":2134,"props":2140,"children":2141},{},[2142],{"type":37,"value":2143},"Naïve SQL",{"type":32,"tag":2134,"props":2145,"children":2146},{},[2147],{"type":37,"value":2148},"Materialized Cube",{"type":32,"tag":2134,"props":2150,"children":2151},{},[2152],{"type":37,"value":2153},"Улучшение",{"type":32,"tag":2155,"props":2156,"children":2157},"tbody",{},[2158,2182,2205,2228,2251],{"type":32,"tag":2130,"props":2159,"children":2160},{},[2161,2167,2172,2177],{"type":32,"tag":2162,"props":2163,"children":2164},"td",{},[2165],{"type":37,"value":2166},"Объём scan'а (D7 retention)",{"type":32,"tag":2162,"props":2168,"children":2169},{},[2170],{"type":37,"value":2171},"2.1 TB",{"type":32,"tag":2162,"props":2173,"children":2174},{},[2175],{"type":37,"value":2176},"18 MB",{"type":32,"tag":2162,"props":2178,"children":2179},{},[2180],{"type":37,"value":2181},"116x",{"type":32,"tag":2130,"props":2183,"children":2184},{},[2185,2190,2195,2200],{"type":32,"tag":2162,"props":2186,"children":2187},{},[2188],{"type":37,"value":2189},"Latency запроса (p95)",{"type":32,"tag":2162,"props":2191,"children":2192},{},[2193],{"type":37,"value":2194},"42 сек",{"type":32,"tag":2162,"props":2196,"children":2197},{},[2198],{"type":37,"value":2199},"0.08 сек",{"type":32,"tag":2162,"props":2201,"children":2202},{},[2203],{"type":37,"value":2204},"525x",{"type":32,"tag":2130,"props":2206,"children":2207},{},[2208,2213,2218,2223],{"type":32,"tag":2162,"props":2209,"children":2210},{},[2211],{"type":37,"value":2212},"BigQuery cost\u002Fзапрос",{"type":32,"tag":2162,"props":2214,"children":2215},{},[2216],{"type":37,"value":2217},"$10.50",{"type":32,"tag":2162,"props":2219,"children":2220},{},[2221],{"type":37,"value":2222},"$0.01",{"type":32,"tag":2162,"props":2224,"children":2225},{},[2226],{"type":37,"value":2227},"1050x",{"type":32,"tag":2130,"props":2229,"children":2230},{},[2231,2236,2241,2246],{"type":32,"tag":2162,"props":2232,"children":2233},{},[2234],{"type":37,"value":2235},"Время загрузки dashboard",{"type":32,"tag":2162,"props":2237,"children":2238},{},[2239],{"type":37,"value":2240},"timeout",{"type":32,"tag":2162,"props":2242,"children":2243},{},[2244],{"type":37,"value":2245},"\u003C1 сек",{"type":32,"tag":2162,"props":2247,"children":2248},{},[2249],{"type":37,"value":2250},"—",{"type":32,"tag":2130,"props":2252,"children":2253},{},[2254,2259,2264,2269],{"type":32,"tag":2162,"props":2255,"children":2256},{},[2257],{"type":37,"value":2258},"Slot usage (peak)",{"type":32,"tag":2162,"props":2260,"children":2261},{},[2262],{"type":37,"value":2263},"2000",{"type":32,"tag":2162,"props":2265,"children":2266},{},[2267],{"type":37,"value":2268},"5",{"type":32,"tag":2162,"props":2270,"children":2271},{},[2272],{"type":37,"value":2273},"400x",{"type":32,"tag":33,"props":2275,"children":2276},{},[2277],{"type":37,"value":2278},"Тестовый запрос: \"кривая 30-дневного retention'а для января 2026 когорты\". Naïve запрос читает events 18 раз (по дню). Materialized cube читает 30 строк.",{"type":32,"tag":33,"props":2280,"children":2281},{},[2282],{"type":37,"value":2283},"С включённым BI-engine cache latency упал с 80ms до 12ms — slot usage = ноль. На dashboard'е протестировали 50 concurrent пользователей, 99.5% uptime, median response = 18ms. Это production SLA — маркетинг может делать real-time когорт сегментацию (например, \"D3 retention \u003C 20% → добавить в push кампанию\").",{"type":32,"tag":33,"props":2285,"children":2286},{},[2287],{"type":37,"value":2288},"Анализ retention — это центр современного growth stack'а, но naïve реализация в production не прошивается. С правильным partition'ом, incremental materialized view, pre-aggregation'ом и BI-engine cache'ем достигаются \u003C100ms latency на миллионах пользователей. Cost падает в 100 раз, slot contention",{"type":32,"tag":2290,"props":2291,"children":2292},"style",{},[2293],{"type":37,"value":2294},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":15,"searchDepth":126,"depth":126,"links":2296},[2297,2298,2301,2304,2305,2306,2307],{"id":42,"depth":92,"text":45},{"id":474,"depth":92,"text":477,"children":2299},[2300],{"id":911,"depth":126,"text":914},{"id":962,"depth":92,"text":965,"children":2302},[2303],{"id":1543,"depth":126,"text":1546},{"id":1581,"depth":92,"text":1584},{"id":1630,"depth":92,"text":1633},{"id":1970,"depth":92,"text":1973},{"id":2112,"depth":92,"text":2115},"markdown","content:ru:data:cohort-table-architecture-production-scaling.md","content","ru\u002Fdata\u002Fcohort-table-architecture-production-scaling.md","ru\u002Fdata\u002Fcohort-table-architecture-production-scaling","md",1785967505213]