<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>Yuriy Gavrilov: posts tagged SQL</title>
<link>https://gavrilov.info/tags/sql/</link>
<description>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</description>
<author></author>
<language>en</language>
<generator>Aegea 11.4 (v4171e)</generator>

<itunes:owner>
<itunes:name></itunes:name>
<itunes:email>yvgavrilov@gmail.com</itunes:email>
</itunes:owner>
<itunes:subtitle>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</itunes:subtitle>
<itunes:image href="https://gavrilov.info/pictures/userpic/userpic-square@2x.jpg?1643451008" />
<itunes:explicit>no</itunes:explicit>

<item>
<title>Trino + OTP верификация запросов в bi – параноя.mode = true</title>
<guid isPermaLink="false">351</guid>
<link>https://gavrilov.info/all/trino-otp-verifikaciya-zaprosov-v-bi-paranoya-mode-true/</link>
<pubDate>Fri, 11 Sep 2026 01:17:04 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/trino-otp-verifikaciya-zaprosov-v-bi-paranoya-mode-true/</comments>
<description>
&lt;p&gt;Представьте: дашборд в BI видят все сотрудники. Закрыть его нельзя — так исторически сложилось. Но данные нужно скрыть. Решение — динамическая фильтрация на уровне SQL: пользователь вводит логин и одноразовый код (TOTP), а запрос показывает только те строки, которые ему разрешены.&lt;/p&gt;
&lt;p&gt;Это не полноценная система безопасности, но это работающий барьер, который можно внедрить без изменения BI-системы. В статье — пошаговая инструкция: от демо-таблиц до настройки прав в Trino, генерации QR-кодов и увеличения окна действия OTP.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Архитектура&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;`catalog.schema.dashboard_users`&lt;/b&gt; — справочник сотрудников: логин, имя, отдел, должность, руководитель, TOTP-секрет, флаг активности.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;`catalog.schema.dashboard_data`&lt;/b&gt; — данные дашборда. У каждой строки есть политика доступа: `ALL`, `OWNER`, `DEPARTMENT`, `MANAGER`, `DIRECTOR`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Функция `verify_totp`&lt;/b&gt; — Python UDF, проверяет OTP-код по секрету и текущему времени.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Итоговый запрос&lt;/b&gt; — аутентифицирует пользователя и фильтрует строки по атрибутам.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Пользователь вводит логин и OTP как параметры BI. Если они верны — получает свои данные. Если нет — пустой результат.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 1. Демо-таблицы&lt;/h3&gt;
&lt;p&gt;Создадим две таблицы в вымышленной схеме `catalog.schema`.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;CREATE TABLE catalog.schema.dashboard_users (
    login         VARCHAR,
    employee_name VARCHAR,
    department    VARCHAR,
    position_name VARCHAR,
    manager_login VARCHAR,
    totp_secret   VARCHAR,
    is_active     BOOLEAN
);

INSERT INTO catalog.schema.dashboard_users VALUES
    ('vasya', 'Василий', 'sales',    'manager',  'ivan',  'JBSWY3DPEHPK3PXP', true),
    ('petya', 'Пётр',    'sales',    'analyst',  'vasya', 'KRSXG5CTMVRXEZLU', true),
    ('masha', 'Мария',   'finance',  'analyst',  'ivan',  'NB2W45DFOIZA',     true),
    ('ivan',  'Иван',    'management','director', null,   'MFRGGZDFMZTWQ2LK', true);

CREATE TABLE catalog.schema.dashboard_data (
    row_id        BIGINT,
    row_title     VARCHAR,
    amount        DECIMAL(12,2),
    owner_login   VARCHAR,
    department    VARCHAR,
    manager_login VARCHAR,
    access_policy VARCHAR
);

INSERT INTO catalog.schema.dashboard_data VALUES
    (1, 'Личная строка Васи',       1000.00, 'vasya', 'sales',   'ivan',  'OWNER'),
    (2, 'Личная строка Пети',       2000.00, 'petya', 'sales',   'vasya', 'OWNER'),
    (3, 'Общая строка отдела Sales',3000.00, null,    'sales',   null,    'DEPARTMENT'),
    (4, 'Общая строка Finance',     4000.00, null,    'finance', null,    'DEPARTMENT'),
    (5, 'Строка команды Васи',      5000.00, 'petya', 'sales',   'vasya', 'MANAGER'),
    (6, 'Строка команды Ивана',     6000.00, 'masha', 'finance', 'ivan',  'MANAGER'),
    (7, 'Директорская строка',      7000.00, null,    null,      null,    'DIRECTOR'),
    (8, 'Публичная строка',         8000.00, null,    null,      null,    'ALL');&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;p&gt;Либо можно сгенерировать коды разово прямо в запросе.&lt;/p&gt;
&lt;h3&gt;Шаг 1.1. Массовая генерация TOTP-секретов и QR-кодов&lt;/h3&gt;
&lt;p&gt;Когда пользователей много, вручную придумывать Base32-секреты неудобно. Можно сгенерировать их прямо в Trino при вставке в таблицу `dashboard_users`. Ниже — готовый блок для демо и тестов.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Важно:&lt;/b&gt; `random()` в Trino не является криптостойким генератором. Для продакшена секреты лучше генерировать вне Trino (например, через `openssl rand -base64 20` или Python `pyotp.random_base32()`) и вставлять уже готовыми. Для демонстрации и параноидального режима «лишь бы закрыть дашборд» этого достаточно.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Вставка пользователей с автоматической генерацией секретов&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;INSERT INTO catalog.schema.dashboard_users (
    login,
    employee_name,
    department,
    position_name,
    manager_login,
    totp_secret,
    is_active
)
SELECT
    login,
    employee_name,
    department,
    position_name,
    manager_login,

    -- Генерация Base32-секрета длиной 16 символов (80 бит)
    array_join(
        transform(
            sequence(1, 16),
            i -&amp;gt; substr(
                'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
                CAST(floor(random() * 32) + 1 AS INTEGER),
                1
            )
        ),
        ''
    ) AS totp_secret,

    true AS is_active
FROM (
    VALUES
        ('vasya', 'Василий', 'sales',     'manager',  'ivan'),
        ('petya', 'Пётр',    'sales',     'analyst',  'vasya'),
        ('masha', 'Мария',   'finance',   'analyst',  'ivan'),
        ('ivan',  'Иван',    'management','director', CAST(NULL AS VARCHAR))
) AS src (
    login,
    employee_name,
    department,
    position_name,
    manager_login
);&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Как получить строку для QR-кода&lt;/h4&gt;
&lt;p&gt;После вставки можно сразу сформировать URI формата `otpauth://`, который останется только превратить в QR-код любым генератором:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;SELECT
    login,
    totp_secret,
    'otpauth://totp/DemoBI:' || login
        || '?secret=' || totp_secret
        || '&amp;amp;issuer=DemoBI' AS otpauth_uri
FROM catalog.schema.dashboard_users;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Пример результата:&lt;/p&gt;
&lt;table cellpadding="0" cellspacing="0" border="0" class="e2-text-table"&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;login&lt;/td&gt;
&lt;td style="text-align: center"&gt;totp_secret&lt;/td&gt;
&lt;td style="text-align: center"&gt;otpauth_uri&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;vasya&lt;/td&gt;
&lt;td style="text-align: center"&gt;JBSWY3DPEHPK3PXP&lt;/td&gt;
&lt;td style="text-align: center"&gt;otpauth://totp/DemoBI:vasya?secret=JBSWY3DPEHPK3PXP&amp;issuer=DemoBI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;petya&lt;/td&gt;
&lt;td style="text-align: center"&gt;KRSXG5CTMVRXEZLU&lt;/td&gt;
&lt;td style="text-align: center"&gt;otpauth://totp/DemoBI:petya?secret=KRSXG5CTMVRXEZLU&amp;issuer=DemoBI&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Эту строку можно вставить в любой генератор QR-кодов (онлайн или офлайн), а затем отсканировать приложением:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Authenticator,&lt;/li&gt;
&lt;li&gt;Yandex ID,&lt;/li&gt;
&lt;li&gt;KeePass (с плагином TOTP),&lt;/li&gt;
&lt;li&gt;и любым другим менеджером паролей, поддерживающим TOTP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Настройка длины секрета&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;`sequence(1, 16)` — 16 символов Base32 = 80 бит. Этого достаточно для большинства случаев и именно такую длину часто используют по умолчанию.&lt;/li&gt;
&lt;li&gt;Если хотите более длинный секрет, замените `16` на `32`. Тогда получится 160 бит.&lt;/li&gt;
&lt;li&gt;Алфавит `’ABCDEFGHIJKLMNOPQRSTUVWXYZ234567’` — это стандартный Base32-алфавит (без цифр 0, 1, 8, 9). Менять его не нужно.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Если нужен детерминированный секрет (не рекомендуется)&lt;/h4&gt;
&lt;p&gt;Иногда для тестов хотят, чтобы секрет зависел от логина. Можно использовать хеш:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;substr(
    upper(to_base32(from_utf8(login || 'some_salt'))),
    1, 16
) AS totp_secret&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Но это &lt;b&gt;небезопасно&lt;/b&gt;: зная логин и соль, можно вычислить секрет. Для реальной эксплуатации используйте случайную генерацию вне Trino.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 2. Inline-функция для тестов в DBeaver&lt;/h3&gt;
&lt;p&gt;В DBeaver можно использовать `WITH FUNCTION` — Python-код прямо в запросе. Это удобно для отладки, но &lt;b&gt;не работает в BI&lt;/b&gt;, потому что BI схлопывает запрос в одну строку и ломает форматирование Python.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;WITH FUNCTION verify_totp(secret VARCHAR, code VARCHAR, time_step BIGINT)
RETURNS BOOLEAN
LANGUAGE PYTHON
WITH (handler = 'verify_handler')
AS $$
import hmac, hashlib, struct, base64

def verify_handler(secret_b32, code, time_step):
    if secret_b32 is None or code is None or time_step is None:
        return False

    code = str(code).strip()
    if not code.isdigit() or len(code) != 6:
        return False

    secret = str(secret_b32).strip().replace(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;).upper()
    padding = &amp;quot;=&amp;quot; * ((8 - len(secret) % 8) % 8)

    try:
        key = base64.b32decode(secret + padding, casefold=True)
    except Exception:
        return False

    current = int(time_step // 30)   # 30 секунд — стандарт TOTP
    for offset in (-1, 0, 1):
        counter = current + offset
        msg = struct.pack(&amp;quot;&amp;gt;Q&amp;quot;, counter)
        digest = hmac.new(key, msg, hashlib.sha1).digest()
        o = digest[19] &amp;amp; 15
        expected = (struct.unpack(&amp;quot;&amp;gt;I&amp;quot;, digest[o:o+4])[0] &amp;amp; 0x7fffffff) % 1000000
        expected_code = f&amp;quot;{expected:06d}&amp;quot;
        if hmac.compare_digest(expected_code, code):
            return True
    return False
$$
SELECT verify_totp('JBSWY3DPEHPK3PXP', '123456', CAST(to_unixtime(now()) AS BIGINT));&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Важно:&lt;/b&gt; тело функции должно начинаться с новой строки после `$$`. Если BI отправляет всё в одну строку — будет ошибка `Function definition must start with a newline after opening quotes`.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Screenshot-from-2026-09-11-00-28-13.png" width="1024" height="691" alt="" /&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 3. Регистрация функции администратором&lt;/h3&gt;
&lt;p&gt;Чтобы BI-запросы не содержали Python-код, функцию нужно создать один раз на стороне Trino. Это делает администратор или пользователь с правами на создание функций.&lt;/p&gt;
&lt;p&gt;Сразу еще скажу, что регистрация не во всех каталогах работает, в iceberg например не работает, а вот в iceberg + rest надо проверять. Такая фича давно в плане, но не уверен готова ли. Можно регать функции в каталоге memory конектора, но это придется делать каждую загрузку.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;DROP FUNCTION IF EXISTS catalog.schema.verify_totp(VARCHAR, VARCHAR, BIGINT);

CREATE FUNCTION catalog.schema.verify_totp(
    secret VARCHAR,
    code VARCHAR,
    time_step BIGINT
)
RETURNS BOOLEAN
LANGUAGE PYTHON
WITH (handler = 'verify_handler')
AS $$
import hmac, hashlib, struct, base64

def verify_handler(secret_b32, code, time_step):
    if secret_b32 is None or code is None or time_step is None:
        return False

    code = str(code).strip()
    if not code.isdigit() or len(code) != 6:
        return False

    secret = str(secret_b32).strip().replace(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;).upper()
    padding = &amp;quot;=&amp;quot; * ((8 - len(secret) % 8) % 8)

    try:
        key = base64.b32decode(secret + padding, casefold=True)
    except Exception:
        return False

    current = int(time_step // 30)
    for offset in (-1, 0, 1):
        counter = current + offset
        msg = struct.pack(&amp;quot;&amp;gt;Q&amp;quot;, counter)
        digest = hmac.new(key, msg, hashlib.sha1).digest()
        o = digest[19] &amp;amp; 15
        expected = (struct.unpack(&amp;quot;&amp;gt;I&amp;quot;, digest[o:o+4])[0] &amp;amp; 0x7fffffff) % 1000000
        expected_code = f&amp;quot;{expected:06d}&amp;quot;
        if hmac.compare_digest(expected_code, code):
            return True
    return False
$$;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Проверка:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;SELECT catalog.schema.verify_totp(
    'JBSWY3DPEHPK3PXP',
    '465617',
    CAST(to_unixtime(now()) AS BIGINT)
) AS is_valid;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Примечание:&lt;/b&gt; в вашей среде создание функции может быть не разрешено без настройки прав. Об этом — следующий шаг.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 4. Настройка прав доступа&lt;/h3&gt;
&lt;p&gt;Нужно разрешить группе &lt;b&gt;admins&lt;/b&gt; создавать функции в схеме `catalog.schema`, а группе &lt;b&gt;bi_users&lt;/b&gt; — только вызывать `verify_totp`. В Trino это делается через system access control.&lt;/p&gt;
&lt;h4&gt;Вариант A. File-based ACL&lt;/h4&gt;
&lt;p&gt;Файл `/etc/trino/access-control.properties`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;access-control.name=file
security.config-file=/etc/trino/rules/access-control.json
security.refresh-period=1m&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Файл `access-control.json`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
  &amp;quot;catalogs&amp;quot;: [
    {
      &amp;quot;group&amp;quot;: &amp;quot;admins&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;allow&amp;quot;: &amp;quot;all&amp;quot;
    },
    {
      &amp;quot;group&amp;quot;: &amp;quot;bi_users&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;allow&amp;quot;: &amp;quot;read-only&amp;quot;
    }
  ],
  &amp;quot;schemas&amp;quot;: [
    {
      &amp;quot;group&amp;quot;: &amp;quot;admins&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;schema&amp;quot;: &amp;quot;schema&amp;quot;,
      &amp;quot;owner&amp;quot;: true
    },
    {
      &amp;quot;group&amp;quot;: &amp;quot;bi_users&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;schema&amp;quot;: &amp;quot;schema&amp;quot;,
      &amp;quot;owner&amp;quot;: false
    }
  ],
  &amp;quot;functions&amp;quot;: [
    {
      &amp;quot;group&amp;quot;: &amp;quot;admins&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;schema&amp;quot;: &amp;quot;schema&amp;quot;,
      &amp;quot;function&amp;quot;: &amp;quot;.*&amp;quot;,
      &amp;quot;privileges&amp;quot;: [&amp;quot;EXECUTE&amp;quot;, &amp;quot;GRANT_EXECUTE&amp;quot;, &amp;quot;OWNERSHIP&amp;quot;]
    },
    {
      &amp;quot;group&amp;quot;: &amp;quot;bi_users&amp;quot;,
      &amp;quot;catalog&amp;quot;: &amp;quot;catalog&amp;quot;,
      &amp;quot;schema&amp;quot;: &amp;quot;schema&amp;quot;,
      &amp;quot;function&amp;quot;: &amp;quot;verify_totp&amp;quot;,
      &amp;quot;privileges&amp;quot;: [&amp;quot;EXECUTE&amp;quot;]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Вариант B. OPA (Rego)&lt;/h4&gt;
&lt;p&gt;Если Trino использует OPA, политика может выглядеть так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;package trino

default allow := false

is_admin {
    input.context.identity.groups[_] == &amp;quot;admins&amp;quot;
}

is_bi_user {
    input.context.identity.groups[_] == &amp;quot;bi_users&amp;quot;
}

is_target_schema {
    input.action.resource.catalog.name == &amp;quot;catalog&amp;quot;
    input.action.resource.schema.name == &amp;quot;schema&amp;quot;
}

is_verify_totp {
    input.action.resource.catalog.name == &amp;quot;catalog&amp;quot;
    input.action.resource.schema.name == &amp;quot;schema&amp;quot;
    input.action.resource.function.name == &amp;quot;verify_totp&amp;quot;
}

allow {
    is_admin
    is_target_schema
    input.action.operation == &amp;quot;CreateFunction&amp;quot;
}

allow {
    is_admin
    is_target_schema
    input.action.operation == &amp;quot;DropFunction&amp;quot;
}

allow {
    is_admin
    is_target_schema
    input.action.operation == &amp;quot;ExecuteFunction&amp;quot;
}

allow {
    is_bi_user
    is_verify_totp
    input.action.operation == &amp;quot;ExecuteFunction&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Точные названия операций (`CreateFunction`, `ExecuteFunction`) лучше уточнить в логах OPA при тестовом запросе.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 5. Запрос для BI (без Python)&lt;/h3&gt;
&lt;p&gt;После регистрации функции BI-запрос становится обычным SQL. Пользователь вводит `:login` и `:otp` как параметры.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;WITH auth_user AS (
    SELECT
        login,
        employee_name,
        department,
        position_name,
        manager_login
    FROM catalog.schema.dashboard_users
    WHERE login = lower(trim(':login'))
      AND is_active = true
      AND catalog.schema.verify_totp(
            totp_secret,
            trim(':otp'),
            CAST(to_unixtime(now()) AS BIGINT)
          )
    LIMIT 1
)
SELECT
    d.row_id,
    d.row_title,
    d.amount,
    d.owner_login,
    d.department,
    d.manager_login,
    d.access_policy,
    a.login AS current_login,
    a.employee_name AS current_employee_name,
    a.department AS current_department,
    a.position_name AS current_position_name,
    CASE
        WHEN d.access_policy = 'ALL' THEN 'Видно всем авторизованным'
        WHEN d.access_policy = 'OWNER' AND d.owner_login = a.login THEN 'Видно владельцу строки'
        WHEN d.access_policy = 'DEPARTMENT' AND d.department = a.department THEN 'Видно сотруднику отдела'
        WHEN d.access_policy = 'MANAGER' AND d.manager_login = a.login THEN 'Видно руководителю'
        WHEN d.access_policy = 'DIRECTOR' AND a.position_name = 'director' THEN 'Видно директору'
        ELSE null
    END AS access_reason
FROM catalog.schema.dashboard_data d
CROSS JOIN auth_user a
WHERE
       d.access_policy = 'ALL'
    OR (d.access_policy = 'OWNER' AND d.owner_login = a.login)
    OR (d.access_policy = 'DEPARTMENT' AND d.department = a.department)
    OR (d.access_policy = 'MANAGER' AND d.manager_login = a.login)
    OR (d.access_policy = 'DIRECTOR' AND a.position_name = 'director');&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Важно про параметры:&lt;/b&gt; если BI подставляет их как сырой текст, нужны кавычки: `’:login’`, `’:otp’`. Если BI использует bind-параметры — кавычки не нужны: `:login`, `:otp`. В предыдущих ошибках (`Column ‘jbswy3dpehpk3pxp’ cannot be resolved`) видно, что в вашем случае нужны именно кавычки.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 6. Логика доступа&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;`auth_user`&lt;/b&gt; — возвращает 0 строк, если логин не найден, пользователь неактивен или OTP неверный.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;`CROSS JOIN auth_user`&lt;/b&gt; — если `auth_user` пуст, результат всего запроса пуст.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;`WHERE` с `OR`&lt;/b&gt; — оставляет только те строки, которые разрешены политикой:
&lt;ul&gt;
  &lt;li&gt;`ALL` — всем авторизованным;&lt;/li&gt;
  &lt;li&gt;`OWNER` — владельцу (`owner_login = a.login`);&lt;/li&gt;
  &lt;li&gt;`DEPARTMENT` — сотруднику того же отдела;&lt;/li&gt;
  &lt;li&gt;`MANAGER` — руководителю (`manager_login = a.login`);&lt;/li&gt;
  &lt;li&gt;`DIRECTOR` — директору (`position_name = ‘director’`).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Пример: `vasya` увидит свои строки, строки отдела `sales`, строки, где он руководитель, и публичные. `petya` — только свои, отдел `sales` и публичные.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 7. Генерация QR-кода для OTP&lt;/h3&gt;
&lt;p&gt;Каждому пользователю нужно выдать TOTP-секрет и удобно передать его в приложение-аутентификатор. Стандартный способ — использовать URI формата `otpauth://`.&lt;/p&gt;
&lt;p&gt;Пример для пользователя `vasya`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;otpauth://totp/DemoBI:vasya?secret=JBSWY3DPEHPK3PXP&amp;amp;issuer=DemoBI&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Здесь:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`DemoBI:vasya` — метка, которая отобразится в приложении. Можно заменить на название отчёта, отдела или что угодно, например `SalesDashboard:vasya`.&lt;/li&gt;
&lt;li&gt;`secret=JBSWY3DPEHPK3PXP` — Base32-секрет.&lt;/li&gt;
&lt;li&gt;`issuer=DemoBI` — имя издателя, обычно название компании или системы.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Screenshot-from-2026-09-11-00-31-21.png" width="596" height="538" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Эту строку можно вставить в любой генератор QR-кодов (онлайн или офлайн). После генерации QR-код сканируется приложением:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Authenticator,&lt;/li&gt;
&lt;li&gt;Yandex ID,&lt;/li&gt;
&lt;li&gt;KeePass (с плагином TOTP),&lt;/li&gt;
&lt;li&gt;и любым другим менеджером паролей, поддерживающим TOTP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Это стандарт, поэтому совместимость широкая. QR-код можно распечатать или отправить пользователю.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Совет:&lt;/b&gt; если у вас много пользователей, сгенерируйте QR-коды автоматически из таблицы `dashboard_users`, подставляя логин и секрет.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 8. Увеличение окна действия OTP (2–5 минут)&lt;/h3&gt;
&lt;p&gt;По умолчанию TOTP-код действует 30 секунд. Это может быть неудобно: пользователь не успевает ввести код. Можно увеличить окно действия, изменив функцию.&lt;/p&gt;
&lt;p&gt;В Python-коде есть строка:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;current = int(time_step // 30)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Здесь `30` — это количество секунд в одном временном шаге. Если заменить `30` на `300`, то код будет действителен 5 минут (300 секунд). При этом приложение-аутентификатор продолжит генерировать коды каждые 30 секунд, но сервер будет принимать любой код, сгенерированный в течение этих 5 минут. Это удобно и безопасно: окно не бесконечное, но достаточное, чтобы успеть ввести код.&lt;/p&gt;
&lt;p&gt;Пример изменённой функции:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;current = int(time_step // 300)   # 5 минут
for offset in (-1, 0, 1):
    ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если оставить смещения `-1, 0, 1`, то общее окно станет 15 минут (5 минут назад, текущие 5 минут, 5 минут вперёд). Это может быть избыточно. Лучше убрать смещения и проверять только текущий интервал:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;current = int(time_step // 300)
counter = current
msg = struct.pack(&amp;quot;&amp;gt;Q&amp;quot;, counter)
...
# без цикла for&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Тогда код будет действителен ровно 5 минут.&lt;/p&gt;
&lt;p&gt;Если хотите 2 минуты — используйте `120` вместо `300`.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;Важно:&lt;/b&gt; после изменения функции её нужно пересоздать ( `DROP FUNCTION` и `CREATE FUNCTION` ). Все пользователи автоматически начнут работать с новым окном.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 9. Безопасность&lt;/h3&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Секреты TOTP&lt;/b&gt; нельзя хранить в открытой схеме. Лучше вынести `dashboard_users` в закрытую схему, например `security.users`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BI-пользователи&lt;/b&gt; не должны иметь прямой доступ к таблице с секретами. Только к функции и к данным.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OTP-код&lt;/b&gt; попадает в логи Trino и BI. Логи могут быть доступны администраторам или храниться в системе. Это не смертельно, потому что код короткоживущий, но помните: любой, кто видит лог, может увидеть введённый OTP. Если окно действия увеличено до 5 минут, риск возрастает. По возможности используйте реального пользователя из аутентификации BI, а не ручной ввод логина.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Права на создание функций&lt;/b&gt; — только у группы `admins`. BI-пользователям — только `EXECUTE` на конкретную функцию.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Заключение&lt;/h3&gt;
&lt;p&gt;Мы построили параноидальный, но рабочий механизм: OTP-аутентификация + динамическая фильтрация строк на уровне SQL. Это не заменяет полноценный RBAC, но позволяет быстро закрыть дашборд от посторонних глаз без изменения BI-системы.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Что проверено:&lt;/b&gt; inline-функция в DBeaver, запрос с фильтрацией, логика доступа.&lt;br /&gt;
&lt;b&gt;Что требует настройки:&lt;/b&gt; регистрация функции и права в Trino. После настройки прав (шаг 4) функция создаётся один раз, а BI-запросы становятся чистыми и безопасными.&lt;/p&gt;
&lt;p&gt;Удачи в паранойе! :)&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="416" data-ratio="1.0833333333333"&gt;
&lt;img src="https://gavrilov.info/pictures/Screenshot-from-2026-09-11-00-33-10.png" width="416" height="384" alt="" /&gt;
&lt;img src="https://gavrilov.info/pictures/Screenshot-from-2026-09-11-01-21-57.png" width="362" height="424" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
</description>
</item>

<item>
<title>StarRocks: Архитектура, Практика и место в современном Data Stack</title>
<guid isPermaLink="false">323</guid>
<link>https://gavrilov.info/all/starrocks-arhitektura-praktika-i-mesto-v-sovremennom-data-stack/</link>
<pubDate>Sun, 15 Mar 2026 19:06:01 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/starrocks-arhitektura-praktika-i-mesto-v-sovremennom-data-stack/</comments>
<description>
&lt;p&gt;&lt;b&gt;StarRocks&lt;/b&gt; — это аналитическая MPP-база данных нового поколения.&lt;br /&gt;
Если коротко, она пытается решить трилемму аналитики: объединить скорость &lt;b&gt;ClickHouse&lt;/b&gt; (за счет векторизации и C++), гибкость &lt;b&gt;Trino&lt;/b&gt; (поддержка сложных JOIN-ов) и простоту использования &lt;b&gt;MySQL&lt;/b&gt; (совместимый протокол).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/logo_starr.svg" width="54" height="62" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Это короткое руководство проведет вас от понимания архитектуры до построения простого конвейера загрузки данных (ETL) в домашнем продакшене.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Часть 1. Архитектура: FE и BE&lt;/h3&gt;
&lt;p&gt;В отличие от PostgreSQL (монолит) или ClickHouse (где узлы часто одноранговые), StarRocks имеет четкое разделение ролей. Это критически важно для понимания масштабирования и эксплуатации.&lt;/p&gt;
&lt;h4&gt;1. FE (Frontend) — “Мозг”&lt;/h4&gt;
&lt;p&gt;Написан на Java.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Роль:&lt;/b&gt; Управляющий слой.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Функции:&lt;/b&gt;
&lt;ul&gt;
  &lt;li&gt;Принимает подключения клиентов (по протоколу MySQL).&lt;/li&gt;
  &lt;li&gt;Хранит метаданные (схемы таблиц, права доступа).&lt;/li&gt;
  &lt;li&gt;Парсит SQL и строит план выполнения запроса (Query Plan).&lt;/li&gt;
  &lt;li&gt;Управляет транзакциями загрузки данных.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Масштабирование:&lt;/b&gt; Обычно запускают 1 или 3 узла для обеспечения высокой доступности (HA).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Важно:&lt;/b&gt; Клиенты (DBeaver, BI, сurl) подключаются &lt;b&gt;только&lt;/b&gt; к FE.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. BE (Backend) — “Мускулы”&lt;/h4&gt;
&lt;p&gt;Написан на C++ (использует SIMD-инструкции процессора).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Роль:&lt;/b&gt; Слой хранения и вычислений.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Функции:&lt;/b&gt;
&lt;ul&gt;
  &lt;li&gt;Физически хранит данные (в колоночном формате).&lt;/li&gt;
  &lt;li&gt;Выполняет “тяжелую” работу: фильтрацию, агрегацию, JOIN-ы.&lt;/li&gt;
  &lt;li&gt;Управляет репликацией данных.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Масштабирование:&lt;/b&gt; Можно добавлять узлы линейно. Чем больше BE, тем быстрее выполняются запросы и тем больше данных можно хранить.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;В Docker All-in-One:&lt;/b&gt; Оба компонента упакованы в один контейнер для удобства, но слушают разные порты:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`9030`: FE (SQL интерфейс, сюда идет DBeaver).&lt;/li&gt;
&lt;li&gt;`8030`: FE (HTTP API для загрузки Stream Load, сюда идет curl).&lt;/li&gt;
&lt;li&gt;`8040`: BE (HTTP API метрик и логов).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Часть 2. Быстрый старт (Docker Compose)&lt;/h3&gt;
&lt;p&gt;Мы поднимем стек StarRocks и MinIO (S3-совместимое хранилище), используя bridge-сеть для связности.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Файл `docker-compose.yml`&lt;/b&gt; (Полностью рабочий пример):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;version: &amp;quot;3.9&amp;quot;

networks:
  starrocks-stack-network:
    driver: bridge

services:
  starrocks:
    image: starrocks/allin1-ubuntu:4.0-latest
    container_name: starrocks
    hostname: starrocks.local.com
    platform: &amp;quot;linux/amd64&amp;quot;
    restart: unless-stopped
    ports:
      - &amp;quot;9030:9030&amp;quot; # MySQL Protocol (SQL клиенты)
      - &amp;quot;8030:8030&amp;quot; # FE HTTP (Stream Load)
      - &amp;quot;8040:8040&amp;quot; # BE HTTP (Logs/Metrics)
    environment:
      - TZ=UTC
    networks:
      starrocks-stack-network:
    volumes:
      # Персистентность данных (чтобы данные не исчезли после рестарта)
      - ${HOME}/dv/starrocks/be/storage:/data/deploy/starrocks/be/storage
      - ${HOME}/dv/starrocks/be/log:/data/deploy/starrocks/be/log
      - ${HOME}/dv/starrocks/fe/meta:/data/deploy/starrocks/fe/meta
      - ${HOME}/dv/starrocks/fe/log:/data/deploy/starrocks/fe/log

  minio:
    image: quay.io/minio/minio
    container_name: minio
    platform: &amp;quot;linux/amd64&amp;quot;
    hostname: minio.local.com
    restart: unless-stopped
    ports:
      - &amp;quot;9000:9000&amp;quot; # S3 API
      - &amp;quot;9001:9001&amp;quot; # Web UI
    networks:
      starrocks-stack-network:
    environment:
      MINIO_ROOT_USER: root
      MINIO_ROOT_PASSWORD: rootroot
    volumes:
      - ${HOME}/dv/minio/data:/data
    command: server /data --console-address &amp;quot;:9001&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Запуск:&lt;br /&gt;
`docker-compose up -d`&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Часть 3. Моделирование данных (Table Design)&lt;/h3&gt;
&lt;p&gt;В StarRocks нельзя просто “создать таблицу”. Нужно выбрать тип ключа (&lt;b&gt;Key Model&lt;/b&gt;), который определит, как база будет хранить и обновлять данные.&lt;/p&gt;
&lt;p&gt;Подключение (DBeaver): `localhost:9030`, User: `root`, Password: (пусто).&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;CREATE DATABASE IF NOT EXISTS demo_db;
USE demo_db;&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;1. Primary Key Model (Для изменяемых данных)&lt;/h4&gt;
&lt;p&gt;Это “флагманская” возможность StarRocks. Она поддерживает быстрые &lt;b&gt;Upsert&lt;/b&gt; (вставка новых или обновление старых записей по ID) в реальном времени.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;CREATE TABLE IF NOT EXISTS users (
    user_id INT NOT NULL,
    username VARCHAR(50),
    email VARCHAR(100),
    register_date DATE, 
    city VARCHAR(50)
)
PRIMARY KEY (user_id) -- Уникальный ключ
DISTRIBUTED BY HASH(user_id) -- Распределение данных
PROPERTIES (
    &amp;quot;replication_num&amp;quot; = &amp;quot;1&amp;quot; -- Для локального теста ставим 1 реплику
);&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;2. Aggregate Key Model (Для витрин данных)&lt;/h4&gt;
&lt;p&gt;База автоматически агрегирует данные при вставке. Если вы вставите новую продажу с *существующими* датой и категорией, StarRocks не создаст новую строку, а прибавит суммы к уже существующей строке. Это экономит место и ускоряет `GROUP BY`.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;CREATE TABLE IF NOT EXISTS daily_sales (
    report_date DATE NOT NULL,
    category VARCHAR(50) NOT NULL,
    
    -- Метрики с функцией агрегации:
    total_amount BIGINT SUM DEFAULT &amp;quot;0&amp;quot;, 
    items_sold INT SUM DEFAULT &amp;quot;0&amp;quot;       
)
AGGREGATE KEY (report_date, category)
DISTRIBUTED BY HASH(report_date) BUCKETS 3
PROPERTIES (
    &amp;quot;replication_num&amp;quot; = &amp;quot;1&amp;quot;
);&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Часть 4. загрузка данных users (Stream Load)&lt;/h3&gt;
&lt;p&gt;Для загрузки данных в продакшене мы используем &lt;b&gt;Service Account&lt;/b&gt; (Техническую учетную запись). Это стандарт безопасности: мы не используем `root` и не используем токены в конфигах (так как они требуют перезагрузки кластера для смены).&lt;/p&gt;
&lt;h4&gt;Шаг 1. Создание сервисного пользователя (SQL)&lt;/h4&gt;
&lt;p&gt;Выполнять под `root`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;-- 1. Создаем пользователя-бота
CREATE USER IF NOT EXISTS 'etl_loader'@'%' IDENTIFIED BY 'SecretPass123!';

-- 2. Даем права ТОЛЬКО на вставку и чтение в базе demo_db
GRANT INSERT, SELECT ON demo_db.* TO 'etl_loader'@'%';

-- Права применяются мгновенно.&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Шаг 2. Загрузка сложного JSON через CURL&lt;/h4&gt;
&lt;p&gt;Stream Load — это самый быстрый способ загрузки (до 100 МБ/сек на узел). Он поддерживает транзакционность (ACID).&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Пример файла `users.json`:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
  &amp;quot;users&amp;quot;: [
    {&amp;quot;user_id&amp;quot;: 101, &amp;quot;username&amp;quot;: &amp;quot;alex&amp;quot;, &amp;quot;email&amp;quot;: &amp;quot;a@test.com&amp;quot;, &amp;quot;city&amp;quot;: &amp;quot;NY&amp;quot;},
    {&amp;quot;user_id&amp;quot;: 102, &amp;quot;username&amp;quot;: &amp;quot;bob&amp;quot;, &amp;quot;email&amp;quot;: &amp;quot;b@test.com&amp;quot;, &amp;quot;city&amp;quot;: &amp;quot;LA&amp;quot;}
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Команда загрузки (Terminal):&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;curl --location-trusted \
    -u etl_loader:SecretPass123! \
    -H &amp;quot;Expect: 100-continue&amp;quot; \
    -H &amp;quot;format: json&amp;quot; \
    -H &amp;quot;strip_outer_array: true&amp;quot; \
    -H &amp;quot;json_root: $.users&amp;quot; \
    -H &amp;quot;jsonpaths: [\&amp;quot;$.user_id\&amp;quot;, \&amp;quot;$.username\&amp;quot;, \&amp;quot;$.email\&amp;quot;, \&amp;quot;$.city\&amp;quot;]&amp;quot; \
    -H &amp;quot;columns: user_id, username, email, city&amp;quot; \
    -T &amp;quot;users.json&amp;quot; \
    -XPUT http://localhost:8030/api/demo_db/users/_stream_load&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ответ&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
    &amp;quot;TxnId&amp;quot;: 9596,
    &amp;quot;Label&amp;quot;: &amp;quot;a9a37ab6-3678-4c08-95b7-2fd8b6ae973e&amp;quot;,
    &amp;quot;Db&amp;quot;: &amp;quot;demo_db&amp;quot;,
    &amp;quot;Table&amp;quot;: &amp;quot;users&amp;quot;,
    &amp;quot;Status&amp;quot;: &amp;quot;Success&amp;quot;,
    &amp;quot;Message&amp;quot;: &amp;quot;OK&amp;quot;,
    &amp;quot;NumberTotalRows&amp;quot;: 2,
    &amp;quot;NumberLoadedRows&amp;quot;: 2,
    &amp;quot;NumberFilteredRows&amp;quot;: 0,
    &amp;quot;NumberUnselectedRows&amp;quot;: 0,
    &amp;quot;LoadBytes&amp;quot;: 177,
    &amp;quot;LoadTimeMs&amp;quot;: 153,
    &amp;quot;BeginTxnTimeMs&amp;quot;: 2,
    &amp;quot;StreamLoadPlanTimeMs&amp;quot;: 2,
    &amp;quot;ReadDataTimeMs&amp;quot;: 0,
    &amp;quot;WriteDataTimeMs&amp;quot;: 26,
    &amp;quot;CommitAndPublishTimeMs&amp;quot;: 121
}%&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Шаг 3. Загрузка в Aggregate Table (Example)&lt;/h4&gt;
&lt;p&gt;Давайте “дольем” данные в таблицу продаж. Агрегация произойдет на лету.&lt;br /&gt;
Файл sales.json (простой список):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[
    {&amp;quot;dt&amp;quot;: &amp;quot;2023-11-01&amp;quot;, &amp;quot;cat&amp;quot;: &amp;quot;Electronics&amp;quot;, &amp;quot;amt&amp;quot;: 100, &amp;quot;qty&amp;quot;: 1},
    {&amp;quot;dt&amp;quot;: &amp;quot;2023-11-01&amp;quot;, &amp;quot;cat&amp;quot;: &amp;quot;Electronics&amp;quot;, &amp;quot;amt&amp;quot;: 50,  &amp;quot;qty&amp;quot;: 1}
]

curl --location-trusted \
    -u etl_loader:SecretPass123! \
    -H &amp;quot;format: json&amp;quot; \
    -H &amp;quot;Expect: 100-continue&amp;quot; \
    -H &amp;quot;strip_outer_array: true&amp;quot; \
    -H &amp;quot;jsonpaths: [\&amp;quot;$.dt\&amp;quot;, \&amp;quot;$.cat\&amp;quot;, \&amp;quot;$.amt\&amp;quot;, \&amp;quot;$.qty\&amp;quot;]&amp;quot; \
    -H &amp;quot;columns: report_date, category, total_amount, items_sold&amp;quot; \
    -T &amp;quot;sales.json&amp;quot; \
    -XPUT http://localhost:8030/api/demo_db/daily_sales/_stream_load&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ответ:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
    &amp;quot;TxnId&amp;quot;: 9613,
    &amp;quot;Label&amp;quot;: &amp;quot;bce0721a-dc2d-4927-be93-e0979a57873d&amp;quot;,
    &amp;quot;Db&amp;quot;: &amp;quot;demo_db&amp;quot;,
    &amp;quot;Table&amp;quot;: &amp;quot;daily_sales&amp;quot;,
    &amp;quot;Status&amp;quot;: &amp;quot;Success&amp;quot;,
    &amp;quot;Message&amp;quot;: &amp;quot;OK&amp;quot;,
    &amp;quot;NumberTotalRows&amp;quot;: 2,
    &amp;quot;NumberLoadedRows&amp;quot;: 2,
    &amp;quot;NumberFilteredRows&amp;quot;: 0,
    &amp;quot;NumberUnselectedRows&amp;quot;: 0,
    &amp;quot;LoadBytes&amp;quot;: 143,
    &amp;quot;LoadTimeMs&amp;quot;: 52,
    &amp;quot;BeginTxnTimeMs&amp;quot;: 3,
    &amp;quot;StreamLoadPlanTimeMs&amp;quot;: 2,
    &amp;quot;ReadDataTimeMs&amp;quot;: 0,
    &amp;quot;WriteDataTimeMs&amp;quot;: 24,
    &amp;quot;CommitAndPublishTimeMs&amp;quot;: 20
}%&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Разбор заголовков:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`-u ...`: Авторизация сервисным пользователем.&lt;/li&gt;
&lt;li&gt;`Expect: 100-continue`: Критически важно для надежности передачи больших файлов.&lt;/li&gt;
&lt;li&gt;`json_root: $.users`: Указывает базе, что данные лежат внутри ключа `users`.&lt;/li&gt;
&lt;li&gt;`strip_outer_array: true`: Говорит базе, что внутри лежит массив `[...]` и его нужно “развернуть” в отдельные строки.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Часть 5. Совместимость и Trino Dialect&lt;/h3&gt;
&lt;p&gt;Одна из сильных сторон StarRocks — способность “притворяться” другими базами данных для облегчения миграции.&lt;/p&gt;
&lt;p&gt;Если у вас есть дашборды, написанные на диалекте &lt;b&gt;Trino (Presto)&lt;/b&gt;, вам не нужно переписывать все SQL-запросы.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Пример трансляции функций:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;-- Функция Trino, которой нет в StarRocks
SELECT doy(date '2022-03-06'); 
-- Ошибка: No matching function...

-- Проверяем, как StarRocks переведет этот запрос
TRANSLATE TRINO select doy(date '2022-03-06');
-- Результат: SELECT dayofyear('2022-03-06')

-- Включаем режим автоматической трансляции в сессии
SET sql_dialect = 'trino'; 

-- Теперь запрос выполняется корректно, но это не правда. а вот так SELECT dayofyear('2022-03-06') работает. Может бага или у меня версия не та. 
SELECT doy(date '2022-03-06');   

-- Возвращаем нативный режим
SET sql_dialect = 'starrocks';&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;*(Примечание: Поддержка диалекта постоянно расширяется, но некоторые специфические функции могут требовать ручной замены).*&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Итог: Сравнение и Выбор решения ( грубо )&lt;/h3&gt;
&lt;table cellpadding="0" cellspacing="0" border="0" class="e2-text-table"&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;Характеристика&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;StarRocks&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;ClickHouse&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Trino (Presto)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Основной сценарий&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;OLAP-витрины с JOIN-ами и обновлениями данных&lt;/td&gt;
&lt;td style="text-align: center"&gt;Сбор логов, событий, метрик (Append-only)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Федерация данных (запрос к S3 + Postgres + Kafka одновременно)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;JOIN производительность&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;⭐⭐⭐ (Excellent, CBO оптимизатор)&lt;/td&gt;
&lt;td style="text-align: center"&gt;⭐ (Слабо, требует денормализации)&lt;/td&gt;
&lt;td style="text-align: center"&gt;⭐⭐⭐ (Excellent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Обновление (UPDATE)&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;⭐⭐⭐ (Работает как в OLTP, Primary Key)&lt;/td&gt;
&lt;td style="text-align: center"&gt;⭐ (Тяжелые асинхронные ALTER)&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ (Обычно только полная перезапись партиций), iceberg не в счёт :)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Язык Engine&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;C++ (SIMD Vectorized)&lt;/td&gt;
&lt;td style="text-align: center"&gt;C++ (SIMD Vectorized)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Java (JVM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Место в стеке&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Serving Layer&lt;/b&gt; (Быстрый доступ для BI)&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Storage Layer&lt;/b&gt; (Хранение логов)&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Query Engine&lt;/b&gt; (Ad-hoc запросы к Data Lake)&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;b&gt;Выбирайте StarRocks, если:&lt;/b&gt;&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Вам нужна “витрина” для BI (Superset/Tableau), где данные должны быть всегда свежими (Real-time updates).&lt;/li&gt;
&lt;li&gt;Ваш бизнес требует сложных аналитических запросов с множеством JOIN-ов, и ClickHouse не справляется/падает по памяти.&lt;/li&gt;
&lt;li&gt;Вы хотите использовать стандартный протокол MySQL без установки проприетарных драйверов.&lt;/li&gt;
&lt;/ol&gt;
</description>
</item>

<item>
<title>Настройка ldap в Querybook</title>
<guid isPermaLink="false">215</guid>
<link>https://gavrilov.info/all/nastroyka-ldap-v-querybook/</link>
<pubDate>Fri, 28 Mar 2025 00:09:54 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/nastroyka-ldap-v-querybook/</comments>
<description>
&lt;p&gt;Казалось не очевидная задача, но победа случилась на второй день.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.39.30.png" width="1384" height="308" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:47] &amp;quot;GET /ds/user/me/ HTTP/1.1&amp;quot; 401 285 0.000000
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:47] &amp;quot;GET /ds/user/login_method/ HTTP/1.1&amp;quot; 200 273 0.000000
querybook_web            | [2025-03-27 Thu 20:36:56] - /opt/querybook/querybook/server/app/auth/ldap_auth.py - DEBUG   &amp;quot;LDAP bind TRY with username: 'uid=jduke,ou=Users,dc=ldap,dc=example'&amp;quot;
querybook_web            | [2025-03-27 Thu 20:36:56] - /opt/querybook/querybook/server/app/auth/ldap_auth.py - DEBUG   &amp;quot;LDAP bind SUCCESS with username: 'uid=jduke,ou=Users,dc=ldap,dc=example'&amp;quot;
querybook_worker         | [2025-03-27 20:36:56,786: INFO/MainProcess] Task celery.local.sync_elasticsearch__debouncer[f407a87d-ae47-450b-95b1-c44df07eb1e7] received
querybook_worker         | [2025-03-27 20:36:56,787: DEBUG/MainProcess] TaskPool: Apply &amp;lt;function fast_trace_task at 0x7fb3791b0d30&amp;gt; (args:('celery.local.sync_elasticsearch__debouncer', 'f407a87d-ae47-450b-95b1-c44df07eb1e7', {'lang': 'py', 'task': 'celery.local.sync_elasticsearch__debouncer', 'id': 'f407a87d-ae47-450b-95b1-c44df07eb1e7', 'shadow': None, 'eta': None, 'expires': None, 'group': None, 'group_index': None, 'retries': 0, 'timelimit': [None, None], 'root_id': 'f407a87d-ae47-450b-95b1-c44df07eb1e7', 'parent_id': None, 'argsrepr': &amp;quot;['users', 1]&amp;quot;, 'kwargsrepr': '{}', 'origin': 'gen100@6ede59afb0cf', 'ignore_result': False, 'properties': {'correlation_id': 'f407a87d-ae47-450b-95b1-c44df07eb1e7', 'reply_to': '70ed7e91-4699-31e4-8f50-86d1a850d89a', 'delivery_mode': 2, 'delivery_info': {'exchange': '', 'routing_key': 'celery'}, 'priority': 0, 'body_encoding': 'base64', 'delivery_tag': '2d6aecef-9aea-4564-9bd0-994731c8eba5'}, 'reply_to': '70ed7e91-4699-31e4-8f50-86d1a850d89a', 'correlation_id': 'f407a87d-ae47-450b-95b1-c44df07eb1e7', 'hostname': 'celery@05a545667216', 'delivery_info': {'exchange': '', 'routing_key': 'celery', 'priority': 0,... kwargs:{})
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:56] &amp;quot;POST /ds/login/ HTTP/1.1&amp;quot; 200 655 0.282608
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:56] &amp;quot;GET /ds/user/me/ HTTP/1.1&amp;quot; 200 490 0.011824
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:56] &amp;quot;GET /ds/user/setting/ HTTP/1.1&amp;quot; 200 215 0.020317
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:56] &amp;quot;GET /ds/announcement/ HTTP/1.1&amp;quot; 200 215 0.011983
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:57] &amp;quot;GET /ds/query/transpile/ HTTP/1.1&amp;quot; 200 419 0.017954
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:57] &amp;quot;GET /ds/user/notifiers/ HTTP/1.1&amp;quot; 200 348 0.018368
querybook_web            | 127.0.0.1 - - [2025-03-27 20:36:57] &amp;quot;GET /ds/query_execution_exporter/ HTTP/1.1&amp;quot; 200 215 0.023621
querybook_web            | 127.0.0.1 - - [2025-03-27 20:37:02] &amp;quot;GET /ds/user/environment/ HTTP/1.1&amp;quot; 200 233 5.029392
querybook_web            | 127.0.0.1 - - [2025-03-27 20:37:05] &amp;quot;GET /static/favicon/querybook.svg HTTP/1.1&amp;quot; 200 5544 0.017975
querybook_web            | 127.0.0.1 - - [2025-03-27 20:37:05] &amp;quot;GET /ds/admin/query_metastore/ HTTP/1.1&amp;quot; 200 215 0.010979
querybook_web            | 127.0.0.1 - - [2025-03-27 20:37:05] &amp;quot;GET /ds/admin/environment/ HTTP/1.1&amp;quot; 200 215 0.029958
querybook_web            | 127.0.0.1 - - [2025-03-27 20:37:05] &amp;quot;GET /ds/admin/query_engine/ HTTP/1.1&amp;quot; 200 215 0.017463
querybook_worker         | [2025-03-27 20:37:06,821: INFO/MainProcess] Task celery.local.sync_elasticsearch[185cb78a-0b66-47de-a3db-71e6bea1e53c] received
querybook_worker         | [2025-03-27 20:37:06,821: DEBUG/MainProcess] basic.qos: prefetch_count-&amp;gt;6
querybook_worker         | [2025-03-27 20:37:06,823: INFO/ForkPoolWorker-3] Task celery.local.sync_elasticsearch__debouncer[f407a87d-ae47-450b-95b1-c44df07eb1e7] succeeded in 5.021026143018389s: None
querybook_elasticsearch  | [2025-03-27T20:37:19,071][INFO ][o.o.i.i.ManagedIndexCoordinator] [d8d3bb7a5243] Cancel background move metadata process.
querybook_elasticsearch  | [2025-03-27T20:37:19,073][INFO ][o.o.i.i.ManagedIndexCoordinator] [d8d3bb7a5243] Performing move cluster state metadata.
querybook_elasticsearch  | [2025-03-27T20:37:19,074][INFO ][o.o.i.i.MetadataService  ] [d8d3bb7a5243] Move metadata has finished.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Мистер Дюк теперь может заходить в Querybook&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.40.48.png.jpg" width="2560" height="619" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Тот кто первый входит будет являться админом&lt;/p&gt;
&lt;p&gt;Далее сделал конвектор к Trino и один eviroment, и добавил конектор в него и юзера&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.45.19.png" width="1322" height="1470" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Можно писать запросики&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.47.03.png" width="1744" height="522" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Красивый прогресс&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.47.44.png" width="2260" height="498" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Тоже красиво&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.51.53.png" width="1218" height="1522" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Работает и даже на русском.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-03-27-v-23.54.19.png" width="640" height="1064" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;PS: Откуда взялся Ldap?&lt;/p&gt;
&lt;p&gt;Вот этот под руку попался &lt;a href="https://github.com/intoolswetrust/ldap-server"&gt;https://github.com/intoolswetrust/ldap-server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Он на базе Apache Directory &lt;a href="https://directory.apache.org/apacheds/downloads.html"&gt;https://directory.apache.org/apacheds/downloads.html&lt;/a&gt;&lt;br /&gt;
Сделан как раз для тестов, то что нужно.&lt;/p&gt;
&lt;p&gt;Вот так делаем:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;docker pull kwart/ldap-server
docker run -it --rm kwart/ldap-server&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Потом так:&lt;/p&gt;
&lt;p&gt;../containers/bundled_querybook_config.yaml&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;AUTH_BACKEND: 'app.auth.ldap_auth' # Same as import path when running Python
LDAP_CONN: 'ldap://192.168.215.2:389'
LDAP_USER_DN: 'uid={},ou=Users,dc=ldap,dc=example'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И так&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;LDAP_CONN: &amp;quot;ldap://192.168.215.2:389&amp;quot;   #  без сертификатов, и так сойдет,  но они нужны и корневые особенно для ldaps. 
LDAP_USER_DN: &amp;quot;uid={},ou=Users,dc=ldap,dc=example&amp;quot;
LDAP_UID_FIELD: &amp;quot;uid&amp;quot;
LDAP_EMAIL_FIELD: &amp;quot;uid&amp;quot;  # У меня нет почты в ldap поэтому схитрил, но надо бы добавить атрибут mail в схему LDAP
LDAP_LASTNAME_FIELD: &amp;quot;sn&amp;quot;
LDAP_FIRSTNAME_FIELD: &amp;quot;cn&amp;quot;  # Используйте cn, если givenName отсутствует
LDAP_FULLNAME_FIELD: &amp;quot;cn&amp;quot;
LDAP_BIND_DN: &amp;quot;uid=admin,ou=system&amp;quot;
LDAP_BIND_PASSWORD: &amp;quot;secret&amp;quot;
LDAP_SEARCH_BASE: &amp;quot;dc=ldap,dc=example&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь осталось придумать как всунуть туда hive matastore – это будет чуть сложнее, чем казалось.&lt;br /&gt;
Один всунуть можно, но одного мало, а много лень. Может сделать еще один как все и уже его одного добавлять? пока не знаю 🧐&lt;/p&gt;
</description>
</item>

<item>
<title>План запросов — Анализируем производительность в Trino</title>
<guid isPermaLink="false">166</guid>
<link>https://gavrilov.info/all/plan-zaprosov-analiziruem-proizvoditelnost-v-trino/</link>
<pubDate>Fri, 04 Oct 2024 19:50:00 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/plan-zaprosov-analiziruem-proizvoditelnost-v-trino/</comments>
<description>
&lt;p&gt;Оригинал: &lt;a href="https://medium.com/@simon.thelin90/query-plans-analyse-sql-performance-in-trino-97ac1e8f8044"&gt;https://medium.com/@simon.thelin90/query-plans-analyse-sql-performance-in-trino-97ac1e8f8044&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Или тут: &lt;a href=""&gt;&lt;a href="https://a.gavrilov.info/data/posts/Query%20Plans%20"&gt;https://a.gavrilov.info/data/posts/Query%20Plans%20&lt;/a&gt;—%20Analyse%20SQL%20Performance%20In%20Trino%20%7C%20by%20Simon%20Thelin%20%7C%20Medium.pdf Query Plans — Analyse SQL Performance In Trino&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ещё одно воскресенье и ещё одна #датаболь для обсуждения.&lt;br /&gt;
Сегодня я хочу углубиться в то, как мы можем понять план запроса в Trino.&lt;/p&gt;
&lt;p&gt;Исследование плана запроса&lt;br /&gt;
Порядок выполнения SQL — Давайте вспомним&lt;br /&gt;
Прежде чем мы начнем рассматривать это, давайте вспомним порядок выполнения в SQL-запросе.&lt;br /&gt;
FROM, JOIN&lt;br /&gt;
WHERE&lt;br /&gt;
GROUP BY&lt;br /&gt;
HAVING&lt;br /&gt;
SELECT&lt;br /&gt;
DISTINCT&lt;br /&gt;
ORDER BY&lt;br /&gt;
LIMIT&lt;br /&gt;
Это поможет нам, когда будем читать план запроса.&lt;br /&gt;
Как определить, является ли ваш SQL производительным?⚡&lt;br /&gt;
Прежде чем запрос может быть запланирован, движок также должен:&lt;br /&gt;
Идентифицировать таблицы&lt;br /&gt;
Идентифицировать столбцы, использованные в запросе&lt;br /&gt;
SQL, простой подсчёт названий должностей, где мы группируем по департаменту.&lt;br /&gt;
EXPLAIN ANALYZE WITH&lt;/p&gt;
&lt;p&gt;count_titles AS (&lt;br /&gt;
SELECT&lt;br /&gt;
department,&lt;br /&gt;
COUNT(job_title) AS count_job_titles&lt;br /&gt;
FROM lakehouse.bronze.jobs&lt;br /&gt;
GROUP BY 1&lt;br /&gt;
)&lt;/p&gt;
&lt;p&gt;SELECT * FROM count_titles&lt;br /&gt;
SQL-запрос использует Общую Табличную Выражение (CTE) под именем count_titles для упрощения структуры и улучшения читаемости запроса.&lt;br /&gt;
Он начинается с выбора данных из таблицы lakehouse.bronze.jobs.&lt;br /&gt;
Внутри CTE данные группируются по столбцу department.&lt;br /&gt;
Для каждой группы департаментов подсчитывается количество вхождений job_title и обозначается как count_job_titles.&lt;br /&gt;
После определения CTE основной запрос выбирает все столбцы из count_titles CTE.&lt;br /&gt;
Основная цель запроса — получить количество названий должностей для каждого департамента из таблицы lakehouse.bronze.jobs.&lt;br /&gt;
План запроса 📣&lt;br /&gt;
В очереди: 1.84ms, Анализ: 85.69ms, Планирование: 58.35ms, Выполнение: 450.11ms&lt;br /&gt;
Фрагмент 1 [HASH]&lt;br /&gt;
CPU: 7.57ms, Запланировано: 11.12ms, Заблокировано: 1.80s (Вход: 933.53ms, Выход: 0.00ns), Вход: 8 строк (176B); на задачу: ср.: 4.00, отклонение: 2.00, Выход: 3 строки (67B)&lt;br /&gt;
Количество входных данных, обработанных рабочими для этого этапа, может быть перекошено&lt;br /&gt;
Выходная структура: [department, count]&lt;br /&gt;
Разделение выхода: SINGLE []&lt;br /&gt;
Агрегат[тип = FINAL, ключи = [department]]&lt;br /&gt;
│   Расклад: [department:varchar, count:bigint]&lt;br /&gt;
│   Оценки: {строк: 3 (68B), cpu: 226, память: 68B, сеть: 0B}&lt;br /&gt;
│   CPU: 3.00ms (15.00%), Запланировано: 3.00ms (6.52%), Заблокировано: 0.00ns (0.00%), Выход: 3 строки (67B)&lt;br /&gt;
│   Ср. вход: 1.00 строки, стандартное отклонение входа: 132.29%&lt;br /&gt;
│   count := count(count_0)&lt;br /&gt;
└─ LocalExchange[разделение = HASH, аргументы = [department::varchar]]&lt;br /&gt;
│   Расклад: [department:varchar, count_0:bigint]&lt;br /&gt;
│   Оценки: {строк: 10 (226B), cpu: 226, память: 0B, сеть: 0B}&lt;br /&gt;
│   CPU: 1.00ms (5.00%), Запланировано: 1.00ms (2.17%), Заблокировано: 716.00ms (31.49%), Выход: 8 строк (176B)&lt;br /&gt;
│   Ср. вход: 1.00 строки, стандартное отклонение входа: 86.60%&lt;br /&gt;
└─ RemoteSource[идентификаторы источников = [2]]&lt;br /&gt;
Расклад: [department:varchar, count_0:bigint]&lt;br /&gt;
CPU: 0.00ns (0.00%), Запланировано: 1.00ms (2.17%), Заблокировано: 933.00ms (41.03%), Выход: 8 строк (176B)&lt;br /&gt;
Ср. вход: 1.00 строки, стандартное отклонение входа: 86.60%&lt;/p&gt;
&lt;p&gt;Фрагмент 2 [HASH]&lt;br /&gt;
CPU: 11.24ms, Запланировано: 19.46ms, Заблокировано: 860.54ms (Вход: 444.24ms, Выход: 0.00ns), Вход: 10 строк (362B); на задачу: ср.: 5.00, отклонение: 2.00, Выход: 8 строк (176B)&lt;br /&gt;
Количество входных данных, обработанных рабочими для этого этапа, может быть перекошено&lt;br /&gt;
Выходная структура: [department, count_0]&lt;br /&gt;
Разделение выхода: HASH [department]&lt;br /&gt;
Агрегат[тип = PARTIAL, ключи = [department]]&lt;br /&gt;
│   Расклад: [department:varchar, count_0:bigint]&lt;br /&gt;
│   Оценки: {строк: 10 (226B), cpu: ?, память: ?, сеть: ?}&lt;br /&gt;
│   CPU: 4.00ms (20.00%), Запланировано: 7.00ms (15.22%), Заблокировано: 0.00ns (0.00%), Выход: 8 строк (176B)&lt;br /&gt;
│   Ср. вход: 1.25 строки, стандартное отклонение входа: 118.32%&lt;br /&gt;
│   count_0 := count(job_title)&lt;br /&gt;
└─ Агрегат[тип = FINAL, ключи = [department, job_title]]&lt;br /&gt;
│   Расклад: [department:varchar, job_title:varchar]&lt;br /&gt;
│   Оценки: {строк: 10 (362B), cpu: 362, память: 362B, сеть: 0B}&lt;br /&gt;
│   CPU: 1.00ms (5.00%), Запланировано: 2.00ms (4.35%), Заблокировано: 0.00ns (0.00%), Выход: 10 строк (362B)&lt;br /&gt;
│   Ср. вход: 1.25 строки, стандартное отклонение входа: 118.32%&lt;br /&gt;
└─ LocalExchange[разделение = HASH, аргументы = [department::varchar, job_title::varchar]]&lt;br /&gt;
│   Расклад: [department:varchar, job_title:varchar]&lt;br /&gt;
│   Оценки: {строк: 10 (362B), cpu: 362, память: 0B, сеть: 0B}&lt;br /&gt;
│   CPU: 0.00ns (0.00%), Запланировано: 0.00ns (0.00%), Заблокировано: 181.00ms (7.96%), Выход: 10 строк (362B)&lt;br /&gt;
│   Ср. вход: 1.25 строки, стандартное отклонение входа: 190.79%&lt;br /&gt;
└─ RemoteSource[идентификаторы источников = [3]]&lt;br /&gt;
Расклад: [department:varchar, job_title:varchar]&lt;br /&gt;
CPU: 0.00ns (0.00%), Запланировано: 0.00ns (0.00%), Заблокировано: 444.00ms (19.53%), Выход: 10 строк (362B)&lt;br /&gt;
Ср. вход: 1.25 строки, стандартное отклонение входа: 190.79%&lt;/p&gt;
&lt;p&gt;Фрагмент 3 [SOURCE]&lt;br /&gt;
CPU: 11.49ms, Запланировано: 32.68ms, Заблокировано: 0.00ns (Вход: 0.00ns, Выход: 0.00ns), Вход: 10 строк (382B); на задачу: ср.: 10.00, std.dev.: 0.00, Выход: 10 строк (362B)&lt;br /&gt;
Выходная структура: [department, job_title]&lt;br /&gt;
Разделение выхода: HASH [department, job_title]&lt;br /&gt;
Агрегат[тип = PARTIAL, ключи = [department, job_title]]&lt;br /&gt;
│   Расклад: [department:varchar, job_title:varchar]&lt;br /&gt;
│   Оценки: {строк: 10 (362B), cpu: ?, память: ?, сеть: ?}&lt;br /&gt;
│   CPU: 1.00ms (5.00%), Запланировано: 3.00ms (6.52%), Заблокировано: 0.00ns (0.00%), Выход: 10 строк (362B)&lt;br /&gt;
│   Ср. вход: 10.00 строк, std.dev.: 0.00%&lt;br /&gt;
└─ Сканирование таблицы[таблица = lakehouse:bronze.jobs]&lt;br /&gt;
Расклад: [job_title:varchar, department:varchar]&lt;br /&gt;
Оценки: {строк: 10 (362B), cpu: 362, память: 0B, сеть: 0B}&lt;br /&gt;
CPU: 10.00ms (50.00%), Запланировано: 29.00ms (63.04%), Заблокировано: 0.00ns (0.00%), Выход: 10 строк (382B)&lt;br /&gt;
Ср. вход: 10.00 строк, std.dev.: 0.00%&lt;br /&gt;
job_title := job_title:varchar:ОБЫЧНО&lt;br /&gt;
department := department:varchar:ОБЫЧНО&lt;br /&gt;
Вход: 10 строк (382B), Физический вход: 1.32kB, Время физического входа: 7.50ms&lt;br /&gt;
Общая информация о выводе и плане запроса&lt;/p&gt;
&lt;p&gt;Высокоуровневый вывод выполнения.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/plan-zaprosov-analiziruem-proizvoditelnost-v-trino.png" width="1174" height="832" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;В очереди: 1.84ms&lt;br /&gt;
Анализ: 85.69ms&lt;br /&gt;
Планирование: 58.35ms&lt;br /&gt;
Выполнение: 467.68ms&lt;br /&gt;
Разделение на фрагменты&lt;br /&gt;
Выполнение запроса делится на три главные фрагмента. Каждый фрагмент представляет этап в процессе выполнения.&lt;br /&gt;
Фрагмент 1 [HASH]&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/plan-zaprosov-analiziruem-proizvoditelnost-v-trino-1.png" width="742" height="1042" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;HASH&lt;br /&gt;
Роль: Этот фрагмент обрабатывает окончательную агрегацию результатов.&lt;br /&gt;
Производительность:&lt;br /&gt;
CPU: 7.57ms&lt;br /&gt;
Запланировано: 11.12ms&lt;br /&gt;
Заблокировано: 1.80s (главным образом ожидание данных от других фрагментов)&lt;br /&gt;
Вход/Выход:&lt;br /&gt;
Вход: 8 строк (176B)&lt;br /&gt;
Выход: 3 строки (67B)&lt;br /&gt;
Выходная структура: [department, count]&lt;br /&gt;
Операции:&lt;br /&gt;
Агрегация: Окончательная агрегация по департаменту для вычисления общего количества названий должностей.&lt;br /&gt;
Локальный обмен: Перераспределение данных по департаменту для подготовки к окончательной агрегации.&lt;br /&gt;
Фрагмент 2 [HASH]&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/plan-zaprosov-analiziruem-proizvoditelnost-v-trino-2.png" width="632" height="1032" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Роль: Этот фрагмент выполняет частичную агрегацию названий должностей по департаменту.&lt;br /&gt;
Производительность:&lt;br /&gt;
CPU: 11.24ms&lt;br /&gt;
Запланировано: 19.46ms&lt;br /&gt;
Заблокировано: 860.54ms&lt;br /&gt;
Вход/Выход:&lt;br /&gt;
Вход: 10 строк (362B)&lt;br /&gt;
Выход: 8 строк (176B)&lt;br /&gt;
Выходная структура: [department, count_0]&lt;br /&gt;
Операции:&lt;br /&gt;
Частичная агрегация: Подсчитывает названия должностей по департаменту.&lt;br /&gt;
Локальный обмен: Перераспределение данных по департаменту, job_title для дальнейшей агрегации.&lt;br /&gt;
Фрагмент 3 [SOURCE]&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/plan-zaprosov-analiziruem-proizvoditelnost-v-trino-3.png" width="876" height="1084" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;SOURCE — Сканирование таблицы&lt;br /&gt;
Роль: Этот фрагмент читает данные из исходной таблицы.&lt;br /&gt;
Производительность:&lt;br /&gt;
CPU: 11.49ms&lt;br /&gt;
Запланировано: 32.68ms&lt;br /&gt;
Вход/Выход:&lt;br /&gt;
Вход: 10 строк (382B)&lt;br /&gt;
Выход: 10 строк (362B)&lt;br /&gt;
Выходная структура: [department, job_title]&lt;/p&gt;
&lt;p&gt;Операции:&lt;br /&gt;
Сканирование таблицы: Чтение столбцов department и job_title из таблицы lakehouse.bronze.jobs.&lt;br /&gt;
Частичная агрегация: Группировка данных по департаменту и job_title и подготовка их к дальнейшей обработке.&lt;/p&gt;
&lt;p&gt;Ключевые выводы&lt;br /&gt;
Чтение плана запроса: Этапы представлены в порядке от наименьшего значения этапа к последнему шагу, и от наибольшего значения этапа к первому шагу. Это также связывается с порядком выполнения, который мы упомянули выше.&lt;/p&gt;
&lt;p&gt;Время блокировки: Значительное время блокировки указывает на ожидание передачи данных между фрагментами, особенно в Фрагменте 1 и Фрагменте 2. Это часто является признаком задержек передачи данных или неравенства во времени обработки между фрагментами.&lt;br /&gt;
Перекос данных: Обратите внимание на перекос в количестве данных, обрабатываемых рабочими на различных этапах, что приводит к тому, что некоторые рабочие обрабатывают больше данных, чем другие. Это может вызывать неэффективность.&lt;br /&gt;
Шаги агрегации: Запрос включает несколько этапов частичных и окончательных агрегаций, которые можно оптимизировать, если возможно, уменьшив переброс данных между фрагментами.&lt;br /&gt;
CPU и планирование: Время работы CPU и планирования относительно низкое по сравнению с заблокированным временем, что предполагает, что ресурсы CPU не являются узким местом.&lt;br /&gt;
Настройка SQL: Измените ваш SQL и изучите план запроса, чтобы найти правильный баланс между этими факторами.&lt;/p&gt;
&lt;p&gt;Предложения по оптимизации&lt;br /&gt;
Модель данных: Убедитесь, что у вас есть правильная модель данных, например, звездная схема, и выполните расчет OBT на основе звездной схемы.&lt;br /&gt;
Материализованное представление: Предварительно посчитайте частые агрегации. Однако я бы предпочел предыдущий шаг этому, это может быть полезно, если у вас нет много времени или вы работаете над первым шагом параллельно, но будьте осторожны, чтобы избежать большого технического долга.&lt;br /&gt;
Упростите агрегации: Минимизируйте сложные шаги агрегации, чтобы уменьшить переброс данных.&lt;/p&gt;
</description>
</item>

<item>
<title>Introduction to Bash and SQL, Git and Docker</title>
<guid isPermaLink="false">152</guid>
<link>https://gavrilov.info/all/introduction-to-bash-and-sql/</link>
<pubDate>Sun, 28 Jul 2024 22:23:10 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/introduction-to-bash-and-sql/</comments>
<description>
&lt;p&gt;Хорошие книги от Bobby Iliev &lt;a href="https://bobbyiliev.com/"&gt;https://bobbyiliev.com/&lt;/a&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/introduction-to-bash-and-sql.png" width="1410" height="2250" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/introduction-to-bash-and-sql-1.png" width="1410" height="2250" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-bash-scripting-dark.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-bash-scripting-dark.pdf&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-bash-scripting-light.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-bash-scripting-light.pdf&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-sql-dark.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-sql-dark.pdf&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-sql-light.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-sql-light.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/bobbyiliev/introduction-to-bash-scripting"&gt;https://github.com/bobbyiliev/introduction-to-bash-scripting&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://github.com/bobbyiliev/introduction-to-sql"&gt;https://github.com/bobbyiliev/introduction-to-sql&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;и есть еще про гит&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/introduction-to-bash-and-sql-2.png" width="1410" height="2250" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-git-and-github-dark.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-git-and-github-dark.pdf&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-git-and-github-light.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-git-and-github-light.pdf&lt;/a&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/introduction-to-bash-and-sql-3.png" width="2240" height="1260" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-docker-dark.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-docker-dark.pdf&lt;/a&gt;&lt;br /&gt;
&lt;a href="https://a.gavrilov.info/data/posts/introduction-to-docker-light.pdf"&gt;https://a.gavrilov.info/data/posts/introduction-to-docker-light.pdf&lt;/a&gt;&lt;/p&gt;
</description>
</item>

<item>
<title>Загрузка данных iceberg в Trino</title>
<guid isPermaLink="false">47</guid>
<link>https://gavrilov.info/all/zagruzka-dannyh-iceberg-v-trino/</link>
<pubDate>Tue, 28 Feb 2023 20:30:19 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/zagruzka-dannyh-iceberg-v-trino/</comments>
<description>
&lt;h2&gt;Создание схем&lt;/h2&gt;
&lt;p&gt;CREATE SCHEMA delta.my_schema_d&lt;br /&gt;
WITH (location = ‘s3a://test/delta_p’);&lt;/p&gt;
&lt;p&gt;CREATE SCHEMA hive.my_schema_h&lt;br /&gt;
WITH (location = ‘s3a://test/hive_p’);&lt;/p&gt;
&lt;p&gt;CREATE SCHEMA iceberg.my_schema_i&lt;br /&gt;
WITH (location = ‘s3a://test/iceberg_p’);&lt;/p&gt;
&lt;h2&gt;Создание таблицы на базе CSV&lt;/h2&gt;
&lt;p&gt;create table hive.my_schema_h.taxi_p (&lt;br /&gt;
VendorID bigint,&lt;br /&gt;
tpep_pickup_datetime varchar,&lt;br /&gt;
tpep_dropoff_datetime varchar,&lt;br /&gt;
passenger_count bigint,&lt;br /&gt;
trip_distance decimal(20,2),&lt;br /&gt;
RatecodeID bigint,&lt;br /&gt;
store_and_fwd_flag varchar,&lt;br /&gt;
PULocationID bigint,&lt;br /&gt;
DOLocationID bigint,&lt;br /&gt;
payment_type bigint,&lt;br /&gt;
fare_amount decimal(20,2),&lt;br /&gt;
extra decimal(20,2),&lt;br /&gt;
mta_tax decimal(20,2),&lt;br /&gt;
tip_amount decimal(20,2),&lt;br /&gt;
tolls_amount decimal(20,2),&lt;br /&gt;
improvement_surcharge decimal(20,2),&lt;br /&gt;
total_amount decimal(20,2))&lt;br /&gt;
WITH (&lt;br /&gt;
format = ‘TEXTFILE’,&lt;br /&gt;
textfile_field_separator = ‘,’,&lt;br /&gt;
skip_header_line_count=1&lt;br /&gt;
);&lt;/p&gt;
&lt;h2&gt;Создание таблицы iceberg&lt;/h2&gt;
&lt;p&gt;create table iceberg.my_schema_i.taxi_i (&lt;br /&gt;
VendorID bigint,&lt;br /&gt;
tpep_pickup_datetime varchar,&lt;br /&gt;
tpep_dropoff_datetime varchar,&lt;br /&gt;
passenger_count bigint,&lt;br /&gt;
trip_distance decimal(20,2),&lt;br /&gt;
RatecodeID bigint,&lt;br /&gt;
store_and_fwd_flag varchar,&lt;br /&gt;
PULocationID bigint,&lt;br /&gt;
DOLocationID bigint,&lt;br /&gt;
payment_type bigint,&lt;br /&gt;
fare_amount decimal(20,2),&lt;br /&gt;
extra decimal(20,2),&lt;br /&gt;
mta_tax decimal(20,2),&lt;br /&gt;
tip_amount decimal(20,2),&lt;br /&gt;
tolls_amount decimal(20,2),&lt;br /&gt;
improvement_surcharge decimal(20,2),&lt;br /&gt;
total_amount decimal(20,2))&lt;/p&gt;
&lt;h2&gt;Запись таблицы iceberg&lt;/h2&gt;
&lt;p&gt;insert into iceberg.my_schema_i.taxi_i select * from hive.my_schema_h.taxi_p&lt;/p&gt;
</description>
</item>

<item>
<title>Классификация в Trino</title>
<guid isPermaLink="false">46</guid>
<link>https://gavrilov.info/all/klassifikaciya-v-trino/</link>
<pubDate>Tue, 28 Feb 2023 20:25:27 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/klassifikaciya-v-trino/</comments>
<description>
&lt;p&gt;CREATE SCHEMA delta.my_schema_d&lt;br /&gt;
WITH (location = ‘s3a://test/delta_p’);&lt;/p&gt;
&lt;p&gt;CREATE TABLE delta.my_schema_d.iris (&lt;br /&gt;
id int&lt;br /&gt;
, sepal_length double&lt;br /&gt;
, sepal_width double&lt;br /&gt;
, petal_length double&lt;br /&gt;
, petal_width double&lt;br /&gt;
, species varchar&lt;br /&gt;
);&lt;/p&gt;
&lt;p&gt;INSERT INTO delta.my_schema_d.iris VALUES&lt;br /&gt;
(1, 5.1, 3.5, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(2, 4.9, 3, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(3, 4.7, 3.2, 1.3, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(4, 4.6, 3.1, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(5, 5, 3.6, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(6, 5.4, 3.9, 1.7, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(7, 4.6, 3.4, 1.4, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(8, 5, 3.4, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(9, 4.4, 2.9, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(10, 4.9, 3.1, 1.5, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(11, 5.4, 3.7, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(12, 4.8, 3.4, 1.6, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(13, 4.8, 3, 1.4, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(14, 4.3, 3, 1.1, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(15, 5.8, 4, 1.2, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(16, 5.7, 4.4, 1.5, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(17, 5.4, 3.9, 1.3, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(18, 5.1, 3.5, 1.4, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(19, 5.7, 3.8, 1.7, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(20, 5.1, 3.8, 1.5, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(21, 5.4, 3.4, 1.7, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(22, 5.1, 3.7, 1.5, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(23, 4.6, 3.6, 1, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(24, 5.1, 3.3, 1.7, 0.5, ‘Iris-setosa’),&lt;br /&gt;
(25, 4.8, 3.4, 1.9, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(26, 5, 3, 1.6, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(27, 5, 3.4, 1.6, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(28, 5.2, 3.5, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(29, 5.2, 3.4, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(30, 4.7, 3.2, 1.6, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(31, 4.8, 3.1, 1.6, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(32, 5.4, 3.4, 1.5, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(33, 5.2, 4.1, 1.5, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(34, 5.5, 4.2, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(35, 4.9, 3.1, 1.5, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(36, 5, 3.2, 1.2, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(37, 5.5, 3.5, 1.3, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(38, 4.9, 3.1, 1.5, 0.1, ‘Iris-setosa’),&lt;br /&gt;
(39, 4.4, 3, 1.3, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(40, 5.1, 3.4, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(41, 5, 3.5, 1.3, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(42, 4.5, 2.3, 1.3, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(43, 4.4, 3.2, 1.3, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(44, 5, 3.5, 1.6, 0.6, ‘Iris-setosa’),&lt;br /&gt;
(45, 5.1, 3.8, 1.9, 0.4, ‘Iris-setosa’),&lt;br /&gt;
(46, 4.8, 3, 1.4, 0.3, ‘Iris-setosa’),&lt;br /&gt;
(47, 5.1, 3.8, 1.6, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(48, 4.6, 3.2, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(49, 5.3, 3.7, 1.5, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(50, 5, 3.3, 1.4, 0.2, ‘Iris-setosa’),&lt;br /&gt;
(51, 7, 3.2, 4.7, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(52, 6.4, 3.2, 4.5, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(53, 6.9, 3.1, 4.9, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(54, 5.5, 2.3, 4, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(55, 6.5, 2.8, 4.6, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(56, 5.7, 2.8, 4.5, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(57, 6.3, 3.3, 4.7, 1.6, ‘Iris-versicolor’),&lt;br /&gt;
(58, 4.9, 2.4, 3.3, 1, ‘Iris-versicolor’),&lt;br /&gt;
(59, 6.6, 2.9, 4.6, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(60, 5.2, 2.7, 3.9, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(61, 5, 2, 3.5, 1, ‘Iris-versicolor’),&lt;br /&gt;
(62, 5.9, 3, 4.2, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(63, 6, 2.2, 4, 1, ‘Iris-versicolor’),&lt;br /&gt;
(64, 6.1, 2.9, 4.7, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(65, 5.6, 2.9, 3.6, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(66, 6.7, 3.1, 4.4, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(67, 5.6, 3, 4.5, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(68, 5.8, 2.7, 4.1, 1, ‘Iris-versicolor’),&lt;br /&gt;
(69, 6.2, 2.2, 4.5, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(70, 5.6, 2.5, 3.9, 1.1, ‘Iris-versicolor’),&lt;br /&gt;
(71, 5.9, 3.2, 4.8, 1.8, ‘Iris-versicolor’),&lt;br /&gt;
(72, 6.1, 2.8, 4, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(73, 6.3, 2.5, 4.9, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(74, 6.1, 2.8, 4.7, 1.2, ‘Iris-versicolor’),&lt;br /&gt;
(75, 6.4, 2.9, 4.3, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(76, 6.6, 3, 4.4, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(77, 6.8, 2.8, 4.8, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(78, 6.7, 3, 5, 1.7, ‘Iris-versicolor’),&lt;br /&gt;
(79, 6, 2.9, 4.5, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(80, 5.7, 2.6, 3.5, 1, ‘Iris-versicolor’),&lt;br /&gt;
(81, 5.5, 2.4, 3.8, 1.1, ‘Iris-versicolor’),&lt;br /&gt;
(82, 5.5, 2.4, 3.7, 1, ‘Iris-versicolor’),&lt;br /&gt;
(83, 5.8, 2.7, 3.9, 1.2, ‘Iris-versicolor’),&lt;br /&gt;
(84, 6, 2.7, 5.1, 1.6, ‘Iris-versicolor’),&lt;br /&gt;
(85, 5.4, 3, 4.5, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(86, 6, 3.4, 4.5, 1.6, ‘Iris-versicolor’),&lt;br /&gt;
(87, 6.7, 3.1, 4.7, 1.5, ‘Iris-versicolor’),&lt;br /&gt;
(88, 6.3, 2.3, 4.4, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(89, 5.6, 3, 4.1, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(90, 5.5, 2.5, 4, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(91, 5.5, 2.6, 4.4, 1.2, ‘Iris-versicolor’),&lt;br /&gt;
(92, 6.1, 3, 4.6, 1.4, ‘Iris-versicolor’),&lt;br /&gt;
(93, 5.8, 2.6, 4, 1.2, ‘Iris-versicolor’),&lt;br /&gt;
(94, 5, 2.3, 3.3, 1, ‘Iris-versicolor’),&lt;br /&gt;
(95, 5.6, 2.7, 4.2, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(96, 5.7, 3, 4.2, 1.2, ‘Iris-versicolor’),&lt;br /&gt;
(97, 5.7, 2.9, 4.2, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(98, 6.2, 2.9, 4.3, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(99, 5.1, 2.5, 3, 1.1, ‘Iris-versicolor’),&lt;br /&gt;
(100, 5.7, 2.8, 4.1, 1.3, ‘Iris-versicolor’),&lt;br /&gt;
(101, 6.3, 3.3, 6, 2.5, ‘Iris-virginica’),&lt;br /&gt;
(102, 5.8, 2.7, 5.1, 1.9, ‘Iris-virginica’),&lt;br /&gt;
(103, 7.1, 3, 5.9, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(104, 6.3, 2.9, 5.6, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(105, 6.5, 3, 5.8, 2.2, ‘Iris-virginica’),&lt;br /&gt;
(106, 7.6, 3, 6.6, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(107, 4.9, 2.5, 4.5, 1.7, ‘Iris-virginica’),&lt;br /&gt;
(108, 7.3, 2.9, 6.3, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(109, 6.7, 2.5, 5.8, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(110, 7.2, 3.6, 6.1, 2.5, ‘Iris-virginica’),&lt;br /&gt;
(111, 6.5, 3.2, 5.1, 2, ‘Iris-virginica’),&lt;br /&gt;
(112, 6.4, 2.7, 5.3, 1.9, ‘Iris-virginica’),&lt;br /&gt;
(113, 6.8, 3, 5.5, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(114, 5.7, 2.5, 5, 2, ‘Iris-virginica’),&lt;br /&gt;
(115, 5.8, 2.8, 5.1, 2.4, ‘Iris-virginica’),&lt;br /&gt;
(116, 6.4, 3.2, 5.3, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(117, 6.5, 3, 5.5, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(118, 7.7, 3.8, 6.7, 2.2, ‘Iris-virginica’),&lt;br /&gt;
(119, 7.7, 2.6, 6.9, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(120, 6, 2.2, 5, 1.5, ‘Iris-virginica’),&lt;br /&gt;
(121, 6.9, 3.2, 5.7, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(122, 5.6, 2.8, 4.9, 2, ‘Iris-virginica’),&lt;br /&gt;
(123, 7.7, 2.8, 6.7, 2, ‘Iris-virginica’),&lt;br /&gt;
(124, 6.3, 2.7, 4.9, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(125, 6.7, 3.3, 5.7, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(126, 7.2, 3.2, 6, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(127, 6.2, 2.8, 4.8, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(128, 6.1, 3, 4.9, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(129, 6.4, 2.8, 5.6, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(130, 7.2, 3, 5.8, 1.6, ‘Iris-virginica’),&lt;br /&gt;
(131, 7.4, 2.8, 6.1, 1.9, ‘Iris-virginica’),&lt;br /&gt;
(132, 7.9, 3.8, 6.4, 2, ‘Iris-virginica’),&lt;br /&gt;
(133, 6.4, 2.8, 5.6, 2.2, ‘Iris-virginica’),&lt;br /&gt;
(134, 6.3, 2.8, 5.1, 1.5, ‘Iris-virginica’),&lt;br /&gt;
(135, 6.1, 2.6, 5.6, 1.4, ‘Iris-virginica’),&lt;br /&gt;
(136, 7.7, 3, 6.1, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(137, 6.3, 3.4, 5.6, 2.4, ‘Iris-virginica’),&lt;br /&gt;
(138, 6.4, 3.1, 5.5, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(139, 6, 3, 4.8, 1.8, ‘Iris-virginica’),&lt;br /&gt;
(140, 6.9, 3.1, 5.4, 2.1, ‘Iris-virginica’),&lt;br /&gt;
(141, 6.7, 3.1, 5.6, 2.4, ‘Iris-virginica’),&lt;br /&gt;
(142, 6.9, 3.1, 5.1, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(143, 5.8, 2.7, 5.1, 1.9, ‘Iris-virginica’),&lt;br /&gt;
(144, 6.8, 3.2, 5.9, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(145, 6.7, 3.3, 5.7, 2.5, ‘Iris-virginica’),&lt;br /&gt;
(146, 6.7, 3, 5.2, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(147, 6.3, 2.5, 5, 1.9, ‘Iris-virginica’),&lt;br /&gt;
(148, 6.5, 3, 5.2, 2, ‘Iris-virginica’),&lt;br /&gt;
(149, 6.2, 3.4, 5.4, 2.3, ‘Iris-virginica’),&lt;br /&gt;
(150, 5.9, 3, 5.1, 1.8, ‘Iris-virginica’);&lt;/p&gt;
&lt;p&gt;select classify(features(5.8, 3, 5.1, 1.8), model) AS prediction&lt;br /&gt;
FROM (&lt;br /&gt;
SELECT learn_classifier(species, features(sepal_length, sepal_width, petal_length, petal_width)) AS model FROM delta.my_schema_d.iris&lt;br /&gt;
) t;&lt;/p&gt;
&lt;p&gt;Iris-virginica&lt;/p&gt;
</description>
</item>


</channel>
</rss>