Indexing a LIKE condition with a b-tree is only possible when the pattern does not begin with a wildcard. A pattern such as %smith may match entries anywhere in the ordered list, so the index would have to be scanned in full. A prefix pattern is different: values that start with the same characters sit next to each other, just as “Black”, “Blackthorn” and “Blacksmith” do in an alphabetically sorted phone book.
That much is true in any database. The differences between PostgreSQL and Oracle show up once collations enter the picture — an area that regularly surprises users coming from Oracle, who expect a plain b-tree index to support LIKE as a matter of course.
Setting up the PostgreSQL example
The test table holds the same strings in three columns, each declared with a different collation: the binary collation C (also known as POSIX), which compares strings byte by byte and never changes; Czech as provided by the operating system's C library; and Czech as provided by ICU.
PostgreSQL does not implement collations itself, with the exception of C; it delegates to the C library or to ICU, a design choice that has its own failure modes.
|
1 2 3 4 5 6 |
CREATE TABLE likeme ( id integer PRIMARY KEY, s_binary text COLLATE 'C' NOT NULL, s_libc text COLLATE 'cs_CZ.utf8' NOT NULL, s_icu text COLLATE 'cs-CZ-x-icu' NOT NULL ); |
A handful of rows are inserted, the table is then padded with enough unimportant data that the optimizer will prefer an index scan when one is available, and finally VACUUM and ANALYZE set hint bits and collect statistics.
|
1 2 3 4 5 6 7 8 9 10 |
INSERT INTO likeme (id, s_binary, s_libc, s_icu) VALUES (1, 'abc', 'abc', 'abc'), (2, 'abč', 'abč', 'abč'), (3, 'abb', 'abb', 'abb'), (4, 'abd', 'abd', 'abd'), (5, 'abcm', 'abcm', 'abcm'), (6, 'abch', 'abch', 'abch'), (7, 'abh', 'abh', 'abh'), (8, 'abz', 'abz', 'abz'), (9, 'Nesmysl', 'Nesmysl', 'Nesmysl'); |
|
1 2 3 |
INSERT INTO likeme (id, s_binary, s_libc, s_icu) SELECT i, 'z'||i, 'z'||i, 'z'||i FROM generate_series(10, 10000) AS i; |
|
1 |
VACUUM (ANALYZE) likeme; |
The binary collation
Under C, indexing LIKE is trivial — a regular b-tree index suffices. PostgreSQL scans the index for everything between 'abc' and 'abd' and removes false positives with a filter, of which there happen to be none here.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE INDEX binary_ind ON likeme (s_binary); EXPLAIN (COSTS OFF) SELECT s_binary FROM likeme WHERE s_binary LIKE 'abc%'; QUERY PLAN ════════════════════════════════════════════════════════════════════════ Index Scan using binary_ind on likeme Index Cond: ((s_binary >= 'abc'::text) AND (s_binary < 'abd'::text)) Filter: (s_binary ~~ 'abc%'::text) (3 rows) |
The catch is sorting. ORDER BY places the upper-case “N” before the lower-case “a”, and č after every ASCII character. That is unusable for natural language ordering, but if no sorting of strings is required, C is the best collation to use.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
SELECT s_binary FROM likeme ORDER BY s_binary FETCH FIRST 9 ROWS ONLY; s_binary ══════════ Nesmysl abb abc abch abcm abd abh abz abč (9 rows) |
Natural language collations and the prefix problem
With ICU-backed s_icu (the libc column behaves identically), sorting is correct. Note where 'abch' lands: after 'abh'. In Czech, the digraph “ch” sorts as a single character, which is why the example uses Czech at all — evidence that natural language collations can be very complicated.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
SELECT s_icu FROM likeme ORDER BY s_icu FETCH FIRST 9 ROWS ONLY; s_icu ═════════ abb abc abcm abč abd abh abch abz Nesmysl (9 rows) |
That peculiarity does not change what LIKE means; the SQL standard defines the predicate in ISO/IEC 9075-2, section 8.5 (<like_predicate>).
|
1 2 3 4 5 6 7 8 |
SELECT s_icu FROM likeme WHERE s_icu LIKE 'abc%'; s_icu ═══════ abc abch abcm (3 rows) |
An index on the column, however, is ignored by PostgreSQL.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE INDEX icu_ind ON likeme (s_icu); EXPLAIN (COSTS OFF) SELECT s_icu FROM likeme WHERE s_icu LIKE 'abc%'; QUERY PLAN ═══════════════════════════════════ Seq Scan on likeme Filter: (s_icu ~~ 'abc%'::text) (2 rows) |
The reason is the sorting above: scanning the index range the way C allowed would never find 'abch', because that string lives elsewhere in the order. With some natural language collations, knowing a prefix does not let you locate a string in the index. Not every collation is that complex, but PostgreSQL cannot inspect the internals of collations defined by external libraries, so it must play safe and skip them for this access path.
The PostgreSQL workaround: text_pattern_ops
PostgreSQL lets an index definition name an operator class, fixing the set of comparison operators the index supports. The operator class text_pattern_ops compares strings character by character, which is precisely what LIKE needs, and the execution plan shows those character-wise operators in use.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE INDEX icu_like_idx ON likeme (s_icu text_pattern_ops); EXPLAIN (COSTS OFF) SELECT s_icu FROM likeme WHERE s_icu LIKE 'abc%'; QUERY PLAN ══════════════════════════════════════════════════════════════════════ Index Only Scan using icu_like_idx on likeme Index Cond: ((s_icu ~>=~ 'abc'::text) AND (s_icu ~<~ 'abd'::text)) Filter: (s_icu ~~ 'abc%'::text) (3 rows) |
Such an index does more than support LIKE: it also serves equality comparisons. It is usually the only index a string column needs, unless ORDER BY must be indexed, and that genuinely requires the natural language index.
Reproducing the example in Oracle
The Oracle table mirrors the PostgreSQL one, with string columns declared NOT NULL so that Oracle can use an index to speed up ORDER BY — Oracle stores no index entry in which every column is NULL.
|
1 2 3 4 5 6 |
CREATE TABLE likeme ( id NUMBER(5) CONSTRAINT likeme_pkey PRIMARY KEY, s_binary VARCHAR2(100 CHAR) NOT NULL, s_czech VARCHAR2(100 CHAR) COLLATE CZECH NOT NULL, s_xczech VARCHAR2(100 CHAR) COLLATE XCZECH NOT NULL ); |
Oracle defaults to the binary collation, which already explains much of the “a plain index is enough” folklore. Two further columns carry the natural language collations Oracle offers:
CZECH, a reduced form that implements only part of the expected behaviourXCZECH, which behaves as a natural language collation should
The Oracle documentation describes the extended variants this way:
Some monolingual collations have an extended version that handles special linguistic cases. The name of the extended version is prefixed with the letter
X. These special cases typically mean that one character is sorted like a sequence of two characters or a sequence of two characters is sorted as one character. For example,chandllare treated as a single character inXSPANISH. Extended monolingual collations may also define special language-specific uppercase and lowercase rules that override standard rules of a character set.
Loading the data is straightforward: export from PostgreSQL with pg_dump and feed the resulting SQL script to Oracle, which guarantees both systems hold identical data. Optimizer statistics are computed afterwards.
|
1 |
pg_dump --inserts --data-only --table=likeme --file=import.sql dbname |
|
1 |
ANALYZE TABLE likeme COMPUTE STATISTICS; |
Oracle: sorting versus pattern matching
Under CZECH, the sort order is wrong — as the documentation said, 'abch' is not ordered as it should be. XCZECH fixes it.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
SELECT s_czech FROM likeme ORDER BY s_czech FETCH FIRST 9 ROWS ONLY; S_CZECH ------- abb abc abch abcm abč abd abh abz Nesmysl 9 rows selected. |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
SELECT s_xczech FROM likeme ORDER BY s_xczech FETCH FIRST 9 ROWS ONLY; S_XCZECH -------- abb abc abcm abč abd abh abch abz Nesmysl 9 rows selected. |
An index on s_xczech speeds up the ordered query. Oracle calls an index whose collation differs from the binary one a linguistic index. Parts of the output are omitted for brevity: Oracle appears to implement FETCH FIRST 9 ROWS ONLY with a window function, though presumably it does not read the entire index.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
CREATE INDEX xczech_ind ON likeme (s_xczech); Index created. SET AUTOTRACE TRACEONLY SELECT s_xczech FROM likeme ORDER BY s_xczech FETCH FIRST 9 ROWS ONLY; Execution Plan -------------- --------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | --------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 9 | 3753 | | 1 | SORT ORDER BY | | 9 | 3753 | |* 2 | VIEW | | 9 | 3753 | |* 3 | WINDOW NOSORT STOPKEY | | 10000 | 17M| | 4 | TABLE ACCESS BY INDEX ROWID| LIKEME | 10000 | 17M| | 5 | INDEX FULL SCAN | XCZECH_IND | 10000 | | --------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter('from$_subquery$_002'.'rowlimit_$$_rownumber'<=9) 3 - filter(ROW_NUMBER() OVER ( ORDER BY 'S_XCZECH')<=9) |
Now the original question: will Oracle use that linguistic index for LIKE?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
SELECT s_xczech FROM likeme WHERE s_xczech LIKE 'abc%'; Execution Plan -------------- -------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | -------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 1809 | |* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| LIKEME | 1 | 1809 | |* 2 | INDEX RANGE SCAN | XCZECH_IND | 45 | | -------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(INSTR('S_XCZECH','abc',1,1)=1) 2 - access(NLSSORT('S_XCZECH','nls_sort=''XCZECH''')>=HEXTORAW('14191E0001010100') AND NLSSORT('S_XCZECH','nls_sort=''XCZECH''')<HEXTORAW('14191F0001010100')) |
It will. And the query result shows the cost of that decision — 'abch' is missing, so the answer is simply wrong. With CZECH, the result is correct.
|
1 2 3 4 5 6 7 8 9 10 |
SET AUTOTRACE OFF SELECT s_xczech FROM likeme WHERE s_xczech LIKE 'abc%'; S_XCZECH -------- abc abcm |
|
1 2 3 4 5 6 7 8 9 |
SELECT s_czech FROM likeme WHERE s_czech LIKE 'abc%'; S_CZECH ------- abc abch abcm |
The user therefore has a choice: correct ORDER BY plus incorrect LIKE under XCZECH, or incorrect ORDER BY plus correct LIKE under CZECH. Adding an explicit COLLATE clause yields the correct result, but the author found no way to build an Oracle index that supports that query.
|
1 2 3 |
SELECT s_xczech FROM likeme WHERE (s_xczech COLLATE CZECH) LIKE 'abc%'; |
Conclusion
PostgreSQL indexes LIKE through a character-wise operator class when the collation is not binary. Oracle never requires a special index for LIKE — not because it is smarter, but because it is comparatively unconcerned with returning correct results.



