Kennisbank · datakwaliteit

Wanneer een spatie geen spatie is

Twee namen kunnen er exact hetzelfde uitzien en toch andere tekens bevatten. Voor een mens is er geen verschil. Voor een database soms wel.

De Van den Bergs

Een paar jaar geleden kreeg ik de vraag waarom de data op een overzicht niet oplopend gesorteerd was, terwijl dat wel zo geïmplementeerd was in de programmatuur. Zo stond “Van den Berg, R.” in het overzicht vóór “Van den Berg, J.”. Wat was er aan de hand?

Ook al zagen de namen er voor een mens exact hetzelfde uit, in de database was er wel degelijk een verschil te bespeuren: de ene spatie bleek de andere niet te zijn. In de ene naam stond een gewone spatie, in de andere een non-breaking space.

Beide tekens nemen op het scherm ruimte in, maar hebben een ander codepoint en een andere UTF-8-representatie. Dat verschil kan de volgorde waarin waarden worden gesorteerd beïnvloeden. De programmatuur sorteerde dus wel degelijk; alleen niet op de tekens die de gebruiker dacht te zien.

Drie ogenschijnlijk lege tekens

Unicode kent verschillende tekens die als een spatie ogen of zelfs helemaal niet zichtbaar zijn. Deze drie komen in gegevensverwerking geregeld voor:

A B Gewone spatie U+0020 SPACE UTF-8: 20

De gebruikelijke spatie tussen woorden.

A B Non-breaking space U+00A0 NO-BREAK SPACE UTF-8: C2 A0

Voorkomt dat de tekst op deze plaats over twee regels wordt verdeeld.

A​B Zero-width space U+200B ZERO WIDTH SPACE UTF-8: E2 80 8B

Is onzichtbaar, maar markeert een mogelijke positie voor een regelafbreking.

De naam zero-width space is enigszins misleidend. U+200B is volgens Unicode geen witruimteteken in de algemene categorie Separator, maar een onzichtbaar opmaakteken in de categorie Format. Voor databases en controles maakt dat onderscheid uit: een functie die “alle whitespace” beweert te verwerken, hoeft U+200B niet mee te nemen.

Er zijn er nog meer

Behalve deze drie kent Unicode onder meer smalle, vaste en cijferbrede spaties. Niet ieder teken is fout: typografie en bepaalde talen hebben er legitieme toepassingen voor. Het risico ontstaat wanneer een systeem impliciet aanneemt dat ieder leeg ogend teken U+0020 is.

Codepoint Naam Voorbeeld van gebruik
U+0020 SPACE Gewone scheiding tussen woorden.
U+00A0 NO-BREAK SPACE Woorden of eenheden bij elkaar houden.
U+2007 FIGURE SPACE Een spatie met de breedte van een cijfer.
U+2009 THIN SPACE Typografische, smalle tussenruimte.
U+200B ZERO WIDTH SPACE Onzichtbare mogelijkheid voor een regelafbreking.
U+202F NARROW NO-BREAK SPACE Smalle, niet-afbrekende tussenruimte.
U+2060 WORD JOINER Onzichtbaar teken dat juist een regelafbreking verhindert.
U+FEFF ZERO WIDTH NO-BREAK SPACE / BOM Byte Order Mark aan het begin van tekst; midden in tekst een onzichtbaar teken.

Hoe komen deze tekens in uw database?

Meestal voert niemand bewust U+00A0 of U+200B in. De tekens komen ongemerkt mee bij het kopiëren uit een website, tekstverwerker, spreadsheet of pdf. Ook exports, imports en koppelingen kunnen typografische tekens doorgeven zonder dat de ontvangende applicatie ze zichtbaar maakt.

Dat is op zichzelf correct gedrag: de gegevens worden behouden. Het wordt een datakwaliteitsprobleem wanneer de bestemming een andere betekenis aan de tekens geeft dan de bron, of wanneer voor de betrokken gegevenssoort nooit is vastgelegd wat is toegestaan.

Waar dit in de praktijk mis kan gaan

  • Sorteren: ogenschijnlijk gelijke namen komen onverwacht in een andere volgorde terecht.
  • Zoeken en koppelen: een zoekopdracht, join of lookup vindt niet alle visueel gelijke waarden.
  • Ontdubbelen: twee records blijven bestaan omdat de sleutelwaarden technisch verschillen.
  • Unieke waarden: een unieke index kan ogenschijnlijk gelijke waarden toelaten.
  • Validatie: een correct uitziend e-mailadres, rekeningnummer of identificatienummer wordt afgekeurd – of ten onrechte geaccepteerd.
  • Lengtecontroles: een onzichtbaar teken telt wel mee als codepoint of byte.
  • Hashes en handtekeningen: verschillende bytepatronen produceren verschillende hashes.

Het verraderlijke is dat een medewerker de oorzaak meestal niet kan zien. Kopiëren en plakken neemt het teken bovendien gewoon mee.

Waarom TRIM() niet voldoende is

Wie het probleem met TRIM() denkt op te lossen, kan opnieuw voor een verrassing komen te staan. In PostgreSQL verwijdert trim() standaard gewone spaties aan het begin en einde van een tekst. Een non-breaking space of zero-width space blijft daarbij staan.

SELECT
  btrim(U&'\00A0waarde\00A0') = 'waarde'
    AS standaard_trim,
  btrim(U&'\00A0waarde\00A0', U&'\0020\00A0') = 'waarde'
    AS expliciete_trim;

Het resultaat is respectievelijk false en true. In het tweede geval is expliciet opgegeven dat zowel U+0020 als U+00A0 aan de randen mag worden verwijderd. Dat lost uitsluitend dit specifieke randgeval op: tekens midden in de tekst en andere onzichtbare tekens vragen nog steeds om een inhoudelijke regel.

Een klein PostgreSQL-experiment

Onderstaand voorbeeld maakt het verschil zichtbaar. De twee namen ogen gelijk, maar hun UTF-8-bytes en de directe vergelijking verschillen.

SELECT
  'Van den Berg' = U&'Van den\00A0Berg' AS gelijk,
  encode(convert_to('Van den Berg', 'UTF8'), 'hex')
    AS gewone_bytes,
  encode(convert_to(U&'Van den\00A0Berg', 'UTF8'), 'hex')
    AS nbsp_bytes;

De vergelijking levert false op. In de hexadecimale weergave staat bij de gewone spatie 20 en bij de non-breaking space c2a0.

Ook een onzichtbaar teken telt gewoon mee:

SELECT
  char_length('AB') AS zonder_teken,
  char_length(U&'A\200BB') AS met_zero_width_space;

Beide waarden zien eruit als AB, maar de lengtes zijn respectievelijk 2 en 3 codepoints.

De juiste regel hangt af van de gegevenssoort

De oplossing is niet om overal alle afwijkende tekens te verwijderen. Daarmee kan betekenis verloren gaan. Bepaal per gegevenssoort welke tekens toegestaan zijn en of een afwijking moet worden geconverteerd, geweigerd of behouden.

Gegevenssoort Mogelijke regel Waarom
Persoonsnaam Zet U+00A0 waar passend om naar U+0020; weiger onverwachte onzichtbare opmaaktekens. Zoeken, sorteren en ontdubbelen vragen om een consistente representatie.
E-mailadres of technische identificatie Weiger onverwachte whitespace en onzichtbare tekens. Stil verwijderen kan een invoerfout of misleiding verhullen.
Getal of bedrag Parseer volgens een expliciet formaat en sla de waarde op in een numeriek datatype. Een typografische spatie hoort niet de opgeslagen numerieke waarde te bepalen.
Vrije tekst Behoud legitieme typografie; controleer gericht op ongewenste besturings- en opmaaktekens. Een brede vervanging kan taal en vormgeving beschadigen.

De database als laatste vangnet

Normalisatie vindt bij voorkeur plaats wanneer gegevens de organisatie of applicatie binnenkomen. De database kan daarnaast afdwingen dat alleen de afgesproken vorm wordt opgeslagen. Voor gegevenssoorten waarin U+00A0 en U+200B niet zijn toegestaan, kan bijvoorbeeld een herbruikbaar PostgreSQL-DOMAIN worden gebruikt:

CREATE DOMAIN tekst_zonder_verborgen_spaties AS text
    CHECK (
        VALUE IS NULL
        OR (
            position(U&'\00A0' in VALUE) = 0
            AND position(U&'\200B' in VALUE) = 0
            AND position(U&'\2060' in VALUE) = 0
            AND position(U&'\FEFF' in VALUE) = 0
        )
    );

CREATE TABLE persoon
(
    persoon_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    naam       tekst_zonder_verborgen_spaties NOT NULL
);

Deze definitie is nadrukkelijk geen universele Unicode-regel. Zij is een voorbeeld van een beleidskeuze voor gegevenssoorten waarin deze tekens niet thuishoren. De invoerlaag kan een non-breaking space eerst gecontroleerd naar een gewone spatie converteren; de database weigert waarden die desondanks niet aan de afspraak voldoen.

Het DOMAIN maakt de regel herbruikbaar. Dat is Single Point of Maintenance in optima forma: definieer de regel één keer voor de betrokken gegevenssoorten, implementeer haar centraal en pas haar overal hetzelfde toe.

Wat goed datamanagement hier vraagt

  • Leg regels vast per data class. Beschrijf toegestane tekens, conversies, validaties en uitzonderingen.
  • Controleer aan de grens. Valideer invoer en ontvangen gegevens voordat ermee wordt gezocht, gekoppeld of gehasht.
  • Maak afwijkingen zichtbaar. Toon codepoints of hexadecimale bytes in diagnose- en kwaliteitsrapportages.
  • Onderzoek bestaande gegevens. Een nieuwe invoerregel corrigeert historische waarden niet automatisch.
  • Test de hele keten. Neem ook import, export, API’s, spreadsheets, rapportages en kopieer-en-plakgedrag mee.

Datakwaliteit wordt soms bepaald door tekens die niemand kan zien. Juist daarom moeten de regels wel zichtbaar, expliciet en centraal beheerd zijn.

Bronnen en verdere verdieping

  1. Unicode Standard, hoofdstuk 6 – Writing Systems and Punctuation
  2. Unicode Standard Annex #14 – Unicode Line Breaking Algorithm
  3. Unicode Consortium – UTF-8, UTF-16, UTF-32 & BOM FAQ
  4. PostgreSQL – String Functions and Operators
  5. Unicode Standard Annex #44 – Unicode Character Database

Wat u niet ziet, kan uw data wel beïnvloeden

Wilt u weten waar impliciete invoerregels, verborgen tekens of inconsistente definities de datakwaliteit binnen uw organisatie beïnvloeden? Met de datamanagement quickscan brengt u sterke punten, risico’s en concrete verbeterkansen in beeld.