Skip to content

Commit 774dc87

Browse files
committed
update hint injection docs for 26.1; add SHOW STATEMENT HINTS page
1 parent 70ebd1d commit 774dc87

9 files changed

Lines changed: 235 additions & 109 deletions

File tree

src/current/_includes/v25.4/misc/force-index-selection.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
By using the explicit index annotation, you can override [CockroachDB's index selection](https://www.cockroachlabs.com/blog/index-selection-cockroachdb-2/) and use a specific [index]({% link {{ page.version.version }}/indexes.md %}) when reading from a named table. This is called an *index hint*.
1+
By using the explicit index annotation, you can override [CockroachDB's index selection](https://www.cockroachlabs.com/blog/index-selection-cockroachdb-2/) and use a specific [index]({% link {{ page.version.version }}/indexes.md %}) when reading from a named table.
22

3+
{{site.data.alerts.callout_info}}
34
Index selection can impact [performance]({% link {{ page.version.version }}/performance-best-practices-overview.md %}), but does not change the result of a query.
4-
5-
{{site.data.alerts.callout_success}}
6-
You can apply index hints without modifying the original query text. Refer to [Hint injection]({% link {{ page.version.version }}/cost-based-optimizer.md %}#hint-injection).
75
{{site.data.alerts.end}}
86

97
##### Force index scan

src/current/_includes/v26.1/misc/force-index-selection.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
By using the explicit index annotation, you can override [CockroachDB's index selection](https://www.cockroachlabs.com/blog/index-selection-cockroachdb-2/) and use a specific [index]({% link {{ page.version.version }}/indexes.md %}) when reading from a named table.
1+
By using the explicit index annotation, you can override [CockroachDB's index selection](https://www.cockroachlabs.com/blog/index-selection-cockroachdb-2/) and use a specific [index]({% link {{ page.version.version }}/indexes.md %}) when reading from a named table. This is called an *index hint*.
22

3-
{{site.data.alerts.callout_info}}
43
Index selection can impact [performance]({% link {{ page.version.version }}/performance-best-practices-overview.md %}), but does not change the result of a query.
4+
5+
{{site.data.alerts.callout_success}}
6+
{% include_cached new-in.html version="v26.1" %} You can use [hint injection]({% link {{ page.version.version }}/cost-based-optimizer.md %}#hint-injection) to apply index hints without modifying the original query text.
57
{{site.data.alerts.end}}
68

79
##### Force index scan

src/current/_includes/v26.1/sidebar-data/sql.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,12 @@
796796
"/${VERSION}/show-sessions.html"
797797
]
798798
},
799+
{
800+
"title": "<code>SHOW STATEMENT HINTS</code>",
801+
"urls": [
802+
"/${VERSION}/show-statement-hints.html"
803+
]
804+
},
799805
{
800806
"title": "<code>SHOW STATEMENTS</code>",
801807
"urls": [

src/current/v25.4/cost-based-optimizer.md

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,6 @@ Due to SQL's implicit `AS` syntax, you cannot specify a join hint with only the
419419

420420
For a join hint example, see [Use the right join type]({% link {{ page.version.version }}/apply-statement-performance-rules.md %}#rule-3-use-the-right-join-type).
421421

422-
{{site.data.alerts.callout_success}}
423-
You can apply join hints without modifying the original query text. Refer to [Hint injection](#hint-injection).
424-
{{site.data.alerts.end}}
425-
426422
### Supported join algorithms
427423

428424
- `HASH`: Forces a hash join; in other words, it disables merge and lookup joins. A hash join is always possible, even if there are no equality columns: CockroachDB treats a nested loop join without an index as a special case of a hash join, where the hash table effectively has one bucket.
@@ -461,105 +457,6 @@ To make the optimizer prefer lookup joins to merge joins when performing foreign
461457

462458
- You should reconsider hint usage with each new release of CockroachDB. Due to improvements in the optimizer, hints specified to work with an older version may cause decreased performance in a newer version.
463459

464-
## Hint injection
465-
466-
<span class="version-tag">New in v25.4.1:</span> *Hint injection* allows you to apply [index hints]({% link {{ page.version.version }}/table-expressions.md %}#force-index-selection) and [join hints](#join-hints) without modifying the original query text. This is useful when you cannot modify application code, need to optimize queries from ORMs or third-party applications, or want to test different hints without changing production queries.
467-
468-
Instead of relying on inline hints (such as `SELECT * FROM table@index_name` or `INNER HASH JOIN`), hint injection stores hints in the `system.statement_hints` table and automatically applies them to statements that match a [fingerprint]({% link {{ page.version.version }}/ui-statements-page.md %}#sql-statement-fingerprints). To inject a hint, invoke the `crdb_internal.inject_hint()` function with a SQL statement fingerprint and a matching statement with hints applied:
469-
470-
{% include_cached copy-clipboard.html %}
471-
~~~ sql
472-
SELECT crdb_internal.inject_hint(
473-
'{statement fingerprint}',
474-
'{statement with hints}'
475-
);
476-
~~~
477-
478-
For example, the following invocation stores the `users_email_idx` hint in the `system.statement_hints` table:
479-
480-
{% include_cached copy-clipboard.html %}
481-
~~~ sql
482-
SELECT crdb_internal.inject_hint(
483-
'SELECT * FROM users WHERE email = _',
484-
'SELECT * FROM users@users_email_idx WHERE email = _'
485-
);
486-
~~~
487-
488-
{{site.data.alerts.callout_info}}
489-
The statement with hints must have the same syntactic structure as the statement fingerprint. Constants in the second parameter are ignored, and only hints are extracted and applied.
490-
{{site.data.alerts.end}}
491-
492-
The function returns a unique hint ID:
493-
494-
~~~
495-
crdb_internal.inject_hint
496-
-----------------------------
497-
1119388019656228865
498-
~~~
499-
500-
Afterward, any query matching the `SELECT * FROM users WHERE email = _` fingerprint will use the `users_email_idx` index, regardless of the `email` value.
501-
502-
Similarly, to force a specific join algorithm:
503-
504-
{% include_cached copy-clipboard.html %}
505-
~~~ sql
506-
SELECT crdb_internal.inject_hint(
507-
'SELECT * FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.id',
508-
'SELECT * FROM orders AS o INNER HASH JOIN customers AS c ON o.customer_id = c.id'
509-
);
510-
~~~
511-
512-
You can inject both index and join hints in a single statement:
513-
514-
{% include_cached copy-clipboard.html %}
515-
~~~ sql
516-
SELECT crdb_internal.inject_hint(
517-
'SELECT * FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.id WHERE o.status = _',
518-
'SELECT * FROM orders@orders_status_idx AS o INNER LOOKUP JOIN customers@primary AS c ON o.customer_id = c.id WHERE o.status = _'
519-
);
520-
~~~
521-
522-
Hint injection supports all inline hint types, including:
523-
524-
- [Index hints]({% link {{ page.version.version }}/table-expressions.md %}#force-index-selection): `@index_name`, `@{FORCE_INDEX=idx}`, `@{FORCE_INDEX=idx,DESC}`, `@{NO_FULL_SCAN}`, `@{AVOID_FULL_SCAN}`, `@{NO_ZIGZAG_JOIN}`, `@{FORCE_ZIGZAG}`
525-
- [Join hints](#join-hints): `HASH`, `MERGE`, `LOOKUP`, `INVERTED`, `STRAIGHT`
526-
527-
### Manage injected hints
528-
529-
To view all injected hints, query the `system.statement_hints` table:
530-
531-
{% include_cached copy-clipboard.html %}
532-
~~~ sql
533-
SELECT row_id, fingerprint, created_at FROM system.statement_hints;
534-
~~~
535-
536-
~~~
537-
row_id | fingerprint | created_at
538-
----------------------+------------------------------------------------------------------------------------------------+--------------------------------
539-
1119388019656228865 | SELECT * FROM users WHERE email = _ | 2025-10-28 19:41:53.416787+00
540-
1119394099211304961 | SELECT * FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.id | 2025-10-28 20:12:48.75263+00
541-
1119397773284343809 | SELECT * FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.id WHERE o.status = _ | 2025-10-28 20:31:29.990728+00
542-
(3 rows)
543-
~~~
544-
545-
{{site.data.alerts.callout_success}}
546-
The `hint` column is stored in a format that is not human-readable. Use the `fingerprint` column to identify which hints are stored.
547-
{{site.data.alerts.end}}
548-
549-
To remove a hint by its hint ID:
550-
551-
{% include_cached copy-clipboard.html %}
552-
~~~ sql
553-
DELETE FROM system.statement_hints WHERE row_id = {hint_id};
554-
~~~
555-
556-
To remove all hints for a specific query fingerprint:
557-
558-
{% include_cached copy-clipboard.html %}
559-
~~~ sql
560-
DELETE FROM system.statement_hints WHERE fingerprint = '{statement_fingerprint}';
561-
~~~
562-
563460
## Zigzag joins
564461

565462
The optimizer may plan a zigzag join when there are at least **two secondary indexes on the same table** and the table is filtered in a query with at least two filters constraining different attributes to a constant. A zigzag join works by "zigzagging" back and forth between two indexes and returning only rows with matching primary keys within a specified range. For example:

src/current/v26.1/cost-based-optimizer.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ Due to SQL's implicit `AS` syntax, you cannot specify a join hint with only the
419419

420420
For a join hint example, see [Use the right join type]({% link {{ page.version.version }}/apply-statement-performance-rules.md %}#rule-3-use-the-right-join-type).
421421

422+
{{site.data.alerts.callout_success}}
423+
{% include_cached new-in.html version="v26.1" %} You can use [hint injection](#hint-injection) to apply join hints without modifying the original query text.
424+
{{site.data.alerts.end}}
425+
422426
### Supported join algorithms
423427

424428
- `HASH`: Forces a hash join; in other words, it disables merge and lookup joins. A hash join is always possible, even if there are no equality columns: CockroachDB treats a nested loop join without an index as a special case of a hash join, where the hash table effectively has one bucket.
@@ -457,6 +461,139 @@ To make the optimizer prefer lookup joins to merge joins when performing foreign
457461

458462
- You should reconsider hint usage with each new release of CockroachDB. Due to improvements in the optimizer, hints specified to work with an older version may cause decreased performance in a newer version.
459463

464+
## Hint injection
465+
466+
{% include_cached new-in.html version="v26.1" %} *Hint injection* allows you to apply [index hints]({% link {{ page.version.version }}/table-expressions.md %}#force-index-selection) and [join hints](#join-hints) without modifying the original query text. This is useful when you cannot modify application code, need to optimize queries from ORMs or third-party applications, or want to test different hints without changing queries in production.
467+
468+
Hint injection supports all inline hint types. Instead of relying on inline hints (such as `SELECT * FROM table@index_name` or `INNER HASH JOIN`), hint injection stores hints in the `system.statement_hints` table and automatically applies them to statements that match a [fingerprint]({% link {{ page.version.version }}/ui-statements-page.md %}#sql-statement-fingerprints).
469+
470+
### Inject hints
471+
472+
{{site.data.alerts.callout_info}}
473+
To inject hints using `crdb_internal.inject_hint()`, users must have the [`REPAIRCLUSTER`]({% link {{ page.version.version }}/security-reference/authorization.md %}#supported-privileges) privilege.
474+
{{site.data.alerts.end}}
475+
476+
To inject a hint, invoke the `crdb_internal.inject_hint()` built-in function with two matching SQL statement fingerprints: a fingerprint identifying which queries to optimize, and a fingerprint with hints that must be applied:
477+
478+
{% include_cached copy-clipboard.html %}
479+
~~~ sql
480+
SELECT crdb_internal.inject_hint(
481+
'{fingerprint}',
482+
'{fingerprint with hints}'
483+
);
484+
~~~
485+
486+
Both fingerprints in the function invocation **must** have the same syntactic structure and use underscores (`_`) as placeholders for constants. For example, use `SELECT * FROM users WHERE city = _` rather than `SELECT * FROM users WHERE city = 'new york'`.
487+
488+
For example, the following invocation stores the `users_city_idx` index hint in the `system.statement_hints` table, returning a unique hint ID:
489+
490+
{% include_cached copy-clipboard.html %}
491+
~~~ sql
492+
SELECT crdb_internal.inject_hint(
493+
'SELECT * FROM users WHERE city = _',
494+
'SELECT * FROM users@users_city_idx WHERE city = _'
495+
);
496+
~~~
497+
498+
~~~
499+
crdb_internal.inject_hint
500+
-----------------------------
501+
1143498239153340417
502+
~~~
503+
504+
Afterward, any executed statement that matches the `SELECT * FROM users WHERE city = _` fingerprint will first be rewritten to use the `users_city_idx` index, regardless of the `city` value.
505+
506+
{{site.data.alerts.callout_info}}
507+
If a query already contains explicit inline hints (such as `SELECT * FROM users@primary WHERE city = 'new york'`), those inline hints will take precedence over the injected hints.
508+
{{site.data.alerts.end}}
509+
510+
To inject a specific [join algorithm](#supported-join-algorithms):
511+
512+
{% include_cached copy-clipboard.html %}
513+
~~~ sql
514+
SELECT crdb_internal.inject_hint(
515+
'SELECT * FROM users AS u INNER JOIN rides AS r ON u.id = r.rider_id',
516+
'SELECT * FROM users AS u INNER MERGE JOIN rides AS r ON u.id = r.rider_id'
517+
);
518+
~~~
519+
520+
{{site.data.alerts.callout_success}}
521+
If multiple hints exist for the same statement fingerprint, only the **most recently created hint** is applied. To replace an existing hint, you can create a new one for the same fingerprint, which will automatically take precedence.
522+
{{site.data.alerts.end}}
523+
524+
### View injected hints
525+
526+
{{site.data.alerts.callout_info}}
527+
To view hints using [`SHOW STATEMENT HINTS`]({% link {{ page.version.version }}/show-statement-hints.md %}), users must have the [`VIEWCLUSTERMETADATA`]({% link {{ page.version.version }}/security-reference/authorization.md %}#supported-privileges) privilege.
528+
{{site.data.alerts.end}}
529+
530+
Use the [`SHOW STATEMENT HINTS`]({% link {{ page.version.version }}/show-statement-hints.md %}) statement to view hints for a specific statement fingerprint:
531+
532+
{% include_cached copy-clipboard.html %}
533+
~~~ sql
534+
SHOW STATEMENT HINTS FOR 'SELECT * FROM users WHERE city = _' WITH DETAILS;
535+
~~~
536+
537+
~~~
538+
row_id | fingerprint | hint_type | created_at | details
539+
----------------------+------------------------------------+----------------------+-------------------------------+--------------------------------------------------------------------
540+
1143470380756697089 | SELECT * FROM users WHERE city = _ | rewrite_inline_hints | 2026-01-21 21:11:06.782818+00 | {"donorSql": "SELECT * FROM users@users_city_idx WHERE city = _"}
541+
~~~
542+
543+
You can also query the `system.statement_hints` table directly to view all injected hints:
544+
545+
{% include_cached copy-clipboard.html %}
546+
~~~ sql
547+
SELECT row_id, fingerprint, created_at FROM system.statement_hints;
548+
~~~
549+
550+
~~~
551+
row_id | fingerprint | created_at
552+
----------------------+--------------------------------------------------------------------------------------+--------------------------------
553+
1143498239153340417 | SELECT * FROM users WHERE city = _ | 2026-01-21 23:32:48.490786+00
554+
1143498276700913665 | SELECT * FROM users AS u INNER JOIN rides AS r ON u.id = r.rider_id | 2026-01-21 23:32:59.949608+00
555+
(2 rows)
556+
~~~
557+
558+
To verify that injected hints are being applied to your queries, use [`EXPLAIN`]({% link {{ page.version.version }}/explain.md %}) or [`EXPLAIN ANALYZE`]({% link {{ page.version.version }}/explain-analyze.md %}). When hints from `system.statement_hints` are applied, the output includes a `statement hints count` field showing the number of hints applied. For example:
559+
560+
{% include_cached copy-clipboard.html %}
561+
~~~ sql
562+
EXPLAIN SELECT * FROM users WHERE city = 'new york';
563+
~~~
564+
565+
~~~
566+
info
567+
-------------------------------------------------------------------------------
568+
distribution: local
569+
statement hints count: 1
570+
571+
• scan
572+
estimated row count: 6 (12% of the table; stats collected 22 minutes ago)
573+
table: users@users_pkey
574+
spans: [/'new york' - /'new york']
575+
~~~
576+
577+
{{site.data.alerts.callout_info}}
578+
The `statement hints count` field shows the number of individual hints applied. A single row in `system.statement_hints` can contain multiple hints (such as an index hint and a join hint).
579+
{{site.data.alerts.end}}
580+
581+
### Remove injected hints
582+
583+
To remove a hint by its hint ID:
584+
585+
{% include_cached copy-clipboard.html %}
586+
~~~ sql
587+
DELETE FROM system.statement_hints WHERE row_id = {hint_id};
588+
~~~
589+
590+
To remove all hints for a specific query fingerprint:
591+
592+
{% include_cached copy-clipboard.html %}
593+
~~~ sql
594+
DELETE FROM system.statement_hints WHERE fingerprint = '{statement_fingerprint}';
595+
~~~
596+
460597
## Zigzag joins
461598

462599
The optimizer may plan a zigzag join when there are at least **two secondary indexes on the same table** and the table is filtered in a query with at least two filters constraining different attributes to a constant. A zigzag join works by "zigzagging" back and forth between two indexes and returning only rows with matching primary keys within a specified range. For example:

src/current/v26.1/explain-analyze.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Property | Description
6868
`execution time` | The time it took for the final statement plan to complete.
6969
`distribution` | Whether the statement was distributed or local. If `distribution` is `full`, execution of the statement is performed by multiple nodes in parallel, then the results are returned by the gateway node. If `local`, the execution plan is performed only on the gateway node. Even if the execution plan is `local`, row data may be fetched from remote nodes, but the processing of the data is performed by the local node.
7070
`plan type` | The plan type used by the query: `generic, re-optimized`, `generic, reused`, or `custom`. For details, refer to [Query plan type]({% link {{ page.version.version }}/cost-based-optimizer.md %}#query-plan-type).
71+
`statement hints count` | <span class="version-tag">New in v26.1:</span> The number of [injected hints]({% link {{ page.version.version }}/cost-based-optimizer.md %}#hint-injection) from `system.statement_hints` applied to the statement. This field is only displayed when hints are applied.
7172
`vectorized` | Whether the [vectorized execution engine]({% link {{ page.version.version }}/vectorized-execution.md %}) was used in this statement.
7273
`rows decoded from KV` | The number of rows read from the [storage layer]({% link {{ page.version.version }}/architecture/storage-layer.md %}).
7374
`cumulative time spent in KV` | The total amount of time spent in the storage layer.

src/current/v26.1/explain.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Node details | The properties, columns, and ordering details for the current sta
6767
|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
6868
| `distribution` | Whether the statement was distributed or local. If `distribution` is `full`, execution of the statement is performed by multiple nodes in parallel, then the results are returned by the gateway node. If `local`, the execution plan is performed only on the gateway node. Even if the execution plan is `local`, row data may be fetched from remote nodes, but the processing of the data is performed by the local node. |
6969
| `vectorized` | Whether the [vectorized execution engine]({% link {{ page.version.version }}/vectorized-execution.md %}) was used in this statement. |
70+
| `statement hints count` | <span class="version-tag">New in v26.1:</span> The number of [injected hints]({% link {{ page.version.version }}/cost-based-optimizer.md %}#hint-injection) from `system.statement_hints` applied to the statement. This field is only displayed when hints are applied. |
7071

7172

7273
### Statement plan tree properties

0 commit comments

Comments
 (0)