<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: sql</title>
<link>http://test.leftjoin.ru/tags/sql/</link>
<description></description>
<generator>E2 (v3365; Aegea)</generator>

<item>
<title>Сквозной идентификатор: решение проблемы мэтчинга персональных данных студентов Refocus</title>
<guid isPermaLink="false">148</guid>
<link>http://test.leftjoin.ru/all/skvoznoy-identifikator-reshenie-problemy-metchinga-personalnyh-d/</link>
<comments>http://test.leftjoin.ru/all/skvoznoy-identifikator-reshenie-problemy-metchinga-personalnyh-d/</comments>
<description>
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/cover.png  border=“0” width=100% height=100%&gt;&lt;/p&gt;
&lt;p&gt;В системах сквозной аналитики ключевую роль играет правильная модель атрибуции. Без нее данные невозможно интерпретировать, и их ценность для бизнеса невелика. При этом важно понимать, что любая модель напрямую зависит от качества данных.&lt;/p&gt;
&lt;p&gt;Частая проблема с сырыми данными в том, что информация об одном клиенте дублируется или, напротив, противоречит друг другу в разных источниках.&lt;/p&gt;
&lt;p&gt;Кроме того, что предобработка данных — база для аналитика, без правильного объединения персональных данных в принципе сложно отследить клиентский путь. Значит, нужно настраивать процессы объединения неоднородных персональных данных.&lt;/p&gt;
&lt;p&gt;Сегодня в любом клиентском бизнесе воронки регистрации устроены таким образом, что клиенты попадают в базу множеством способов — часто через маркетинговые каналы, которых всегда много (рассылки, реклама, соцсети). В каждом таком канале может быть ссылка на форму подписки, регистрацию на платформе или чат, и один клиент часто проходит все эти этапы. Сразу же образуется путаница в идентификации, которая сильно влияет на качество данных и результаты аналитики, если ее не лечить.&lt;/p&gt;
&lt;p&gt;Мы столкнулись с этой проблемой, работая с одним из наших клиентов, и решили ее, создав сквозной идентификатор. Это уникальный номер, который присваивается реальному клиенту и дублируется во все источники, где есть данные об этом клиенте, тем самым избавляя от путаницы.&lt;/p&gt;
&lt;h2&gt;Кейс Refocus: данные и путь клиента&lt;/h2&gt;
&lt;p&gt;Мы разрабатывали кастомную систему сквозной аналитики для эдтех-стартапа Refocus. Данные каждого студента в системы Refocus попадали из нескольких источников и были записаны несколько раз — как минимум при регистрации на курс, при первом входе на образовательную платформу и при входе в чат сопровождения.&lt;/p&gt;
&lt;p&gt;В нашем случае мэтчинг был важнее всего по трем источникам из тринадцати:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;amoCRM,&lt;/b&gt; где фиксируется весь клиентский путь студента;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Discord,&lt;/b&gt; где проходило сопровождение студентов;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thinkific,&lt;/b&gt; сама образовательная платформа с курсами.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Остальные источники, с которыми мы работали, либо не содержали данных студентов (например, цифры эффективности работы sales-менеджеров были завязаны на данных сотрудников и трекались через другие системы), либо дублировали информацию из указанных трех.&lt;/p&gt;
&lt;p&gt;В Discord и Thinkific данные попадали напрямую, от студентов при регистрации в системах, а затем подтягивались в amoCRM. Основные причины несовпадения клиентских данных как у Refocus, так и в похожих случаях — человеческий фактор (опечатки), наличие у людей более чем одного телефона или адреса почты и ограничения самих платформ, с которых приходят данные: разный заданный формат полей и их количество.&lt;/p&gt;
&lt;p&gt;Часть этих факторов может решаться корректировкой самой клиентской воронки. Правда, не все платформы позволяют одинаково настроить вводные поля, а просьбы вводить данные в конкретном формате не всегда работают и не страхуют от ошибок. Плюс, задача аналитиков — получить чистые данные в любом случае.&lt;/p&gt;
&lt;h2&gt;Задача и поиск решения&lt;/h2&gt;
&lt;p&gt;Данные в Refocus мы подгружали в хранилище в BigQuery напрямую из интересующих нас источников (рекламных кабинетов, LMS и т. д.), используя Python. В дальнейшем на этих данных строились дашборды в Tableau.&lt;/p&gt;
&lt;p&gt;Обнаружить проблему несложно — при создании хранилища и дальнейшей выгрузке данных из него мы в любом случае чистим датасет от дубликатов и несовпадений.&lt;/p&gt;
&lt;p&gt;Поля, в которых возникали ошибки и для которых нам важен был мэтчинг, чтобы правильно отследить клиентский путь:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;имя — да, люди иногда вводят разные вариации ФИО (Юлия, Юля и Бля — на деле один человек!);&lt;/li&gt;
&lt;li&gt;телефон — с кодом страны или без, с пробелами, дефисами или слитно;&lt;/li&gt;
&lt;li&gt;электронная почта — длинные строки сложного формата, в которых легко опечататься.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Поначалу, пока количество студентов Refocus было относительно небольшим, достаточно было скриптов, которые объединяли данные по одному из этих полей. В полученных таблицах в Tableau проводился поиск строк с пустым значением в соответствующем поле — и вот видно всех студентов, чьи данные не сошлись.&lt;/p&gt;
&lt;p&gt;Количество таких строк было в пределах пары десятков, и трекать и объединять их было несложно вручную. Это делалось прямо в первоисточниках сотрудниками Refocus, которые могли поправить опечатки и ошибки у себя в системах. После этого наш код выгрузки в хранилище перезапускался и тянул уже чистые данные. Если после этого что-то не сходилось, то наши аналитики правили информацию на уровне базы данных.&lt;/p&gt;
&lt;p&gt;Но при росте компании в какой-то момент число студентов, потерянных при мэтчинге, могло достигать сотни за месяц. Пока ошибка обнаружится, данные поправят в источниках, а мы перезапустим код выгрузки, могло пройти несколько часов — а это критичный интервал. Да и перезапускать выгрузку каждый день ради нескольких несовпадений — неэффективно. Стало понятно, что масштаб проблемы требует более точного и универсального решения.&lt;/p&gt;
&lt;p&gt;Вообще, в такой ситуации возможны несколько вариантов. Можно бесконечно править скрипты мэтчинга, учитывая новые и новые случаи и создавая костыли. А можно, например, настроить алерты в оркестраторе процессов (в нашем случае  — Airflow), которые позволят моментально узнавать о появившемся несовпадении и объединять “потерянные” клиентские сущности по паре за раз. Но это все еще неполная автоматизация, и она только ускоряет, а не упрощает процесс.&lt;/p&gt;
&lt;p&gt;Руководствуясь соображениями эффективности, мы предложили ввести сквозной идентификатор — одно значение ID, присваиваемое одному клиенту после автоматической интеграции его данных из разных источников.&lt;/p&gt;
&lt;h2&gt;Реализация решения и рабочий процесс&lt;/h2&gt;
&lt;p&gt;Чтобы понять масштаб проблемы, мы начали с того, что создали таблицы несовпадающих персональных данных. Для этого мы использовали скрипты на Python. Эти скрипты объединяли данные из разных источников и создавали из них большую сводную таблицу. Для того, чтобы свести данные о студенте в одну сущность, использовался мэтчинг по адресу электронной почты. Мы попробовали мэтчить по имени, фамилии, телефону (который сначала надо было привести к одному формату!) и почте, и именно последний вариант показал самую высокую точность. Возможно, дело в том, что из всех данных почта имеет самый однородный формат, поэтому остается учитывать только опечатки.&lt;/p&gt;
&lt;p&gt;Например, нам нужно было мэтчить данные для создания дашборда по возвратам, о которых информация объединялась как раз из наших трех основных источников. В ранней версии скрипта данные отбирались таким образом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH snapshot_ AS (
      SELECT DISTINCT s.*,
        IFNULL(ae.name, ap.name) as contact_name,
        ap.phone, ae.email,
        split(replace(trim(lower(ae.email)),' ',''),'@')[OFFSET(0)] as email_first_part,
        ai.thinkific_id, ai.intercom_id, ac.student_id
      FROM (
        SELECT *,
          ROW_NUMBER() OVER(PARTITION BY lead_id ORDER BY updated_at DESC) as num_,
        FROM `Differture.amocrm_leads_snapshot`
      ) s
      LEFT JOIN (
        SELECT DISTINCT lead_id, contact_id, name, email,
          ROW_NUMBER() OVER(PARTITION BY lead_id ORDER BY contact_id) as num1_
        FROM `Differture.amo_emails`
      ) ae using(lead_id)
      LEFT JOIN (
        SELECT DISTINCT lead_id, contact_id, name, phone,
          ROW_NUMBER() OVER(PARTITION BY lead_id ORDER BY contact_id) as num2_
        FROM `Differture.amo_phones`
      ) ap using(lead_id)
      LEFT JOIN `Differture.amo_contact_thinkific_intercom_match` ai using(lead_id)
      LEFT JOIN `Differture.AmoContacts` ac on cast(ae.contact_id as string)=ac.amo_id
      WHERE (num_=1 or num_ is null) and (num1_=1 or num1_ is null) and (num2_=1 or num2_ is null)
        and s.pipeline_id in (4920421,5245535) and s.status_id=142 and lower(s.lead_name) not like '%test%'
    )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Как можно заметить, идея сквозного идентификатора здесь уже присутствует — фигурирует &lt;tt&gt;student_id&lt;/tt&gt;. На самом деле, в этой версии скрипта это графа из AmoContacts — таблицы, в которой хранятся только данные из amoCRM. Никаких джойнов по &lt;tt&gt;student_id&lt;/tt&gt; пока не происходит. А происходят по &lt;tt&gt;email_first_part&lt;/tt&gt;, адресу почты до символа @:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select distinct * from th_amo_ds_rf
    left join calendly ce using(email_first_part)
    left join typeform_live tfl on email_first_part=tf_email_first_part
    left join typeform tf using(email_first_part)
    left join csat using(email_first_part)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Первым шагом по практическому введению идентификатора была таблица &lt;b&gt;students_main_info&lt;/b&gt;, созданная в BigQuery in-house специалистом Refocus. К сожалению, у нас нет доступа к коду, который использовался для присвоения идентификатора. Зато мы можем показать вид этой таблицы:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_full_name&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_email&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_country_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_country_name&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_courses_ids&lt;/td&gt;
&lt;td style="text-align: center"&gt;array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_courses_names&lt;/td&gt;
&lt;td style="text-align: center"&gt;array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_cohort_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_cohort_name&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;cohort_community_manager_name&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;cohort_community_manager_email&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_onboarding_live_session_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_onboarding_live_session_time&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;student_onboarding_live_session_zoom_url&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;amo_contact_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;intercom_contact_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;thinkific_student_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;discord_user_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;discord_user_discord_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;discord_guild_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;discord_channel_id&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;discord_roles&lt;/td&gt;
&lt;td style="text-align: center"&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;В students_main_info хранились данные из нужных источников с общим идентификатором в первой строке, и объединение проходило через сравнение этого поля.&lt;/p&gt;
&lt;p&gt;При этом поле &lt;tt&gt;student_id&lt;/tt&gt; использовалось пока не везде; также использовались другие поля этой таблицы — например, &lt;tt&gt;thinkific_student_id&lt;/tt&gt; или &lt;tt&gt;discord_user_id&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;После выгрузки и мэтчинга данных с помощью students_main_info студентов, которые потерялись при объединении, стало меньше, чем при первой схеме мэтчинга. Так мы убедились, что движемся в верном направлении. Тем не менее, использование одной таблицы, которая содержит больше десятка полей обо всех имеющихся персональных данных, не очень эффективно. Данные в ней уже обработаны скриптом специалиста Refocus, и если надо сверить их с сырыми источниками или ввести новый критерий отслеживания, все придется менять на бэкенде.&lt;/p&gt;
&lt;h2&gt;Что получилось в итоге&lt;/h2&gt;
&lt;p&gt;После теста сквозного идентификатора через одну большую таблицу мы продолжили улучшать структуру данных на бэке. Вместо students_main_info усилиями специалиста Refocus появилась подробная сеть более мелких таблиц, которые могут обращаться друг к другу и лежат в одном хранилище с нашими таблицами сырых данных.&lt;/p&gt;
&lt;p&gt;Вот так выглядела схема соотношения этих таблиц:&lt;/p&gt;
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/image-2.png  border=“0” width=100% height=100%&gt;&lt;/p&gt;
&lt;p&gt;А вот так выглядела основная таблица Students:&lt;/p&gt;
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/students.png border=“0” width=40% height=40%&gt;&lt;/p&gt;
&lt;p&gt;В ней-то и находились основные персональные данные студентов с присвоенным идентификатором, и к ней можно было обращаться для мэтчинга из остальных источников.&lt;/p&gt;
&lt;p&gt;Остальные таблицы выглядели похоже: всегда было поле с идентификатором и информация о какой-то характеристике студента — когорта, курс, роль в дискорде и так далее.&lt;/p&gt;
&lt;p&gt;Финальный код, написанный нашими аналитиками,  объединял данные при выгрузке из хранилища, и больше не опирался на ненадежный мэтчинг через имейл.&lt;/p&gt;
&lt;p&gt;Сначала он отбирал собранные нами данные из amoCRM &lt;tt&gt;(amocrm_leads_snapshot)&lt;/tt&gt; и объединял их с контактной информацией клиентов. Затем в таблицу добавлялось поле &lt;tt&gt;student_id&lt;/tt&gt; и отбирались данные, которые понадобятся нам дальше.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH snapshot_ AS (
      SELECT DISTINCT s.*,
        ac.name as contact_name, ac.phone, ac.email,
        split(replace(trim(lower(ac.email)),' ',''),'@')[OFFSET(0)] as email_first_part,
        ac.intercom_id, ac.student_id
      FROM (
        SELECT *,
          ROW_NUMBER() OVER(PARTITION BY lead_id ORDER BY updated_at DESC) as num_,
        FROM `Differture.amocrm_leads_snapshot`
      ) s
      LEFT JOIN (
        select cast(al.amo_id as INT64) as lead_id, cast(ac.amo_id as INT64) as contact_id,
          ac.name, emails as email, phone, student_id, ic.intercom_id,
          ROW_NUMBER() OVER(PARTITION BY al.amo_id ORDER BY ac.amo_id) as num1_
        from `Differture.AmoContacts` ac
        left join `Differture.AmoLeads` al on al.amo_contact_id=ac.id
        left join `Differture.IntercomContacts` ic using(student_id)
        , unnest(ac.emails) emails
      ) ac using(lead_id)
      WHERE (num_=1 or num_ is null) and (num1_=1 or num1_ is null)
        and s.pipeline_id in (4920421,5245535) and s.status_id=142 and lower(s.lead_name) not like '%test%'
    )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь при создании общей таблицы о возвратах с данными из amo, Thinkific и Discord объединение проходило через student_id:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;th_amo_ds_rf as (
      select distinct * except (channel_id, channel),
        ifnull(channel_id, 'Not in discord') as channel_id,
        ifnull(channel, 'Not in discord') as channel
      from thinkific_amo_refunds
      full outer join discord using(student_id)
    )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Когда объединенные таблицы данных студентов были созданы, получить таблицы несовпадений можно было простой строкой кода в Tableau:&lt;/p&gt;
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/filter.png border=“0” width=50% height=50%&gt;&lt;/p&gt;
&lt;p&gt;Пустое значение поля student_id означает, что мэтча не случилось — где-то информация расходилась слишком сильно и не подтянулась в таблицы с идентификатором. Раньше, до введения идентификатора, поиск был таким же, но обращался к полям почты, телефона или имени-фамилии.&lt;/p&gt;
&lt;p&gt;Ниже можно увидеть таблицу, где данные из Thinkific не совпадали с amoCRM после перехода на Student ID. В этом случае студент есть в LMS, значит, на курсе учится — но его либо нет в системе учета, либо данные в ней разнятся с LMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/unnamed.png  border=“0” width=100% height=100%&gt;&lt;/p&gt;
&lt;p&gt;А вот таблица, где данные из Discord не совпадали с amoCRM. Все так же, как выше — студент есть в чатах сопровождения, но не ищется по своим данным в amoCRM.&lt;/p&gt;
&lt;p&gt;&lt;img src=http://test.leftjoin.ru/pictures/discord.png  border=“0” width=100% height=100%&gt;&lt;/p&gt;
&lt;p&gt;Оба скриншота показывают количество несовпадений примерно за месяц. Как видно по этим таблицам, количество несовпадений уменьшилось с 80-90 до пары десятков — примерно на 75%. Это позволило сократить количество перезапусков кода выгрузки вручную и уменьшить затраты времени и технических ресурсов на поддержание системы.&lt;/p&gt;
&lt;h2&gt;Выводы&lt;/h2&gt;
&lt;p&gt;Сквозной идентификатор — эффективное решение проблемы мэтчинга персональных данных. Он позволяет максимально автоматизировать процесс отслеживания и устранения несовпадений или дубликатов клиентских сущностей при выгрузке данных для анализа. В случаях, когда объем данных в системе невелик, а у компании нет возможности выделить ресурсы на реализацию такого решения, можно воспользоваться и другими вариантами. Например, алерты в оркестраторе процессов хорошо справятся в ситуации, когда объединить данные — вопрос ручного запуска одного скрипта раз в неделю. Но сквозной идентификатор — наверное, самое универсальное из доступных решений, которое покроет большинство ошибок и заметно уменьшит погрешность в качестве данных.&lt;/p&gt;
</description>
<pubDate>Mon, 16 Sep 2024 17:57:31 +0300</pubDate>
</item>

<item>
<title>Десять советов, чтобы писать SQL-код, который приятно читать и использовать</title>
<guid isPermaLink="false">130</guid>
<link>http://test.leftjoin.ru/</link>
<comments>http://test.leftjoin.ru/</comments>
<description>
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/1*NGniUKoit2YaJY08brT01g.png" border="0" width="100%" height="100%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p class="note"&gt;Перевод статьи &lt;a href="https://towardsdatascience.com/10-best-practices-to-write-readable-and-maintainable-sql-code-427f6bb98208"&gt;”10 Best Practices to Write Readable and Maintainable SQL Code”&lt;/a&gt; автора &lt;a href="https://davidjmartins.medium.com/?source=post_page-----427f6bb98208-----------------------------------"&gt;David Martins&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Как писать SQL-запросы, которые ваша команда сможет легко читать и использовать?&lt;/h2&gt;
&lt;p&gt;Без хорошей культуры написания SQL-запросы очень легко становятся запутанными. У каждого члена команды могут быть свои привычки написания запросов на SQL и вы очень быстро можете получить запутанный код, который будет понятен лишь одному человеку, а все остальные не смогут его использовать.&lt;/p&gt;
&lt;p&gt;Думаю, вы осознаёте важность наличия общих правил по написанию SQL-запросов в команде. Эта статья может стать отличным руководством для формирования таких правил!&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;1. Используйте верхний регистр для ключевых слов&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Начнем с базовых вещей: используйте заглавные буквы для ключевых слов SQL и строчные буквы для обозначения таблиц и столбцов. Также рекомендуется использовать прописные буквы для функций SQL (FIRST_VALUE(), DATE_TRUNC() и т. д.), хотя это спорно.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select id, name from company.customers&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT id, name FROM company.customers&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;b&gt;2. Используйте Snake Case для схем, таблиц, столбцов&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;В разных языках программирования есть разные оптимальные стили написания названий из нескольких слов: camelCase, PascalCase, kebab-case, and snake_case являются наиболее распространенными.&lt;/p&gt;
&lt;p&gt;Если говорить про SQL, Snake Case (иногда называемый регистром подчеркивания) является наиболее широко используемым правилом. Чтобы писать в стиле snake_case, нужно заменить пробелы знаками подчеркивания. При этом все слова пишутся строчными буквами.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT Customers.id, 
       Customers.name, 
       COUNT(WebVisit.id) as nbVisit
FROM COMPANY.Customers
JOIN COMPANY.WebVisit ON Customers.id = WebVisit.customerId

WHERE Customers.age &amp;lt;= 30
GROUP BY Customers.id, Customers.name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       COUNT(web_visit.id) as nb_visit
FROM company.customers
JOIN company.web_visit ON customers.id = web_visit.customer_id

WHERE customers.age &amp;lt;= 30
GROUP BY customers.id, customers.name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Хотя некоторым нравится использовать разные стили составных названий (чтобы различать схемы, таблицы и столбцы), я бы рекомендовал придерживаться Snake Case.&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;3. Используйте псевдонимы, чтобы сделать запрос понятнее&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Хорошо известно, что псевдонимы — это удобный способ для переименования таблиц или столбцов, чтобы добавить точность смысловой нагрузкой. Не стесняйтесь давать псевдонимы таблицам и столбцам, чтобы название лучше описывало процесс в а функциях.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       customers.context_col1,
       nested.f0_
FROM company.customers
JOIN (
          SELECT customer_id,
                 MIN(date)
          FROM company.purchases
          GROUP BY customer_id
      ) ON customer_id = customers.id&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       customers.context_col1 as ip_address,
       first_purchase.date    as first_purchase_date
FROM company.customers
JOIN (
          SELECT customer_id,
                 MIN(date) as date
          FROM company.purchases
          GROUP BY customer_id
      ) AS first_purchase 
        ON first_purchase.customer_id = customers.id&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Я обычно использую для столбцов строчные буквы “as”, а для таблиц — прописные “AS”.&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;4. Форматирование: осторожно используйте отступы и пробелы&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Это базовый принцип. Один из ключевых элементов, чтобы все функции в вашем запросе были четко видны. Если вы знаете Python (в нем без грамотных отступов код не будет работать), то примените этот навык здесь.&lt;/p&gt;
&lt;p&gt;Используйте пробелы после ключевого слова и при обозначении подзапроса или производной таблицы.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, customers.name, customers.age, customers.gender, customers.salary, first_purchase.date
FROM company.customers
LEFT JOIN ( SELECT customer_id, MIN(date) as date FROM company.purchases GROUP BY customer_id ) AS first_purchase 
ON first_purchase.customer_id = customers.id 
WHERE customers.age&amp;lt;=30&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       customers.age, 
       customers.gender, 
       customers.salary,
       first_purchase.date
FROM company.customers
LEFT JOIN (
              SELECT customer_id,
                     MIN(date) as date 
              FROM company.purchases
              GROUP BY customer_id
          ) AS first_purchase 
            ON first_purchase.customer_id = customers.id
WHERE customers.age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Обратите внимание, как использованы пробелы в условии “where”.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT id WHERE customers.age&amp;lt;=30&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT id WHERE customers.age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;b&gt;5. Избегайте Select&lt;/b&gt;*&lt;/h3&gt;
&lt;p&gt;Не забывайте об этом правиле: вам следует точно указывать, какие элементы таблицы вы хотите выбрать и забыть про Select*!&lt;/p&gt;
&lt;p&gt;Select* делает ваш запрос неясным, поскольку он скрывает намерения, стоящие за запросом. Кроме того, помните, что ваши таблицы могут эволюционировать и влиять на Select*. Вот почему я не большой поклонник инструкции EXCEPT().&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT * EXCEPT(id) FROM company.customers&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT name,
       age,
       salary
FROM company.customers&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;b&gt;6. Используйте синтаксис JOIN ANSI-92&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;…для соединения таблиц, вместо условия WHERE. Несмотря на то, что для соединения таблиц можно использовать как условие WHERE, так и условие JOIN, лучше использовать синтаксис JOIN/ANSI-92.&lt;/p&gt;
&lt;p&gt;Хотя с точки зрения производительности разницы нет, условие JOIN отделяет логику отношения от фильтров.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       COUNT(transactions.id) as nb_transaction
FROM company.customers, company.transactions
WHERE customers.id = transactions.customer_id
      AND customers.age &amp;lt;= 30
GROUP BY customers.id, customers.name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       COUNT(transactions.id) as nb_transaction
FROM company.customers
JOIN company.transactions ON customers.id = transactions.customer_id
WHERE customers.age &amp;lt;= 30
GROUP BY customers.id, customers.name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Синтаксис, основанный на условии “Where”, также известный как ANSI-89, старше нового ANSI-92, поэтому он всё ещё очень распространен. Сегодня большинство разработчиков и аналитиков данных используют синтаксис JOIN.&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;7. Используйте обобщённое табличное выражение (Common Table Expression — CTE)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;CTE позволяет создать запрос, результат которого существует временно и может использоваться в более крупном запросе. CTE доступны в большинстве современных баз данных.&lt;/p&gt;
&lt;p&gt;Он работает как производная таблица с двумя преимуществами:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Использование CTE улучшает читабельность вашего запроса.&lt;/li&gt;
&lt;li&gt;CTE определяется один раз, после чего на него можно ссылаться многократно.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CTE объявляется с помощью инструкции &lt;b&gt;WITH … AS&lt;/b&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH my_cte AS
(
  SELECT col1, col2 FROM table
)
SELECT * FROM my_cte&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT customers.id, 
       customers.name, 
       customers.age, 
       customers.gender, 
       customers.salary,
       persona_salary.avg_salary as persona_avg_salary,
       first_purchase.date
FROM company.customers
JOIN (
          SELECT customer_id,
                 MIN(date) as date 
          FROM company.purchases
          GROUP BY customer_id
      ) AS first_purchase 
        ON first_purchase.customer_id = customers.id
JOIN (
          SELECT age,
             gender,
             AVG(salary) as avg_salary
         FROM company.customers
         GROUP BY age, gender
      ) AS persona_salary 
        ON persona_salary.age = customers.age
           AND persona_salary.gender = customers.gender
WHERE customers.age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH first_purchase AS
(
   SELECT customer_id,
          MIN(date) as date 
   FROM company.purchases
   GROUP BY customer_id
),
persona_salary AS
(
   SELECT age,
          gender,
          AVG(salary) as avg_salary
   FROM company.customers
   GROUP BY age, gender
)
SELECT customers.id, 
       customers.name, 
       customers.age, 
       customers.gender, 
       customers.salary,
       persona_salary.avg_salary as persona_avg_salary,
       first_purchase.date
FROM company.customers
JOIN first_purchase ON first_purchase.customer_id = customers.id
JOIN persona_salary ON persona_salary.age = customers.age
                       AND persona_salary.gender = customers.gender
WHERE customers.age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;&lt;b&gt;8. Иногда стоит разделить запрос на несколько&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;...но не увлекайтесь. Давайте разберем на примере.&lt;/p&gt;
&lt;p&gt;Я часто использую AirFlow для выполнения SQL-запросов в BigQuery, преобразования данных и подготовки визуализации данных. В нем есть оркестратор рабочих процессов (Airflow), который выполняет запросы в определенном порядке. В некоторых ситуациях лучше разбивать сложные запросы на несколько более мелких.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Вместо:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;CREATE TABLE customers_infos AS
SELECT customers.id,
       customers.salary,
       traffic_info.weeks_since_last_visit,
       category_info.most_visited_category_id,
       purchase_info.highest_purchase_value
FROM company.customers
LEFT JOIN ([..]) AS traffic_info
LEFT JOIN ([..]) AS category_info
LEFT JOIN ([..]) AS purchase_info&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Вы могли бы использовать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;## STEP1: Create initial table
CREATE TABLE public.customers_infos AS
SELECT customers.id,
       customers.salary,
       0 as weeks_since_last_visit,
       0 as most_visited_category_id,
       0 as highest_purchase_value
FROM company.customers
## STEP2: Update traffic infos
UPDATE public.customers_infos
SET weeks_since_last_visit = DATE_DIFF(CURRENT_DATE,
                                       last_visit.date, WEEK)
FROM (
         SELECT customer_id, max(visit_date) as date
         FROM web.traffic_info
         GROUP BY customer_id
     ) AS last_visit
WHERE last_visit.customer_id = customers_infos.id
## STEP3: Update category infos
UPDATE public.customers_infos
SET most_visited_category_id = [...]
WHERE [...]
## STEP4: Update purchase infos
UPDATE public.customers_infos
SET highest_purchase_value = [...]
WHERE [...]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;ПРЕДУПРЕЖДЕНИЕ!&lt;/b&gt; &lt;br /&gt;
Несмотря на то, что этот метод отлично подходит для упрощения сложных запросов, вместе с повышением читабельности кода, вы можете здорово понизить его производительность.&lt;/p&gt;
&lt;p&gt;Это особенно важно, если вы работаете с базой данных OLAP или любой колоночной базой данных, оптимизированной для агрегационных и аналитических запросов (SELECT, AVG, MIN, MAX, …), но менее производительной, когда речь идет о транзакциях (UPDATE).&lt;/p&gt;
&lt;p&gt;Несмотря на это в некоторых случаях это может улучшить производительность работы с базой данных. Даже в современной базе данных, ориентированной на колонки, слишком большое количество JOIN’ов приведет к проблемам с памятью или производительностью. В таких ситуациях разделение вашего запроса обычно помогает улучшить производительность и оптимизировать используемую память.&lt;/p&gt;
&lt;p&gt;Кроме того, не стоит забывать, что вам нужен инструмент или оркестратор для выполнения ваших запросов в определенном порядке.&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;9. Осмысленные названия, основанные на ваших внутренних правилах&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;Правильно называть схемы и таблицы сложно. Какие варианты возможных имён использовать — вопрос дискуссионный, но задача выбора единых правил присвоения имён — это не сложно. Вы должны определить &lt;b&gt;свои&lt;/b&gt; правила и использовать их всей командой.&lt;/p&gt;
&lt;p&gt;“В компьютерных науках есть только две сложные проблемы: аннулирование кэша и придумывание названий.” — Фил Карлтон&lt;/p&gt;
&lt;p&gt;Вот примеры правил, которые я использую:&lt;/p&gt;
&lt;h4&gt;&lt;b&gt;Схемы&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;Если вы работаете с аналитической базой данных, которая служит нескольким целям, хорошей практикой является организация таблиц в выразительные схемы.&lt;/p&gt;
&lt;p&gt;В нашей базе данных BigQuery у нас есть одна схема для каждого источника данных. Что еще более важно, мы выводим результаты в разных схемах в зависимости от их назначения.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Любая таблица, которая будет доступна для стороннего инструмента, находится в *&lt;b&gt;общедоступной&lt;/b&gt;* схеме. Инструменты визуализации данных, такие как DataStudio или Tableau, получают данные из неё.&lt;/li&gt;
&lt;li&gt;Поскольку мы используем машинное обучение с BQML, ****&lt;b&gt;у нас есть специальная схема &lt;/b&gt;*machine_learning.***&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Таблицы&lt;/h4&gt;
&lt;p&gt;Сами таблицы должны называться в соответствии с правилами. В Agorapulse у нас есть несколько дашбордов для визуализации данных, каждый из которых имеет свое назначение: дашборд управления маркетингом, дашборд управления продуктом, дашборд управления для руководителей и многие другие.&lt;/p&gt;
&lt;p&gt;Каждая таблица в нашей общедоступной схеме имеет префикс имени дашборда. Выглядит это примерно так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;product_inbox_usage
product_addon_competitor_stats
marketing_acquisition_agencies
Executive_funnel_overview&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;При командной работе стоит уделить время определению общих правил. Когда вы придумываете название новой таблицы, то не используйте быстрое и заезженное имя, которое вы «измените позже» — вы наверняка этого не сделаете.&lt;/p&gt;
&lt;p&gt;Не стесняйтесь использовать эти примеры для создания своих правил.&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;10. Пишите полезные комментарии… но не слишком много&lt;/b&gt;&lt;/h3&gt;
&lt;p class="note"&gt;&lt;i&gt;Примечание переводчика:&lt;/i&gt; Автор пишет, что “хорошо написанный код с правильными названиями не нуждается в комментариях” и иронично продемонстрировал детально расписанный код вообще без комментариев. Мы все-таки за то, чтобы необходимые пояснения в коде были, ведь это сильно ускоряет понимание сути запроса.&lt;/p&gt;
&lt;p&gt;Я согласен с тезисом, что хорошо написанный код с правильными названиями не нуждается в комментариях. Тот, кто читает ваш код, должен понимать логику и замысел еще до того, как появится результат работы кода.&lt;/p&gt;
&lt;p&gt;Тем не менее, комментарии могут быть полезны в некоторых ситуациях. Но перебарщивать с ними не стоит!&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Избегать:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH fp AS
(
   SELECT c_id,               # customer id
          MIN(date) as dt     # date of first purchase
   FROM company.purchases
   GROUP BY c_id
),
ps AS
(
   SELECT age,
          gender,
          AVG(salary) as avg
   FROM company.customers
   GROUP BY age, gender
)
SELECT customers.id, 
       ct.name, 
       ct.c_age,            # customer age
       ct.gender,
       ct.salary,
       ps.avg,              # average salary of a similar persona
       fp.dt                # date of first purchase for this client
FROM company.customers ct
# join the first purchase on client id
JOIN fp ON c_id = ct.id
# match persona based on same age and genre
JOIN ps ON ps.age = c_age
           AND ps.gender = ct.gender
WHERE c_age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Предпочтительно:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH first_purchase AS
(
   SELECT customer_id,
          MIN(date) as date 
   FROM company.purchases
   GROUP BY customer_id
),
persona_salary AS
(
   SELECT age,
          gender,
          AVG(salary) as avg_salary
   FROM company.customers
   GROUP BY age, gender
)
SELECT customers.id, 
       customers.name, 
       customers.age, 
       customers.gender, 
       customers.salary,
       persona_salary.avg_salary as persona_avg_salary,
       first_purchase.date
FROM company.customers
JOIN first_purchase ON first_purchase.customer_id = customers.id
JOIN persona_salary ON persona_salary.age = customers.age
                       AND persona_salary.gender = customers.gender
WHERE customers.age &amp;lt;= 30&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;&lt;b&gt;Вывод&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;SQL великолепен. Это одна из основ анализа данных, науки о данных, инжиниринга данных и даже разработки программного обеспечения: неправильный код не простит ошибки. Его гибкость является силой, но может быть ловушкой.&lt;/p&gt;
&lt;p&gt;Сначала вы можете этого не осознавать, особенно если с кодом работаете только вы. Но когда вы работаете в команде или если кто-то должен будет продолжать вашу работу, SQL-код написанный без соблюдения этих правил будет ночным кошмаром аналитика.&lt;/p&gt;
&lt;p&gt;В этой статье я обобщил наиболее распространенные рекомендации по написанию SQL-запросов. Конечно, некоторые из них дискуссионные или основаны на личном мнении — вы можете черпать отсюда вдохновение и создавать собственные правила со своей командой.&lt;/p&gt;
&lt;p&gt;Я надеюсь, что эта информация поможет вам вывести качество SQL на новый уровень!&lt;/p&gt;
</description>
<pubDate>Tue, 22 Feb 2022 16:24:18 +0300</pubDate>
</item>

<item>
<title>Регулярные выражения как способ решения задач в SQL</title>
<guid isPermaLink="false">127</guid>
<link>http://test.leftjoin.ru/all/sql-regular-expressions/</link>
<comments>http://test.leftjoin.ru/all/sql-regular-expressions/</comments>
<description>
&lt;p&gt;Использование регулярных выражений для выбора определенных ячеек таблицы используется в SQL не так часто, как могло бы. И очень зря — этот инструмент легко позволяет найти в таблице нужные значения, так как использует шаблон для поиска последовательности метасимволов в тексте. Такие задачи встречаются как в различных тренажерах или на собеседованиях на позиции аналитика, так и в реальной практике аналитиков, которые работают в базах SQL. Подобные шаблоны для поиска определенных элементов и последовательностей в тексте используются в самых разных областях. Например, на многих сайтах существует проверка email-адреса, который вы вводите при регистрации, на соответствие стандартному  шаблону.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://test.leftjoin.ru/pictures/email.png.jpg"  border="0" width="100%" height="100%"&gt;&lt;/p&gt;
&lt;p&gt;Как это сделать, мы разберёмся постепенно, а пока давайте начнем с самого начала: с определения.&lt;/p&gt;
&lt;p&gt;&lt;i&gt;Что такое «регулярное выражение»?&lt;/i&gt;&lt;br /&gt;
&lt;b&gt;Регулярное выражение —&lt;/b&gt; последовательность букв и/или символов, которая может встречаться в слове. Например, есть достаточно простое регулярное выражение “bat”. Оно читается как буква b, за которой следует буква a и t, и этому шаблону соответствуют такие слова, как, bat, combat и batalion.&lt;br /&gt;
Давайте разберем несколько типовых задачек, чтобы вам было понятнее, как правильно работать с регулярными выражениями в SQL. Для решения всех задач, которые мы сегодня рассмотрим, мы будем использовать функцию regexp_matches(), которая будет сравнивать значения в ячейках с шаблоном, который задается внутри этой функции.&lt;/p&gt;
&lt;h2&gt;Количество гласных букв в выражении&lt;/h2&gt;
&lt;p&gt;Итак, предположим, вам нужно посчитать количество гласных букв в каждой ячейке определенного столбца таблицы. Именно для такой задачи и нужны регулярные выражения. Код, который приведен ниже (вы можете легко его прогнать в своем SQL), на простом примере показывает, как легко решить эту задачу. В результате, мы получаем еще одну колонку Count, в которой хранится искомая информация.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (select * from (values (1, 'google'), (2, 'yahoo'), (3, 'bing'), (4, 'rambler')) 
as map(id, source_type))

select source_type, count(1) from (
select *, regexp_matches(source_type,'([aeiou])','g') as pattern from example_table ) as t
group by source_type&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Количество согласных букв в выражении&lt;/h2&gt;
&lt;p&gt;Если мы хотим решить обратную задачу, то можно подойти к решению двумя способами. Первый способ — аналогично предыдущему можно перечислить все согласные буквы английского алфавита в квадратных скобках. Но почему бы не решить задачу элегантнее? Для этого есть второй способ — использовать отрицание, то есть посчитать количество всех букв, которые не являются гласными. Для этого используется оператор ^.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (select * from (values (1, 'google'), (2, 'yahoo'), (3, 'bing'), (4, 'rambler')) 
as map(id, source_type))

select source_type, count(1) from (
select *, regexp_matches(source_type,'([^aeiou])','g') as pattern from example_table ) as t
group by source_type&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Количество цифр в выражении равно 3&lt;/h2&gt;
&lt;p&gt;Если нужно найти конкретное число определенных символов в выражении, то в конце запроса нужно указать оператор HAVING.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (select * from (values (1, '1a2s3d'), (2, 'qw12e'), (3, 'q56we1651qwe'), (4, 'qw4e2')) 
as map(id, source_type))

select source_type, COUNT(*) from (
select *, regexp_matches(source_type,'\d','g') as pattern from example_table ) as t
GROUP BY source_type
HAVING COUNT(*) = 3&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;В номере телефона есть два дефиса&lt;/h2&gt;
&lt;p&gt;Теперь давайте перейдем к более конкретным запросам, которые могут пригодиться в реальной практике. Например, у аналитика может стоять задача найти все номера телефона, в которых присутствует два или более дефисов.&lt;br /&gt;
В первом блоке кода мы создаем тестовую таблицу, затем считаем количество дефисов в каждой ячейке (ячейки без дефисов не включаются в финальную таблицу), а после этого проставляем значения True/False относительно условия на количество дефисов. Сделать это можно с помощью оператора CASE WHEN COUNT ().&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (
  select * from (
    values 
    (1, '8931-123-456'), 
    (2, '8931123-456'), 
    (3, '+7812123456'), 
    (4, '8-931-123-42-24')
  )
as map(id, source_type))

select source_type, CASE WHEN COUNT(1) &amp;gt;= 2 THEN 'True' ELSE 'False' END from (
select *, regexp_matches(source_type,'-','g') as pattern from example_table ) as t
GROUP BY 1&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Все имена, которые написаны с большой буквы&lt;/h2&gt;
&lt;p&gt;Тут мы уже приступаем к задаче посложнее: нужно найти имена людей, которые написаны с заглавной буквы со всем датасете. Для этого нам нужно найти все значения, подходящие под заданный шаблон: первая буква слова — заглавная.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (
  select * from (
    values 
    (1, 'alex'), 
    (2, 'Alex'), 
    (3, 'Vasya'), 
    (4, 'petya')
  )
as map(id, source_type))

select source_type from (
select *, regexp_matches(source_type,'^[A-Z]','g') as pattern from example_table ) as t
GROUP BY 1&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Вывести номера телефонов, которые попадают под паттерн +71234564578&lt;/h2&gt;
&lt;p&gt;Последней задачей мы разберем поиск телефонных номеров в списке. Для этого нам нужно найти те значения, которые начинаются со знака “+”, затем идет цифра 7 и 10 любых цифр после этого.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (
  select * from (
    values 
    (1, '+7(931)1234546'), 
    (2, '+79312991809'), 
    (3, '89311234565'), 
    (4, '244-02-38')
  )
as map(id, source_type))

select source_type from (
select *, regexp_matches(source_type,'^\+7[0-9]{10}','g') as pattern from example_table ) as t
GROUP BY 1&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Вывести все настоящие email-адреса&lt;/h2&gt;
&lt;p&gt;Как мы говорили в начале, регулярные выражения могут использоваться для таких задач как поиск сложных выражений по определённому шаблону. На самом деле, ничего особенного в такой задаче нет — главное, грамотно сформировать шаблон выражения и дело в шляпе!&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with example_table as (
    select * from (
    values
        (1, 'email.asd@ya.ru'),
        (2, 'something@new.ru'),
        (3, '@ya.ru'),
        (4, 'asdasd'),
        (5, '_asdasdasd@mail.ru'),
        (6, 'asd_asdas@mail.ru'),
        (7, '.asdasd@mail.ru'),
        (8, '007asd@email.com')
        ) as map(id, source_type)
)
​
select source_type 
from (
    select source_type, regexp_matches(source_type, '^[^_.0-9][a-z0-9._]+@[a-z]+\.[a-z]+$')
    from example_table ) as t&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Использование регулярных выражений может помочь легко и просто решить достаточно трудные задачи. Пишите в комментариях, если у вас есть какая-то задача по поиску определенных шаблонов в тексте, которая вам никак не дается. Попробуем решить её вместе!&lt;/p&gt;
</description>
<pubDate>Mon, 17 Jan 2022 16:48:16 +0300</pubDate>
</item>

<item>
<title>Три способа рассчитать накопленную сумму в SQL</title>
<guid isPermaLink="false">125</guid>
<link>http://test.leftjoin.ru/all/sql-running-total/</link>
<comments>http://test.leftjoin.ru/all/sql-running-total/</comments>
<description>
&lt;p&gt;Расчет накопленной (или кумулятивной, что то же самое) суммы SQL — это очень распространенный запрос, который часто используют в анализе финансов, динамики прибыли и прочих показателей компании. В сегодняшней статье вы узнаете, что такое накопленная сумма и как можно написать SQL-запрос для ее вычисления.&lt;/p&gt;
&lt;p&gt;Если вы вдруг являетесь начинающим пользователем SQL, то давайте, как в школьной задаче, поймем, что нам дано и что нам необходимо найти. Накопленная сумма — это совокупная сумма предыдущих чисел в столбце. Давайте посмотрим на пример ниже, чтобы точно знать, какой результат мы ожидаем увидеть в итоге. Итак, существует таблица leftjoin.daily_sales_sample, в которой есть всего два столбца date и revenue. По столбцу revenue нам нужно рассчитать накопленную сумму и записать результат в отдельный столбец.&lt;/p&gt;
&lt;h3&gt;Что у нас есть?&lt;/h3&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Date&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Revenue&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3&gt;Что мы хотим найти?&lt;/h3&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Date&lt;/td&gt;
&lt;td style="text-align: center"&gt;Revenue&lt;/td&gt;
&lt;td style="text-align: right"&gt;Cumulative Revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;1200 ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;td&gt;2800↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;12.11.2021&lt;/td&gt;
&lt;td style="text-align: left"&gt;800&lt;/td&gt;
&lt;td&gt;3600 ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;13.11.2021&lt;/td&gt;
&lt;td style="text-align: left"&gt;3000&lt;/td&gt;
&lt;td&gt;6600&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;На графике две этих переменных выглядят следующим образом:&lt;br /&gt;
&lt;img src="http://test.leftjoin.ru/pictures/sql_graph.png"  border="0" width="100%" height="100%"&gt;&lt;/p&gt;
&lt;p&gt;Итак, без лишних слов, давайте приступать к решению задачи.&lt;/p&gt;
&lt;h2&gt;Способ 1 — Идеальный —  Используем оконные функции&lt;/h2&gt;
&lt;p&gt;Итак, если в базе данных можно пользоваться оконными функциями, то жизнь хороша и прекрасна. С их помощью можно написать простой запрос, который будет суммировать значения из столбца revenue по мере увеличения даты и сразу вернет нам таблицу с кумулятивной суммой в столбце, который мы назвали total.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT
	date,
	revenue,
	SUM(revenue) OVER (ORDER BY date asc) as total
FROM leftjoin.daily_sales_sample 
ORDER BY date;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Способ 2 — Хитрый — Решение без оконных функций&lt;/h2&gt;
&lt;p&gt;Вполне возможно, что вам понадобится решить такую задачу без использования оконных функций. К примеру, если вы используете MySQL (до 8 версии) или любую другую БД, в которой оконных функций нет. Тогда решение задачи чуть усложняется. Однако, вы ведь знаете, что нет ничего невозможного?&lt;br /&gt;
Чтобы провернуть все то же самое без оконных функций, нужно использовать INNER JOIN для присоединения таблицы к себе самой. Так, к каждой строке таблицы мы присоединяем строки, которые соответствуют всем предыдущим датам до текущей даты включительно. В нашем примере, для 10 ноября — 10 ноября, для 11 ноября — 10 и 11 ноября и так далее. Промежуточный запрос будет выглядеть вот так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT * 
FROM leftjoin.daily_sales_sample ds1 
INNER JOIN leftjoin.daily_sales_sample ds2 on ds1.date&amp;gt;=ds2.date
ORDER BY ds1.date, ds2.date;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А его результат:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Date 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Revenue 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Date 2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Revenue 2&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;10.11.2021&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;11.11.2021&lt;/td&gt;
&lt;td&gt;1600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;12.11.2021&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;13.11.2021&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;А затем, нужно просуммировать прибыли, группируя их по каждой дате. Если собрать все в единый запрос, то он будет выглядеть вот так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT
	ds1.date,
	ds1.revenue,
	SUM(ds2.revenue) as total
FROM leftjoin.daily_sales_sample ds1 
INNER JOIN leftjoin.daily_sales_sample ds2 on ds1.date&amp;gt;=ds2.date
GROUP BY ds1.date, ds1.revenue
ORDER BY ds1.date;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Способ 3 — Специфический — Решение с помощью массивов в ClickHouse&lt;/h2&gt;
&lt;p&gt;Если вы используете Clickhouse, то в этой системе есть специальная функция, которая может помочь рассчитать кумулятивную сумму. Для начала, нам нужно преобразовать все столбцы таблицы в массивы и рассчитать показатель «Moving Sum» для столбца revenue.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT groupArray(date) dates, groupArray(revenue) as revs, 
groupArrayMovingSum(revenue) AS total
FROM (SELECT date, revenue FROM leftjoin.daily_sales_sample
	  ORDER BY date)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;i&gt;Спасибо &lt;a href="https://t.me/unamedrus"&gt;Дмитрию Титову&lt;/a&gt; из Altinity за комментарий про сортировку в подзапросе&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Так, мы получим три массива значений:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;b&gt;dates&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;revs&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;&lt;b&gt;total&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[’10.11.2021’,’11.11.2021’,’12.11.2021’,’13.11.2021’]&lt;/td&gt;
&lt;td style="text-align: center"&gt;[1200, 1600, 800, 300]&lt;/td&gt;
&lt;td style="text-align: right"&gt;[1200, 2800, 3600, 3900]&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;Но три массива, которые записаны в ячейки — это не то, что мы хотим получить, хотя значения этих массивов уже абсолютно соответствуют искомому результату. Теперь массивы нужно привести обратно к табличному виду с помощью функции ARRAY JOIN.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT dates, revs, total FROM
(SELECT groupArray(date) dates, groupArray(revenue) as revs, 
groupArrayMovingSum(revenue) AS total
FROM (SELECT date, revenue FROM leftjoin.daily_sales_sample
	  ORDER BY date)) as t
ARRAY JOIN dates, revs, total;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Бонус — Оконные функции в Clickhouse&lt;/h2&gt;
&lt;p&gt;Если вам не хочется иметь дело с массивами, что иногда и правда бывает затратно по времени, то есть еще один вариант решения задачи.  Можно использовать оконные функции, например функцию &lt;a href="https://clickhouse.com/docs/en/sql-reference/functions/other-functions/"&gt;runningAccumulate(&lt;/a&gt;), которая суммирует  значения всех ячеек с первой до текущей.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT date, runningAccumulate(revenue)
  FROM 
  (
    SELECT date, sumState(revenue) AS revenue
    FROM leftjoin.daily_sales_sample
    GROUP BY date 
    ORDER BY date ASC
  )
ORDER BY date&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если вы столкнетесь с необходимостью рассчитать кумулятивную сумму в SQL, то теперь вы сможете решить эту задачу, в какой бы системе управления баз данных ни была организована работа :)&lt;/p&gt;
</description>
<pubDate>Fri, 10 Dec 2021 19:10:34 +0300</pubDate>
</item>

<item>
<title>Моделирование LTV в SQL</title>
<guid isPermaLink="false">115</guid>
<link>http://test.leftjoin.ru/all/modeling-ltv-with-sql/</link>
<comments>http://test.leftjoin.ru/all/modeling-ltv-with-sql/</comments>
<description>
&lt;p&gt;У большинства игровых и мобильных компаний имеется кривая Retention, ранее мы писали о том, &lt;a href="http://test.leftjoin.ru/all/retention-rate/"&gt;что такое Retention и как его посчитать&lt;/a&gt;. Вкратце — это метрика, которая позволяет понять насколько хорошо продукт вовлекает пользователей в ежедневное использование. А ещё при помощи Retention и ARPDAU можно посчитать LTV (Lifetime Value), пожизненный доход с одного пользователя. Зная средний доход с пользователя за день и кривую Retention мы можем смоделировать ее и спрогнозировать LTV.&lt;/p&gt;
&lt;p class="note"&gt;Для материала были взяты данные из одного реального игрового проекта. Нулевой день не отображен для того, чтобы видеть динамику в деталях&lt;/p&gt;
&lt;p&gt;В сегодняшнем материале мы подробно разберём, как смоделировать LTV для 180 дней при помощи SQL и просто линейной регрессии.&lt;/p&gt;
&lt;h2&gt;Как посчитать LTV?&lt;/h2&gt;
&lt;p&gt;В общем случае формула LTV выглядит как ARPDAU умноженное на Lifetime — время жизни пользователя в проекте.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart.png" width="223" height="19" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Посмотрим на классический график Retention:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.54.53.png" width="942" height="413" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Lifetime — это площадь фигуры под Retention:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.55.13.png" width="923" height="410" alt="" /&gt;
&lt;/div&gt;
&lt;p class="note"&gt;Откуда взялись интегралы и площади можно подробнее узнать в &lt;a href="https://gdcuffs.com/ltv-integrals-and-areas/"&gt;этом материале&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Значит, чтобы посчитать Lifetime, нужно взять интеграл от функции удержания по времени. Формула приобретает следующий вид:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-2.png" width="198" height="28" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Для описания кривой Retention лучше всего подходит степенная функция a*x^b. Вот как она выглядит в сравнении с кривой Retention:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.58.19.png" width="802" height="403" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;При этом x — номер дня, a и b — параметры функции, которую мы построим при помощи линейной регрессии. Регрессия появилась неслучайно — эту степенную функцию можно привести к виду линейной функции, логарифмируя:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-4.png" width="64" height="18" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-5.png" width="154" height="20" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-6.png" width="156" height="19" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;ln(a) — intercept, b — slope. Остаётся найти эти параметры — в линейной регрессии для этого используют метод наименьших квадратов. Lifetime — кумулятивная сумма прогноза за 180 дней. Посчитав её, остаётся умножить Lifetime на ARPDAU и получим LTV за 180 дней.&lt;/p&gt;
&lt;h2&gt;Строим LTV&lt;/h2&gt;
&lt;p&gt;Перейдём к практике. Для всех расчётов мы использовали данные одной игровой компании и СУБД PostgreSQL — в ней уже реализованы функции поиска параметров для линейной регрессии. Начнём с построения Retention: соберём общее количество пользователей в период с 1 марта по 1 апреля 2021 года — мы изучаем активность за один месяц:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;--общее количество юзеров в когорте
with cohort as (
    select count(distinct id) as total_users_of_cohort
    from users
    where date(registration) between date '2021-03-01' and date '2021-03-30'
),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь посмотрим, как ведут себя эти пользователи в последующие 90 дней:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;--количество активных юзеров на 1ый день, 2ой, 3ий и тд. из когорты
active_users as (
    select date_part('day', activity.date - users.registration) as activity_day, 
               count(distinct users.id) as active_users_of_day
    from activity
    join users on activity.user_id = users.id
    where date(registration) between date '2021-03-01' and date '2021-03-30' 
    group by 1
    having date_part('day', activity.date - users.registration) between 1 and 90 --берем только первые 90 дней, остальные дни предсказываем.
),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Кривая Retention — отношение количества активных пользователей к размеру когорты текущего дня. В нашем случае она выглядит так:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.54.53.png" width="942" height="413" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;По данным кривой посчитаем параметры для линейной регрессии. regr_slope(x, y) — функция для вычисления наклона регрессии, regr_intercept(x, y) — функция для вычисления перехвата по оси Y. Эти функции являются стандартными &lt;a href="https://www.postgresql.org/docs/9.4/functions-aggregate.html"&gt;агрегатными функциями в PostgreSQL&lt;/a&gt; и для известных X и Y по методу наименьших квадратов.&lt;/p&gt;
&lt;p&gt;Вернёмся к нашей формуле — мы получили линейное уравнение, и хотим найти коэффициенты линейной регрессии. Перехват по оси Y и коэффициент наклона можем найти по дефолтным для PostgreSQL функциям. Получается:&lt;/p&gt;
&lt;p class="note"&gt;Подробнее о том, как работают функции intercept(x, y) и slope(x, y) можно почитать в &lt;a href="https://www.mathsisfun.com/data/least-squares-regression.html"&gt;этом мануале&lt;/a&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-6.png" width="156" height="19" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-10.png" width="217" height="21" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Из свойства натурального логарифма следует, что:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-11.png" width="165" height="25" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Наклон считаем аналогичным образом:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/chart-12.png" width="157" height="21" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Эти же вычисления запишем в подзапрос для расчёта коэффициентов регрессии:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;--рассчитываем коэффициенты регрессии
coef as (
    select exp(regr_intercept(ln(activity), ln(activity_day))) as a, 
                regr_slope(ln(activity), ln(activity_day)) as b
    from(
                select activity_day,
                            active_users_of_day::real / total_users_of_cohort as activity
                from active_users 
                cross join cohort order by activity_day 
            )
),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И получим прогноз на 180 дней, подставив параметры в степенную функцию, описанную ранее. Заодно посчитаем Lifetime — кумулятивную сумму спрогнозированных данных. В подзапросе coef мы получим только два числа — параметр наклона и перехвата. Чтобы эти параметры были доступны каждой строке подзапроса lt, делаем cross join к coef:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;lt as(
    select generate_series as activity_day,
               active_users_of_day::real/total_users_of_cohort as real_data,
               a*power(generate_series,b) as pred_data, 	 
               sum(a*power(generate_series,b)) over(order by generate_series) as cumulative_lt
    from generate_series(1,180,1)
    cross join coef
    join active_users on generate_series = activity_day::int
),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Сравним прогноз на 180 дней с Retention:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.30.59.png" width="936" height="425" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Наконец, считаем сам LTV — Lifetime, умноженный на ARPDAU. В нашем случае ARPDAU равняется $83.7:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select cumulative_lt as LT,
           cumulative_lt * 83.7 as LTV
from lt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наконец, построим график LTV на 180 дней:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-13--15.51.05.png" width="852" height="416" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Весь запрос:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;--общее количество юзеров в когорте
with cohort as (
    select count(*) as total_users_of_cohort
    from users
    where date(registration) between date '2021-03-01' and date '2021-03-30'
),
--количество активных юзеров на 1ый день, 2ой, 3ий и тд. из когорты
active_users as (
    select date_part('day', activity.date - users.registration) as activity_day, 
               count(distinct users.id) as active_users_of_day
    from activity
    join users on activity.user_id = users.id
    where date(registration) between date '2021-03-01' and date '2021-03-30' 
    group by 1
    having date_part('day', activity.date - users.registration) between 1 and 90 --берем только первые 90 дней, остальные дни предсказываем.
),
--рассчитываем коэффициенты регрессии
coef as (
    select exp(regr_intercept(ln(activity), ln(activity_day))) as a, 
                regr_slope(ln(activity), ln(activity_day)) as b
    from(
                select activity_day,
                            active_users_of_day::real / total_users_of_cohort as activity
                from active_users 
                cross join cohort order by activity_day 
            )
),
lt as(
    select generate_series as activity_day,
               active_users_of_day::real/total_users_of_cohort as real_data,
               a*power(generate_series,b) as pred_data, 	 
               sum(a*power(generate_series,b)) over(order by generate_series) as cumulative_lt
    from generate_series(1,180,1)
    cross join coef
    join active_users on generate_series = activity_day::int
),
select cumulative_lt as LT,
            cumulative_lt * 83.7 as LTV
from lt&lt;/code&gt;&lt;/pre&gt;</description>
<pubDate>Mon, 16 Aug 2021 09:32:37 +0300</pubDate>
</item>

<item>
<title>Тренинг по Clickhouse от Altinity</title>
<guid isPermaLink="false">113</guid>
<link>http://test.leftjoin.ru/all/altinity-clickhouse-training-101/</link>
<comments>http://test.leftjoin.ru/all/altinity-clickhouse-training-101/</comments>
<description>
&lt;p&gt;Буквально на днях закончил обучение &lt;a href="https://altinity.com/clickhouse-training/?utm_source=leftjoin"&gt;Clickhouse от Altinity (101 Series Training).&lt;/a&gt; Для тех, кто только знакомится с Clickhouse Altinity предлагает базовый бесплатный тренинг: &lt;a href="https://altinity.com/data-warehouse-basics/?utm_source=leftjoin"&gt;Data Warehouse Basics&lt;/a&gt;. Рекомендую начать с него, если планируете погружаться в обучение.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/altinity-clickhouse-developer-300px.png" width="300" height="300" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;Сертификация от Altinity&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Хочу поделиться своими впечатлениями об обучении и поделиться своим &lt;a href="https://valiotti-analytics.notion.site/Clickhouse-Training-101-by-Altinity-notes-120f1b6467f44a30956d6d7ffeff7b08"&gt;конспектом с тренинга&lt;/a&gt;.&lt;br /&gt;
Обучение стоит $500 и длится четыре дня по два часа, проводится в наше вечернее время (начиная с 19:00 GMT+3).&lt;/p&gt;
&lt;h2&gt;Сессия №1&lt;/h2&gt;
&lt;p&gt;Первый день в бОльшей степени повторяет пройденное в Data Warehouse Basics, однако в нем есть несколько новых идей, например о том, как можно получить полезную информацию о запросах из системных таблиц.&lt;/p&gt;
&lt;p&gt;Например, такой query выдаст какие команды запущены и в каком они статусе:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT command, is_done
FROM system.mutations
WHERE table = 'ontime'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Помимо этого, для меня было очень полезно узнать про компрессию колонок с использованием кодеков:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;ALTER TABLE ontime
 MODIFY COLUMN TailNum LowCardinality(String) CODEC(ZSTD(1))&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-01--11.53.59.png" width="1732" height="1048" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Для тех, кто начинает погружение в Clickhouse первый день будет супер-полезным в том, чтобы разобраться с движками таблиц и синатксисом их создания, партициями, вставкой данных (к примеру, напрямую из S3).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;INSERT INTO sdata
SELECT * FROM s3(
 'https://s3.us-east-1.amazonaws.com/d1-altinity/data/sdata*.csv.gz',
 'aws_access_key_id',
 'aws_secret_access_key',
 'Parquet',
 'DevId Int32, Type String, MDate Date, MDatetime
DateTime, Value Float64')&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Сессия №2&lt;/h2&gt;
&lt;p&gt;Второй день мне представляется максимально насыщенным и полезным, потому что в рамках него Robert из Altinity подробно рассказывает про агрегирующие функции в Clickhouse и про создание материализованных представлений (подробно по шагам разбирается &lt;a href="https://www.notion.so/Session-2-35af1ed8d2c54c6fa7fcbea3c9385810#f36adc3df7d74deebedcb3c04e019661"&gt;схема создания материализованного представления&lt;/a&gt;).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-02--18.11.45.png" width="877" height="495" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;Отдельное внимание устройству джойнов в Clickhouse&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Мне было супер-полезно узнать про типы индексов в CH&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-02--18.20.35.png" width="874" height="472" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Сессия №3&lt;/h2&gt;
&lt;p&gt;В рамках третьего дня коллеги делятся знаниями о том как работать с Kafka, JSON-объектами, которые хранятся в таблицах.&lt;br /&gt;
Интересно было узнать, что работа с типами данных массив в Clickhouse очень похоже на работу с массивами в Python:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WITH [1, 2, 4] AS array
SELECT
 array[1] AS First,
 array[2] AS Second,
 array[3] AS Third,
 array[-1] AS Last,
 length(array) AS Length&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И при работе с массивами крутая фича это ARRAY JOIN, который «разворачивает» массив в плоскую реляционную таблицу:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-02--19.14.28.png" width="812" height="518" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Clickhouse позволяет эффективно взаимодействовать с JSON-объектами, которые хранятся в таблице:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;-- Get a JSON string value
SELECT JSONExtractString(row, 'request') AS request
FROM log_row LIMIT 3
-- Get a JSON numeric value
SELECT JSONExtractInt(row, 'status') AS status
FROM log_row LIMIT 3&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На примере этого кусочка кода отдельно извлекаются элементы JSON-массива ’request’ и ’status’.&lt;/p&gt;
&lt;p&gt;Их можно сложить в ту же таблицу:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;ALTER TABLE log_row
 ADD COLUMN
status Int16 DEFAULT
 JSONExtractInt(row, 'status')
ALTER TABLE log_row
UPDATE status = status WHERE 1 = 1&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Сессия №4&lt;/h2&gt;
&lt;p&gt;А на заключительный четвертый день оставлена самая трудная тема с моей точки зрения: &lt;a href="https://www.notion.so/Session-4-f2aa33b6fe434a4e8542f0f64f9439bc#3a3038e94dbf4b47a10284dc1dc226ec"&gt;построение шардированных и реплицированных кластеров&lt;/a&gt;, построение запросов на распределенных серверах Clickhouse.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="938" data-ratio="1.8073217726397"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-02--19.43.22.png" width="938" height="519" alt="" /&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-08-02--19.47.52.png" width="934" height="520" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Отдельный респект Altinity за отличную подборку лабораторных заданий в ходе обучения.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Ссылки&lt;/b&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://capable-stream-f18.notion.site/Clickhouse-Training-101-by-Altinity-notes-120f1b6467f44a30956d6d7ffeff7b08"&gt;Конспект в Notion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://altinity.com/clickhouse-training/?utm_source=leftjoin"&gt;ClickHouse 101 Training от Altinity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<pubDate>Mon, 09 Aug 2021 08:41:19 +0300</pubDate>
</item>

<item>
<title>Нормализация данных через запрос в SQL</title>
<guid isPermaLink="false">109</guid>
<link>http://test.leftjoin.ru/all/data-scaling-with-sql/</link>
<comments>http://test.leftjoin.ru/all/data-scaling-with-sql/</comments>
<description>
&lt;p&gt;Главный принцип анализа данных GIGO (от англ. garbage in — garbage out, дословный перевод «мусор на входе — мусор на выходе») говорит нам о том, что ошибки во входных данных всегда приводят к неверным результатам анализа. От того, насколько хорошо подготовлены  данные, зависят результаты всей вашей работы.&lt;/p&gt;
&lt;p&gt;Например, перед нами стоит задача подготовить выборку для использования в алгоритме машинного обучения (модели k-NN, k-means, логической регрессии и др). Признаки в исходном наборе данных могут быть в разном масштабе, как, например, возраст и рост человека. Это может привести к некорректной работе алгоритма. Такого рода данные нужно предварительно масштабировать.&lt;/p&gt;
&lt;p&gt;В данном материале мы рассмотрим способы масштабирования данных через запрос в SQL: масштабирование методом min-max, min-max для произвольного диапазона и z-score нормализация. Для каждого из методов мы подготовили по два примера написания запроса — один с помощью подзапроса SELECT, а второй используя оконную функцию OVER().&lt;/p&gt;
&lt;p&gt;Для работы возьмем таблицу &lt;b&gt;students&lt;/b&gt; с данными о росте учащихся.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Min-Max масштабирование&lt;/h2&gt;
&lt;p&gt;Подход min-max масштабирования заключается в том, что данные масштабируются до фиксированного диапазона, который обычно составляет от 0 до 1. В данном случае мы получим все данные в одном масштабе, что исключит влияние выбросов на выводы.&lt;/p&gt;
&lt;p&gt;Выполним масштабирование по формуле:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.42.52.png" width="460" height="104" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Умножаем числитель на 1.0, чтобы в результате получилось число с плавающей точкой.&lt;/p&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       1.0 * (height-t1.min_height)/(t1.max_height - t1.min_height) AS scaled_minmax
  FROM students, 
      (SELECT min(height) as min_height, 
              max(height) as max_height 
         FROM students
      ) as t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - MIN(height) OVER ()) * 1.0 / (MAX(height) OVER () - MIN(height) OVER ()) AS scaled_minmax
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате мы получим переменные в диапазоне [0...1], где за 0 принят рост самого невысокого учащегося, а 1 рост самого высокого.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;scaled_minmax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;0.46809&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;0.61702&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;0.12766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;0.57447&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;0.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;0.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;0.53191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;0.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Масштабирование для заданного диапазона&lt;/h2&gt;
&lt;p&gt;Вариант min-max нормализации для произвольных значений. Не всегда, когда речь идет о масштабировании данных, диапазон значений находится в промежутке между 0 и 1.&lt;br /&gt;
Формула для вычисления в этом случае такая:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.43.04.png" width="530" height="104" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Это даст нам возможность масштабировать данные к произвольной шкале. В нашем примере пусть а=10.0, а b=20.0.&lt;/p&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       ((height - min_height) * (20.0 - 10.0) / (max_height - min_height)) + 10 AS scaled_ab
  FROM students,
      (SELECT MAX(height) as max_height, 
              MIN(height) as min_height
         FROM students  
      ) t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       ((height - MIN(height) OVER() ) * (20.0 - 10.0) / (MAX(height) OVER() - MIN(height) OVER())) + 10.0 AS scaled_ab
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получаем аналогичные результаты, что и в предыдущем методе, но данные распределены в диапазоне от 10 до 20.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;scaled_ab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;14.68085&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;16.17021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;11.2766&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;15.74468&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;12.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;17.65957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;15.31915&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;12.76596&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2&gt;Нормализация с помощью z-score&lt;/h2&gt;
&lt;p&gt;В результате z-score нормализации данные будут масштабированы таким образом, чтобы они имели свойства стандартного нормального распределения — среднее (μ) равно 0, а стандартное отклонение (σ) равно 1.&lt;/p&gt;
&lt;p&gt;Вычисляется z-score по формуле:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-05-12--16.43.19.png" width="368" height="101" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;SQL-запрос с подзапросом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - t1.mean) * 1.0 / t1.sigma AS zscore
  FROM students,
      (SELECT AVG(height) AS mean, 
              STDDEV(height) AS sigma
         FROM students
        ) t1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQL-запрос с оконной функцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;SELECT height, 
       (height - AVG(height) OVER()) * 1.0 / STDDEV(height) OVER() AS z-score
  FROM students;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В результате мы сразу заметим выбросы, которые выходят за пределы стандартного отклонения.&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;zscore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Иван&lt;/td&gt;
&lt;td style="text-align: right"&gt;174&lt;/td&gt;
&lt;td&gt;0.01488&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Петр&lt;/td&gt;
&lt;td style="text-align: right"&gt;181&lt;/td&gt;
&lt;td&gt;0.53582&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Денис&lt;/td&gt;
&lt;td style="text-align: right"&gt;199&lt;/td&gt;
&lt;td&gt;1.87538&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ксения&lt;/td&gt;
&lt;td style="text-align: right"&gt;158&lt;/td&gt;
&lt;td&gt;-1.17583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сергей&lt;/td&gt;
&lt;td style="text-align: right"&gt;179&lt;/td&gt;
&lt;td&gt;0.38698&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ольга&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;-0.65489&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Юлия&lt;/td&gt;
&lt;td style="text-align: right"&gt;152&lt;/td&gt;
&lt;td&gt;-1.62235&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кирилл&lt;/td&gt;
&lt;td style="text-align: right"&gt;188&lt;/td&gt;
&lt;td&gt;1.05676&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Антон&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td&gt;0.23814&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Софья&lt;/td&gt;
&lt;td style="text-align: right"&gt;165&lt;/td&gt;
&lt;td&gt;-0.65489&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
</description>
<pubDate>Thu, 13 May 2021 11:15:58 +0300</pubDate>
</item>

<item>
<title>Транзакции в SQLAlchemy</title>
<guid isPermaLink="false">94</guid>
<link>http://test.leftjoin.ru/all/tranzakcii-v-sqlalchemy/</link>
<comments>http://test.leftjoin.ru/all/tranzakcii-v-sqlalchemy/</comments>
<description>
&lt;p&gt;Транзакция — последовательность действий, связанных с базой данных. Их основная польза заключается в том, что при возникновении какой-то ошибки или достижении других нужных условий всю транзакцию можно отменить, и все изменения, примененные к базе данных, будут отменены. Сегодня мы напишем небольшой скрипт, который при помощи транзакций SQLAlchemy пишет информацию о подписчиках сообщества в базу данных MySQL, а при возникновении ошибки отменяет текущую транзакцию.&lt;/p&gt;
&lt;h2&gt;Сбор информации об участниках через VK API&lt;/h2&gt;
&lt;p&gt;Для начала напишем пару маленьких функций — первая будет возвращать число подписчиков сообщества, а вторая — отправлять запрос и формировать датафрейм с информацией о подписчиках сообщества.&lt;/p&gt;
&lt;p class="note"&gt;Подробнее о том, как получить токен, можно прочитать в материале &lt;a href="http://test.leftjoin.ru/all/get-data-from-vk/" class="nu"&gt;«&lt;u&gt;Собираем данные по рекламным кампаниям ВКонтакте&lt;/u&gt;»&lt;/a&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from sqlalchemy import create_engine
import pandas as pd
import requests
import time

token = '42hj2ehd3djdournf48fjurhf9r9o2eurnf48fjurhf9r9734'
group_id = 'leftjoin'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Чтобы узнать число подписчиков достаточно отправить метод groups.getMembers с любыми параметрами — в ответе всегда возвращается количество в поле count.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_subs_count(group_id):
    count = requests.get('https://api.vk.com/method/groups.getMembers', params={
        'access_token':token,
        'v':5.103,
        'group_id':group_id
    }).json()['response']['count']
    return count&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для примера будем брать имена, id, фамилии подписчиков, некоторую расширенную информацию и получать только по 10 подписчиков за раз, чтобы рассмотреть работу транзакций детально — каждые 10 подписчиков будут вставляться одной транзакцией. Введём дополнительное поле offset, чтобы знать, в какой итерации добавлены строки.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_subs_info(group_id, offset):
    response = requests.get('https://api.vk.com/method/groups.getMembers', params={
        'access_token':token,
        'v':5.103,
        'group_id':group_id,
        'offset':offset,
        'count':10,
        'fields':'sex, has_mobile, relation, can_post'
    }).json()['response']['items']
    df = pd.DataFrame(response)
    df['offset'] = offset
    return df&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Транзакции&lt;/h2&gt;
&lt;p&gt;Наконец, можем подсоединиться к базе данных при помощи SQLAlchemy:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;engine = create_engine('mysql+mysqlconnector://' +
                           'root' + ':' + '' + '@' +
                           'localhost' + '/' +
                           'transaction', echo=False)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;У транзакций всегда должно быть начало — begin, и конец — commit. В случае, если произошла какая-то ошибка, можно сделать откат — rollback. Сперва получаем число подписчиков сообщество, и в каждой итерации цикла при помощи контекстного менеджера with ... as создаём новое подключение. Сразу после объявляем начало транзакции по этому подключению и с обработчиком исключений пробуем получить информацию о десяти подписчиках через функцию get_subs_info. Вставляем полученный датафрейм в таблицу методом to_sql и завершаем транзакцию при помощи метода commit(). В случае, если возникла какая-то ошибка — печатаем её на экран и отменяем транзакцию.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;offset = 0
subs_count = get_subs_count(group_id)
while offset &amp;lt; subs_count:
    with engine.connect() as conn:
        transaction = conn.begin()
        try:
            df = get_subs_info(group_id, offset)
            df.to_sql('subscribers', con=conn, if_exists='append', index=False)
            transaction.commit()
        except Exception as E:
            print(E)
            transaction.rollback()
    time.sleep(1)
    offset += 10&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Чтобы протестировать работу транзакций слегка обновим последний блок кода — добавим вызов ошибки ValueError после вставки данных в базу, если текущий offset равен 10.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;offset = 0
subs_count = get_subs_count(group_id)
while offset &amp;lt; subs_count:
    with engine.connect() as conn:
        transaction = conn.begin()
        try:
            df = get_subs_info(group_id, offset)
            df.to_sql('subscribers', con=conn, if_exists='append', index=False)
            if offset == 10:
                raise(ValueError)
            transaction.commit()
        except Exception as E:
            print(E)
            transaction.rollback()
    time.sleep(1)
    offset += 10&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Как и планировалось, данные за итерацию с offset = 10 не занесены в таблицу. Несмотря на то, что ошибка возникла уже после добавления новых данных, транзакция была прервана методом rollback() и завершение транзакции было отменено.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-23.png" width="759" height="562" alt="" /&gt;
&lt;/div&gt;
</description>
<pubDate>Fri, 12 Feb 2021 11:10:22 +0300</pubDate>
</item>

<item>
<title>UNPIVOT данных с использованием CROSS JOIN</title>
<guid isPermaLink="false">89</guid>
<link>http://test.leftjoin.ru/all/unpivot-with-cross-join/</link>
<comments>http://test.leftjoin.ru/all/unpivot-with-cross-join/</comments>
<description>
&lt;p&gt;Зачастую мы получаем данные в предагрегированном виде, когда каждая отдельная колонка является посчитанной метрикой. По аналогии мы получаем подобный результат, когда строим сводную таблицу в Excel и используем некоторое количество фактов для агрегации. Но что делать, если нам нужно произвести обратную операцию — Unpivot?&lt;/p&gt;
&lt;p&gt;Как поступить, если в датасете понадобилось трансформировать данные в реляционный вид? В Tableau есть фича &lt;a href="https://help.tableau.com/current/pro/desktop/en-us/pivot.htm"&gt;Unpivot&lt;/a&gt;, которая сделает всё сама: если датасет построен из файла, достаточно выделить нужные колонки и нажать на кнопку «Pivot». А в некоторых диалектах SQL, например, в Transact, уже есть &lt;a href="https://docs.microsoft.com/ru-ru/sql/t-sql/queries/from-using-pivot-and-unpivot"&gt;встроенные функции&lt;/a&gt;, которые тоже делают это сами.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/qs_pivot_example.png" width="650" height="263" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Но в случае, если датасет построен на Custom SQL Query из базы данных, у которой в арсенале отсутствуют встроенные функции для трансформации в сводную и обратно, необходим какой-то другой подход, и Tableau порекомендует для такой таблицы:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ID&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;a&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;b&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;c&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;a1&lt;/td&gt;
&lt;td&gt;b1&lt;/td&gt;
&lt;td&gt;c1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;a2&lt;/td&gt;
&lt;td&gt;b2&lt;/td&gt;
&lt;td&gt;c2&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;Воспользоваться таким стандартным универсальным, но не очень эффективным решением:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select id, ‘a’ AS col, a AS value
from yourtable
union all
select id, ‘b’ AS col, b AS value
from yourtable
union all
select id, ‘c’ AS col, c AS value
from yourtable&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И в результате получить таблицу вида:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;col&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;value&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;a1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;a2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;b1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;b2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;c1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;c2&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;Порой, когда мы работаем с физической таблицей и нам надо быстро получить результаты для двух-трех колонок, действительно, подобное решение можно быстро применить, не задумываясь. Однако в случае, когда вместо таблицы содержится, например, сложный подзапрос с несколькими джойнами и нужно сделать Pivot для 5+ колонок, подзапрос вызовется целых 5+ раз, согласитесь, не очень действенно считать одно и тоже неоднократно. Вместо этого можно воспользоваться рецептом с CROSS JOIN, найденным на просторах Stack Overflow:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select t.id,
c.col,
    case c.col
        when 'a' then a
        when 'b' then b
        when 'c' then c
    end as data
from yourtable t
cross join
(
    select 'a' as col
    union all select 'b'
    union all select 'c'
) c&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Разберём запрос подробнее. CROSS JOIN — перекрёстное соединение, декартово произведение, или, проще говоря, произведение всех строк со всеми. За ненадобностью в синтаксисе CROSS JOIN отсутствует ON — мы объединяем не по какому-то конкретному полю две таблицы, а сразу по всем существующим строкам.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/Background_2.png" width="730" height="388" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Сначала мы формируем таблицу со всеми колонками, предназначенными для преобразования в строки. В нашем случае это колонки a, b и c: поэтому мы сделали таблицу c, в которой будет колонка col со значениями a, b и c:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;(
    select 'a' as col
    union all select 'b'
    union all select 'c'
) c&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Выглядит она так:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;col&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;Затем таблицы yourtable и c объединятся перекрестным соединением, а после мы возьмём поля id, col и в зависимости от того, как называется ячейка в col, подставим соответствующие данные в поле data.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;select t.id,
c.col,
    case c.col
        when 'a' then a
        when 'b' then b
        when 'c' then c
    end as value
from yourtable t
cross join
(
    select 'a' as col
    union all select 'b'
    union all select 'c'
) c&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В итоге получим ту же самую искомую таблицу, с которой уже можно удобно работать любым аналитическим инструментом:&lt;/p&gt;
&lt;div class="e2-text-table"&gt;
&lt;table cellpadding="0" cellspacing="0" border="0"&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;col&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;value&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;a1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;a2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;b1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;b2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;c1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;c2&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
</description>
<pubDate>Fri, 08 Jan 2021 16:30:14 +0300</pubDate>
</item>

<item>
<title>Конференция Coalesce от dbt: что посмотреть?</title>
<guid isPermaLink="false">83</guid>
<link>http://test.leftjoin.ru/all/coalesce2020-dbt/</link>
<comments>http://test.leftjoin.ru/all/coalesce2020-dbt/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/Eg2IiVMX0AI1eLj.jpg-large.jpeg" width="1516" height="760" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;С 7 по 11 декабря проходила &lt;a href="https://www.getdbt.com/coalesce"&gt;конференция Coalesce&lt;/a&gt;, о которой я рассказывал ранее. В этом году все организаторы решили проводить конференции по 5 дней с кучей докладов.&lt;/p&gt;
&lt;p&gt;С одной стороны это плюс — ощущение, что информации много и можно выбрать, что интересно. С другой стороны такое количество информации несколько изматывает, потому что часто по названию доклада не очень понятно насколько он окажется полезным и интересным. Мне все же кажется, что более трех дней для конференции это много, т. к. интерес аудитории теряется, да и необходимость заниматься своими личными и профессиональными делами не может испариться из-за события, которое хоть и в онлайне, но занимает твое внимание.&lt;/p&gt;
&lt;p&gt;Однако мне удалось посмотреть большую часть докладов, кое-что пролистывая. Для начала коротко в целом о впечатлениях: очень круто изучать доклады с подобной конференции как Coalesce, потому что речь идет в основном о современных инструментах и облачных решениях. Почти в каждом докладе можно услышать про Redshift / BigQuery / Snowflake, а с точки зрения BI: Mode / Tableau / Looker / Metabase. В центре всего, разумеется, dbt.&lt;/p&gt;
&lt;p&gt;Мой шорт-лист докладов, которые рекомендую изучить:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/dbt-101-eu-and-us-friendly"&gt;dbt 101&lt;/a&gt; — вводный доклад и интро в то, что такое dbt и как его используют&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/kimball-in-the-context-of-the-modern-data-warehouse-whats-worth-keeping-and-whats-not"&gt;Kimball in the context of the modern data warehouse: what’s worth keeping, and what’s not&lt;/a&gt; — интересный и очень-очень спорный доклад, который вызвал массу вопросов в slack dbt. Вкратце, автор предлагает перейти на «широкие» аналитические таблицы и отказаться от нормальных форм всюду.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/building-a-robust-data-pipeline-with-dbt-airflow-and-great-expectations"&gt;Building a robust data pipeline with dbt, Airflow, and Great Expectations&lt;/a&gt; — в докладе про небезынтересный инструмент &lt;a href="https://greatexpectations.io"&gt;greatexpectations&lt;/a&gt;, суть которого в валидации данных&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/orchestrating-dbt-with-dagster"&gt;Orchestrating dbt with Dagster&lt;/a&gt; — мне было несколько скучновато слушать, но если хочется познакомиться с Dagster — самое то&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/supercharging-your-data-team"&gt;Supercharging your data team&lt;/a&gt; — ребята сделали обертку к dbt, назвали dbt executor 9000 и рассказывают о нем&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/presenting-sqlfluff"&gt;Presenting: SQLFluff&lt;/a&gt; — про очень классную штуку &lt;a href="https://www.sqlfluff.com"&gt;SQLFluff&lt;/a&gt;, которая автоматически редактирует SQL-код согласно канонам&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/quickstart-your-analytics-with-fivetran-dbt-packages"&gt;Quickstart your analytics with Fivetran dbt packages&lt;/a&gt; — из доклада можно узнать, что такое Fivetran и как его используют совместно с dbt&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getdbt.com/coalesce/agenda/perfect-complements-using-dbt-with-looker-for-effective-data-governance"&gt;Perfect complements: Using dbt with Looker for effective data governance&lt;/a&gt; — про взаимодействие dbt и looker, про различия и схожие части инструментов&lt;/li&gt;
&lt;/ol&gt;
</description>
<pubDate>Fri, 11 Dec 2020 13:27:14 +0300</pubDate>
</item>


</channel>
</rss>