— Artikel — № 042

042 —AI

AI-SQL: wanneer mag je de voorgestelde JOIN vertrouwen?

Een AI schrijft in twee seconden een JOIN over zes tabellen. De helft daarvan klopt. Zo zie je welke helft klopt, voordat je 'run' drukt op productie.

Bovenaanzicht van inkt SQL JOIN-diagram met tabelkaarten, potloodlijnen, rode vraagtekens en waszegel op linnen.
Hero · gestileerd stilleven№ 042

Een Nederlands bureau waarmee we werken stuurde zondagavond om 23:41 een Loom door. De lead developer had aan een AI-assistent gevraagd om een query die alle orders van de afgelopen 90 dagen ophaalde, met het factuuremailadres van de klant en de gekochte SKU's, tegen een Magento 1.9-database die was overgenomen van een vorige freelancer. De AI-SQL kwam terug als een zelfverzekerde JOIN over acht tabellen. Hij draaide. Hij gaf 412.000 rijen terug. De winkel had dat kwartaal ongeveer 9.000 orders gedaan.

Dit stuk gaat over het gat tussen draaide en klopt. AI-gegenereerde SQL is echt nuttig tegen het schema van een verouderde site (WordPress, Drupal, Joomla, Magento, het op maat gebouwde PHP-CRM dat iemands neef in 2014 in elkaar zette), maar de suggesties falen op een specifieke, voorspelbare manier. Zodra je dat patroon herkent, houd je de snelheid zonder de verrassing van 412.000 rijen.

De vorm van een foute JOIN

Bijna elke slechte AI-SQL-query tegen een verouderd schema gaat om één van drie redenen mis, en die zijn het waard om te benoemen omdat ze op het eerste gezicht identiek lijken.

De eerste is een cardinality blowout. De assistent heeft een one-to-many op een andere one-to-many gejoind zonder ontdubbelingsstap, en je krijgt het cartesisch product van beide. Dat is wat er gebeurde met de Magento-query hierboven: sales_flat_order_item gejoind met sales_flat_order_payment gejoind met sales_flat_order_address zonder filter WHERE address_type = 'billing'. Drie rijen per order aan de adreskant, vier tot zes aan de items-kant, vermenigvuldigd over 9.000 orders.

De tweede is een stiekem foute key. WordPress is het schoolvoorbeeld. Een assistent schrijft vrolijk:

SELECT p.post_title, u.user_email
FROM wp_posts p
JOIN wp_users u ON u.ID = p.post_author
WHERE p.post_status = 'publish';

Dat ziet er prima uit. Het draait. Het geeft data terug die er redelijk uitziet. Maar als de site een membership-plugin gebruikt die het auteurschap na een redactieworkflow opslaat in wp_postmeta onder _original_author, dan toont elke byline de redacteur in plaats van de auteur. De assistent kan dat onmogelijk weten, en de query geeft geen waarschuwing.

De derde is de EAV-val, en die speelt vooral bij Magento en Drupal 7. De assistent schrijft een nette JOIN tegen catalog_product_entity en haalt vervolgens name uit een kolom die niet bestaat, omdat de naam in catalog_product_entity_varchar staat, gekoppeld via attribute_id uit eav_attribute. Je krijgt of overal NULL's terug, of, als de assistent probeert te compenseren, een veertien regels lange subquery die bijna goed is en stilletjes producten zonder default-store-waarde weglaat.

De audit van twee minuten

Voordat je een AI-voorgestelde JOIN op een productiedatabase loslaat, loop je hem hierdoor. Het duurt ongeveer twee minuten en vangt ruwweg 80% van de fouten die je anders pas op productie zou ontdekken.

Tel de brontabellen. Pak de query, zet elke tabel op de FROM- en JOIN-regels op een rij, en schrijf de verwachte cardinaliteit ernaast. Als je niet in gewone taal kunt zeggen "dit is één rij per order" of "dit zijn meerdere rijen per product", dan ken je het schema niet goed genoeg om de query te draaien. Stop en lees de tabel.

Draai hem eerst met COUNT(*). Wikkel het geheel in:

SELECT COUNT(*) FROM (
  -- plak de AI-query hier, maar SELECT 1 in plaats van de kolommen
) AS audit;

Vergelijk de telling met wat je uit het hoofd zou schatten. In het Magento-geval wist de ontwikkelaar dat het kwartaal ongeveer 9.000 orders had. 412.000 is 45 keer te hoog. Dat getal alleen al vertelt je dat de JOIN fout is voordat je één regel hebt gelezen.

Let op een ontbrekende DISTINCT of GROUP BY. Als de assistent iets heeft gejoind met 'items', 'meta', 'address', 'option' of 'variant' in de naam en de buitenste query aggregeert niet, dan heb je waarschijnlijk rijvermenigvuldiging. Voeg GROUP BY op de parent key toe, of herstructureer als subquery.

Lees de WHERE-clausule van achter naar voren. Begin bij het meest specifieke filter en werk naar buiten. De meeste stiekem foute queries missen één enkele voorwaarde: address_type = 'billing', store_id = 0, meta_key = '_customer_user', language = 'en'. De assistent komt daar zelden uit zichzelf mee. Je moet weten dat ze bestaan.

Wat je altijd zelf schrijft

Sommige soorten queries zijn het niet waard om aan een AI te vragen, omdat de kosten van een subtiele fout veel hoger zijn dan de tijdwinst. Schrijf deze met de hand.

Alles dat schrijft. UPDATE, DELETE, INSERT, ALTER. Een assistent die in een SELECT een kolom hallucineert geeft je gewoon een foutmelding. Een assistent die in de WHERE van een UPDATE een kolom hallucineert update stilletjes elke rij in de tabel. De OWASP-pagina over SQL-injection is om diezelfde reden de moeite van het herlezen waard: assistent of niet, de failure modes voor write-queries zijn catastrofaal waar die voor read-queries enkel vervelend zijn.

Alles dat geld raakt. Ordertotalen, factuurregels, btw-berekeningen, terugbetalingen. De Magento sales_flat_order-tabel heeft kolommen die grand_total, base_grand_total, total_paid, total_refunded en base_total_invoiced heten, en ze betekenen vijf verschillende dingen in vijf verschillende valuta. Een assistent kiest degene die in zijn trainingsdata het meest voorkwam. Dat is een muntje opgooien dat je niet wilt doen.

Alles met soft deletes of statusvlaggen. WordPress' post_status kan publish, draft, private, trash, auto-draft, inherit of wat een plugin ook heeft verzonnen zijn. Drupal's node_field_data.status is 0 of 1, maar de betekenis hangt ervan af of de node ook in de moderatieworkflow gepubliceerd is. Als de assistent niet vraagt welke statussen je wilt, heeft hij er één gekozen. Jij moet verifiëren welke.

Wat je de AI wel laat doen

De andere kant van de medaille: er is een reëel gebied waar AI-SQL sneller is dan zelf schrijven, en weigeren om het te gebruiken is gewoon koppigheid.

Standaard joins waar het schema bekend is en de keys voor de hand liggen: wp_posts aan wp_postmeta op post_id, users aan orders op customer_id, het standaard Drupal-patroon van node naar veldtabel. Pivotqueries waar de SQL vervelend is om te schrijven maar makkelijk te verifiëren. Date-bucketing met DATE_FORMAT of window-functies waarvoor je anders de syntax moet opzoeken. Een schema dat je al hebt gelezen vertalen naar een query die je al voor je zag. Daar verdient het zijn brood.

De vuistregel: als je de query in vijf minuten zou kunnen schrijven maar geen zin hebt, is de assistent prima. Als je eerst het schema moet lezen om hem te schrijven, moet de assistent dat waarschijnlijk ook, en dat kan hij niet.

Een werkwijze die maandagochtend overleeft

Het bureau uit het openingsverhaal haalt nu elke AI-voorgestelde query door een ritueel van drie stappen voordat hij de productie raakt. Eerst EXPLAIN, om te zien wat de planner echt gaat doen en het cartesisch product te onderscheppen voordat het iets kost. COUNT(*) eromheen, om de telling tegen je geheugen te checken. Dan de echte query, maar eerst op een read replica of een recente dump, nooit op de live database, zelfs niet voor SELECTs. Een slecht gevormde JOIN op een orders-tabel van 40 GB legt de machine tien minuten plat, of hij nu read-only is of niet.

Niets van dit alles is nieuws. EXPLAIN zit al in MySQL sinds voordat een van ons PHP schreef voor zijn brood. Wat nieuw is, is dat het volume aan queries dat je in de verleiding komt te draaien is gegroeid, omdat de moeite van het schrijven is verdwenen. De discipline moet meegroeien met de doorstroom.

Toen we Pier bouwden liepen we precies hiertegenaan. De chat stelt een JOIN voor, we runnen hem, het resultaat is op een manier fout die tien minuten kost om te debuggen. Hoe we dat uiteindelijk hebben opgelost: rijaantallen en EXPLAIN-output inline tonen in de MySQL editor voordat een statement commit, en elke uitgevoerde query bewaren in versiegeschiedenis zodat een foute UPDATE één klik terug is.

Het kleinste wat je vandaag kunt doen: open de verouderde database die het dichtst bij je staat, schrijf de cardinaliteit van de drie grootste tabellen op een geeltje, en plak het naast je monitor. De volgende keer dat de assistent je een query aanreikt, vang je de blowout vóór je op run drukt.

— Vragen —

Is het veilig om een AI SQL te laten schrijven tegen een productiedatabase?

Reads met een COUNT(*) eromheen en eerst een snelle EXPLAIN: meestal prima. Writes: nooit zonder elke clausule zelf te lezen. Test op een recente dump of read replica.

Waarom gaan Magento- en Drupal-queries vaker fout dan WordPress?

Beide gebruiken EAV-schema's waarin attribuutwaarden in zijtabellen staan, gekoppeld via attribute_id. Assistenten missen de join naar eav_attribute en geven of NULL's terug of filteren rijen stilletjes weg.

Wat is de snelste manier om een cartesisch product in een AI-JOIN te herkennen?

Wikkel de query in SELECT COUNT(*) FROM (...) AS audit en vergelijk met het rijaantal dat je uit het hoofd zou schatten. Een tienvoudige overshoot betekent een ontbrekend filter of DISTINCT.