Thread
-
Experimenting with wider Unicode storage
Thomas Munro <thomas.munro@gmail.com> — 2026-04-15T13:43:26Z
Hi, We only allow one character encoding per database. The SQL standard and most comparable RDBMSs are more flexible, though the details vary. In most, you can attach CHARACTER SET to column definitions, a whole CREATE TABLE (non-standard), CREATE DOMAIN, CREATE SCHEMA and probably more places. In some, encoding is implied by COLLATE instead (non-standard). When different encodings meet, text is transcoded, which works but is bad for performance. Our single database encoding also has some restrictions: it must encode ASCII as ASCII, and any byte that is part of a multibyte sequence must not look like ASCII, since many code paths require that and fixing that is hard. That excludes a few encodings that people want. That's all OK by now, as modern information systems use Unicode everywhere. There is one practical problem that causes people to complain[2] about PostgreSQL though: about half of the global population uses a language that arbitrarily gained a byte per character by switching to UTF-8 compared to various legacy encodings or GB18030. That's storage and RAM that you have to pay for up front and forever (space, I/O[3]). I wondered about inventing new PostgreSQL-backend-compatible encodings that swizzle bits around to make CJK/I languages fit, but I kept coming back to Unicode. In many systems there is a special way to use UTF-16, which gets you back to around two bytes per character for Chinese, Japanese, Korean and probably Indian languages. In MySQL/MariaDB, you can use various encodings of UTF-16 as a CHARACTER SET with the normal text types, and in Oracle, DB2, SQL Server/Sybase you can use a separate NVARCHAR type for UTF-16, alongside the regular text types whose encodings are controlled by CHARACTER SET or COLLATE. N* types are shorthand for the standardese "NATIONAL <TYPE>", a string in an unspecified special encoding, for which they all chose some kind of UTF-16. At first I thought that with first class extension types as one of our superpowers, we might be able to do that with an extension, but the problem there is that text is so tangled up with locales, and I certainly didn't want to convert all over the place. So I tried to hack up a minimal demonstration of what a separate UTF-16 text type might look like as a core data type. You can save a lot of space if you have a separate "utf16" type ("national text"?), but only for East and South Asian languages: + language | octets | delta | string +----------+---------+-------+------------------------------------------------- + English | 45→90 | +100% | In a hole in the ground there lived a hobbit. + Spanish | 44→86 | +95% | En un agujero en el suelo, vivía un hobbit. + Russian | 59→66 | +12% | В норе под землей жил-был хоббит. + Arabic | 57→64 | +12% | كان يعيش هوبيت في حفرة في الأرض. + Hebrew | 43→48 | +12% | בתוך חור באדמה חי הוביט. + Greek | 74→82 | +11% | Σε μια τρύπα στο έδαφος ζούσε ένα χόμπιτ. + Korean | 55→46 | -16% | 땅속 어느 구멍에 한 호빗이 살고 있었다. + Hindi | 111→86 | -23% | जमीन में बने एक गड्ढे में एक हॉबिट रहता था। + Tamil | 146→108 | -26% | அந்த நிலத்தில் ஒரு துளையில் ஒரு ஹாபிட் வசித்து வந்தது. + Chinese | 51→34 | -33% | 在地下一个洞里,住着一个霍比特人。 + Japanese | 66→44 | -33% | 穴のなかに、ひとりのホビットが暮らしていた。 It's actually -33% in Korean too without spaces, or if you use double-width spaces, a more common stylistic choice IIUC (this Korean string happens to use ASCII spaces). The same sort of thing explains why Russian, Arabic, Hewbrew and Greek lose ~12% instead of breaking even: they share space and some punctuation with ASCII. The attached is highly exploratory concept code to try the idea out and see if experts in CJK computing, defenders of the type system and others think it might be worth exploring further. Some of the technical challenges and observations I spotted along the way: * "text" (etc) and "utf16" need to be comparable incrementally without conversion * N^2 explosions in cross-type support function definitions must surely be avoided * the solution to that is surely generic programming, but PostgreSQL is written in ye olde C * therefore, this POC patch is big on macros as poor-man's C++ * to support fast paths like memcmp()-based ucs_basic comparison you need big endian UTF-16 * to stuff UTF-16 into varlena you need to tolerate unaligned access * by a happy coincidence, ICU supports big endian, unaligned UTF-16 * Windows libc locales might in theory allow UTF-16, but that'd have to be native endian, which I didn't implement * other systems actually allow both endiannesses as encodings or subtypes, at the user's option * they also allow you to control whether surrogates are allowed In this patch you can see some string iterator concepts that I have been hacking on for an entirely different purpose, namely trying to figure out how to make our multibyte string support go faster (and also be safer) by hoisting all the character-at-a-time loops out to specialisations in string handling functions. That's not shown here. Neither is any kind of silent transcoding that plagues other systems that do this kind of thing: * the "utf16" type is only allowed to contain text that is a ASCII, LATIN1 or Unicode, depending on the database encoding * for UTF8, it's all of Unicode, for which mb_iterator can trivally produce UTF-16 or UTF-32 * for LATIN1, that becomes trivial casting since LATIN1 is a strict subset of Unicode * for anything else, utf16 only allows ASCII characters, as a degraded mode just to allow the tests to pass In other words, so far utf16 is not allowed to represent anything that "text" could not represent and convert trivially. That was originally a decision to make a quick proof-of-concept plausible, but maybe it's even a good idea... I have no doubt that there are lots of complicated problems that I haven't met yet, when you add more types. What I was trying to explore here was whether you can exclude most of them by providing enough conversion-free (incremental) cross-type support. This is something SQL Server DBAs talk about: mixtures of NVARCHAR and VARCHAR columns and index befuddle the planner and introduce hidden execution costs if you're not careful. It struck me that with the above restriction you could perhaps keep all strings cheaply and incrementally comparable. There are still some things you can't do: * if you're comparing "text" with "utf16" then you lose the length-based not-equal fast path * that's a big deal if it means detoasting Sharing for discussion only. It has enough working to support converting individual columns text->utf16 and use btree indexes. Many more text functions would need to be converted to generic form, and many more support functions would be needed for full functionality. (Individual Indian languages could in theory be compacted even further to single-byte ISCII. ISCII is in general infeasible as a server encoding because it has stateful shifts between many scripts, but single-script variants of ISCII as supported on some Unixen would in theory be plausible. Since India has so many languages and scripts and information systems often need to support all of them, I am reliably informed that UTF-8 reigns supreme there despite taking 3 bytes to represent the tiny 6 bit (?) character set of any individual language like Devanagari (Hindi etc). GNU/Linux doesn't even support ISCII, so that idea is basically DOA.) [1] https://www.postgresql.org/docs/current/infoschema-character-sets.html [2] https://www.postgresql.org/message-id/flat/ME2PR01MB2532E72B514DC46ED0E10F798A0C0%40ME2PR01MB2532.ausprd01.prod.outlook.com#7d490f97a3df6dfef61e485161e72e06 -
Re: Experimenting with wider Unicode storage
Henson Choi <assam258@gmail.com> — 2026-04-16T01:23:32Z
Hi Thomas, Thank you for sharing this very interesting and creative approach. Encoding is indeed a crucial factor in capacity planning and performance benchmarking — I find this direction quite compelling. I'm currently working on a few other things, so my responses may not always be quick, but I wanted to let you know I'm genuinely interested in following this work. As it happens, I'm currently collaborating with Ishii-san — who, as you know, is one of the original architects of multibyte/CJK support in PostgreSQL — on Row Pattern Recognition; that might also be a thread worth keeping an eye on. It also strikes me that this is a topic worth considering in the context of the rapid growth of SNS and AI-generated data. The pervasive use of emoji — which cannot be represented in legacy encodings like EUC-KR at all — is in fact accelerating the migration toward Unicode in Korea and other Asian markets. This makes the storage efficiency of Unicode for CJK characters an increasingly practical concern, not just a theoretical one. I'd like to take some time to analyze the current situation around character encoding in Korea — where both EUC-KR legacy systems and UTF-8 coexist in complex ways — review the patches you've attached, and then share some thoughts and feedback. Best regards, Henson
-
Re: Experimenting with wider Unicode storage
Henson Choi <assam258@gmail.com> — 2026-04-21T01:16:26Z
Hi Thomas, Thank you again for sharing this exploration, and for including Korean in your experiment table. Rather than comment on the patch itself, let me offer a ground-level report on where Korean encoding reality sits in April 2026, because the picture has shifted enough that I think it is worth entering into the record before this thread accumulates momentum on motivations that may no longer fully hold on this side of the region. UTF-8 has already won in Korea, largely by inertia rather than active choice. Public web statistics put .kr sites at roughly 96% UTF-8 with a small EUC-KR residual of about 4% [1] — noticeably higher than the ~1% Shift-JIS residual on .jp [2], but steadily shrinking. The mechanism is mundane: modern Linux distributions default to UTF-8 locales, PostgreSQL's initdb inherits that, and every new cluster is therefore UTF-8 from birth. The remaining legacy installations are not "haven't migrated yet" — they are "have decided not to migrate," which is a different and much slower population. A clarification that often trips people up: in Korean practice, "EUC-KR" is the label written down and CP949 is what actually moves on the wire. Microsoft's UHC has been the Windows default for decades, and the MIME label has simply stuck. The historical stack goes KS X 1001 (완성형, 2,350 syllables) → EUC-KR → CP949 (11,172 syllables) → UTF-8. PostgreSQL's strict EUC_KR decoder rejects the bytes CP949 adds, which occasionally causes real incidents when Windows-exchanged files are loaded. For any design choice about "Korean legacy support", this matters — what needs supporting is usually CP949, not EUC-KR proper. Server encoding and client encoding are also routinely split. A common Korean deployment pattern is a PostgreSQL cluster with UTF-8 as server encoding, while legacy Windows / Delphi / C++ / older Java clients connect with client_encoding set to EUC-KR or CP949 and let PostgreSQL transcode at the wire boundary. Many systems that look like "EUC-KR systems" from the outside are actually UTF-8 storage with an EUC-KR wire. The storage-layer share of legacy is therefore probably smaller still than the 3.8% web figure would suggest. On the Korean row of your table landing at -16% under UTF-16: that is structural, not noise. Modern Korean writing mandates word-space separation (unlike Chinese and Japanese), has effectively abandoned hanja since the 1990s, and freely interleaves ASCII acronyms (IT, AI, CEO). As a result Korean carries the highest ASCII share among CJK languages, and UTF-16 pays for each ASCII position (one byte → two) in exactly the range where the Hangul savings are meant to come from. Columns without spaces — names, titles, addresses — could approach -33%, but general prose cannot. Those same short columns are, however, exactly where the compression angle I return to further below captures the equivalent saving without a new data type. Storage pressure, to the extent modern operators feel it at all, has largely migrated to other layers. Memory and disk have both followed exponential price/volume curves, and the CPU cost of text comparison has disappeared inside other costs — network, storage I/O, planning, JIT — to the point of invisibility in profiler output. For OLTP, the 2-vs-3-byte difference on Korean columns does not feel meaningful on modern hardware. For bulk scans where byte counts still do matter, the industry answer has already been columnar + zstd, which routinely reaches 90%+ compression on natural-language text and flattens the CJK-vs-Latin ratio to irrelevance. Embedded and edge are not PostgreSQL's primary target, and archival sits in zstd territory too. The domains that historically motivated "we must narrow CJK storage" have either moved outside the PostgreSQL shape or been absorbed by general-purpose compression. Meanwhile the cultural arrow points toward more Unicode, not less. KakaoTalk (which saturates domestic messaging), Naver comments, Instagram captions, and YouTube normalise emoji in everyday prose, while AI-generated Korean text contributes middle dots, em dashes, and curly quotes at a scale that was not present a few years ago. The share of non-EUC-KR content in everyday Korean prose is, informally, rising steadily. Each emoji is four UTF-8 bytes and is unrepresentable in any legacy encoding at all. A partial-coverage alternative looks increasingly awkward against that trend. Korean upstream feedback on encoding has also been notably quiet despite a very active de-Oracle migration wave in the late 2010s. I suspect this silence is not apathy but absence of a felt problem — most of the community has simply moved on. I should be careful here. The "Korean side needs narrower CJK storage" argument was genuinely strong around 2010, and I remember when it motivated serious engineering time. It is much weaker in 2026: UTF-8 has won by default, legacy survivors are confined to wire protocols and specific applications, OLTP does not feel the byte cost, and bulk scan is already handled elsewhere. I raise this not to dismiss the technical work — the patch shows real craft and the exploration is interesting on its own terms. But if the cover-letter motivation rests partly on "this will help East Asian users, including Korea," I wanted you to have a ground-level report: for Korean users specifically, the pressure may no longer be strong enough to justify the complexity described. The calculus may well differ in Japanese or Chinese markets — that is not for me to say. One broader question, then, that I wanted to put to you: there are three distinct axes on which utf16 could be pursued — as a server character set, as a data type, or as a compression angle. The character-set direction runs straight into the "continuation byte must not look like ASCII" rule, as you already noted, and is therefore effectively closed on PostgreSQL. The data-type direction is the current patch, which carries substantial catalogue and operator surface, while the storage wins mostly accrue on wider values — where columnar + zstd is already doing the work. What still seems genuinely unaddressed in practice is the short-value regime: word-sized strings such as names, titles, cities, and tags, which fall below the TOAST compression threshold and therefore never see a compressor at all. Would framing this as "a compression method effective on word-sized values" be a more productive angle than either of the other two? The storage outcome could be similar with much less surface area to maintain. A fair counter on memory, before I go on: disk pressure has clearly migrated elsewhere, but shared_buffers and work_mem remain finite, and compression primarily addresses the disk side. A data-type approach that goes far enough to shrink the in-memory representation — modifying every string function along the way — tends to become a degraded form of a new character set: doing most of the character-set work without the character-set slot in PostgreSQL's encoding machinery, which as above is closed. None of the three axes therefore cleanly solves the in-memory case; for truly memory-bound CJK workloads the honest answer is probably just more RAM. One concrete instantiation of that compression angle, if Korean capacity specifically is the example that matters: take CP949 (which is what actually circulates under the EUC-KR label) as a compression base and, for any character CP949 cannot represent, spell it inline as a readable textual escape such as \u2603 or U+2603 rather than a binary marker byte. Native Korean text then stays at two bytes per Hangul, emoji and modern Unicode remain fully representable (at a modest cost per occurrence), the in-memory representation stays plain UTF-8, and the on-disk byte stream stays entirely within ASCII + CP949 — no new marker byte, no collision with existing code paths that scan for raw ASCII bytes. If the source text itself contains sequences that look like the escape syntax (for instance documentation quoting \u-style literals), a simple doubling rule disambiguates them; such cases are vanishingly rare in Korean business data. This targets exactly the short-value regime above, with far less surface than a new data type. For tighter byte density, one could go further by devising a dedicated binary-level encoding, or by wiring zstd's external dictionary feature into the column-compression path with a pre-trained per-language dictionary — but either of those paths carries its own implementation and operational costs. Should you nonetheless decide to press on with utf16 as a data type, I am willing to take the patch through a proper review; I have already applied it on top of master and confirmed that the regression tests pass, so the mechanical footing is in place. [1] https://w3techs.com/technologies/segmentation/tld-kr-/character_encoding [2] https://w3techs.com/technologies/segmentation/tld-jp-/character_encoding Best regards, Henson >
-
Re: Experimenting with wider Unicode storage
Thomas Munro <thomas.munro@gmail.com> — 2026-04-30T00:40:52Z
On Tue, Apr 21, 2026 at 1:16 PM Henson Choi <assam258@gmail.com> wrote: > Thank you again for sharing this exploration, and for including > Korean in your experiment table. Rather than comment on the > patch itself, let me offer a ground-level report on where Korean > encoding reality sits in April 2026, because the picture has > shifted enough that I think it is worth entering into the record > before this thread accumulates momentum on motivations that may > no longer fully hold on this side of the region. Hi Henson, Thank you for this thoughtful and broad feedback, which provided a lot of useful context. I appreciated all of it, and have responses to a couple of the most actionable paragraphs: > One broader question, then, that I wanted to put to you: there > are three distinct axes on which utf16 could be pursued — as a > server character set, as a data type, or as a compression angle. > The character-set direction runs straight into the "continuation > byte must not look like ASCII" rule, as you already noted, and > is therefore effectively closed on PostgreSQL. The data-type > direction is the current patch, which carries substantial > catalogue and operator surface, while the storage wins mostly > accrue on wider values — where columnar + zstd is already doing > the work. What still seems genuinely unaddressed in practice is > the short-value regime: word-sized strings such as names, > titles, cities, and tags, which fall below the TOAST compression > threshold and therefore never see a compressor at all. Would > framing this as "a compression method effective on word-sized > values" be a more productive angle than either of the other two? > The storage outcome could be similar with much less surface area > to maintain. Yeah, that is an interesting angle that I hadn't considered, at least not with that framing. There are even a couple of Unicode standards that might apply here, and that I believe some other systems are using: https://en.wikipedia.org/wiki/Standard_Compression_Scheme_for_Unicode https://en.wikipedia.org/wiki/Binary_Ordered_Compression_for_Unicode https://www.unicode.org/notes/tn6/ BOCU-1 maintains binary codepoint order and reports typical English/French as no size change compared to UTF-8, Greek/Russian/Arabic/Hebrew as -40%, Hindi as -60% (this makes sense: it's almost a generalised ISCII, so you get down to one byte per character in any given Indian language), Japanese as -40% and Chinese/Korean as -25% (Japanese presumably wins with kana sequences). One of the ideas already mentioned in comments in the experimental patch was that the iterator abstraction could allow for incremental decompression, and I suppose there might be a way to expand BOCU-1 or similar to UTF-8 incrementally in that layer. I haven't looked into that seriously though; so far I had only been thinking of that as a way of generalising some open coded special cases that appear in a few places to avoid detoasting. ICU might also be able to consume it incrementally, IDK. zstd etc can clearly compress much more than that, as you say, but then you have to deal with dictionary problems and it's hard to do that for small values in a row-oriented system, as you say. BOCU-1 is dictionary-free, so you read it in direct byte order with only a tiny state in a register or two, which seems to be potentially along the lines you're suggesting. Food for thought. > A fair counter on memory, before I go on: disk pressure has > clearly migrated elsewhere, but shared_buffers and work_mem > remain finite, and compression primarily addresses the disk > side. A data-type approach that goes far enough to shrink the > in-memory representation — modifying every string function > along the way — tends to become a degraded form of a new > character set: doing most of the character-set work without the > character-set slot in PostgreSQL's encoding machinery, which as > above is closed. None of the three axes therefore cleanly > solves the in-memory case; for truly memory-bound CJK workloads > the honest answer is probably just more RAM. Yeah. It's an annoying set of constraints that led me to consider this, while surveying text handling choices made in lots of database systems. Of course it wouldn't be my preference to introduce a new type, but I couldn't see how how else to fit it in, and since I was already investigating "modifying every string function along the way" for other reasons, I wanted to explore what it would take to do that generically enough to handle something as different as this while remaining maintainable... BTW here is the link that I forgot to add to the bottom of my earlier email as reference [3], which is a blog from when SQL Server introduced the *opposite* thing: UTF-8 support (like Windows itself, in 2019). Previously they had only legacy single/multi-byte encodings in VARCHAR and UTF-16 in NVARCHAR, so there they were discussing this tradeoff in reverse, ie space savings for some languages, but reported 25% increase in disk I/O for CJK databases moved to UTF-8. (I don't immediately know why SCSU didn't fix that.) https://techcommunity.microsoft.com/blog/sqlserver/introducing-utf-8-support-for-sql-server/734928 > Should you nonetheless decide to press on with utf16 as a data > type, I am willing to take the patch through a proper review; I > have already applied it on top of master and confirmed that the > regression tests pass, so the mechanical footing is in place. Thanks. I'm not planning to do more with the "separate UTF-16 type" concept at this stage, based on your feedback so far. I am still working on a couple of text/encoding refactoring prototypes with other goals, and will try to think about that "special Unicode compression" angle while doing so.