> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 创建你的第一个 materialized view

> 了解如何在 ClickHouse 中使用 materialized view，以不同的排序顺序预先计算并存储查询结果，从而支持对主键未覆盖列进行快速查找。

export const e_1 = undefined

export const e_0 = undefined

<a href="/docs/get-started/quickstarts/home" onClick={(e_0) => { e_0.preventDefault(); window.location.href = (window.location.pathname.startsWith('/docs') ? '/docs' : '') + '/get-started/quickstarts/home'; }} className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-zinc-500 hover:text-gray-900 dark:hover:text-[#fdff75] transition-colors font-normal no-underline"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>All quickstarts</a>

<div className="mt-2 flex flex-wrap gap-2">
  <Badge size="lg" color="blue">实时分析</Badge>
  <Badge size="lg" color="blue">数据仓储</Badge>
  <Badge size="lg" color="orange">Cloud</Badge>
</div>

<div id="prerequisites">
  ## 前置条件
</div>

To successfully follow this guide, you'll need the following:

* A running ClickHouse Cloud service. If you don't have one yet, complete the [ClickHouse Cloud quick start](/docs/get-started/setup/cloud) first.

你还应完成[创建你的第一个 MergeTree 表](/docs/zh/get-started/quickstarts/create-your-first-mergetree-table)快速入门，因为本指南会直接基于其中创建的 `uk_price_paid` 表展开。

<div id="what-youll-build">
  ## 你将构建的内容
</div>

在 MergeTree 快速入门中，你已经看到，按 `town` 或 `county` 查询 `uk_price_paid` 时需要全表扫描，因为该表按 `(postcode, addr1, addr2)` 排序。
在本快速入门中，你将通过创建一个按 `(town, date)` 排序、存储相同数据的 **materialized view** 来解决这个问题，从而在不更改原始表的情况下实现按 town 的快速查找。
完成后，你将了解 materialized view 如何作为插入触发器工作、如何回填现有数据，以及将数据存储两次带来的磁盘空间权衡。

<Steps titleSize="h3">
  <Step title="理解为什么需要 materialized view" id="understand-why-you-need-a-materialized-view">
    你的 `uk_price_paid` 表按 `(postcode, addr1, addr2)` 排序。这意味着，当你按 `postcode`、`addr1` 或 `addr2` 过滤时，ClickHouse 可以跳过大量数据块；但按 `town` 过滤的查询则必须扫描每一行——整整 3000 万行。

    你可以再创建一张使用不同 `ORDER BY` 的表，但这样一来，每次有新数据到达时，你都得记得同时向两张表插入数据。**materialized view** 可以将这个过程自动化：它会监视源表中的插入操作，对这些行进行转换，并自动将结果写入目标表。

    可以把 materialized view 看作一种**插入触发器**——每当有行被插入源表时，MV 的 `SELECT` 查询都会针对新插入的数据块运行，并将结果插入目标表。
  </Step>

  <Step title="创建目标表" id="create-the-destination-table">
    materialized view 需要一个地方来存储其输出。这只是一个普通的 MergeTree 表——你可以完全控制它的 schema、`ORDER BY` 和 `PARTITION BY`。

    创建一个按 `(town, date)` 排序、只包含按 town 查询所需列的表：

    ```sql theme={null}
    CREATE TABLE uk_price_paid_by_town
    (
        town       LowCardinality(String),
        date       Date,
        price      UInt32,
        type       Enum8('terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'other' = 0)
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(date)
    ORDER BY (town, date);
    ```

    这个表并没有什么特别之处——它就是一个标准的 MergeTree 表。你接下来要创建的 materialized view 只是将数据写入其中。

    确认该表已创建：

    ```sql theme={null}
    SHOW CREATE TABLE uk_price_paid_by_town;
    ```
  </Step>

  <Step title="创建 materialized view" id="create-the-materialized-view">
    现在创建一个 materialized view，将源表 (`uk_price_paid`) 连接到目标表 (`uk_price_paid_by_town`) ：

    ```sql theme={null}
    CREATE MATERIALIZED VIEW uk_price_paid_by_town_mv
    TO uk_price_paid_by_town
    AS SELECT
        town,
        date,
        price,
        type
    FROM uk_price_paid;
    ```

    `TO uk_price_paid_by_town` 子句会告诉 ClickHouse 将 `SELECT` 的输出写入目标表。从现在起，每当有行插入到 `uk_price_paid` 时，这个 MV 都会触发，并将转换后的行插入到 `uk_price_paid_by_town`。

    这里有一个重要的注意事项：materialized views 只会在 **inserts** 时触发。如果你删除或更新源表中的行，目标表并不会感知到——MV 不会与删除或更新保持同步。如果你需要这种同步机制，请考虑改用 [projections](/docs/zh/reference/statements/alter/projection)。
  </Step>

  <Step title="回填现有数据" id="backfill-existing-data">
    materialized view 只会处理*后续的*插入操作。`uk_price_paid` 中现有的 3000 万行是在 MV 创建之前插入的，因此目标表目前还是空的。

    请手动回填：

    ```sql theme={null}
    INSERT INTO uk_price_paid_by_town
    SELECT
        town,
        date,
        price,
        type
    FROM uk_price_paid;
    ```

    这会直接插入目标表中 - 此步骤不会经过 MV。完成后，验证行数是否一致：

    ```sql theme={null}
    SELECT
        'uk_price_paid' AS table,
        count() AS rows
    FROM uk_price_paid
    UNION ALL
    SELECT
        'uk_price_paid_by_town' AS table,
        count() AS rows
    FROM uk_price_paid_by_town;
    ```

    两个表的行数应当相同。
  </Step>

  <Step title="查询 materialized view 的目标表" id="query-the-materialized-view-destination-table">
    现在，在目标表上运行一个按 `town` 过滤的查询，并将其与直接查询源表的结果进行比较。

    首先，查询源表：

    ```sql theme={null}
    SELECT
        toYear(date) AS year,
        round(avg(price)) AS avg_price,
        count() AS sales
    FROM uk_price_paid
    WHERE town = 'LONDON'
    GROUP BY year
    ORDER BY year DESC;
    ```

    查看查询统计信息——由于 `town` 不在源表的 `ORDER BY` 中，因此会读取全部 3000 万行。

    现在在 materialized view 的目标表上运行相同的查询：

    ```sql theme={null}
    SELECT
        toYear(date) AS year,
        round(avg(price)) AS avg_price,
        count() AS sales
    FROM uk_price_paid_by_town
    WHERE town = 'LONDON'
    GROUP BY year
    ORDER BY year DESC;
    ```

    再次查看查询统计信息——读取的行数明显更少，因为目标表按 `(town, date)` 排序，ClickHouse 可以跳过所有与 `LONDON` 不匹配的数据。

    运行 `SHOW TABLES`，查看已创建的内容：

    ```sql theme={null}
    SHOW TABLES;
    ```

    你会同时看到 `uk_price_paid_by_town` (目标表) 和 `uk_price_paid_by_town_mv` (视图) 。由于你使用了 `CREATE MATERIALIZED VIEW ... TO`，因此可以自行控制目标表的名称。如果省略 `TO` 子句，ClickHouse 会创建一个使用隐式名称的目标表 (`.inner.xxx`) ，这会让直接操作变得更困难。
    因此，建议在创建 materialized view 时使用 `TO` 子句。
  </Step>

  <Step title="观察数据被存储了两遍" id="observe-the-data-is-stored-twice">
    materialized views 以占用额外磁盘空间为代价，换来更快的读取速度。查询 `system.parts`，查看每个表占用了多少空间：

    ```sql theme={null}
    SELECT
        table,
        count() AS parts,
        sum(rows) AS total_rows,
        formatReadableSize(sum(bytes_on_disk)) AS compressed_size
    FROM system.parts
    WHERE table IN ('uk_price_paid', 'uk_price_paid_by_town')
      AND active = true
    GROUP BY table;
    ```

    数据在物理上会存储两次——一次存储在按 `(postcode, addr1, addr2)` 排序的 `uk_price_paid` 中，另一次存储在按 `(town, date)` 排序的 `uk_price_paid_by_town` 中。这就是最根本的权衡：你需要用更多磁盘空间，来换取针对不同访问模式时更快的读取性能。

    目标端表在磁盘上可能会更小，因为它包含的列更少，而且 `(town, date)` 这种排序方式的压缩效果可能与原始表不同。
  </Step>
</Steps>

<div id="next-steps">
  ## 后续步骤
</div>

在本快速入门中，你创建了一个 materialized view，以不同的排序顺序存储英国房产交易数据，因此无需修改原始表，就能按城镇快速查找。你还了解到，MV 会充当插入触发器，现有数据必须手动回填，而代价是需要额外的磁盘空间。

接下来可以查看以下快速入门：

* [创建你的第一个 projection](/docs/zh/get-started/quickstarts/create-your-first-projection)

或者通过参考文档进一步深入了解：

* [materialized view 参考](/docs/zh/reference/statements/create/view#materialized-view)
* [增量 materialized view](/docs/zh/concepts/features/materialized-views/incremental-materialized-view)
* [Projections](/docs/zh/reference/statements/alter/projection)

<Frame caption="Check out the ClickHouse academy for on-demand and live training">
  <a href="https://learn.clickhouse.com/" target="_blank">
    <img src="https://mintcdn.com/private-7c7dfe99/EDr8ydtGBgFPOQea/images/academy.webp?fit=max&auto=format&n=EDr8ydtGBgFPOQea&q=85&s=27e92fc656183cc2f176211907a7aa49" alt="ClickHouse Academy — Master ClickHouse with expert-designed training for every skill level" width="560" noZoom data-path="images/academy.webp" />
  </a>
</Frame>

<div className="mt-8">
  <a href="/docs/get-started/quickstarts/home" onClick={(e_1) => { e_1.preventDefault(); window.location.href = (window.location.pathname.startsWith('/docs') ? '/docs' : '') + '/get-started/quickstarts/home'; }} className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-zinc-500 hover:text-gray-900 dark:hover:text-[#fdff75] transition-colors font-normal no-underline"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>All quickstarts</a>
</div>
