> ## 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.

# Proyecciones

> Página que describe qué son las proyecciones, cómo pueden usarse para mejorar el rendimiento de las consultas y en qué se diferencian de las vistas materializadas.

export const RunnableCode = ({children, run = false, showStats = true}) => {
  const [results, setResults] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
  const [showResults, setShowResults] = useState(false);
  const [stats, setStats] = useState(null);
  const [isDark, setIsDark] = useState(false);
  const [hoveredRow, setHoveredRow] = useState(-1);
  const codeRef = useRef(null);
  useEffect(() => {
    if (typeof window !== "undefined") {
      const check = () => setIsDark(document.documentElement.classList.contains("dark"));
      check();
      const observer = new MutationObserver(check);
      observer.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ["class"]
      });
      return () => observer.disconnect();
    }
  }, []);
  useEffect(() => {
    if (codeRef.current) {
      const block = codeRef.current.querySelector(".code-block");
      if (block) {
        block.style.marginBottom = "0";
        block.style.marginTop = "0";
        block.style.borderBottomLeftRadius = "0";
        block.style.borderBottomRightRadius = "0";
      }
    }
  });
  const getSqlText = () => {
    if (!codeRef.current) return "";
    const code = codeRef.current.querySelector("code");
    return (code || codeRef.current).textContent.trim();
  };
  const executeQuery = async () => {
    const sql = getSqlText();
    if (!sql) return;
    setLoading(true);
    setError(null);
    setResults(null);
    setShowResults(true);
    try {
      const cleanQuery = sql.replace(/;$/, "").trim();
      const params = new URLSearchParams({
        query: cleanQuery,
        default_format: "JSONCompact",
        result_overflow_mode: "break",
        read_overflow_mode: "break",
        allow_experimental_analyzer: "1"
      });
      const res = await fetch(`https://sql-clickhouse.clickhouse.com/?${params.toString()}`, {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`demo:`)}`
        }
      });
      const text = await res.text();
      if (!res.ok) {
        setError(text || `HTTP ${res.status}`);
        setLoading(false);
        return;
      }
      const json = JSON.parse(text);
      setResults(json);
      setStats(json.statistics || null);
    } catch (err) {
      setError(err.message || "Error al ejecutar la consulta");
    }
    setLoading(false);
  };
  useEffect(() => {
    if (run) executeQuery();
  }, []);
  const formatRows = n => {
    if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
    if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
    if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
    return String(n);
  };
  const formatBytes = b => {
    if (b >= 1e9) return `${(b / 1e9).toFixed(2)} GB`;
    if (b >= 1e6) return `${(b / 1e6).toFixed(2)} MB`;
    if (b >= 1e3) return `${(b / 1e3).toFixed(2)} KB`;
    return `${b} B`;
  };
  const isNumericType = type => {
    return (/^(UInt|Int|Float|Decimal)/).test(type);
  };
  const isHyperlink = value => {
    return typeof value === "string" && (/^https?:\/\//).test(value);
  };
  const computeColumnExtremes = (meta, data) => {
    const extremes = {};
    for (let i = 0; i < meta.length; i++) {
      if (isNumericType(meta[i].type)) {
        let min = Infinity, max = -Infinity;
        for (const row of data) {
          const v = Number(row[i]);
          if (!isNaN(v)) {
            if (v < min) min = v;
            if (v > max) max = v;
          }
        }
        if (max > -Infinity) {
          extremes[i] = {
            min,
            max
          };
        }
      }
    }
    return extremes;
  };
  const computeColumnWidths = (meta, data) => {
    const lengths = meta.map((col, i) => {
      const headerLen = col.name.length + col.type.length + 1;
      let maxData = 0;
      for (const row of data) {
        const v = row[i];
        const len = v === null ? 4 : String(v).length;
        if (len > maxData) maxData = len;
      }
      return Math.max(headerLen, maxData);
    });
    const total = lengths.reduce((s, l) => s + l, 0);
    return lengths.map(l => `${(l / total * 100).toFixed(1)}%`);
  };
  const copyResultsAsTSV = () => {
    if (!results || !results.meta || !results.data) return;
    const header = results.meta.map(col => col.name).join("\t");
    const rows = results.data.map(row => row.map(cell => cell === null ? "NULL" : String(cell)).join("\t"));
    const tsv = [header, ...rows].join("\n");
    navigator.clipboard.writeText(tsv);
  };
  const borderColor = isDark ? "rgba(255,255,255,0.15)" : "#e5e7eb";
  const bgColor = isDark ? "rgba(255,255,255,0.05)" : "#f9fafb";
  const headerBg = isDark ? "#2a2a2a" : "#f3f4f6";
  const textColor = isDark ? "#e5e7eb" : "#1f2937";
  const mutedColor = isDark ? "#d1d5db" : "#6b7280";
  const accentColor = isDark ? "#FAFF69" : "#323232";
  const accentTextColor = isDark ? "#000" : "#fff";
  const barColor = isDark ? "#35372f" : "#d2d2d2";
  const cellBg = isDark ? "#1f201b" : "#ffffff";
  const cellBgHover = isDark ? "lch(15.8 0 0)" : "#f0f0f0";
  const extremes = results && results.meta && results.data ? computeColumnExtremes(results.meta, results.data) : {};
  const colWidths = results && results.meta && results.data ? computeColumnWidths(results.meta, results.data) : [];
  const getCellBarStyle = (cell, ci, ri) => {
    if (cell === null) return null;
    const colMeta = results.meta[ci];
    if (!isNumericType(colMeta.type) || !extremes[ci] || results.data.length <= 1 || extremes[ci].max <= 0) return null;
    const ratio = 100 * Number(cell) / extremes[ci].max;
    const bg = ri === hoveredRow ? cellBgHover : cellBg;
    return {
      background: `linear-gradient(to right, ${barColor} 0%, ${barColor} ${ratio}%, ${bg} ${ratio}%, ${bg} 100%)`
    };
  };
  const renderCell = (cell, ci) => {
    if (cell === null) {
      return <span style={{
        color: mutedColor,
        fontStyle: "italic"
      }}>NULL</span>;
    }
    const value = String(cell);
    if (isHyperlink(value)) {
      return <a href={value} target="_blank" rel="noopener noreferrer" style={{
        color: accentColor,
        textDecoration: "underline",
        cursor: "pointer"
      }}>
          {value}
        </a>;
    }
    return value;
  };
  return <div className="not-prose" style={{
    margin: "1rem 0",
    width: "100%",
    boxSizing: "border-box",
    contain: "inline-size"
  }}>
      {}
      <div>
        <div ref={codeRef}>{children}</div>

        {}
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "6px 12px",
    backgroundColor: headerBg,
    borderWidth: "0 1px 1px 1px",
    borderStyle: "solid",
    borderColor: isDark ? "rgba(255,255,255,0.1)" : "rgba(11,11,11,0.1)",
    borderRadius: "0 0 4px 4px"
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    gap: "12px"
  }}>
            {results && <button onClick={() => setShowResults(!showResults)} style={{
    background: "none",
    border: "none",
    cursor: "pointer",
    color: mutedColor,
    fontSize: "12px",
    padding: "2px 4px"
  }}>
                {showResults ? "▼ Ocultar resultados" : "▶ Mostrar resultados"}
              </button>}
            {showStats && stats && <span style={{
    fontSize: "11px",
    color: mutedColor,
    fontStyle: "italic"
  }}>
                Leídas {formatRows(stats.rows_read)} filas, {formatBytes(stats.bytes_read)} en {stats.elapsed.toFixed(3)}s
              </span>}
          </div>
          <button onClick={() => executeQuery()} disabled={loading} style={{
    display: "flex",
    alignItems: "center",
    gap: "6px",
    padding: "4px 14px",
    borderRadius: "4px",
    border: "none",
    cursor: loading ? "wait" : "pointer",
    backgroundColor: accentColor,
    color: accentTextColor,
    fontSize: "12px",
    fontWeight: 600
  }}>
            {loading ? <span>Ejecutando...</span> : <>
                <span style={{
    fontSize: "10px"
  }}>▶</span>
                <span>Ejecutar</span>
              </>}
          </button>
        </div>
      </div>

      {}
      {showResults && <div className="not-prose" style={{
    marginTop: "8px",
    maxHeight: "350px",
    overflow: "auto",
    border: `1px solid ${borderColor}`,
    borderRadius: "4px"
  }}>
          <div>
            {loading && <div style={{
    padding: "24px",
    textAlign: "center",
    color: mutedColor
  }}>Ejecutando consulta...</div>}

            {error && <div style={{
    padding: "12px 16px",
    color: "#ef4444",
    backgroundColor: isDark ? "rgba(239,68,68,0.1)" : "#fef2f2",
    fontSize: "13px",
    fontFamily: "monospace",
    whiteSpace: "pre-wrap"
  }}>
                {error}
              </div>}

            {results && results.meta && results.data && <div style={{
    display: "grid",
    gridTemplateColumns: colWidths.join(" "),
    width: "100%",
    fontSize: "13px",
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
  }}>
                {results.meta.map((col, i) => <div key={`h-${i}`} style={{
    position: "sticky",
    top: 0,
    zIndex: 1,
    padding: "6px 12px",
    textAlign: isNumericType(col.type) && results.meta.length > 1 ? "right" : "left",
    backgroundColor: headerBg,
    borderBottom: `1px solid ${borderColor}`,
    color: textColor,
    fontWeight: 600,
    fontSize: "12px",
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis"
  }}>
                    {col.name}
                    <span style={{
    color: mutedColor,
    fontWeight: 400,
    marginLeft: "4px",
    fontSize: "10px"
  }}>{col.type}</span>
                  </div>)}
                {results.data.map((row, ri) => row.map((cell, ci) => <div key={`${ri}-${ci}`} onMouseEnter={() => setHoveredRow(ri)} onMouseLeave={() => setHoveredRow(-1)} style={{
    padding: "4px 12px",
    color: textColor,
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis",
    textAlign: isNumericType(results.meta[ci].type) && results.meta.length > 1 ? "right" : "left",
    borderBottom: `1px solid ${borderColor}`,
    backgroundColor: ri === hoveredRow ? cellBgHover : ri % 2 === 0 ? "transparent" : bgColor,
    transition: "background-color 0.1s",
    ...getCellBarStyle(cell, ci, ri)
  }}>
                      {renderCell(cell, ci)}
                    </div>))}
              </div>}

            {results && results.data && <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "4px 12px",
    fontSize: "11px",
    color: mutedColor,
    borderTop: `1px solid ${borderColor}`,
    backgroundColor: headerBg
  }}>
                <span>
                  {results.rows} fila{results.rows !== 1 ? "s" : ""}
                </span>
                <button onClick={copyResultsAsTSV} style={{
    background: "none",
    border: "none",
    cursor: "pointer",
    color: mutedColor,
    fontSize: "11px",
    padding: "2px 6px",
    borderRadius: "3px"
  }} onMouseEnter={e => e.target.style.color = textColor} onMouseLeave={e => e.target.style.color = mutedColor}>
                  ⧉ Copiar TSV
                </button>
              </div>}
          </div>
        </div>}
    </div>;
};

export const Image = ({img, alt, size = "lg", background}) => {
  const normalizedSize = ["sm", "md", "lg"].includes(size) ? size : "lg";
  const backgroundColor = background === "white" ? "white" : background === "black" ? "rgb(31 31 28)" : undefined;
  return <div className={`ch-image-${normalizedSize}`}>
      <Frame>
        <img src={img} alt={alt} style={{
    backgroundColor
  }} />
      </Frame>
    </div>;
};

<div id="introduction">
  ## Introducción
</div>

ClickHouse ofrece varios mecanismos para acelerar las consultas analíticas sobre
grandes volúmenes de datos en escenarios en tiempo real. Uno de esos
mecanismos para acelerar las consultas es el uso de *proyecciones*. Las proyecciones ayudan a optimizar las
consultas creando una reordenación de los datos según los atributos de interés. Esto puede ser:

1. Una reordenación completa
2. Un subconjunto de la tabla original con un orden diferente
3. Una agregación precalculada (similar a una vista materializada), pero con un orden
   alineado con la agregación.

<br />

<Frame>
  <iframe src="https://www.youtube.com/embed/6CdnUdZSEG0?si=1zUyrP-tCvn9tXse" title="Reproductor de video de YouTube" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />
</Frame>

<div id="how-do-projections-work">
  ## ¿Cómo funcionan las Proyecciones?
</div>

En la práctica, una Proyección puede entenderse como una tabla adicional oculta de la
tabla original. La proyección puede tener un orden de filas distinto y, por tanto, un
índice primario diferente al de la tabla original, y puede precomputar
valores agregados de forma automática e incremental. Como resultado, el uso de Proyecciones
ofrece dos "palancas de ajuste" para acelerar la ejecución de consultas:

* **Uso correcto de índices primarios**
* **Precomputación de agregados**

Las Proyecciones son, en cierto modo, similares a las [vistas materializadas](/docs/es/concepts/features/materialized-views/index)
, que también permiten tener varios órdenes de filas y precomputar agregaciones
en el momento de la inserción.
Las Proyecciones se actualizan automáticamente y
se mantienen sincronizadas con la tabla original, a diferencia de las vistas materializadas, que se
actualizan de forma explícita. Cuando una consulta se dirige a la tabla original,
ClickHouse muestrea automáticamente las claves primarias y elige la tabla que puede
generar el mismo resultado correcto, pero requiere leer la menor cantidad posible de datos,
como se muestra en la figura siguiente:

<Image img="https://mintcdn.com/private-7c7dfe99/fc_oxFgK6Bxv68B9/images/data-modeling/projections_1.webp?fit=max&auto=format&n=fc_oxFgK6Bxv68B9&q=85&s=5eb02a58c942e04c145f23b1d7ef85ee" size="md" alt="Proyecciones en ClickHouse" width="1920" height="1920" data-path="images/data-modeling/projections_1.webp" />

<div id="smarter_storage_with_part_offset">
  ### Almacenamiento más inteligente con `_part_offset`
</div>

Desde la versión 25.5, ClickHouse admite la columna virtual `_part_offset` en las
proyecciones, lo que ofrece una nueva forma de definir una proyección.

Ahora hay dos formas de definir una proyección:

* **Almacenar columnas completas (el comportamiento original)**: La proyección contiene datos
  completos y puede leerse directamente, lo que ofrece un mejor rendimiento cuando los filtros coinciden
  con el orden de ordenación de la proyección.

* **Almacenar solo la clave de ordenación + `_part_offset`**: La proyección funciona como un índice.
  ClickHouse utiliza el índice primario de la proyección para localizar las filas coincidentes, pero lee los
  datos reales de la tabla base. Esto reduce la sobrecarga de almacenamiento a costa de
  un poco más de E/S en el momento de la consulta.

Los enfoques anteriores también pueden combinarse, almacenando algunas columnas en la proyección y
otras indirectamente mediante `_part_offset`.

<div id="when-to-use-projections">
  ## ¿Cuándo usar proyecciones?
</div>

Las proyecciones son una funcionalidad atractiva para los usuarios nuevos, ya que se
mantienen automáticamente a medida que se insertan los datos. Además, las consultas pueden enviarse simplemente a una
única tabla, donde las proyecciones se aprovechan siempre que sea posible para reducir
el tiempo de respuesta.

Esto contrasta con las vistas materializadas, donde el usuario tiene que seleccionar la
tabla de destino optimizada adecuada o reescribir su consulta, según los
filtros. Esto traslada más responsabilidad a las aplicaciones del usuario y aumenta
la complejidad del lado del cliente.

A pesar de estas ventajas, las proyecciones tienen algunas limitaciones inherentes que
debe conocer y, por lo tanto, deben implementarse con moderación.

* Las proyecciones no permiten usar distintos TTL para la tabla de origen y la
  tabla de destino (oculta); las vistas materializadas sí permiten distintos TTL.
* Las actualizaciones ligeras y las eliminaciones no son compatibles con tablas con proyecciones.
* Las vistas materializadas pueden encadenarse: la tabla de destino de una vista materializada
  puede ser la tabla de origen de otra vista materializada, y así sucesivamente. Esto no es
  posible con las proyecciones.
* Las definiciones de proyecciones no admiten joins, pero las vistas materializadas sí. Sin embargo, las consultas sobre tablas con proyecciones pueden usar joins libremente.
* Las definiciones de proyecciones no admiten filtros (cláusula `WHERE`), pero las vistas materializadas sí. Sin embargo, las consultas sobre tablas con proyecciones pueden filtrar libremente.

Recomendamos usar proyecciones cuando:

* Se requiere una reordenación completa de los datos. Aunque la expresión de la
  proyección puede, en teoría, usar un `GROUP BY,` las vistas materializadas son más
  eficaces para mantener agregaciones. También es más probable que el optimizador de consultas
  aproveche proyecciones que usan una reordenación simple, es decir, `SELECT * ORDER BY x`.
  Puede seleccionar un subconjunto de columnas en esta expresión para reducir la
  huella de almacenamiento.
* Los usuarios se sienten cómodos con el posible aumento de la huella de almacenamiento y
  la sobrecarga asociada de escribir los datos dos veces. Pruebe el impacto en la velocidad de inserción y
  [evalúe la sobrecarga de almacenamiento](/docs/es/guides/clickhouse/data-modelling/compression/compression-in-clickhouse).

<div id="examples">
  ## Ejemplos
</div>

<div id="filtering-without-using-primary-keys">
  ### Filtrado por columnas que no están en la clave primaria
</div>

En este ejemplo, mostraremos cómo agregar una proyección a una tabla.
También veremos cómo puede usarse la proyección para acelerar consultas que filtran
por columnas que no están en la clave primaria de una tabla.

Para este ejemplo, usaremos el conjunto de datos New York Taxi Data,
disponible en [sql.clickhouse.com](https://sql.clickhouse.com/), que está ordenado
por `pickup_datetime`.

Escribamos una consulta sencilla para encontrar todos los ID de viaje en los que los pasajeros
dejaron a su conductor una propina superior a \$200:

<RunnableCode>
  ```sql theme={null}
  SELECT
    tip_amount,
    trip_id,
    dateDiff('minutes', pickup_datetime, dropoff_datetime) AS trip_duration_min
  FROM nyc_taxi.trips WHERE tip_amount > 200 AND trip_duration_min > 0
  ORDER BY tip_amount, trip_id ASC
  ```
</RunnableCode>

Observa que, como estamos filtrando por `tip_amount`, que no está en el `ORDER BY`, ClickHouse
tuvo que hacer un escaneo completo de la tabla. Aceleremos esta consulta.

Para conservar la tabla original y los resultados, crearemos una tabla nueva y copiaremos los datos mediante un `INSERT INTO SELECT`:

```sql theme={null}
CREATE TABLE nyc_taxi.trips_with_projection AS nyc_taxi.trips;
INSERT INTO nyc_taxi.trips_with_projection SELECT * FROM nyc_taxi.trips;
```

Para añadir una proyección, usamos la sentencia `ALTER TABLE` junto con la sentencia
`ADD PROJECTION`:

```sql theme={null}
ALTER TABLE nyc_taxi.trips_with_projection
ADD PROJECTION prj_tip_amount
(
    SELECT *
    ORDER BY tip_amount, dateDiff('minutes', pickup_datetime, dropoff_datetime)
)
```

Es necesario, después de agregar una proyección, usar la sentencia `MATERIALIZE PROJECTION`
para que los datos que contiene se ordenen físicamente y se reescriban de acuerdo
con la consulta especificada anteriormente:

```sql theme={null}
ALTER TABLE nyc.trips_with_projection MATERIALIZE PROJECTION prj_tip_amount
```

Volvamos a ejecutar la consulta ahora que hemos añadido la proyección:

<RunnableCode>
  ```sql theme={null}
  SELECT
    tip_amount,
    trip_id,
    dateDiff('minutes', pickup_datetime, dropoff_datetime) AS trip_duration_min
  FROM nyc_taxi.trips_with_projection WHERE tip_amount > 200 AND trip_duration_min > 0
  ORDER BY tip_amount, trip_id ASC
  ```
</RunnableCode>

Observa cómo pudimos reducir considerablemente el tiempo de consulta y escanear
menos filas.

Podemos confirmar que nuestra consulta anterior efectivamente utilizó la proyección que creamos
consultando la tabla `system.query_log`:

```sql theme={null}
SELECT query, projections 
FROM system.query_log 
WHERE query_id='<query_id>'
```

```response theme={null}
   ┌─query─────────────────────────────────────────────────────────────────────────┬─projections──────────────────────┐
   │ SELECT                                                                       ↴│ ['default.trips.prj_tip_amount'] │
   │↳  tip_amount,                                                                ↴│                                  │
   │↳  trip_id,                                                                   ↴│                                  │
   │↳  dateDiff('minutes', pickup_datetime, dropoff_datetime) AS trip_duration_min↴│                                  │
   │↳FROM trips WHERE tip_amount > 200 AND trip_duration_min > 0                   │                                  │
   └───────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────┘
```

<div id="using-projections-to-speed-up-UK-price-paid">
  ### Uso de proyecciones para acelerar las consultas sobre UK Price Paid
</div>

Para demostrar cómo se pueden usar las proyecciones para mejorar el rendimiento de las consultas, veamos
un ejemplo con un conjunto de datos real. Para este ejemplo, usaremos
la tabla de nuestro tutorial [UK Property Price Paid](/docs/es/get-started/sample-datasets/uk-price-paid),
con 30,03 millones de filas. Este conjunto de datos también está disponible en nuestro
entorno de [sql.clickhouse.com](https://sql.clickhouse.com/?query_id=6IDMHK3OMR1C97J6M9EUQS).

Si quieres ver cómo se creó la tabla y cómo se insertaron los datos, puedes
consultar la página ["El conjunto de datos de precios de propiedades del Reino Unido"](/docs/es/get-started/sample-datasets/uk-price-paid).

Podemos ejecutar dos consultas sencillas sobre este conjunto de datos. La primera muestra los condados de Londres con
los precios pagados más altos, y la segunda calcula el precio medio por condado:

<RunnableCode>
  ```sql theme={null}
  SELECT
    county,
    price
  FROM uk.uk_price_paid
  WHERE town = 'LONDON'
  ORDER BY price DESC
  LIMIT 3
  ```
</RunnableCode>

<RunnableCode>
  ```sql theme={null}
  SELECT
      county,
      avg(price)
  FROM uk.uk_price_paid
  GROUP BY county
  ORDER BY avg(price) DESC
  LIMIT 3
  ```
</RunnableCode>

Observa que, a pesar de ser muy rápidas, ambas consultas realizaron un escaneo completo de la tabla con sus 30,03 millones de filas,
ya que ni `town` ni `price` estaban en nuestra cláusula `ORDER BY` cuando
creamos la tabla:

```sql highlight={6} theme={null}
CREATE TABLE uk.uk_price_paid
(
  ...
)
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2);
```

Veamos si podemos acelerar esta consulta con proyecciones.

Para conservar la tabla original y los resultados, crearemos una nueva tabla y copiaremos los datos mediante un `INSERT INTO SELECT`:

```sql theme={null}
CREATE TABLE uk.uk_price_paid_with_projections AS uk_price_paid;
INSERT INTO uk.uk_price_paid_with_projections SELECT * FROM uk.uk_price_paid;
```

Creamos y poblamos la proyección `prj_oby_town_price`, que genera una
tabla adicional (oculta) con un índice primario, ordenada por localidad y precio, para
optimizar la consulta que enumera los condados de una localidad específica con los precios
pagados más altos:

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections
  (ADD PROJECTION prj_obj_town_price
  (
    SELECT *
    ORDER BY
        town,
        price
  ))
```

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections
  (MATERIALIZE PROJECTION prj_obj_town_price)
SETTINGS mutations_sync = 1
```

La configuración [`mutations_sync`](/docs/es/reference/settings/session-settings/mutations#mutations_sync) se
utiliza para forzar la ejecución síncrona.

Creamos y poblamos la proyección `prj_gby_county` – una tabla adicional (oculta)
que precomputa de forma incremental los valores agregados de avg(price) para los 130
condados del Reino Unido existentes:

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections
  (ADD PROJECTION prj_gby_county
  (
    SELECT
        county,
        avg(price)
    GROUP BY county
  ))
```

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections
  (MATERIALIZE PROJECTION prj_gby_county)
SETTINGS mutations_sync = 1
```

<Note>
  Si se usa una cláusula `GROUP BY` en una proyección como la proyección `prj_gby_county`
  anterior, el motor de almacenamiento subyacente de la tabla (oculta)
  pasa a ser `AggregatingMergeTree`, y todas las funciones de agregación se convierten en
  `AggregateFunction`. Esto garantiza una correcta agregación incremental de los datos.
</Note>

La figura siguiente muestra una visualización de la tabla principal `uk_price_paid_with_projections`
y sus dos proyecciones:

<Image img="https://mintcdn.com/private-7c7dfe99/fc_oxFgK6Bxv68B9/images/data-modeling/projections_2.webp?fit=max&auto=format&n=fc_oxFgK6Bxv68B9&q=85&s=e15402bc7210c8c3cec2e2fc68a08c0e" size="md" alt="Visualización de la tabla principal uk_price_paid_with_projections y sus dos proyecciones" width="1920" height="1080" data-path="images/data-modeling/projections_2.webp" />

Si ahora ejecutamos de nuevo la consulta que muestra los condados de Londres con los tres precios
pagados más altos, veremos una mejora en el rendimiento de la consulta:

<RunnableCode>
  ```sql theme={null}
  SELECT
    county,
    price
  FROM uk.uk_price_paid_with_projections
  WHERE town = 'LONDON'
  ORDER BY price DESC
  LIMIT 3
  ```
</RunnableCode>

Del mismo modo, para la consulta que muestra los condados del Reino Unido con los tres precios
medios pagados más altos:

<RunnableCode>
  ```sql theme={null}
  SELECT
      county,
      avg(price)
  FROM uk.uk_price_paid_with_projections
  GROUP BY county
  ORDER BY avg(price) DESC
  LIMIT 3
  ```
</RunnableCode>

Ten en cuenta que ambas consultas se dirigen a la tabla original y que ambas dieron como resultado
un escaneo completo de la tabla (los 30,03 millones de filas se leyeron del disco) antes de que
creáramos las dos proyecciones.

Además, ten en cuenta que la consulta que muestra los condados de Londres para los tres precios
pagados más altos está procesando 2,17 millones de filas. Cuando usamos directamente una segunda tabla
optimizada para esta consulta, solo se leyeron 81,92 mil filas del disco.

La razón de la diferencia es que actualmente la optimización `optimize_read_in_order`
mencionada anteriormente no es compatible con las proyecciones.

Inspeccionamos la tabla `system.query_log` para ver que ClickHouse
utilizó automáticamente las dos proyecciones para las dos consultas anteriores (consulta la
columna `projections` a continuación):

```sql theme={null}
SELECT
  tables,
  query,
  query_duration_ms::String ||  ' ms' AS query_duration,
        formatReadableQuantity(read_rows) AS read_rows,
  projections
FROM clusterAllReplicas(default, system.query_log)
WHERE (type = 'QueryFinish') AND (tables = ['default.uk_price_paid_with_projections'])
ORDER BY initial_query_start_time DESC
  LIMIT 2
FORMAT Vertical
```

```response theme={null}
Row 1:
──────
tables:         ['uk.uk_price_paid_with_projections']
query:          SELECT
    county,
    avg(price)
FROM uk_price_paid_with_projections
GROUP BY county
ORDER BY avg(price) DESC
LIMIT 3
query_duration: 5 ms
read_rows:      132.00
projections:    ['uk.uk_price_paid_with_projections.prj_gby_county']

Row 2:
──────
tables:         ['uk.uk_price_paid_with_projections']
query:          SELECT
  county,
  price
FROM uk_price_paid_with_projections
WHERE town = 'LONDON'
ORDER BY price DESC
LIMIT 3
SETTINGS log_queries=1
query_duration: 11 ms
read_rows:      2.29 million
projections:    ['uk.uk_price_paid_with_projections.prj_obj_town_price']

2 rows in set. Elapsed: 0.006 sec.
```

<div id="further-examples">
  ### Más ejemplos
</div>

Los siguientes ejemplos usan el mismo conjunto de datos de precios del Reino Unido y comparan consultas con y sin proyecciones.

Para conservar la tabla original (y el rendimiento), volvemos a crear una copia de la tabla con `CREATE AS` e `INSERT INTO SELECT`.

```sql theme={null}
CREATE TABLE uk.uk_price_paid_with_projections_v2 AS uk.uk_price_paid;
INSERT INTO uk.uk_price_paid_with_projections_v2 SELECT * FROM uk.uk_price_paid;
```

<div id="build-projection">
  #### Crear una proyección
</div>

Vamos a crear una proyección de agregación por las dimensiones `toYear(date)`, `district` y `town`:

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections_v2
    ADD PROJECTION projection_by_year_district_town
    (
        SELECT
            toYear(date),
            district,
            town,
            avg(price),
            sum(price),
            count()
        GROUP BY
            toYear(date),
            district,
            town
    )
```

Rellene la proyección para los datos existentes. (Si no se materializa, la proyección solo se creará para los datos que se inserten a partir de ese momento):

```sql theme={null}
ALTER TABLE uk.uk_price_paid_with_projections_v2
    MATERIALIZE PROJECTION projection_by_year_district_town
SETTINGS mutations_sync = 1
```

Las siguientes consultas muestran el contraste de rendimiento con y sin proyecciones. Para desactivar el uso de proyecciones, usamos la configuración [`optimize_use_projections`](/docs/es/reference/settings/session-settings/optimize-use#optimize_use_projections), que está habilitada de forma predeterminada.

<div id="average-price-projections">
  #### Consulta 1. Precio medio por año
</div>

<RunnableCode>
  ```sql theme={null}
  SELECT
      toYear(date) AS year,
      round(avg(price)) AS price,
      bar(price, 0, 1000000, 80)
  FROM uk.uk_price_paid_with_projections_v2
  GROUP BY year
  ORDER BY year ASC
  SETTINGS optimize_use_projections=0
  ```
</RunnableCode>

<RunnableCode>
  ```sql theme={null}
  SELECT
      toYear(date) AS year,
      round(avg(price)) AS price,
      bar(price, 0, 1000000, 80)
  FROM uk.uk_price_paid_with_projections_v2
  GROUP BY year
  ORDER BY year ASC

  ```
</RunnableCode>

Los resultados deberían ser los mismos, pero el rendimiento será mejor en este último ejemplo.

<div id="average-price-london-projections">
  #### Consulta 2. Precio promedio por año en Londres
</div>

<RunnableCode>
  ```sql theme={null}
  SELECT
      toYear(date) AS year,
      round(avg(price)) AS price,
      bar(price, 0, 2000000, 100)
  FROM uk.uk_price_paid_with_projections_v2
  WHERE town = 'LONDON'
  GROUP BY year
  ORDER BY year ASC
  SETTINGS optimize_use_projections=0
  ```
</RunnableCode>

<RunnableCode>
  ```sql theme={null}
  SELECT
      toYear(date) AS year,
      round(avg(price)) AS price,
      bar(price, 0, 2000000, 100)
  FROM uk.uk_price_paid_with_projections_v2
  WHERE town = 'LONDON'
  GROUP BY year
  ORDER BY year ASC
  ```
</RunnableCode>

<div id="most-expensive-neighborhoods-projections">
  #### Consulta 3. Los barrios más caros
</div>

La condición (date >= '2020-01-01') debe modificarse para que coincida con la dimensión de la proyección (`toYear(date) >= 2020)`):

<RunnableCode>
  ```sql theme={null}
  SELECT
      town,
      district,
      count() AS c,
      round(avg(price)) AS price,
      bar(price, 0, 5000000, 100)
  FROM uk.uk_price_paid_with_projections_v2
  WHERE toYear(date) >= 2020
  GROUP BY
      town,
      district
  HAVING c >= 100
  ORDER BY price DESC
  LIMIT 100
  SETTINGS optimize_use_projections=0
  ```
</RunnableCode>

<RunnableCode>
  ```sql theme={null}
  SELECT
      town,
      district,
      count() AS c,
      round(avg(price)) AS price,
      bar(price, 0, 5000000, 100)
  FROM uk.uk_price_paid_with_projections_v2
  WHERE toYear(date) >= 2020
  GROUP BY
      town,
      district
  HAVING c >= 100
  ORDER BY price DESC
  LIMIT 100
  ```
</RunnableCode>

De nuevo, el resultado es el mismo, pero observa la mejora en el rendimiento de la consulta en la segunda consulta.

<div id="combining-projections">
  ### Combinar proyecciones en una sola consulta
</div>

A partir de la versión 25.6, y sobre la base del soporte de `_part_offset` introducido en
la versión anterior, ClickHouse ahora puede usar varias proyecciones para acelerar
una única consulta con múltiples filtros.

Es importante destacar que ClickHouse sigue leyendo datos de una sola proyección (o de la tabla base),
pero puede usar los índices primarios de otras proyecciones para descartar partes innecesarias antes de leer.
Esto resulta especialmente útil para consultas que filtran por varias columnas, cada
una de las cuales puede coincidir con una proyección distinta.

> Actualmente, este mecanismo solo descarta partes completas. La poda a nivel de
> gránulo aún no es compatible.

Para demostrarlo, definimos la tabla (con proyecciones que usan columnas `_part_offset`)
e insertamos cinco filas de ejemplo que coinciden con los diagramas anteriores.

```sql theme={null}
CREATE TABLE page_views
(
    id UInt64,
    event_date Date,
    user_id UInt32,
    url String,
    region String,
    PROJECTION region_proj
    (
        SELECT _part_offset ORDER BY region
    ),
    PROJECTION user_id_proj
    (
        SELECT _part_offset ORDER BY user_id
    )
)
ENGINE = MergeTree
ORDER BY (event_date, id)
SETTINGS
  index_granularity = 1, -- una fila por gránulo
  max_bytes_to_merge_at_max_space_in_pool = 1; -- desactivar la fusión
```

A continuación, insertamos datos en la tabla:

```sql theme={null}
INSERT INTO page_views VALUES (
1, '2025-07-01', 101, 'https://example.com/page1', 'europe');
INSERT INTO page_views VALUES (
2, '2025-07-01', 102, 'https://example.com/page2', 'us_west');
INSERT INTO page_views VALUES (
3, '2025-07-02', 106, 'https://example.com/page3', 'us_west');
INSERT INTO page_views VALUES (
4, '2025-07-02', 107, 'https://example.com/page4', 'us_west');
INSERT INTO page_views VALUES (
5, '2025-07-03', 104, 'https://example.com/page5', 'asia');
```

<Note>
  Nota: La tabla usa ajustes personalizados con fines ilustrativos, como gránulos de una sola fila
  y fusiones de partes deshabilitadas, que no se recomiendan para entornos de producción.
</Note>

Esta configuración produce:

* Cinco partes independientes (una por cada fila insertada)
* Una entrada del índice primario por fila (en la tabla base y en cada proyección)
* Cada parte contiene exactamente una fila

Con esta configuración, ejecutamos una consulta que filtra por `region` y `user_id`.
Como el índice primario de la tabla base se construye a partir de `event_date` e `id`,
no resulta útil en este caso, por lo que ClickHouse usa:

* `region_proj` para descartar partes por región
* `user_id_proj` para seguir descartando por `user_id`

Este comportamiento puede verse con `EXPLAIN projections = 1`, que muestra cómo
ClickHouse selecciona y aplica las proyecciones.

```sql theme={null}
EXPLAIN projections=1
SELECT * FROM page_views WHERE region = 'us_west' AND user_id = 107;
```

```response theme={null}
    ┌─explain────────────────────────────────────────────────────────────────────────────────┐
 1. │ Expression ((Project names + Projection))                                              │
 2. │   Expression                                                                           │                                                                        
 3. │     ReadFromMergeTree (default.page_views)                                             │
 4. │     Projections:                                                                       │
 5. │       Name: region_proj                                                                │
 6. │         Description: Projection has been analyzed and is used for part-level filtering │
 7. │         Condition: (region in ['us_west', 'us_west'])                                  │
 8. │         Search Algorithm: binary search                                                │
 9. │         Parts: 3                                                                       │
10. │         Marks: 3                                                                       │
11. │         Ranges: 3                                                                      │
12. │         Rows: 3                                                                        │
13. │         Filtered Parts: 2                                                              │
14. │       Name: user_id_proj                                                               │
15. │         Description: Projection has been analyzed and is used for part-level filtering │
16. │         Condition: (user_id in [107, 107])                                             │
17. │         Search Algorithm: binary search                                                │
18. │         Parts: 1                                                                       │
19. │         Marks: 1                                                                       │
20. │         Ranges: 1                                                                      │
21. │         Rows: 1                                                                        │
22. │         Filtered Parts: 2                                                              │
    └────────────────────────────────────────────────────────────────────────────────────────┘
```

La salida de `EXPLAIN` (mostrada arriba) revela el plan lógico de la consulta, de arriba hacia abajo:

| Número de fila | Descripción                                                                                                       |
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
| 3              | Planea leer desde la tabla base `page_views`                                                                      |
| 5-13           | Usa `region_proj` para identificar 3 partes donde region = 'us\_west', descartando 2 de las 5 partes              |
| 14-22          | Usa user`_id_proj` para identificar 1 parte donde `user_id = 107`, descartando además 2 de las 3 partes restantes |

Al final, solo **1 de las 5 partes** se lee desde la tabla base.
Al combinar el análisis de índices de múltiples proyecciones, ClickHouse reduce significativamente la cantidad de datos analizados,
mejorando el rendimiento y manteniendo baja la sobrecarga de almacenamiento.

<div id="related-content">
  ## Contenido relacionado
</div>

* [Introducción práctica a los índices primarios en ClickHouse](/docs/es/guides/clickhouse/data-modelling/sparse-primary-indexes#option-3-projections)
* [Vistas materializadas](/docs/es/concepts/features/materialized-views/index)
* [ALTER PROJECTION](/docs/es/reference/statements/alter/projection)
