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

# Performance Tuning Recipes

export const InternalLink = ({version, path = "", children, ...props}) => {
  let detectedVersion = version || "stable";
  if (typeof window !== 'undefined' && !version) {
    const match = window.location.pathname.match(/\/docs\/([^/]+)/);
    if (match) {
      detectedVersion = match[1];
    }
  }
  const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
  return <a href={`/docs/${detectedVersion}/${normalizedPath}`} {...props}>
      {children}
    </a>;
};

This page provides recipes for fixing performance issues in your applications.

## Problems

This section describes how to use CockroachDB commands and dashboards to identify performance problems in your applications.

<a id="setting-goschedstats-always-use-short-sample-period-enabled" />

<table>
  <tr>
    <th>Observation</th>
    <th>Diagnosis</th>
    <th>Solution</th>
  </tr>

  <tr>
    <td><ul><li>Your application takes a long time to return results.</li></ul></td>
    <td><ul><li>Excess network latency.</li></ul></td>
    <td><ul><li>Use the correct <InternalLink path="topology-patterns">topology pattern</InternalLink> for your cluster.</li></ul></td>
  </tr>

  <tr>
    <td>
      <ul>
        <li>The Transactions page in the <InternalLink version="cockroachcloud" path="transactions-page">CockroachDB Cloud Console</InternalLink> or <InternalLink path="ui-transactions-page#active-executions-table">DB Console</InternalLink> shows transactions with <code>Waiting</code> status.</li>
        <li>Your application is experiencing degraded performance with <code>SQLSTATE: 40001</code> and a <InternalLink path="transaction-retry-error-reference#transaction-retry-error-reference">transaction retry error</InternalLink> message.</li>
        <li>Querying the <InternalLink path="crdb-internal#transaction_contention_events"><code>crdb\_internal.transaction\_contention\_events</code></InternalLink> table indicates that your transactions have experienced contention.</li>
        <li>The SQL Statement Contention graph in the <InternalLink version="cockroachcloud" path="metrics-sql#sql-statement-contention">CockroachDB Cloud Console</InternalLink> or <InternalLink path="ui-sql-dashboard#sql-statement-contention">DB Console</InternalLink> is showing spikes over time.</li>
        <li>The Transaction Restarts graph in the <InternalLink version="cockroachcloud" path="metrics-sql#transaction-restarts">CockroachDB Cloud Console</InternalLink> or <InternalLink path="ui-sql-dashboard#transaction-restarts">DB Console</InternalLink> is showing spikes in retries over time.</li>
      </ul>
    </td>

    <td><ul><li>Your application is experiencing <InternalLink path="performance-best-practices-overview#transaction-contention">transaction contention</InternalLink>.</li></ul></td>

    <td>
      <ul>
        <li><a href="#transaction-contention">Reduce transaction contention.</a></li>
      </ul>
    </td>
  </tr>

  <tr>
    <td>
      <ul>
        <li>The <b>Hot Ranges</b> page (DB Console) displays a higher-than-expected QPS for a range.</li>
        <li>The <b>Key Visualizer</b> (DB Console) shows ranges with much higher-than-average write rates for the cluster.</li>
      </ul>
    </td>

    <td><ul><li>Your cluster has <InternalLink path="performance-best-practices-overview#hot-spots">hot spots</InternalLink>.</li></ul></td>
    <td><ul><li><a href="#hot-spots">Reduce hot spots</a>.</li></ul></td>
  </tr>

  <tr>
    <td>
      <ul>
        <li>The statement plan produced by <InternalLink path="explain"><code>EXPLAIN</code></InternalLink> or <InternalLink path="explain-analyze"><code>EXPLAIN ANALYZE</code></InternalLink> indicates that the statement uses a full table scan.</li>
        <li>Querying the <code>crdb\_internal.node\_statement\_statistics</code> table indicates that you have full table scans in some statement's plans.</li>
        <li>Viewing the statement plan on the <InternalLink path="ui-statements-page#statement-fingerprint-page">Statement Fingerprint page</InternalLink> in the DB Console indicates that the plan contains full table scans.</li>
        <li>Running the <InternalLink path="show-full-table-scans"><code>SHOW FULL TABLE SCANS</code></InternalLink> statement returns results.</li>
        <li>The <InternalLink path="ui-sql-dashboard">Full Table/Index Scans graph</InternalLink> in the DB Console is showing spikes over time.</li>
      </ul>
    </td>

    <td><ul><li>Poor quality statement plans retrieve more rows than are required, leading to longer execution times.</li></ul></td>
    <td><ul><li><a href="#statements-with-full-table-scans">Use indexes to reduce full table scans.</a></li></ul></td>
  </tr>

  <tr>
    <td>
      <ul>
        <li>The <InternalLink path="ui-hardware-dashboard">Hardware metrics dashboard</InternalLink> in the DB Console shows high resource usage per node.</li>
        <li>The Problem Ranges report on the <InternalLink path="ui-debug-pages">Advanced Debug page</InternalLink> in the DB Console indicates a high number of queries per second on a subset of ranges or nodes.</li>
      </ul>
    </td>

    <td><ul><li>You have resource contention.</li></ul></td>
    <td><ul><li><a href="#suboptimal-primary-keys">Improve primary key usage.</a></li></ul></td>
  </tr>

  <tr>
    <td><ul><li>The <InternalLink path="ui-overview-dashboard#">Overview dashboard</InternalLink> in the DB Console shows high service latency and QPS for <code>INSERT</code> and <code>UPDATE</code> statements.</li></ul></td>
    <td><ul><li>Your tables have long write times.</li></ul></td>
    <td><ul><li><a href="#slow-writes">Remove unnecessary indexes.</a></li></ul></td>
  </tr>

  <tr>
    <td><ul><li>You experience high latency on queries that cannot be explained by high contention or a suboptimal query plan. You might also see high CPU on one or more nodes.</li></ul></td>
    <td><ul><li>You may be scanning over large numbers of <InternalLink path="architecture/storage-layer#mvcc">MVCC versions</InternalLink>. This is similar to how a full table scan can be slow.</li></ul></td>
    <td><ul><li><a href="#too-many-mvcc-values">Configure CockroachDB to purge unneeded MVCC values.</a></li></ul></td>
  </tr>
</table>

## Solutions

This section provides solutions for common performance issues in your applications.

### Transaction contention

<InternalLink path="performance-best-practices-overview#transaction-contention">Transaction contention</InternalLink> is a state of conflict that occurs when:

* A <InternalLink path="transactions">transaction</InternalLink> is unable to complete due to another concurrent or recent transaction attempting to write to the same data. This is also called *lock contention*.
* A transaction is <InternalLink path="transactions#automatic-retries">automatically retried</InternalLink> because it could not be placed into a <InternalLink path="demo-serializable">serializable ordering</InternalLink> among all of the currently-executing transactions. If the automatic retry is not possible or fails, a <InternalLink path="transaction-retry-error-reference">*transaction retry error*</InternalLink> is emitted to the client, requiring a client application running under <InternalLink path="demo-serializable">`SERIALIZABLE`</InternalLink> isolation to <InternalLink path="transaction-retry-error-reference#client-side-retry-handling">retry the transaction</InternalLink>. This is also called a *serialization conflict*, or an *isolation conflict*.

#### Indicators that your application is experiencing transaction contention

##### Waiting transaction

These are indicators that a transaction is trying to access a row that has been <InternalLink path="architecture/transaction-layer#writing">"locked"</InternalLink> by another, concurrent transaction issuing a <InternalLink path="architecture/transaction-layer#write-intents">write</InternalLink> or <InternalLink path="select-for-update#lock-strengths">locking read</InternalLink>.

* The **Active Executions** table on the **Transactions** page ( <InternalLink version="cockroachcloud" path="transactions-page">CockroachDB Cloud Console</InternalLink> or <InternalLink path="ui-transactions-page#active-executions-table">DB Console</InternalLink> ) shows transactions with `Waiting` in the **Status** column. You can sort the table by **Time Spent Waiting**.
* Querying the <InternalLink path="crdb-internal#cluster_locks">`crdb_internal.cluster_locks`</InternalLink> table shows transactions where <InternalLink path="crdb-internal#cluster-locks-columns">`granted`</InternalLink> is `false`.

These are indicators that lock contention occurred in the past:

* Querying the <InternalLink path="crdb-internal#transaction_contention_events">`crdb_internal.transaction_contention_events`</InternalLink> table `WHERE contention_type='LOCK_WAIT'` indicates that your transactions have experienced lock contention.
  * This is also shown in the **Transaction Executions** view on the **Insights** page ( <InternalLink version="cockroachcloud" path="insights-page#transaction-executions-view">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-insights-page#transaction-executions-view">DB Console</InternalLink> ). Transaction executions will display the <InternalLink path="ui-insights-page#high-contention">**High Contention** insight</InternalLink>.

    The default tracing behavior captures a small percent of transactions, so not all contention events will be recorded. When investigating transaction contention, you can set the <InternalLink path="cluster-settings">`sql.trace.txn.enable_threshold` cluster setting</InternalLink> to always capture contention events.

* The **SQL Statement Contention** graph (<InternalLink version="cockroachcloud" path="metrics-sql#sql-statement-contention">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-sql-dashboard#sql-statement-contention">DB Console</InternalLink>) is showing spikes over time.<img src="https://mintcdn.com/cockroachlabs/6qI-DTIWMnaeNI5F/images/v24.1/ui-statement-contention.png?fit=max&auto=format&n=6qI-DTIWMnaeNI5F&q=85&s=a147441dc146ddb9d1c125643388db9d" alt="SQL Statement Contention graph in DB Console" width="1007" height="432" data-path="images/v24.1/ui-statement-contention.png" />

If a long-running transaction is waiting due to <InternalLink path="performance-best-practices-overview#transaction-contention">lock contention</InternalLink>:

1. [Identify the blocking transaction](#identify-conflicting-transactions).
2. Evaluate whether you can cancel the transaction. If so, [cancel it](#cancel-a-blocking-transaction) to unblock the waiting transaction.
3. Optimize the transaction to [reduce further contention](#reduce-transaction-contention). In particular, break down larger transactions such as <InternalLink path="bulk-delete-data">bulk deletes</InternalLink> into smaller ones to have transactions hold locks for a shorter duration, and use <InternalLink path="as-of-system-time">historical reads</InternalLink> when possible to reduce conflicts with other writes.

If lock contention occurred in the past, you can [identify the transactions and objects that experienced lock contention](#identify-transactions-and-objects-that-experienced-lock-contention).

##### Transaction retry error

These are indicators that a transaction has failed due to <InternalLink path="performance-best-practices-overview#transaction-contention">contention</InternalLink>.

* A <InternalLink path="transaction-retry-error-reference">transaction retry error</InternalLink> with `SQLSTATE: 40001`, the string <InternalLink path="common-errors#restart-transaction">`restart transaction`</InternalLink>, and an error code such as <InternalLink path="transaction-retry-error-reference#retry_write_too_old">`RETRY_WRITE_TOO_OLD`</InternalLink> or <InternalLink path="transaction-retry-error-reference#retry_serializable">`RETRY_SERIALIZABLE`</InternalLink>, is emitted to the client. These errors are typically seen under <InternalLink path="demo-serializable">`SERIALIZABLE`</InternalLink> and not <InternalLink path="read-committed">`READ COMMITTED`</InternalLink> isolation.
* Querying the <InternalLink path="crdb-internal#transaction_contention_events">`crdb_internal.transaction_contention_events`</InternalLink> table `WHERE contention_type='SERIALIZATION_CONFLICT'` indicates that your transactions have experienced serialization conflicts.
  * This is also shown in the **Transaction Executions** view on the **Insights** page ( <InternalLink version="cockroachcloud" path="insights-page#transaction-executions-view">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-insights-page#transaction-executions-view">DB Console</InternalLink> ). Transaction executions will display the <InternalLink path="ui-insights-page#serialization-conflict-due-to-transaction-contention">**Failed Execution** insight due to a serialization conflict</InternalLink>.

These are indicators that transaction retries occurred in the past:

* The **Transaction Restarts** graph ( <InternalLink version="cockroachcloud" path="metrics-sql#transaction-restarts">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-sql-dashboard#transaction-restarts">DB Console</InternalLink> is showing spikes in transaction retries over time.

In most cases, the correct actions to take when encountering transaction retry errors are:

1. Under `SERIALIZABLE` isolation, update your application to support <InternalLink path="transaction-retry-error-reference#client-side-retry-handling">client-side retry handling</InternalLink> when transaction retry errors are encountered. Follow the guidance for the <InternalLink path="transaction-retry-error-reference#transaction-retry-error-reference">specific error type</InternalLink>.
2. Take steps to <InternalLink path="transaction-retry-error-reference#minimize-transaction-retry-errors">minimize transaction retry errors</InternalLink> in the first place. This means reducing transaction contention overall, and increasing the likelihood that CockroachDB can <InternalLink path="transactions#automatic-retries">automatically retry</InternalLink> a failed transaction.

#### Fix transaction contention problems

Identify the transactions that are in conflict, and unblock them if possible. In general, take steps to [reduce transaction contention](#reduce-transaction-contention).

When running under `SERIALIZABLE` isolation, implement <InternalLink path="transaction-retry-error-reference#client-side-retry-handling">client-side retry handling</InternalLink> so that your application can respond to <InternalLink path="transaction-retry-error-reference">transaction retry errors</InternalLink> that are emitted when CockroachDB cannot <InternalLink path="transactions#automatic-retries">automatically retry</InternalLink> a transaction.

##### Identify conflicting transactions

* In the **Active Executions** table on the **Transactions** page ( <InternalLink version="cockroachcloud" path="transactions-page">CockroachDB Cloud Console</InternalLink> or <InternalLink path="ui-transactions-page#active-executions-table">DB Console</InternalLink> ), look for a **waiting** transaction ( `Waiting` status).

<Tip>
  If you see many waiting transactions, a single long-running transaction may be blocking transactions that are, in turn, blocking others. In this case, sort the table by **Time Spent Waiting** to find the transaction that has been waiting for the longest amount of time. Unblocking this transaction may unblock the other transactions.
</Tip>

Click the transaction's execution ID and view the following transaction execution details:<img src="https://mintcdn.com/cockroachlabs/LmGUwqXk_Q0o006z/images/v24.1/waiting-transaction.png?fit=max&auto=format&n=LmGUwqXk_Q0o006z&q=85&s=b948eea73f50afcd5d2390460b3af175" alt="Movr rides transactions" width="3238" height="1716" data-path="images/v24.1/waiting-transaction.png" />

* **Last Retry Reason** shows the last [transaction retry error](#transaction-retry-error) received for the transaction, if applicable.
* The details of the **blocking** transaction, directly below the **Contention Insights** section. Click the blocking transaction to view its details.

##### Cancel a blocking transaction

1. [Identify the **blocking** transaction](#identify-conflicting-transactions) and view its transaction execution details.
2. Click its **Session ID** to open the **Session Details** page.<img src="https://mintcdn.com/cockroachlabs/6qI-DTIWMnaeNI5F/images/v24.1/ui-sessions-details-page.png?fit=max&auto=format&n=6qI-DTIWMnaeNI5F&q=85&s=0e5f4b070be07110229e8a4ba3dae29d" alt="Sessions Details Page" width="2164" height="2176" data-path="images/v24.1/ui-sessions-details-page.png" />
3. Click **Cancel Statement** to cancel the **Most Recent Statement** and thus the transaction, or click **Cancel Session** to cancel the session issuing the transaction.

##### Identify transactions and objects that experienced lock contention

To identify transactions that experienced <InternalLink path="performance-best-practices-overview#transaction-contention">lock contention</InternalLink> in the past:

* In the **Transaction Executions** view on the **Insights** page ( <InternalLink version="cockroachcloud" path="insights-page#transaction-executions-view">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-insights-page#transaction-executions-view">DB Console</InternalLink> ), look for a transaction with the **High Contention** insight. Click the transaction's execution ID and view the transaction execution details, including the details of the blocking transaction.
* Visit the **Transactions** page ( <InternalLink version="cockroachcloud" path="transactions-page">CockroachDB Cloud Console</InternalLink> and <InternalLink path="ui-transactions-page">DB Console</InternalLink> ) and sort transactions by **Contention Time**.

To view tables and indexes that experienced <InternalLink path="performance-best-practices-overview#transaction-contention">contention</InternalLink>:

* Query the <InternalLink path="crdb-internal#transaction_contention_events">`crdb_internal.transaction_contention_events`</InternalLink> table to view <InternalLink path="crdb-internal">transactions that have blocked other transactions</InternalLink>.
* Query the <InternalLink path="crdb-internal#cluster_contended_tables">`crdb_internal.cluster_contended_tables`</InternalLink> table to <InternalLink path="crdb-internal#view-all-tables-that-have-experienced-contention">view all tables that have experienced contention</InternalLink>.
* Query the <InternalLink path="crdb-internal#cluster_contended_indexes">`crdb_internal.cluster_contended_indexes`</InternalLink> table to <InternalLink path="crdb-internal#view-all-indexes-that-have-experienced-contention">view all indexes that have experienced contention</InternalLink>.
* Query the <InternalLink path="crdb-internal#cluster_contention_events">`crdb_internal.cluster_contention_events`</InternalLink> table to <InternalLink path="crdb-internal">view the tables, indexes, and transactions with the most time under contention</InternalLink>.

##### Reduce transaction contention

<InternalLink path="performance-best-practices-overview#transaction-contention">Contention</InternalLink> is often reported after it has already resolved. Therefore, preventing contention before it affects your cluster's performance is a more effective approach:

* Limit the number of affected rows by following <InternalLink path="apply-statement-performance-rules">optimizing queries</InternalLink> (e.g., avoiding full scans, creating secondary indexes, etc.). Not only will transactions run faster, lock fewer rows, and hold locks for a shorter duration, but the chances of <InternalLink path="architecture/transaction-layer#read-refreshing">read invalidation</InternalLink> when the transaction's <InternalLink path="architecture/transaction-layer#timestamp-cache">timestamp is pushed</InternalLink>, due to a conflicting write, are decreased because of a smaller read set (i.e., a smaller number of rows read).
* Break down larger transactions (e.g., <InternalLink path="bulk-delete-data">bulk deletes</InternalLink>) into smaller ones to have transactions hold locks for a shorter duration. For example, use <InternalLink path="common-table-expressions">common table expressions</InternalLink> to group multiple clauses together in a single SQL statement. This will also decrease the likelihood of <InternalLink path="architecture/transaction-layer#timestamp-cache">pushed timestamps</InternalLink>. For instance, as the size of writes (number of rows written) decreases, the chances of the transaction's timestamp getting bumped by concurrent reads decreases.
* Use <InternalLink path="select-for-update">`SELECT FOR UPDATE`</InternalLink> to aggressively lock rows that will later be updated in the transaction. Updates must operate on the most recent version of a row, so a concurrent write to the row will cause a retry error (<InternalLink path="transaction-retry-error-reference#retry_write_too_old">`RETRY_WRITE_TOO_OLD`</InternalLink>). Locking early in the transaction forces concurrent writers to block until the transaction is finished, which prevents the retry error. Note that this locks the rows for the duration of the transaction; whether this is tenable will depend on your workload. For more information, see [When and why to use `SELECT FOR UPDATE` in CockroachDB](https://www.cockroachlabs.com/blog/when-and-why-to-use-select-for-update-in-cockroachdb).
* Use historical reads (<InternalLink path="as-of-system-time">`SELECT... AS OF SYSTEM TIME`</InternalLink>), preferably <InternalLink path="follower-reads#when-to-use-bounded-staleness-reads">bounded staleness reads</InternalLink> or <InternalLink path="follower-reads#run-queries-that-use-exact-staleness-follower-reads">exact staleness with follower reads</InternalLink> when possible to reduce conflicts with other writes. This reduces the likelihood of <InternalLink path="transaction-retry-error-reference#retry_serializable">`RETRY_SERIALIZABLE`</InternalLink> errors as fewer writes will happen at the historical timestamp. More specifically, writes' timestamps are less likely to be pushed by historical reads as they would <InternalLink path="architecture/transaction-layer#transaction-conflicts">when the read has a higher priority level</InternalLink>. Note that if the `AS OF SYSTEM TIME` value is below the closed timestamp, the read cannot be invalidated.
* When replacing values in a row, use <InternalLink path="upsert">`UPSERT`</InternalLink> and specify values for all columns in the inserted rows. This will usually have the best performance under contention, compared to combinations of <InternalLink path="select-clause">`SELECT`</InternalLink>, <InternalLink path="insert">`INSERT`</InternalLink>, and <InternalLink path="update">`UPDATE`</InternalLink>.
* If applicable to your workload, assign <InternalLink path="column-families#default-behavior">column families</InternalLink> and separate columns that are frequently read and written into separate columns. Transactions will operate on disjoint column families and reduce the likelihood of conflicts.
* As a last resort, consider adjusting the <InternalLink path="architecture/transaction-layer#closed-timestamps">closed timestamp interval</InternalLink> using the `kv.closed_timestamp.target_duration` <InternalLink path="cluster-settings">cluster setting</InternalLink> to reduce the likelihood of long-running write transactions having their <InternalLink path="architecture/transaction-layer#timestamp-cache">timestamps pushed</InternalLink>. This setting should be carefully adjusted if **no other mitigations are available** because there can be downstream implications (e.g., historical reads, change data capture feeds, statistics collection, handling zone configurations, etc.). For example, a transaction *A* is forced to refresh (i.e., change its timestamp) due to hitting the maximum <InternalLink path="architecture/transaction-layer#closed-timestamps">*closed timestamp*</InternalLink> interval (closed timestamps enable <InternalLink path="follower-reads#how-stale-follower-reads-work">Follower Reads</InternalLink> and <InternalLink path="change-data-capture-overview">Change Data Capture (CDC)</InternalLink>). This can happen when transaction *A* is a long-running transaction, and there is a write by another transaction to data that *A* has already read.

<Note>
  If you increase the `kv.closed_timestamp.target_duration` setting, it means that you are increasing the amount of time by which the data available in <InternalLink path="follower-reads">Follower Reads</InternalLink> and <InternalLink path="change-data-capture-overview">CDC changefeeds</InternalLink> lags behind the current state of the cluster. In other words, there is a trade-off here: if you absolutely must execute long-running transactions that execute concurrently with other transactions that are writing to the same data, you may have to settle for longer delays on Follower Reads and/or CDC to avoid frequent serialization errors. The anomaly that would be exhibited if these transactions were not retried is called [write skew](https://www.cockroachlabs.com/blog/what-write-skew-looks-like).
</Note>

### Hot spots

<InternalLink path="performance-best-practices-overview#hot-spots">Hot spots</InternalLink> are a symptom of *resource contention* and can create problems as requests increase, including excessive [transaction contention](#transaction-contention).

#### Indicators that your cluster has hot spots

* The **CPU Percent** graph on the <InternalLink path="ui-hardware-dashboard">**Hardware**</InternalLink> and <InternalLink path="ui-overload-dashboard">**Overload**</InternalLink> dashboards (DB Console) shows spikes in CPU usage.
* The **Hot Ranges** list on the <InternalLink path="ui-hot-ranges-page">**Hot Ranges** page</InternalLink> (DB Console) displays a higher-than-expected QPS for a range.
* The <InternalLink path="ui-key-visualizer">**Key Visualizer**</InternalLink> (DB Console) shows <InternalLink path="ui-key-visualizer#identifying-hot-spots">ranges with much higher-than-average write rates</InternalLink> for the cluster.

If you find hot spots, use the <InternalLink path="ui-hot-ranges-page#range-report">**Range Report**</InternalLink> and <InternalLink path="ui-key-visualizer">**Key Visualizer**</InternalLink> to identify the ranges with excessive traffic. Then take steps to [reduce hot spots](#reduce-hot-spots).

#### Reduce hot spots

* Use index keys with a random distribution of values, so that transactions over different rows are more likely to operate on separate data ranges. See the <InternalLink path="sql-faqs">SQL FAQs</InternalLink> on row IDs for suggestions.
* Place parts of the records that are modified by different transactions in different tables. That is, increase [normalization](https://wikipedia.org/wiki/Database_normalization). However, there are benefits and drawbacks to increasing normalization.
  * Benefits of increasing normalization:
    * Can improve performance for write-heavy workloads. This is because, with increased normalization, a given business fact must be written to one place rather than to multiple places.
    * Allows separate transactions to modify related underlying data without causing [contention](#transaction-contention).
    * Reduces the chance of data inconsistency, since a given business fact must be written only to one place.
    * Reduces or eliminates data redundancy.
    * Uses less disk space.
  * Drawbacks of increasing normalization:
    * Can reduce performance for read-heavy workloads. This is because increasing normalization results in more joins, and can make the SQL more complicated in other ways.
    * More complex data model.
  * In general:
    * Increase normalization for write-intensive and read/write-intensive transactional workloads.
    * Do not increase normalization for read-intensive reporting workloads.
* If the application strictly requires operating on very few different index keys, consider using <InternalLink path="alter-table#split-at">`ALTER... SPLIT AT`</InternalLink> so that each index key can be served by a separate group of nodes in the cluster.
* If you are working with a table that **must** be indexed on sequential keys, consider using <InternalLink path="hash-sharded-indexes">hash-sharded indexes</InternalLink>. For details about the mechanics and performance improvements of hash-sharded indexes in CockroachDB, see the blog post [Hash Sharded Indexes Unlock Linear Scaling for Sequential Workloads](https://www.cockroachlabs.com/blog/hash-sharded-indexes-unlock-linear-scaling-for-sequential-workloads). As part of this, we recommend doing thorough performance testing with and without hash-sharded indexes to see which works best for your application.
* To avoid read hot spots:
  * Increase data distribution, which will allow for more ranges. The hot spot exists because the data being accessed is all co-located in one range.
  * Increase <InternalLink path="recommended-production-settings#load-balancing">load balancing</InternalLink> across more nodes in the same range. Most transactional reads must go to the leaseholder in CockroachDB, which means that opportunities for load balancing over replicas are minimal.

    However, the following features do permit load balancing over replicas:

    * <InternalLink path="global-tables">Global tables</InternalLink>.
    * <InternalLink path="follower-reads">Follower reads</InternalLink> (both the bounded staleness and the exact staleness kinds).

      In these cases, more replicas will help, up to the number of nodes in the cluster.

### Statements with full table scans

Full table scans often result in poor statement performance.

#### Indicators that your application has statements with full table scans

* The following query returns statements with full table scans in their statement plan:

  ```sql theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
  SHOW FULL TABLE SCANS;
  ```
* The following query against the `crdb_internal.node_statement_statistics` table returns results:

  ```sql theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
  SELECT count(*) as total_full_scans
  FROM crdb_internal.node_statement_statistics
  WHERE full_scan = true;
  ```
* Viewing the statement plan on the <InternalLink path="ui-statements-page#statement-fingerprint-page">**Statement Fingerprint** page</InternalLink> in the DB Console indicates that the plan contains full table scans.
* The statement plans returned by the <InternalLink path="sql-tuning-with-explain">`EXPLAIN`</InternalLink> and <InternalLink path="explain-analyze">`EXPLAIN ANALYZE` commands</InternalLink> indicate that there are full table scans.
* The <InternalLink path="ui-sql-dashboard">Full Table/Index Scans graph</InternalLink> in the DB Console is showing spikes over time.

#### Fix full table scans in statements

Not every full table scan is an indicator of poor performance. The <InternalLink path="cost-based-optimizer">cost-based optimizer</InternalLink> may decide on a full table scan when other <InternalLink path="indexes">index</InternalLink> or <InternalLink path="joins">join scans</InternalLink> would result in longer execution time.

<InternalLink path="sql-tuning-with-explain">Examine the statements</InternalLink> that result in full table scans and consider adding <InternalLink path="schema-design-indexes#create-a-secondary-index">secondary indexes</InternalLink>.

In the DB Console, visit the <InternalLink path="ui-insights-page#schema-insights-tab">**Schema Insights** tab</InternalLink> on the <InternalLink path="ui-insights-page">**Insights** page</InternalLink> and check if there are any insights to create missing indexes. These missing index recommendations are generated based on <InternalLink path="ui-insights-page#detect-slow-executions">slow statement execution</InternalLink>. A missing index may cause a statement to have a <InternalLink path="ui-insights-page#suboptimal-plan">suboptimal plan</InternalLink>. If the execution was slow, based on the insights threshold, then it's likely the create index recommendation is valid. If the plan had a full table scan, it's likely that it should be removed with an index.

Also see <InternalLink path="performance-best-practices-overview#table-scan-best-practices">Table scans best practices</InternalLink>.

### Suboptimal primary keys

#### Indicators that your tables are using suboptimal primary keys

* The <InternalLink path="ui-hardware-dashboard">Hardware metrics dashboard</InternalLink> in the DB Console shows high resource usage per node.
* The Problem Ranges report on the <InternalLink path="ui-debug-pages">Advanced Debug page</InternalLink> in the DB Console indicates a high number of queries per second on a subset of ranges or nodes.

#### Fix suboptimal primary keys

Evaluate the schema of your table to see if you can redistribute data more evenly across multiple ranges. Specifically, make sure you have followed <InternalLink path="schema-design-table#primary-key-best-practices">best practices when selecting your primary key</InternalLink>.

If your application with a small dataset (for example, a dataset that contains few index key values) is experiencing resource contention, consider splitting your tables and indexes to <InternalLink path="alter-table#split-a-table">distribute ranges across multiple nodes</InternalLink> to reduce resource contention.

### Slow writes

#### Indicators that your tables are experiencing slow writes

If the <InternalLink path="ui-overview-dashboard">Overview dashboard</InternalLink> in the DB Console shows high service latency when the QPS of `INSERT` and `UPDATE` statements is high, your tables are experiencing slow writes.

#### Fix slow writes

<InternalLink path="schema-design-indexes">Secondary indexes</InternalLink> can improve application read performance. However, there is overhead in maintaining secondary indexes that can affect your write performance. You should profile your tables periodically to determine whether an index is worth the overhead. To identify infrequently accessed indexes that could be candidates to drop, do one of the following:

* In the DB Console, visit the <InternalLink path="ui-insights-page#schema-insights-tab">**Schema Insights** tab</InternalLink> on the <InternalLink path="ui-insights-page">**Insights** page</InternalLink> and check if there are any insights to drop unused indexes.
* In the DB Console, visit the <InternalLink path="ui-databases-page">**Databases** page</InternalLink> and check databases and tables for <InternalLink path="ui-databases-page#index-recommendations">**Index Recommendations**</InternalLink> to drop unused indexes.
* Run a join query against the <InternalLink path="crdb-internal#index_usage_statistics">`crdb_internal.index_usage_statistics`</InternalLink> and `crdb_internal.table_indexes` tables:

  ```sql theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
  SELECT ti.descriptor_name as table_name, ti.index_name, total_reads, last_read
  FROM crdb_internal.index_usage_statistics AS us
  JOIN crdb_internal.table_indexes ti
  ON us.index_id = ti.index_id
  AND us.table_id = ti.descriptor_id
  ORDER BY total_reads ASC;
  ```

  ```text theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
                table_name     |                  index_name                   | total_reads |           last_read
  -----------------------------+-----------------------------------------------+-------------+--------------------------------
    vehicle_location_histories | vehicle_location_histories_pkey               |           1 | 2021-09-28 22:59:03.324398+00
    rides                      | rides_auto_index_fk_city_ref_users            |           1 | 2021-09-28 22:59:01.500962+00
    rides                      | rides_auto_index_fk_vehicle_city_ref_vehicles |           1 | 2021-09-28 22:59:02.470526+00
    user_promo_codes           | user_promo_codes_pkey                         |         456 | 2021-09-29 00:01:17.063418+00
    promo_codes                | promo_codes_pkey                              |         910 | 2021-09-29 00:01:17.062319+00
    vehicles                   | vehicles_pkey                                 |        3591 | 2021-09-29 00:01:18.261658+00
    users                      | users_pkey                                    |        5401 | 2021-09-29 00:01:18.260198+00
    rides                      | rides_pkey                                    |       45658 | 2021-09-29 00:01:18.258208+00
    vehicles                   | vehicles_auto_index_fk_city_ref_users         |       87119 | 2021-09-29 00:01:19.071476+00
  (9 rows)
  ```

  Use the values in the `total_reads` and `last_read` columns to identify indexes that have low usage or are stale and can be dropped.

### Too many MVCC values

#### Indicators that your tables have too many MVCC values

In the <InternalLink path="ui-databases-page#tables-view">Databases</InternalLink> page in the DB Console, the Tables view shows the percentage of live data for each table. For example:

<img src="https://mintcdn.com/cockroachlabs/6qI-DTIWMnaeNI5F/images/v24.1/ui_databases_live_data.png?fit=max&auto=format&n=6qI-DTIWMnaeNI5F&q=85&s=11b7b182f2fe36480530851ed4da1c03" alt="Table live data" width="1114" height="626" data-path="images/v24.1/ui_databases_live_data.png" />

In this example, at `37.3%` the `vehicles` table would be considered to have a low percentage of live data. In the worst cases, the percentage can be `0%`.

A low percentage of live data can cause statements to scan more data (<InternalLink path="architecture/storage-layer#mvcc">MVCC values</InternalLink>) than required, which can reduce performance.

#### Configure CockroachDB to purge MVCC values

Reduce the <InternalLink path="configure-replication-zones">`gc.ttlseconds`</InternalLink> zone configuration of the table as much as possible.

## See also

If you aren't sure whether SQL query performance needs to be improved, see <InternalLink path="query-behavior-troubleshooting#identify-slow-queries">Identify slow queries</InternalLink>.
