Robert Hartskeerl

Het karakter van tekst

4 Augustus 2020

Soms zijn er van die dingen die je je niet afvraagt maar gewoon aanneemt. Persoonlijk wil ik wel graag weten hoe iets in elkaar steekt omdat ik het dan beter begrijp. Een van die dingen is bijvoorbeeld tekst in een database. Als je alleen westerse tekst gebruikt doe je varchar en als je internationaal gaat met je database kies je als datatype nvarchar. Toen ik betrokken was bij een migratie traject om een Oracle database om te zetten naar SQL Server werd ik geconfronteerd met de vele verschillen in Oracle en de invloed die dit had op de weergave van bepaalde karakters. Een van de vragen die toen naar boven kwam was hoe dit zou werken in SQL Server. Een hele goede vraag waar ik zelf het antwoord niet direct op wist.

Een stukje historie

Hoe zit dat nu met tekst in een database. Of beter gezegd, hoe verwerkt de computer tekst. Ik herinner mij de ASCII tabel uit de, toen nog papieren handleiding van de Commodore 64. De ASCII tabel bestaat uit 127 “karakters”. De set van printbare karakters begint op positie 32 met de spatie. Een hoofdletter A is 65 en wanneer je de 5e bit flipt krijg je de lowercase variant. Interessant is dan weer dat de ASCII set op positie 127 afsluit met DEL. In de vroegere jaren waren ponsbanden een gebruikelijke methode van data overdracht. Als je dan eerder een fout had gemaakt kon je 7 gaatjes maken wat gelijk staat aan delete.

ASCII vs Unicode

Op zoek naar standarisatie zijn er verschillende formaten voorbij gekomen. Zo maakt ASCII gebruik van 7 bits en is er geen ruimte voor speciale tekens. Veel landen gebruikten daarom de 8e bit om zo de benodigde karakters zichtbaar te kunnen maken. Later kwam ISO-8859-1 om veel van de westerse tekens weer te kunnen geven. In Windows zie je 1152 voorbij komen als set. Deze set is gebaseerd op de ISO-8859 set en bevat enkele aanvullingen. Omdat veel van de sets landspecifieke implementaties hebben en de digitale wereld steeds mondialer wordt was er behoefte aan een eenduidige standaard. Dat is Unicode en deze bestaat uit 4 bytes. De laatste versie bevat tekens uit ruim 150 talen. Daarnaast bevat Unicode ook emojis en andere veel gebruikte tekens.

UTF-8, UTF-16 en UTF-32

Hoe passen UTF-8, UTF-16 en UTF-32 dan in het plaatje? Zoals gezegd bestaat unicode uit 4 bytes. Maar niet alle karakters beslaan 4 bytes. De ASCII set is bijvoorbeeld een onderdeel van de unicode set. De letter A past daarom in 1 byte. Maar de letter Ā is twee bytes groot. Omdat Unicode 21 bits gebruikt is het mogelijk voor UTF-8 onderscheid te maken tussen 1,2,3 of 4-byte karakters. Als je er helemaal in wilt duiken is Wikipedia een goede bron [https://en.wikipedia.org/wiki/UTF-8]. Waarom de een over de ander? UTF-8 gaat simpelweg slimmer om met de ruimte. Een standaard karakter, bijvoorbeeld de letter A wordt in UTF-8 opgeslagen als 1 byte. In UTF-16 zijn dit al twee bytes en in UTF-32 vier bytes. Als ruimte belangrijk is, bijvoorbeeld omdat je het over het internet verstuurt is UTF-8 wellicht de beste keuze. Maar UTF-8 moet wel uit de significante bits halen of het om 1,2,3 of 4 byte karakter gaat. Door bijvoorbeeld altijd UTF-16 te gebruiken bespaar je een deel van deze berekeningen en dus processor cycli.

varchar vs nvarchar

In SQL Server zijn ruwweg twee datatypen voor tekst. Varchar en nvarchar. Varchar gebruikt minimaal 1 byte voor de opslag. Maar is hier niet toe beperkt. Hoeveel karakters varchar kan weergeven hangt af van de collation setting. Ook in SQL Server 2019 waar UTF-8 is geintroduceerd moet je een UTF-8 collation kiezen om alle karakters weer te kunnen geven. Het is dus niet per definitie zo dat je met (var)char bepaalde tekens niet kunt weergeven. Dat is sterk afhankelijk van de gekozen collation. Voor nvarchar is de opslag UTF-16 en is de volledige unicode set beschikbaar. Maar ook hier is dat weer afhankelijk van de gebruikte collation.

Geen plaats voor kerstbomen

Er zit een verschil tussen weergave en opslag. Ongeacht de gekozen collation is de opslag altijd hetzelfde. Wat vaak vergeten wordt is dat de grootte die je opgeeft bij de kolomdefinitie is de opslagruimte. Niet het aantal karakters. Zo heeft een nvarchar(16) ruimte voor 16 bytes. Dit is anders dan bijvoorbeeld Oracle waar je onderscheid kunt maken tussen ruimte varchar2(16) en aantal varchar2(16 char). Neem bijvoorbeeld het volgende script:

CREATE TABLE dbo.demochar (
    col1 nvarchar(2)
)
GO
INSERT INTO dbo.demochar (col1) VALUES (N'🎄');
GO
SELECT col1 FROM dbo.demochar
GO
DROP TABLE dbo.demochar
GO

Dit werkt zonder enige twijfel. Maar wat als je nog een kerstboom wilt plaatsen?

CREATE TABLE dbo.demochar (
    col1 nvarchar(2)
)
GO
INSERT INTO dbo.demochar (col1) VALUES (N'🎄🎄');
GO
SELECT col1 FROM dbo.demochar
GO
DROP TABLE dbo.demochar
GO

Het resultaat is een foutmelding: Msg 8152, Level 16, State 30, Line 5 String or binary data would be truncated.. De kerstboom is een unicode karakter dat uit 4 bytes bestaat. Het datatype van de kolom is nvarchar(2). Dit betekent dat er ruimte is voor 4 bytes. Hoeveel karakters je kan opslaan hangt van het karakter af. Het komt neer op 2 ASCII karakters of 2 karakters van 2 bytes. Maar andere karakters, zoals de kerstboom in dit voorbeeld bestaan uit 4 bytes. Er is maar ruimte voor één kerstboom. Het is iets om in ieder geval rekening mee te houden. Overigens heeft dit niets te maken met de weergave. De opslag is altijd hetzelfde maar de weergave hangt af van de gebruikte codepage van de client.