Row Pattern Recognition Fördjupning
En guidad rundtur genom ISO/IEC 19075-5 · SQL R020 i PostgreSQL — vad som har byggts, vad som ännu återstår och hur det faktiskt körs.
Skumma standarden, gå igenom genomarbetade exempel, bläddra i designen och kör sedan en interaktiv NFA-simulator med egna mönster.
Diskussion: pgsql-hackers-tråd · senaste patchen (v47) · commitfest #4460.
AI-föröversatt — fel möjliga.
§1. Standarden, delmängden och de öppna frågorna
1.1 Standardens omfattning
Row Pattern Recognition introducerades i SQL genom ISO/IEC 9075-2:2016 och beskrivs i kompletterande dokument ISO/IEC 19075-5:2021 (del 5, "Row Pattern Recognition").
Standarden definierar två ytor för samma underliggande maskineri. Funktion R010 placerar en MATCH_RECOGNIZE-sats i FROM-listan för att producera en härledd tabell; funktion R020 integrerar samma mönsterlogik i en WINDOW-specifikation för att producera utdata från fönsterfunktioner. De två ytorna delar huvuddelen av sin syntax och all sin semantik, men är funktionellt distinkta ingångar — en databas kan implementera den ena, den andra eller båda.
Den patch-serie som diskuteras här implementerar en delmängd av endast R020; tabellsats-formen är medvetet utanför omfattningen.
R020-ytan är liten men uttrycksfull. En fråga kopplar ett mönster till ett fönster genom att lägga till tre satser inuti fönsterspecifikationen: PATTERN beskriver ett reguljärt uttryck över namngivna mönstervariabler, DEFINE tillhandahåller det booleska predikat som identifierar varje variabel och AFTER MATCH SKIP styr hur efterföljande matchningar placeras inom partitionen.
Valfria MEASURES, SUBSET, INITIAL/SEEK och hjälpfunktionen CLASSIFIER() kompletterar funktionen. (Standardens MATCH_NUMBER()-funktion tillhör endast R010-ytan — 19075-5 §6.3.3 (6) utesluter den uttryckligen från R020.)
Patchen täcker tillräckligt av detta ordförråd för att icke-triviala frågor ska fungera, men stannar före flera konstruktioner vars implementation beror på infrastruktur som ännu inte har byggts.
Resten av detta avsnitt delar upp standardens ordförråd i det som patchen redan stöder och det som den medvetet lämnar till senare. Listorna nedan återspeglar patch-seriens aktuella tillstånd.
1.2 Implementerade funktioner
Det implementerade ordförrådet räcker för att uttrycka de kanoniska V-form-, W-form- och vändmönster som från början motiverar row pattern recognition. Det täcker även varje standardkvantifierare — inklusive alla sju motvilliga varianter *?, +?, ??, {n}?, {n,}?, {,m}?, {n,m}? — samt de fyra navigationsprimitiv som DEFINE-villkor behöver för att jämföra angränsande rader.
- PATTERN-sats
- Definierar radmönstret inuti en fönsterspecifikation.
- Regex: + * ? |
- Standardkvantifierare och alternation.
- Regex: ( ) gruppering
- Delmönster inom parentes.
- Regex: {n} {n,} {,m} {n,m}
- Begränsade upprepningsantal.
- Motvilliga *? +? ?? {n}? {n,}? {,m}? {n,m}?
- Alla sju motvilliga (icke-giriga) varianter som standarden definierar (uteslutna från absorptionsoptimering).
- DEFINE-sats
- Booleska villkor som identifierar varje mönstervariabel.
- Universell radmönstervariabel
- Okvalificerade kolumnreferenser i
DEFINE(t.ex. baraPrice) löses till den aktuella raden, enligt 19075-5 §4.10/§4.16. - PREV, NEXT (med offset)
- Når n rader före/efter den aktuella raden (standard 1); det inre argumentet är ett godtyckligt värdeuttryck som utvärderas vid den navigerade raden.
- FIRST, LAST (med offset)
- Når en matchningsrelativ rad;
FIRST(expr, n)riktar sig mot raden n efter matchningens start,LAST(expr, n)raden n före den senaste matchningsraden. - Sammansatt navigation (fyra former)
- Yttre PREV/NEXT-steg lagrat ovanpå en inre FIRST/LAST:
PREV(FIRST(expr)),NEXT(FIRST(expr)),PREV(LAST(expr)),NEXT(LAST(expr))— både det inre och yttre steget accepterar var sin offset. - INITIAL
- Mönstermatchningar måste börja vid den aktuella raden (standard).
- AFTER MATCH SKIP PAST LAST ROW
- Standardläge för hopp; berättigar till kontextabsorption.
- AFTER MATCH SKIP TO NEXT ROW
- Tillåter överlappande matchningar; inaktiverar absorption.
1.3 Ej implementerat
De funktioner som förblir oimplementerade faller in i tre löst sammanhållna grupper. Den första — och i särklass största — är familjen av konstruktioner centrerade kring MEASURES: själva MEASURES-satsen, SUBSET, CLASSIFIER(), mönsterkvalificerade kolumnreferenser såsom B.price och mönsterkvalificerad navigation såsom LAST(B.price) eller PREV(B.price).
Dessa är inte oberoende luckor utan snarare olika vyer av en enda saknad pusselbit: executor måste behålla en per-matchningshistorik — en post, för varje rad i matchningen, över vilken mönstervariabel den mappades till — och ingen av dessa konstruktioner har någon meningsfull definition utan den. CLASSIFIER() läser ut variabelnamnet från den posten; B.price läser pris-kolumnen för de rader vars post säger B; LAST(B.price) går igenom samma raduppsättning och plockar den sista; SUBSET U = (A, B) definierar en virtuell variabel som aggregerar över båda hinkarna; och MEASURES utvärderar uttryck som AVG(U.Price) med exakt den aggregeringen.
Den nuvarande executorn utvärderar DEFINE-predikat rad för rad men registrerar inte de resulterande variabeltilldelningarna någonstans — det finns inget att fråga efter senare. Att bygga historikinfrastrukturen är därför en förutsättning för hela gruppen; de enskilda funktionerna faller ut som små projektioner när posterna väl existerar.
Den andra gruppen rör alternativa hoppmål: AFTER MATCH SKIP TO label, AFTER MATCH SKIP TO FIRST var och AFTER MATCH SKIP TO LAST var. Dessa beror också på matchningshistorik, eftersom executorn måste kunna peka på en specifik etiketterad rad inuti den senaste matchningen.
De återstående punkterna är en heterogen svans: nyckelordet SEEK (alternativet till INITIAL), det tomma mönstret (), exklusionsformen {- … -} och den ordningsokänsliga PERMUTE-operatorn.
- MEASURES
- Namngivna per-matchningsutdataruttryck, t.ex.
MEASURES LAST(B.Price) AS Bottom, AVG(U.Price) AS Avg; i R020 exponerade viaOVERsom en fönsterfunktion. Accepterar nyckelorden FINAL / RUNNING (19075-5 §5.4), vilka i R020 kollapsar till samma värde eftersom mått alltid utvärderas på matchningens sista rad (19075-5 §6.9 (3)). - SUBSET
- Namngivna unioner av mönstervariabler, t.ex.
SUBSET U = (A, B, C). Användbara överallt där en mönstervariabel kan refereras —MEASURES, mönsterkvalificerade referenser iDEFINE,CLASSIFIER(U)— vilka alla behöver matchningshistorik. - CLASSIFIER()
- Returnerar vilken mönstervariabel en given rad matchades som. Definierad för både DEFINE och MEASURES (19075-5 §5.9); svaret vid varje rad är variabelnamnet som registrerats i matchningshistoriken för den raden.
- Mönsterkvalificerad kolumnreferens
- Bart
B.priceiDEFINEellerMEASURES— värdet från kolumnen för den rad som mappats som den namngivna variabeln. - Mönsterkvalificerad navigation
- Begränsa navigation till rader som matchats som en namngiven variabel, t.ex.
LAST(B.price),FIRST(B.price),PREV(B.price),NEXT(B.price). - SEEK
- Matchning får börja var som helst i fönstret (jämfört med INITIAL).
- AFTER MATCH SKIP TO label
- Hoppa till en etiketterad rad.
- AFTER MATCH SKIP TO FIRST var
- Hoppa till den första raden som matchats som den namngivna variabeln.
- AFTER MATCH SKIP TO LAST var
- Hoppa till den sista raden som matchats som den namngivna variabeln.
- Regex: {- -}
- Exklusion — tar bort matchade rader från per-matchningsutdata.
- Regex: ()
- Tomt mönster — matchar den tomma sekvensen.
- PERMUTE(...)
- Ordningsokänslig matchning över listade variabler.
- Aggregatfunktioner i DEFINE
- Aggregat såsom
SUM,AVG,COUNTtillåts inte iDEFINE-predikat. Standarden tillåter dem som löpande aggregat över den partiella matchningen (per-rad-utvärdering mot rader som redan mappats till en variabel), vilket beror på samma per-matchningshistorik somMEASURES/CLASSIFIER()kräver.
Fyra ytterligare punkter är värda att nämna här, eftersom de lätt kan misstas för utelämnanden.
Den första är att mönsterankarna ^ och $ inte tillåts med RPR i WINDOW-satsen enligt själva standarden (19075-5 §6.13: "the anchors (^ and $) are not permitted with row pattern matching in windows"; med den underliggande definitionen i 19075-5 §4.14.1); deras frånvaro är överensstämmelse, inte en lucka.
Den andra är att MATCH_NUMBER() likaledes utesluts från R020 av standarden (19075-5 §6.3.3 (6) och 19075-5 §6.9 (1)) — den är endast del av R010-ytan, och dess frånvaro här är åter igen överensstämmelse snarare än en saknad funktion.
Den tredje är en uppsättning strukturella förbud som standarden ålägger R020 (19075-5 §6.17): row pattern recognition kan inte nästlas inom en annan row pattern recognition; MEASURES och DEFINE får inte innehålla yttre referenser; underfrågor inuti MEASURES eller DEFINE får inte referera till mönstervariabler; och RPR får inte användas inuti en rekursiv fråga.
Den fjärde är att själva MATCH_RECOGNIZE — funktion R010, tabellsats-ytan för samma maskineri — är utanför omfattningen för denna patch. Huruvida PostgreSQL så småningom lägger till R010 är en fråga för en framtida serie, inte ett mått på denna.
§2. Exempel — hur det faktiskt körs
Exemplen i detta avsnitt byggs upp stegvis. Vi börjar med det begreppsmässiga skälet till att radmönstermatchning verkligen skiljer sig från strängmönstermatchning, introducerar sedan de fyra navigationsfunktioner som gör DEFINE-villkor intressanta och går slutligen igenom tre kompletta spårningar: en enstaka matchning (NFA-rörelse), kontextabsorption i det enkla fallet samt fallet där absorption blir osäker.
Den interna mekanism som gör navigation billig — 1-slots-tupelbytet — hör till executors design och behandlas i nästa avsnitt, inte här.
2.1 Varför radmönster skiljer sig från textmönster
Ett reguljärt textuttryck matchar en ström av tecken, och vid varje position finns exakt en symbol att betrakta. Körtidsfrågan — "är den aktuella symbolen lika med 'A'?" — har ett enda ja/nej-svar. Backtrackande automater utnyttjar detta: högst en gren överlever per tecken, och kostnaden för tvetydighet betalas genom att läsa om indata.
Ett radmönster är annorlunda på ett grundläggande sätt. En rad är inte en enskild symbol; den är en tupel av kolumner som utvärderas mot en uppsättning booleska predikat, DEFINE-villkoren. Två predikat kan vara sanna vid samma rad samtidigt, och inget i standarden säger att de måste vara ömsesidigt uteslutande. Betrakta:
DEFINE A AS price > 100, B AS volume > 1000, C AS price > 200
En rad med price = 150, volume = 2000 uppfyller både A och B men inte C. Mönstermatcharen kan inte kollapsa detta till ett enda tillstånd — två mönstervariabler är aktiva, och varje mönsterelement som namnger någon av dem kan avancera. NFA:n måste därför bära flera samtidiga tillstånd per partitionsrad, inte ett.
Denna enda observation styr resten av arkitekturen. Exekveringstillståndet är en lista av kontexter, varje kontext är en lista av tillstånd, och vid varje rad frågar körtiden varje tillstånd: "givet de variabler som är sanna här, vart kan jag gå?" De grenar som uppstår är skälet till att körtiden behöver flera lager av beskärning — tillståndsdeduplicering inom en kontext, absorption mellan kontexter och de övriga regler som beskrivs i §3.6 — utan vilka antalet aktiva tillstånd och kontexter växer med partitionen och mönstermatchningen blir superlinjär.
2.2 Navigationsfunktioner
DEFINE-villkor som endast refererar den aktuella raden är begränsade; nästan varje intressant mönster behöver jämföra den aktuella raden med sina grannar eller med de rader som redan accepterats tidigare i matchningen. Standarden tillhandahåller fyra navigationsfunktioner för detta, och patchen implementerar alla.
- PREV(expr [, n])
- Raden n rader före den aktuella raden (standard n = 1). Används för jämförelser av typen "i dag jämfört med i går".
- NEXT(expr [, n])
- Raden n rader efter den aktuella raden. Framåtblickande; ovanlig i fönsterform eftersom fönstret är monotont.
- FIRST(expr [, n])
- Den n:te matchade raden i den aktuella matchningen, räknat från början. "Jämför med raden som startade denna matchning."
- LAST(expr [, n])
- Den n:te senast matchade raden. "Jämför med den senast matchade raden."
De fyra primitiven sammansätts: PREV och NEXT kan omsluta ett FIRST- eller LAST-anrop, vilket ger fyra sammansatta former vars semantik är "gå till en matchningsrelativ rad och stega sedan ett fast avstånd bort från den". Det är detta som låter ett DEFINE-uttryck läsa till exempel raden omedelbart före matchningens start.
- PREV(FIRST(expr [, n]) [, m])
- Stega m rader bakåt från matchningens n:te rad (standard m = 1). "Vilket var värdet just innan denna matchning började?"
- NEXT(FIRST(expr [, n]) [, m])
- Stega m rader framåt från matchningens n:te rad. Når längre in i matchningen utan att vara beroende av den aktuella raden.
- PREV(LAST(expr [, n]) [, m])
- Stega m rader bakåt från den n:te senast matchade raden. "Jämför med en rad strax före den senaste matchningen."
- NEXT(LAST(expr [, n]) [, m])
- Stega m rader framåt från den n:te senast matchade raden.
Två praktiska poänger är värda att lyfta här. För det första kan navigation sammansättas: PREV(FIRST(price)) läser raden omedelbart före matchningens start, och patchen stöder detta. För det andra har navigation konsekvenser för executors optimeringar. PREV och NEXT är relativa den aktuella raden och är alltid säkra; FIRST och offset-varianter av LAST är relativa matchningsgränserna, vilket interagerar med kontextabsorption och tvingar planeraren att hålla vissa kontexter vid liv som den annars skulle kasta. Mekanismen bakom denna interaktion är ämnet för §2.6.
2.3 Genomgånget exempel 1 — NFA-rörelse
SELECT company, tdate, price, first_value(price) OVER w AS start_price, last_value(price) OVER w AS end_price FROM stock WINDOW w AS ( PARTITION BY company ORDER BY tdate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN (START UP+ DOWN+) DEFINE UP AS price > PREV(price), DOWN AS price < PREV(price) );
Mönstret plattas ut till fyra element:
[0] START quant 1..1 next → 1 [1] UP quant 1..∞ next → 2 [2] DOWN quant 1..∞ next → 3 [3] #FIN
För prisserien 100, 110, 120, 115, 108, 130:
| Rad | Pris | Sanna variabler | Åtgärd |
|---|---|---|---|
| 0 | 100 | START | Ny kontext. START matchar och avslutas omedelbart (max=1); tillståndet avancerar till UP+. |
| 1 | 110 | START, UP | UP matchar. Avanceringen grenar sig: ett tillstånd loopar vid UP, ett annat avslutas till DOWN+. |
| 2 | 120 | START, UP | UP matchar i det loopande tillståndet och grenar sig igen. DOWN-tillståndet från rad 1 dör (120 ≮ 110). |
| 3 | 115 | START, DOWN | UP misslyckas i det loopande tillståndet och dör. DOWN-tillståndet från rad 2 matchar. Enda aktiva tillståndet. |
| 4 | 108 | START, DOWN | DOWN matchar. Avanceringen grenar sig: loop vid DOWN och avslut till #FIN — FIN-tillståndet är en matchningskandidat över raderna 0–4. |
| 5 | 130 | START, UP | Det loopande DOWN-tillståndet misslyckas (130 ≮ 108). Utan andra aktiva tillstånd färdigställs FIN-kandidaten som matchningen. En ny kontext startar vid rad 5 och avancerar till UP+ men ser aldrig någon DOWN före partitionens slut. |
Den centrala poängen: vid rad 3 uppfyller raden både START (alltid sant) och DOWN, men det enda tillstånd som överlevde rad 2 ligger på DOWN-utgångsgrenen, så endast övergången UP → DOWN tas. Multitillståndsnaturen från §2.1 syns som förgrening vid varje UP-matchning (tillståndet kan stanna vid UP+ eller avancera mot DOWN+). Girig preferens är "loopa igen före utgång", så med tillräckligt mycket data skulle de loopande grenarna förlänga matchningen ytterligare; här dör den loopande DOWN vid rad 5 (130 ≮ 108), och den tidigare FIN-kandidaten (raderna 0–4) — producerad när DOWN avslutades vid rad 4 — färdigställs som matchningen.
Frågans resultat följer direkt av denna spårning. Under RPR-semantik utvärderas fönsterfunktionerna first_value(price) och last_value(price) endast vid raden som startade matchningen — varje annan rad i matchningen producerar NULL för dessa fönsterfunktioner, eftersom dess reducerade ram är tom. Utdata för våra data ser därför ut som tabellen affischen visar i sin övre högra panel:
| Rad | Pris | start_price | end_price |
|---|---|---|---|
| 0 | 100 | 100 | 108 |
| 1 | 110 | — | — |
| 2 | 120 | — | — |
| 3 | 115 | — | — |
| 4 | 108 | — | — |
| 5 | 130 | — | — |
Rad 0 är matchningens start, så dess ram täcker raderna 0–4 och fönsterfunktionerna rapporterar V-formens öppningspris ($100) och bottenpris ($108). Raderna 1–4 ligger inuti matchningen men är inte dess start, så de får NULL. Rad 5 (pris $130) ligger utanför alla matchningar och får likaså NULL.
2.4 Genomgånget exempel 2 — alternation och lexikal ordning
Formen (A | B) ger matcharen ett val: vid varje rad testas de två alternativen oberoende, och valfritt antal av dem kan matcha. Det är här multitillståndsnaturen från §2.1 blir synlig inom en enda kontext — inte över kontexter (det är absorption), utan längs parallella grenar som executor utforskar i takt.
PATTERN ((UP | HIGH) DONE) DEFINE UP AS price > PREV(price), HIGH AS price > 100, DONE AS volume > 1000
Tänk dig en rad där priset både har stigit och överstigit $100 — både UP och HIGH är sanna. Varje alternativ skapar sitt eget tillstånd: ett som följer UP-grenen, ett som följer HIGH-grenen. De fortskrider parallellt tills DONE löser upp dem.
| Rad | Sanna variabler | Aktiva tillstånd |
|---|---|---|
| R | UP, HIGH | Tillstånd A på UP-grenen, tillstånd B på HIGH-grenen — båda vid "nästa: DONE" |
| R+1 | DONE | Båda tillstånden når #FIN vid samma rad |
Båda grenarna producerar en matchning av samma längd över samma rader, vilket lämnar matcharen med två kandidatvägar — en märkt UP, DONE och en märkt HIGH, DONE. Standarden löser detta genom lexikal ordning: bland alternativen som skrivs i (UP | HIGH) vinner det som skrivs först, oavsett matchningslängd. Eftersom UP kommer före HIGH är den överlevande vägen UP, DONE.
Lexikal ordning är det som gör CLASSIFIER() väldefinierad när den så småningom implementeras — den låter körtiden säga till användaren "denna rad matchade UP, inte HIGH" även när båda predikaten var sanna. Lexikal ordning är den primära regeln för alternation: en lex-tidigare gren vinner över en lex-senare även om den lex-senare grenen skulle producera en längre matchning, och en lex-senare (möjligen kortare) gren kan ändå vinna om varje lex-tidigare gren dör innan den når FIN. Girig längd avgörs inom en enskild kvantifierare — givet två kompletteringar av samma alternationsgren föredrar den giriga kvantifieraren fler iterationer.
2.5 Genomgånget exempel 3 — kontextabsorption (samma öde)
Det enklaste mönstret som uppvisar absorption är PATTERN (A+) med DEFINE A AS TRUE. Varje rad matchar A, och standarden kräver att matcharen betraktar varje rad som en möjlig matchningsstart. Naivt innebär detta N kontexter för en partition med N rader. Ta en fem-radspartition (vilka data som helst — predikatet är konstant sant):
| Rad | Naiva kontexter | Med absorption |
|---|---|---|
| 0 | C1[A:1] | C1[A:1] |
| 1 | C1[A:2], C2[A:1] | C1[A:2] — C2 absorberad |
| 2 | C1[A:3], C2[A:2], C3[A:1] | C1[A:3] |
| 3 | C1[A:4], C2[A:3], C3[A:2], C4[A:1] | C1[A:4] |
| 4 | fem kontexter | C1[A:5] |
Minnet växer från O(n) till O(1). Absorptionsregeln som motiverar bortkastningen är okomplicerad när kvantifieraren är obegränsad:
Affischens nedre vänstra panel ("① Context Absorption") är exakt denna regel visualiserad över fem rader.
En subtil men viktig poäng döljer sig i regeln: bortkastningen är säker eftersom predikatet A AS TRUE utvärderas till samma värde vid varje rad oavsett vilken kontext som frågar. Samma gäller för varje predikat som endast refererar den aktuella raden eller en fast offset från den — inklusive PREV. Nästa exempel växlar till en konkret handelsvecka av priser med ett PREV-baserat predikat, och §2.6 återanvänder exakt samma vecka för att göra symmetrin mellan säker och osäker absorption uppenbar:
PATTERN (RISE+) DEFINE RISE AS price > PREV(price)
Ta handelsveckan $100, $108, $112, $116, $110 från måndag till fredag — fyra uppgångar följda av ett plötsligt fall. Antag att C1 startar på tisdag (första dagen där RISE matchar: $108 > $100), och att executorn spekulativt även följer C2 som startar på onsdag. Varje rads RISE-villkor jämför radens pris med dess omedelbara föregångare — ett partitionsfaktum, inte ett kontextfaktum — så de två kontexterna tvingas beräkna samma boolean vid varje rad de delar:
| Dag | Pris | C1 — start tis price > PREV(price) | C2 — start ons price > PREV(price) |
|---|---|---|---|
| Mån | $100 | — | — |
| Tis | $108 | $108 > $100 ✓ (just startad) | — |
| Ons | $112 | $112 > $108 ✓ | $112 > $108 ✓ (just startad) |
| Tor | $116 | $116 > $112 ✓ | $116 > $112 ✓ |
| Fre | $110 | $110 > $116 ✗ — färdigställ | $110 > $116 ✗ — färdigställ |
Berättelsen brister i samma stund predikatet börjar bero på varje kontexts egna gränser — och det är vad §2.6 handlar om.
2.6 Genomgånget exempel 4 — när absorption blir osäker
DEFINE refererar FIRST — absorptionsregeln gäller inte längre och körtiden måste hålla kontexter vid liv.
Antag att en analytiker vill hitta efterföljande handelsdagar där en aktie hållit sig inom tio dollar från den dag som löpan startade — ett slags konsolideringsfönster. Mönstret och DEFINE ser ut så här:
PATTERN (STABLE+) DEFINE STABLE AS price < FIRST(price) + 10
Villkoret jämför nu den aktuella radens pris med priset vid den aktuella löpans start. Två löpor som startat på olika dagar har olika FIRST(price)-värden, så två tillstånd vid samma mönsterelement och samma räknare är inte längre utbytbara: deras framtider beror på den gräns som absorption skulle kasta bort.
Ta exakt samma handelsvecka som i §2.5 — $100, $108, $112, $116, $110 från måndag till fredag. Körtiden håller åter två kandidatlöpor vid liv samtidigt: C1 startade på måndag, C2 startade på tisdag (varje rad är en potentiell löpestart under STABLE+).
| Dag | Pris | C1 — start mån FIRST = $100 price < $100 + 10 | C2 — start tis FIRST = $108 price < $108 + 10 |
|---|---|---|---|
| Mån | $100 | $100 < $110 ✓ | — |
| Tis | $108 | $108 < $110 ✓ | $108 < $118 ✓ (just startad) |
| Ons | $112 | $112 < $110 ✗ — färdigställ mån–tis | $112 < $118 ✓ |
| Tor | $116 | — | $116 < $118 ✓ |
| Fre | $110 | — | $110 < $118 ✓ (löper fortfarande) |
Under absorption skulle C2 ha slagits samman med C1 på tisdag — den sammanslagna kontexten behåller bara ett tak, så C2:s distinkta vy (tak $118, den fyradagars löpa som slutar först på lördag) går inte längre att återskapa från det interna tillståndet. C2 måste hållas vid liv eftersom den är en oberoende matchningskandidat som körtiden kan behöva: när C1:s matchning slutar på onsdag måste nästa försök plocka upp från en fortfarande löpande C2 i stället för att börja om från noll. Närhelst ett DEFINE-predikat bär matchningsstartberoende inaktiverar planeraren därför absorption preemptivt.
(När den ledande kontextens matchning verkligen lyckas kastas kontexter som startat inom dess accepterade omfång — under standardvärdet AFTER MATCH SKIP PAST LAST ROW — helt enkelt; de hålls vid liv endast för att körtiden ska ha något att falla tillbaka på om den ledande matchningen misslyckas.)
Beroendetabellen i affischens nedre högra panel ("② Navigation") sammanfattar vilka navigationsformer som skapar matchningsstartberoende:
| Navigation | Matchningsstart-bero. | Absorberbar? |
|---|---|---|
| PREV, NEXT | ingen | ja |
| LAST (utan offset) | ingen | ja |
| LAST med offset | gränskontroll | nej |
| FIRST (alla) | direkt | nej |
De två exemplen i §2.5 och §2.6 reduceras till en enda regel. Samma öde är det som gör absorption säker: om varje kontext vid samma mönsterelement kommer att beräkna samma svar på varje framtida predikat behöver endast den äldsta behållas. Olika öden gör absorption osäker: så snart predikat förgrenar sig på kontext-privat tillstånd — vilket är precis vad FIRST och offset-bärande LAST gör — representerar varje aktiv kontext en framtid som ingen annan kontext kan återskapa, och att kasta någon av dem riskerar att producera ett felaktigt svar.
Planeraren upptäcker denna skillnad vid kompileringstillfället och avgör per kontext om absorption gäller. Detta är också varför affischens benchmark i panel ③ förblir linjär i både framgångs- och misslyckandefallen: när absorption är säker kollapsar körtiden kontexter, och när den inte är det accepterar planeraren multikontextkostnaden hellre än att riskera ett felaktigt svar.
Benchmark-siffrorna på affischen är samma algoritm utspelad i skala. På framgångsmönstret (A+ B+ C+ D) skalar både PostgreSQL och Trino O(n) när en matchning bekräftats, och PostgreSQL:s ledning — ungefär 16× till 33× — är till största delen JVM-glappet.
På misslyckandemönstret (A+ B+ C+ E) har Trino ingen absorption och degraderas till O(n²) genom att jaga varje potentiell matchningsstart; vid 100 000 rader tar det mer än fem timmar, medan PostgreSQL fortfarande blir klar på 92 ms, en uppsnabbning på ungefär 217 000×.
Det glappet är inte ingenjörspolering — det är exakt samma öde / olika öden-distinktionen från §2.5 och §2.6, tillämpad på varje rad av varje potentiell matchningsstart i partitionen.
2.7 Genomgånget exempel 5 — när SKIP inaktiverar absorption
Föregående exempel bröt absorption från data-sidan: FIRST i DEFINE fick varje kontext att utvärdera predikat olika. Absorption kan också brytas från utdata-sidan, och AFTER MATCH SKIP-satsen är det som styr det.
Betrakta mönstret PATTERN (A+) med DEFINE A AS TRUE över fem rader som alla matchar A. Under standardvärdet AFTER MATCH SKIP PAST LAST ROW hittar matcharen den längsta matchningen som börjar vid den tidigaste raden och hoppar sedan förbi den; varje kontext som startat inuti den matchningen kastas implicit som redundant — exakt den situation absorption är designad att hantera. Utdata är en enda matchning, raderna 0–4, och körtiden behöver endast en aktiv kontext.
Växla hopp-läge till AFTER MATCH SKIP TO NEXT ROW så ändras kontraktet:
PATTERN (A+) DEFINE A AS TRUE AFTER MATCH SKIP TO NEXT ROW
Nu måste varje potentiell startposition rapporteras separat, även när matchningar överlappar. För samma fem rader är körtiden tvungen att avge fem matchningar: raderna 0–4, 1–4, 2–4, 3–4 och 4–4. Ingen av dessa kan ersättas av en "längre som startar tidigare", eftersom standarden säger att användaren vill ha alla.
| Rad | SKIP PAST LAST ROW | SKIP TO NEXT ROW |
|---|---|---|
| 0 | matchningen börjar; raderna 0–4 blir den enda matchningen | matchningen börjar vid rad 0 |
| 1 | (inuti matchning 0) | matchningen börjar vid rad 1 — måste hållas vid liv |
| 2 | (inuti matchning 0) | matchningen börjar vid rad 2 — måste hållas vid liv |
| 3 | (inuti matchning 0) | matchningen börjar vid rad 3 — måste hållas vid liv |
| 4 | matchning 0 färdigställs (raderna 0–4) | fem matchningar färdigställs: 0–4, 1–4, 2–4, 3–4, 4–4 |
AFTER MATCH SKIP TO NEXT ROW är varje sent startad kontext sin egen utdatarad. Två kontexter vid samma mönsterelement är inte längre redundanta — de är båda obligatoriska utdata, och att kasta en av dem skulle tyst släppa en matchning som användaren bett om.
Observera att predikatet inte har ändrats. A AS TRUE utvärderas likadant vid varje rad oavsett vilken kontext som frågar, så samma öde-villkoret från §2.5 är fortfarande uppfyllt. Det som ändras är utdata-kravet: även kontexter med identiska framtider måste samexistera eftersom de motsvarar distinkta rader i resultatet. Planeraren inaktiverar därför kontextabsorption närhelst AFTER MATCH SKIP TO NEXT ROW är aktivt, oavsett DEFINE-satsen.
Att ställa §2.6 och §2.7 sida vid sida ger den fullständiga bilden av när absorption misslyckas:
FIRST eller offset-bärande LAST i DEFINE.AFTER MATCH SKIP TO NEXT ROW.
Endera villkoret räcker för att inaktivera absorption för de berörda kontexterna. När ingetdera är aktivt — standardvärdet AFTER MATCH SKIP PAST LAST ROW med DEFINE-villkor som endast använder PREV, NEXT eller bart LAST — kollapsar körtiden till en enda kontext per mönsterposition och förblir linjär hela vägen.
§3. Design — från parser till executor
Row Pattern Recognition implementeras som tre stadier som lämnar över arbete via väldefinierade mellanformer. Parsern omvandlar SQL-text till ett mönsterträd och en lista över DEFINE-predikat; planeraren kompilerar trädet till en platt array av mönsterelement och avgör vilka av dem som kan delta i kontextabsorption; executor kör arrayen mot partitionen rad för rad i en trefaslupp. Varje stadium har sin egen dataform, och det mesta av designens påhittighet finns vid gränserna: en platt NFA som ryms i cache, en navigationsmodell som återanvänder en enda tupel-slot snarare än att materialisera en per referens, och en absorptionsregel som omvandlar O(n²)-minne till O(n).
SQL-text
│
│ parserstadium
▼ validera ram
bygg mönsterträd
typkontrollera DEFINE
mönsterträd + DEFINE-lista
│
│ planerarstadium
▼ optimera trädet
kompilera till platt NFA-array
avgör absorberbarhet
platt NFA + absorptionsflaggor
│
│ executorstadium
▼ per-rad-motor:
Match → Absorb → Advance
matchningsresultat:
startrad, längd, framgång/misslyckande
Avsnitten nedan följer denna pipeline. §3.1 täcker parsern och mönsterträdets form; §3.2 täcker kompileringen som omvandlar trädet till den platta NFA:n; §3.3 täcker navigationsmodellen som DEFINE-predikat använder för att titta på angränsande rader; §3.4 täcker hanteringen av matchningsgränser — SKIP-, INITIAL- och begränsad ram-reglerna som fastställer var en matchning börjar och slutar; §3.5 är per-rad-motorn i tre faser; §3.6 samlar alla beskärningsmekanismer som håller tillståndsrymden begränsad; och §3.7 går igenom vad implementationen exponerar i EXPLAIN-utdata.
3.1 Parser — bygga mönsterträdet
Parsern känner igen mönsteridentifiering genom närvaron av en PATTERN-sats inuti en fönsterspecifikation. Dess första uppgift är ramvalidering, eftersom RPR ålägger begränsningar som vanliga fönsterfrågor inte gör: ramläget måste vara ROWS (inte RANGE eller GROUPS), startgränsen måste vara CURRENT ROW och EXCLUDE-alternativ är förbjudna. Dessa är överensstämmelsekontroller, inte optimeringar — RPR:s uppfattning om en ram är matchningsomfånget, och standarden överväger inte att fylla det med något annat än de matchade raderna.
När ramen klarat valideringen omvandlar parsern PATTERN-satsen till ett träd byggt av fyra slags noder — en variabelreferens, en sekvens (konkatenering), en alternation och en grupp (delmönster inom parentes). Varje nod bär kvantifieraren som tre tal — en undre gräns, en övre gräns (möjligen oändlig) och en flagga för motvillig matchning:
| Källa | Trädkodning |
|---|---|
| A | VAR(A, min=1, max=1) |
| A+ | VAR(A, min=1, max=∞) |
| A* | VAR(A, min=0, max=∞) |
| A{3,5} | VAR(A, min=3, max=5) |
| A+? | VAR(A, min=1, max=∞, reluctant=true) |
Varje DEFINE-predikat typkontrolleras sedan mot partitionens kolumner och tvingas till ett booleskt uttryck. Två praktiska saker sker som en del av detta. Först registreras varje kolumn som refereras av ett DEFINE-predikat som en del av frågans utdatakrav, så att planeraren propagerar dessa kolumner genom till executorstadiet även om den omgivande frågan inte väljer dem — utan det skulle körtiden inte ha något att utvärdera mot. För det andra är variabler som förekommer i PATTERN men aldrig i DEFINE implicit sanna: de matchar varje rad.
3.2 Kompilering — från AST till en platt NFA
Planeraren omvandlar parserns träd till den datastruktur som executor kommer att köra: en platt array av mönsterelement adresserad via index. Kompileringen fortskrider som en sexstegs-pipeline:
compile(astTree):
1. optimera AST
2. mät storlek och djup
3. allokera elementarrayen
4. fyll från AST
(tilldela next/jump-pekare)
5. färdigställ — sätt upp FIN-sentinel
6. markera absorptionsberättigade element
Den platta formen valdes av ett enkelt skäl: executor behöver traversera mönstret många gånger per partition, och en sammanhängande indexerbar array är den billigaste datastrukturen att gå igenom. Stegen 1 och 6 är de intressanta — steg 1 för att det avgör hur stor arrayen blir, och steg 6 för att det avgör om absorptionsoptimeringen i §2.5 alls aktiveras.
AST-optimering
Optimeringen lönar sig två gånger: en gång i den statiska elementräkningen av den platta arrayen, och igen för varje rad som bearbetas vid körtid. Varje transformation reducerar tillståndsrymden som körtiden måste räkna upp. Optimeraren tillämpar åtta omskrivningsregler i tur och ordning tills AST:n slutar förändras:
- SEQ-utplattning
SEQ(A, SEQ(B, C))→SEQ(A, B, C)- Sammanslagning av efterföljande variabler
-
A A→A{2}
A{2,3} A{1,2}→A{3,5} - Sammanslagning av efterföljande grupper
(A B)+ (A B)+→(A B){2,∞}- Sammanslagning av efterföljande ALT
(A | B) (A | B) (A | B)→(A | B){3}- Prefix/suffix-absorption
A B (A B)+→(A B){2,∞}- ALT-utplattning & dedup
-
(A | (B | C))→(A | B | C)
(A | B | A)→(A | B) - Kvantifierare-multiplikation
-
(A+)+→A+
(A{2,3}){5}→A{10,15} - Inpackning av enbarn
-
SEQ(A)→A
(A){1,1}→A
Kvantifierare-multiplikation är den enda transformationen som kräver en säkerhetskontroll: optimeraren kollapsar endast i tre säkra fall — båda kvantifierarna obegränsade ((A+)+ → A+), yttre exakt ((A{2,3}){5} → A{10,15}) eller barnet trivialt {1,1} ((A){2,5} → A{2,5}). Andra kombinationer kan introducera luckor som den platta formen skulle missa — t.ex. (A{2}){2,3} accepterar bara {4, 6}, men ett naivt A{4,6} skulle också acceptera 5 — så optimeraren lämnar dem orörda.
Elementform
Varje element i den platta arrayen representerar en enskild position i mönstret. Det finns fem logiska sorter: en variabelreferens (den enda sorten som konsumerar en rad); markörerna group begin och group end, vilka öppnar och stänger ett delmönster inom parentes; en alternationsmarkör, som leder en lista av grenar; och en slutmarkör i slutet av mönstret.
Varje element bär även ett djup (dess gruppnästningsnivå), kvantifieraren (en min- och max-upprepningsräknare, möjligen oändlig) och två övergångspekare — next, "vart ska man gå efter att ha konsumerat detta element," och jump, "vart ska man hoppa till" (används av alternation för att kedja grenar, av group begin för att kringgå kroppen när kvantifieraren tillåter noll, och av group end för att loopa tillbaka till kroppen).
För PATTERN ((A B)+) ser den kompilerade arrayen ut så här:
PATTERN ((A B)+) kompileras till:
[0] BEGIN depth 0 quant 1..∞
next → 1 jump → 4
(öppnar grupp; jump hoppar
till FIN när min = 0)
[1] A depth 1 quant 1..1
next → 2
[2] B depth 1 quant 1..1
next → 3
[3] END depth 0 quant 1..∞
next → 4 jump → 1
(stänger grupp; jump loopar tillbaka)
[4] FIN mönstret klart
Mönstret läses från vänster till höger via next, med jump som hanterar de icke-linjära kanterna. Vid idx 3 pekar END:s jump tillbaka till idx 1, vilket är hur den obegränsade yttre kvantifieraren loopar; vid idx 0 skulle BEGIN:s jump hoppa förbi END till idx 4 om gruppen var valfri. Körtiden behöver aldrig konstruera en graf vid körtiden — den följer bara dessa två pekare när den går igenom arrayen.
Per-element-attribut
Utöver formen bär varje element fyra logiska attribut som styr körtidens beteende vid den positionen:
- Motvillig
- Vänder ordningen för kvantifierarexpansion. Giriga kvantifierare försöker "loopa igen" före "avsluta"; motvilliga kvantifierare försöker "avsluta" först. Bärs av variabeln för
A+?; bärs av gruppens BEGIN och END för((…)+?). - Tomt-loopbar
- Sätts på gruppslut vars kropp är nollbar (
(A?)*,(A? B?)+,(A | B*)). Säger åt körtiden att lägga till en snabbavslutning vid sidan av den normala återloopningen, så att cykelvakten inte dödar legitima matchningar i grupper som kan producera tomma iterationer. - I-absorberbar-region
- Markerar varje element som ligger inom en absorptionsberättigad region — används av körtiden för att spåra om det aktuella tillståndet fortfarande befinner sig i säkert territorium.
- Absorptionsjämförelsepunkt
- Markerar det specifika element där absorptionsjämförelser ska köras. För ett enkelt
A+hamnar den på variabeln; för en obegränsad grupp som(A B)+hamnar den endast på gruppslutet.
Uppdelningen mellan "i regionen" och "jämförelsepunkt" har betydelse eftersom absorption endast är meningsfull vid punkter där iterationer stängs. Inuti kroppen av (A B)+ är körtiden mitt i en iteration och räknaren har ännu inte nått sitt slutgiltiga värde för den rundan; att jämföra här skulle innebära att jämföra ojämförbara värden. Tillståndet måste nå gruppslutet innan körtiden kan avgöra. Det första attributet säger därför "du är fortfarande i absorberbart territorium"; det andra säger "du har nått jämförelsepunkten — kontrollera nu".
Absorberbarhetsanalys
Steg 6 i kompileringen är det som ger §2.5:s "samma öde"-regel dess kompileringstidsvittne. Beslutet är skiktat:
isAbsorbable(query):
if SKIP-läge != SKIP PAST LAST ROW
→ return false
if ramslut != UNBOUNDED FOLLOWING
→ return false
if någon DEFINE beror på match_start
→ return false
gå igenom AST och markera
element som uppfyller
ett strukturellt fall
De tre första kontrollerna är frågenivå: de motsvarar exakt villkoren §2.7 (utdatasida: SKIP-läge), begränsad ram (gränsen bryter monotoniciteten) och §2.6 (datasida: FIRST eller offset-bärande LAST i DEFINE). När någon av dem misslyckas sätter analysen inga flaggor och absorption är inaktiverad för hela frågan. När alla godkänns tillåter AST-genomgången tre strukturella former:
- Fall 1 — enkel obegränsad variabel
-
Varje iteration är exakt en rad. En tidigare kontexts räknare är alltid ≥ en senares vid varje delad position.
A+,A*,A{2,∞} - Fall 2 — grupp med fast längd, obegränsad ytter
-
Varje barn har
(A B)+,(A B{2})+,((A (B C){2}){2})+min == max, så kroppen är semantiskt ekvivalent med sin utrullade{1,1}-form —(A B{2})+beter sig som(A B B)+. Varje iteration konsumerar ett fast antal rader; räknardominans gäller fortfarande. - Fall 3 — grupp vars kropp börjar med en obegränsad variabel
-
Den ledande obegränsade variabeln är själv absorberbar (Fall 1) och skyddar tidigare kontexter. Absorption stannar så snart tillståndet flyttar förbi A — resten av kroppen har ingen monotonicitetsgaranti, så flaggor sätts endast på A.
(A+ B)+
De strukturella fall som inte täcks av dessa tre former är lika instruktiva. A B+ kan inte absorberas av den nuvarande regeln eftersom det ledande A konsumerar en rad innan den obegränsade delen börjar, så kontexter som startar en rad isär befinner sig vid olika positioner inuti den obegränsade kroppen. (En uppföljande "PREFIX-absorptionsutvidgning" som hanterar fasta prefix via en skuggväg har designats och planeras till en separat patch.) Motvilliga kvantifierare som A+? utesluts genom konstruktion: absorptionsregeln antar girig semantik där längre matchningar subsumerar kortare, och motvillig matchning vänder den riktningen.
Resultatet är ett per-element-beslut snarare än ett per-mönster-beslut. En enda frågeplan kan ha absorption aktiverad för det ledande A+ i mönster som (A+ B+ C) eller (A+ B+)+ — det senare är bara Fall 3 tillämpat på dess ledande element — och inaktiverad för allt därefter; körtiden konsulterar helt enkelt jämförelsepunktsattributet på det aktuella tillståndets element varje gång den överväger en absorptionsomgång. När ett tillstånd flyttar in i en icke-absorberbar region är absorption permanent av för det tillståndet — exakt vad §2.5 och §2.6 behöver för att vara sant på den algoritmiska nivån.
3.3 Navigation — 1-slots-tupelbytet
DEFINE-uttryck är vanliga SQL-uttryck som utvärderas mot en rad, men de kan innehålla PREV, NEXT, FIRST eller LAST — referenser som pekar på olika rader. Raderna själva buffras redan i fönstrets tuplestore; det executorn fortfarande måste hantera är tupel-sloten som SQL-uttrycksmaskineriet läser från, eftersom kolumnreferenser inuti uttrycket är bundna till en slot vid plantid. Implementationen återanvänder en enda navigations-slot för varje navigationsanrop: sloten byts in före utvärderingen av det inre uttrycket och återställs efter, så att resten av SQL-uttrycksmaskineriet aldrig märker skillnaden.
Modellen som executor ser är liten: det finns en aktuell rad-slot som håller raden DEFINE-uttrycket utvärderas mot, och en navigations-slot som körtiden tillfälligt kan dirigera om till en annan rad. Runt varje navigationsanrop sätter körtiden upp navigations-sloten, utvärderar det inre uttrycket som om det läste den aktuella raden och återställer sedan den ursprungliga raden. Pseudokod:
eval_navigation(call):
targetPos = compute_target_position(call)
if targetPos är utanför sitt giltiga intervall:
return NULL
spara current_row_slot
hämta rad vid targetPos
in i current_row_slot
result = eval_inner_expression()
återställ current_row_slot
return result
Knepet är att det finns exakt en slot att spara och återställa. Det inre uttrycket — vad det än är, inklusive aritmetik, funktionsanrop eller andra kolumnreferenser — körs mot den utbytta sloten via samma utvärderingsväg som för den aktuella raden. Ingen alternativ utvärderare, ingen skugg-slot, ingen tupelkopia.
Sammansatt navigation plattas ut vid parstid så att bytet fortfarande sker en gång. PREV(FIRST(price)) identifieras som en tvåstegs-navigation — "gå till den första matchade raden, stega sedan en rad tidigare" — och lagras som ett enda navigationsanrop av sammansatt typ. Körtiden beräknar målpositionen i två stadier men utför endast ett slot-byte för att hämta den slutgiltiga raden:
compute_target_position(call):
# relativt aktuell rad
PREV(n):
return currentPos − n
NEXT(n):
return currentPos + n
# matchningsrelativt
FIRST(n):
return matchStart + n
LAST(n):
return lastMatchedRow − n
# sammansatt: matchnings-rel., sedan steg
PREV(FIRST(n), m):
return (matchStart + n) − m
NEXT(FIRST(n), m):
return (matchStart + n) + m
PREV(LAST(n), m):
return (lastMatchedRow − n) − m
NEXT(LAST(n), m):
return (lastMatchedRow − n) + m
validera mot lämpligt intervall
(matchningsintervall för FIRST/LAST inre,
partitionsintervall för yttre steg)
En positions-cache kortsluter tuplestore-hämtningen när flera navigationsanrop i samma DEFINE riktar sig mot samma rad — vanligt i uttryck som price > PREV(price) AND volume > PREV(volume) där båda anropen löses till föregående rad. Cachen lagrar endast "senast hämtad position", och bytet i sig förblir en enda operation.
Klassificeringen av navigationsanrop är planerarens bidrag till absorptionsbeslutet. Planeraren går igenom varje DEFINE-uttryck och sorterar varje variabel i en av två hinkar, baserat på om alla kontexter kommer att beräkna samma boolean vid samma rad eller om varje kontext kommer att beräkna sin egen. Hinken bestämmer två saker vid körtid: hur ofta variabeln utvärderas (delat en gång eller en gång per berörd kontext — §3.5 fas 1), och om det omgivande tillståndet är berättigat till kontextabsorption (§2.5 vs §2.6).
- Ingen navigation
- Endast
PREV/NEXT LASTutan offset- Sammansatt med inre
LAST(utan offset)
LASTmed offsetFIRST(alla former)- Sammansatt med inre
FIRST - Sammansatt med inre
LAST(med offset)
Klassificeringen utförs vid plantid och lagras vid sidan av varje DEFINE-variabel, så att körtiden inte lägger någon tid på att avgöra — den läser helt enkelt hinken för varje variabel när den behandlar en rad.
Bevarandebudget
Navigation når in i rader som fönsterfunktionsmaskineriet annars har strömmat förbi. För att hålla dessa rader tillgängliga är executor byggd ovanpå en tuplestore som behåller ett glidande fönster av senaste rader; frågan är hur brett det fönstret behöver vara. Patchen avgör vid kompileringstillfället utifrån två kompletterande offsets:
- Lookback-budget
- Hur långt bakåt från den aktuella raden ett DEFINE-anrop kan nå.
- Lookahead-från-start-budget
- Hur långt framåt (eller bakåt, när negativ) från den äldsta aktiva kontextens matchningsstart ett DEFINE-anrop kan nå.
Vid körtid sätts tuplestores trim-märke till den tidigare av två positioner — aktuell rad minus lookback-budgeten, och den äldsta aktiva kontextens matchningsstart plus lookahead-budgeten. Allt före det märket kan inte nås av något navigationsanrop i någon aktiv kontext, och tuplestoren är fri att kasta det. De två Nav Mark-räknarna som EXPLAIN rapporterar (§3.7) — Lookback och Lookahead — är de uppmätta topparna för de två budgetarna, det djupaste executorn någonsin behövde nå i endera riktningen under frågans gång.
3.4 Matchningsgränser — SKIP, INITIAL och begränsad ram
En lyckad matchning registreras som ett litet bunt med värden: en giltighetsflagga, en framgångs-/misslyckandeflagga, raden där matchningen börjar och antalet rader den konsumerade. När giltighetsflaggan är satt kan efterföljande frågor till executor — "är denna rad inuti en matchning?" — besvaras i O(1) genom att inspektera bunten. En längd noll är ett verkligt utfall, inte ett fel: det representerar ett mönster som matchade utan att konsumera någon rad, vilket executor måste skilja från "ingen matchning har försökts vid denna position än".
AFTER MATCH SKIP-satsen avgör var nästa matchningsförsök startar. AFTER MATCH SKIP PAST LAST ROW flyttar till raden efter matchningsslutet och producerar det icke-överlappande utdata som de flesta frågor vill ha samt aktiverar absorptionsoptimeringen.
AFTER MATCH SKIP TO NEXT ROW flyttar endast till raden efter matchningens start och tillåter överlappande matchningar; planeraren inaktiverar därför absorption för hela frågeplanen när detta läge är aktivt.
De två hoppmål som standarden också definierar — AFTER MATCH SKIP TO FIRST var och AFTER MATCH SKIP TO LAST var — beror på per-matchningshistorik som denna patch inte behåller. De finns inte alls i grammatiken, så parsern rapporterar ett generiskt syntaxfel om endera anges.
Detsamma gäller SEEK, specifikationens alternativ till INITIAL. Under SEEK kan ett matchningsförsök som startar vid rad R lyckas vid vilken rad som helst från R till ramens slut, inte bara vid R själv. Patchen implementerar endast INITIAL: varje potentiell matchning förankras vid en specifik rad. Parsern rapporterar ett fel om SEEK begärs. Begränsade ramar får sin egen behandling — när användaren skriver ROWS BETWEEN CURRENT ROW AND N FOLLOWING i stället för UNBOUNDED FOLLOWING, kortsluter executor varje kontext vars matchning nått gränsen genom att tvinga fram en miss, och absorption inaktiveras eftersom gränsen bryter monotonicitetsantagandet som absorption vilar på.
3.5 Per-rad-motorn i tre faser
Executors per-rad-drivrutin anropas av det omgivande fönsterfunktionsmaskineriet närhelst det behöver veta om en given rad tillhör en matchad ram. Drivrutinen hittar eller skapar kontexten för den aktuella startpositionen, utvärderar varje DEFINE-predikat en gång mot den aktuella raden för att producera en per-variabel-boolesk array, och driver sedan NFA:n framåt med en rad. Själva framåtsteget består av tre faser i fast ordning — Match, Absorb, Advance — inneslutna i samma yttre loop:
processRow(currentPos):
# Fas 1 — MATCH (konvergens)
for varje kontext:
if kontext överskrider begränsad ram:
tvinga fram miss (färdigställ tidigt)
continue
if matchStartRow skiljer sig från
den delade utvärderingspositionen:
omutvärdera matchningsstart-
beroende DEFINE-vars (§3.3)
match(kontext, varMatched)
# Fas 2 — ABSORB
if mönstret är absorberbart:
uppdatera varje kontexts flaggor
absorb_contexts()
# Fas 3 — ADVANCE (divergens)
for varje kontext:
advance(kontext, currentPos)
Ordningen är inte ett stilval. Match måste köras först eftersom absorption jämför post-match-räknare; att köra Absorb före Match skulle jämföra tillstånd som är på väg att dö. Advance måste köras sist eftersom det är den enda fasen som skapar nya tillstånd — den expanderar varje överlevande tillstånd genom epsilonövergångar tills vart och ett når en variabel som väntar på nästa rad. Att köra Absorb efter Advance skulle innebära att jämföra divergerade efterföljare och missa den punkt där tillstånden är som renast jämförbara.
Fas 1 — Match
Match är en "konvergens"-fas: tillstånd antingen överlever med en inkrementerad räknare eller dör. En subtil poäng är att för variabler som sitter i en absorberbar region utför Match också en liten mängd framåtprogress, så att Absorb kan jämföra rent. Täckningsvillkoret aktiveras endast vid den absorberbara jämförelsepunkten — END för den obegränsade gruppen — så tillstånd som just matchat mellan-iterations-variabler (som B inuti (A B)+) behöver förflyttas upp till den jämförelsepunkten under själva Match; annars hittar Absorb inget berättigat att jämföra och optimeringen aktiveras aldrig.
match(context, varMatched):
for varje tillstånd i kontexten:
elem = pattern[state.elemIdx]
if elem inte är en variabel:
continue # advance hanterar det
if not varMatched[elem.varId]:
släpp tillstånd # död gren
continue
state.counts[elem.depth] += 1
# Inline framåtprogress så att
# nästa fas kan jämföra vid
# jämförelsepunktselementet i stället
# för mitt i iterationen.
if elem är in-region men inte
jämförelsepunkten,
och har nått sin max-räknare,
och elem.next är ett gruppslut:
gå END-kedjan:
inkrementera yttre grupps räknare
avancera state.elemIdx till END
fortsätt så länge fortfarande in-region,
måste-avsluta, och next är END
(stannar vid jämförelsepunkten
eller vid ett fortfarande loopbart element)
Genomgången av END-kedjan är det som gör nästlade grupper med fast längd absorberbara. I ((A B){2})+, när B når sin max (B är {1,1}), måste den inre gruppens räknare inkrementeras; om den räknaren också når sin max — och stänger den inre {2} — måste även den yttre gruppens räknare inkrementeras, och så vidare, tills tillståndet hamnar på den yttersta absorptionspunkten — END för den obegränsade yttre gruppen (+). Att göra detta arbete i Match låter Absorb jämföra mot kontexter som redan konsoliderat sina post-iterations-räknare.
Fas 2 — Absorb
Absorb-passet går igenom kontexter från nyast (svans) till äldst (huvud). För varje pågående kontext som är fullt absorberbar söker det bakåt efter en äldre pågående kontext som täcker den, och frigör den nyare om någon hittas. Eftersom kontexter hålls i skapandeordning och varje rad skapar högst en kontext betyder "nyare" och "äldre" verkligen "startade senare" och "startade tidigare".
absorb_contexts():
for ctx från svans bakåt:
if ctx är klar
eller har något icke-absorberbart tillstånd:
hoppa över
for äldre från ctx.prev bakåt:
if äldre är klar
eller inte har något absorberbart tillstånd:
hoppa över
if covered(äldre, ctx):
free(ctx)
registrera absorptionslängd
break
covered(äldre, nyare):
for varje tillstånd i nyare:
elem = pattern[state.elemIdx]
if elem inte är jämförelsepunkten:
return false
if inget tillstånd i äldre med:
samma elemIdx
och isAbsorbable
och äldre.counts[depth]
>= nyare.counts[depth]:
return false
return true
Två mikrobeslut följer av detta. Det första är att täckningskontrollen avvisar omedelbart om något tillstånd i den nyare kontexten sitter någon annanstans än vid en jämförelsepunkt — att jämföra vid icke-bedömningspunkter skulle inte vara en meningsfull jämförelse.
Det andra är det asymmetriska flaggparet på varje kontext: har-absorberbart-tillstånd besvarar "kan denna kontext absorbera en nyare?" och är monotont (det kan endast gå sant→falskt när tillstånd dör), medan alla-tillstånd-absorberbara besvarar "kan denna kontext absorberas?" och är dynamisk (det vänder tillbaka till sant när ett icke-absorberbart tillstånd tas bort). Båda flaggorna kontrolleras i konstant tid innan täckningssökningen ens startar, så absorption betalar sin fulla kostnad endast på kontexter som har en verklig chans att absorberas.
Fas 3 — Advance
Advance är "divergens"-fasen: varje överlevande tillstånd expanderas genom epsilonövergångar tills varje gren når antingen en variabel som väntar på nästa rad eller FIN-sentinelen. Expansionen är djupet-först, och den ordning i vilken syskongrenar gås igenom är det som gör att standardens preferensregel faktiskt får effekt — den lexikalt första grenen läggs alltid till först, och dedupliceringssteget vid tillståndsinsättning kastar tyst ekvivalenta senare tillägg.
advance(context, currentPos):
plocka ut alla nuvarande tillstånd;
bygg om ctx.states från grunden
for varje tillstånd i lexikal ordning:
rensa besökta-element-bitmappen
advance_state(state) # DFS
# Preferens: när en DFS når FIN
# är återstående tillstånd mindre
# föredragna och kan släppas.
if FIN nåddes och tillstånd kvarstår:
frigör resten
break
advance_state(state):
gå via state.elemIdx,
rekursera genom:
ALT-grenar (i ordning),
BEGIN (gå in i grupp; plus valfri
hopppath om min = 0),
END (loopa tillbaka / avsluta / snabbavslut —
se nedan),
FIN (registrera matchEndRow,
spara matchedState och beskär
senare kontexter inom denna
kandidats omfång — se nedan),
stannar vid varje variabel som påträffas:
lägg till tillstånd i ctx.states
(deduplicerar via elemIdx + counts)
Att registrera ett tillstånd som nådde FIN gör mer än att bara bokmärka kandidatmatchningen. Under AFTER MATCH SKIP PAST LAST ROW måste nästa rapporterbara matchning starta strikt efter den aktuella — så i samma stund en kandidat registreras beskärs varje efterföljande kontext vars startrad ligger inom kandidatens omfång omedelbart, även om kontexten som träffade FIN kan fortsätta söka efter en längre matchning genom girig fallback.
Beskärningen är säker eftersom oavsett hur den sökningen slutar (en längre matchning ersätter kandidaten, eller kandidaten står sig) så startade alla beskurna kontexter inom ett omfång som nästa rapporterbara matchning måste hoppa förbi och kunde därför aldrig producera egna utdata.
Detta är ett av två FIN-tids-beskärningssteg — det andra, beskrivet tidigare i detta avsnitt som del av Advance, kastar lexikalt underordnade tillstånd inom samma kontext.
Den knepigaste per-element-logiken finns i END-hanteraren. När en grupps iterationsräknare är under den nedre gränsen måste körtiden loopa tillbaka; när den är vid eller över den övre gränsen måste den avsluta; däremellan är båda alternativen giltiga och kvantifierarens girighet avgör vilket som ska prövas först:
advance_end(state, elem):
count = state.counts[elem.depth]
if count < elem.min:
loopa tillbaka in i kroppen
if elem är empty-loopable:
# nollbar kropp, se §3.2
klona även ett snabbavsluts-
tillstånd som lämnar gruppen
med räknaren uppfylld
(räddar legitima matchningar
som cykelvakten annars skulle
döda)
elif count >= elem.max:
återställ räknare på detta djup
avsluta via next
(END→END: inkrementera yttre
END:s räknare)
else:
# min <= count < max: förgrena
bygg ett avslutningstillstånd
(räknare på detta djup nollställs)
if girig:
loopa först, avsluta sedan
# föredra längre
else (motvillig):
avsluta först
if avslut nådde FIN:
släpp loopen
# föredra kortare
else loopa sedan
Varje tillstånd som läggs till i en kontext går igenom en dedupliceringskontroll som jämför dess position och räknare med den befintliga tillståndslistan. Eftersom Advance bearbetar grenar i DFS-ordning hamnar tillståndet från den första grenen i varje alternation först — och varje senare gren som producerar samma position och räknare avvisas vid insättning. Detta är exakt vad §2.4:s regel om lexikal ordning ber om, implementerat längst ned i körtiden utan ett separat pass.
Tomt-loopbara grupper
Ett subtilt fall som körtiden måste oskadliggöra är den nollbara gruppen — en grupp vars kropp kan matcha noll rader för att varje barn till kroppen självt är valfritt. Mönster som (A?)*, (A? B?)+ och (A | B*) har alla denna egenskap: beroende på data kan kroppen slutföra en iteration utan att konsumera någon rad alls. Det är okej i princip, men det skapar en verklig fara för NFA:ns epsilonexpansion. Om kroppen producerar en tom matchning loopar END-elementet tillbaka till BEGIN, kroppen producerar återigen en tom matchning, BEGIN loopar till END, och så vidare. Utan något som stoppar det skulle Advance:s DFS aldrig terminera.
Körtiden stoppar det med en besökt-element-bitmapp (en bit per mönsterelement), nollställd före varje tillstånds DFS-expansion: så snart något element besöks en andra gång inom samma expansion överges den vägen. Cykelvakten är ovillkorlig och billig, men har en bieffekt — den kan också överge vägar som borde nå den nedre gränsen genom legitima tomma iterationer. Ta (A?){2,3} utan att något A matchar någonstans i radströmmen:
önskat beteende:
iter 1: A? matchar noll rader
→ END, count = 1 (under min)
iter 2: A? matchar noll rader
→ END, count = 2 (uppfyller min)
avsluta med en noll-längds matchning
vad cykelvakten gör på egen hand:
iter 1: A? hoppas över → END, count = 1
→ loopa tillbaka till BEGIN
iter 2: BEGIN redan besökt
→ DFS avbryter
count når aldrig min
→ matchningen misslyckas (felaktigt)
Lösningen avgörs vid kompileringstillfället och utförs vid körtid. Närhelst planeraren ser en grupp vars kropp är nollbar märker den den gruppens END-element som tomt-loopbar. Vid körtid, när END-hanteraren är på väg att loopa tillbaka eftersom iterationsräknaren fortfarande är under den nedre gränsen, säger tomt-loop-taggen åt den att klona ett ytterligare snabbavsluts-tillstånd vid sidan av den normala återloopnings-vägen:
advance_end (count under min):
primärväg:
loopa tillbaka in i kroppen
(håller dörren öppen för en verklig,
icke-tom matchning i nästa
iteration)
if elem är empty-loopable:
snabbavslutsväg:
återställ detta djups räknare
lämna gruppen via next,
behandla återstående obligatoriska
iterationer som tomma matchningar
De två vägarna spelar kompletterande roller. Den primära återloopningen är det som fångar fallet där kroppen fortfarande har verkliga matchningar att producera senare — till exempel, i (A?){2,3} följt av data där A matchar endast på senare rader, är det återloopningen som låter den andra och tredje iterationen hitta icke-tomma matchningar. Snabbavslutet är det som fångar fallet där kroppen aldrig producerar något: det kringgår cykelvakten genom att helt lämna gruppen och förklarar "resten av de obligatoriska iterationerna är tomma", och låter matchningen lyckas med en noll-längds kropp. Båda tillstånden läggs till i kontexten; det av dem som extenderas framgångsrikt vinner, och dedupliceringskontrollen vid insättningstillfället förhindrar att någondera vägen genererar fler tillstånd än sin andel.
Utöver cykelvakten finns ytterligare ett uppstartstrick som disambiguerar noll-rad-utfall vid själva starten av en kontext. Kontextskapandesteget vid varje potentiell startrad kör en initial advance genom epsilonövergångar innan någon rad konsumeras, med en position en rad före den faktiska starten. Varje väg som når FIN-sentinelen genom enbart epsilonövergångar — utan att konsumera en rad — producerar därför en slutposition mindre än startpositionen; den negativa spann-koordinaten, kombinerad med huruvida FIN faktiskt nåddes, kodar skillnaden mellan en tom matchning (längd-0-matchning accepterad) och en omatchad start utan att behöva en separat flagga.
3.6 Hur tillståndsrymden förblir begränsad
Körtidens linjäritet är inte resultatet av en enda optimering. Den är den ackumulerade effekten av en skiktad uppsättning beskärningsregler, där var och en fångar en annan orsak till tillståndsrymdtillväxt vid en annan punkt i per-rad-cykeln. Vissa avgörs vid kompileringstillfället och konsulteras endast vid körtid; andra aktiveras dynamiskt. Vissa dödar enskilda tillstånd; andra dödar hela kontexter. Avsnitten ovan introducerade var och en i sitt sammanhang; tabellen nedan samlar dem på en sida.
- Misslyckat predikat Match
- Variabeltillstånd vars DEFINE utvärderades till falskt på den aktuella raden — döda grenar.
- Inline END-kedjeavanceringen Match
- Variabler som har nått sin max-räknare och som annars skulle lämna tillståndet mitt i en iteration; avancerade genom gruppslut med fast längd så att absorption kan jämföra vid rätt punkt.
- Kontextabsorption Absorb
- Nyare kontexter vars varje tillstånd täcks av en äldre kontexts tillstånd med en högre räknare — se §2.5 för den begreppsmässiga regeln, §3.2 för kompileringstidsberättigandet, §3.5 för per-par-kontrollen.
- Tillståndsdeduplicering Advance
- Tillstånd nådda via olika DFS-grenar som hamnar vid samma position med samma räknare — endast det första (lexikalt tidigaste) överlever; senare ekvivalenter släpps tyst, vilket också är hur preferens upprätthålls (§2.4).
- FIN-tidig terminering (inom kontext) Advance
- Tillstånd som fortfarande väntar i DFS-kön när en gren når FIN — enligt lexikal ordning är alla återstående tillstånd mindre föredragna och kan kastas omedelbart.
- Kandidatmatchningsbeskärning (mellan kontexter) Vid FIN
- Andra kontexter vars startrad ligger inom kandidatmatchningens omfång — kontexten som träffade FIN kan fortfarande söka efter en längre matchning (girig fallback), men under
AFTER MATCH SKIP PAST LAST ROWkan ingen kontext inom kandidatens omfång längre producera en rapporterbar utdata, oavsett hur den sökningen slutar, och beskärs omedelbart. - Cykelvakt Advance
- Epsilonexpansioner som återbesöker samma mönsterelement inom samma DFS — skulle annars loopa för evigt i nollbara grupper.
- Tomt-loop-snabbavslut Advance
- Legitima tom-iterations-matchningar som cykelvakten skulle döda i nollbara grupper — kringgår cykeln genom att lämna gruppen med återstående obligatoriska iterationer förklarade tomma.
- Begränsad-ram-avskärning Match
- Kontexter vars matchning har nått den användarspecificerade ramens övre gräns — tvingas till en miss så att de inte kan sträcka sig bortom den (§3.4).
Att läsa posterna efter deras fas-emblem följer en kontexts livstid: beskärning aktiveras vid varje rad genom de tre huvudfaserna (Match, Absorb, Advance) och igen vid matchningsavslutning (Vid FIN). Att i stället läsa beskrivningarna grupperar reglerna efter vad de riktar sig mot: döda grenar, redundanta kontexter, ekvivalenta tillstånd, oändliga loopar och översträckning förbi användarpålagda gränser.
Ingen enskild regel räcker på egen hand. Cykelvakten ensam skulle döda legitima matchningar i nollbara grupper; tomt-loop-snabbavslutet ensamt skulle inte stoppa oändliga epsilonloopar; absorption ensamt skulle överkonsolidera när DEFINE refererar matchningsstart; deduplicering ensamt skulle inte ta bort redundanta kontexter, endast redundanta tillstånd. Körtiden förblir linjär i de fall som spelar roll — PATTERN (A+ B+ C+ D) på 100 000 rader, affischens panel ③-benchmark — endast för att varje skikt fångar det som skikten ovanför missar.
3.7 EXPLAIN-utdata
EXPLAIN ANALYZE på en fråga med RPR exponerar statistik på NFA-nivå som inte finns för vanliga fönsterfunktioner. Tre grupper av räknare emitteras vid sidan av fönsteroperatorn:
NFA States: peak, total, merged NFA Contexts: peak, total, pruned NFA: matched (len min/max/avg) NFA: mismatched (len min/max/avg) NFA: absorbed (len min/max/avg) NFA: skipped (len min/max/avg) Nav Mark Lookback:| runtime | retain all Nav Mark Lookahead: | runtime | retain all (endast när frågan använder FIRST, PREV(FIRST(...)) eller NEXT(FIRST(...)))
Peak och total är direkt instrumentering av körtiden: hur många tillstånd som någonsin var aktiva samtidigt, hur många som skapades under frågans livstid och hur många som slogs bort av deduplicering. Matchningslängdshistogrammen separerar fyra utfall — lyckade matchningar, misslyckade matchningsförsök, absorberade kontexter och kontexter som beskars (hoppades över) utan att utvärderas — och att rapportera dem med min/max/avg gör prestationspatologier synliga med en blick: en frisk körning visar de flesta kontexter som antingen matchade eller absorberade, med små miss-längder.
De två Nav Mark-räknarna rapporterar tuplestore-bevarandebudgeten som §3.3 härleder vid kompileringstillfället. Lookback är den djupaste bakåträckvidden över PREV, LAST med offset och de sammansatta formerna med inre LAST; Lookahead är den djupaste framåt- (eller bakåt-, när negativ) räckvidden mätt från den äldsta aktiva kontextens matchningsstart, som FIRST och de sammansatta formerna med inre FIRST bidrar till.
Varje räknare skrivs ut som ett fast heltal när offset är en konstant, "runtime" när offset är ett icke-konstant uttryck som måste utvärderas vid varje anrop, och "retain all" när körtiden behöver en obegränsad budget. Lookahead emitteras endast när frågan faktiskt använder matchningsstartrelativ navigation.
Tillsammans gör dessa räknare det möjligt att felsöka RPR-prestanda utan att koppla gdb till backend.
Utöver räknarna återger EXPLAIN också troget de ursprungliga PATTERN- och DEFINE-satserna, inklusive motvilliga kvantifierare, gruppupprepning och AFTER MATCH SKIP-alternativet. Implementationen gör viss ansträngning för att göra denna rundresa stabil så att pg_dump och pg_upgrade kan bevara RPR-objekt utan semantisk drift — regressionssviten under rpr_explain verifierar detta fall för fall (se §4).
§4. Tester — täckningskarta
Patchen levereras med fem regressionssviter som tillsammans tränar varje skikt som beskrivs i §3 — ungefär 13 000 rader SQL, där varje svit fokuserar på olika hänsyn. Uppdelningen är medveten: att hålla parserhänsyn, körtidskorrekthet, planerareinteraktioner och utdataformatering i separata filer gör fel lättare att lokalisera och förhindrar att en orelaterad förändring i ett skikt oavsiktligt ogiltigförklarar tester i ett annat. De fem sviterna är:
- rpr
- Frågesemantik från ände till ände — realistiska fönsterscenarier på syntetisk aktiedata (V-form, W-form, efterföljande uppgångar, vändningar).
- rpr_base
- Parserskiktet — nyckelordsacceptans, syntaxformer, kvantifierare, navigeringsparsning, felmeddelanden och stabilitet i pg_dump/pg_upgrade-rundresa.
- rpr_nfa
- NFA-körtid — trefasloopen, absorption i varje absorberbar form och gränsfall i kontextens livscykel.
- rpr_explain
- Utdataformatering — NFA-statistik, mönsterdeparse, identifierarcitering, formatstabilitet vid omladdning.
- rpr_integration
- Planerareinteraktioner — skyddsvallar som hindrar orelaterade fönsteroptimeringar från att korrumpera RPR-semantik.
4.1 rpr — scenarier från ände till ände
Scenariosviten är testuppsättningens publika ansikte: den använder en syntetisk aktiekurs-datamängd på ungefär 1 600 rader och kör realistiska frågor mot den — V-form-återhämtning, W-form (dubbel botten), efterföljande uppgångar och fall, vändningsmönster, partitioner med flera symboler. Det är den enda sviten där indata och utdata läses som frågor som en användare faktiskt kan tänkas skriva; de andra är medvetet reduktiva och fokuserade på ett skikt åt gången.
Eftersom dessa frågor kombinerar varje skikt (parser, planerare, executor, EXPLAIN) talar ett enskilt fel i rpr sällan om var felet ligger. De fyra sviterna som följer är bisektionen: ett fel i rpr plus en passerande rpr_base utesluter parsern; plus en passerande rpr_nfa snävar in det till scenariospecifika dataformer; plus en passerande rpr_integration utesluter planerareinblandning; och eventuell deparse-drift dyker upp i rpr_explain.
4.2 rpr_base — parserytan
Bas-sviten är den största, och den är störst av en anledning: den är ansvarig för att bevisa att varje laglig syntaxbit i §1.2 faktiskt parsas, varje olaglig bit i §1.3 avvisas med ett användbart fel och varje accepterad form överlever en serialiseringsrundresa. Huvuddelen är formad som små fokuserade utdrag — en per syntaktisk funktion — snarare än långa realistiska frågor, eftersom målet är täckning snarare än scenariorealism.
Serialiseringstesterna förtjänar särskild uppmärksamhet. RPR-objekt (vyer, materialiserade vyer, pg_dump-utdata) måste rundresa genom katalogrepresentationen utan semantisk drift, inklusive finesser som motvillig-flaggan på en kvantifierare eller den exakta formen av ett sammansatt navigationsuttryck. En liten uppsättning serialiseringsspecifika objekt (vyerna rpr_serial_v* och deras stödjande tabeller) lämnas medvetet på plats vid körningens slut, så att den omgivande regressionsinfrastrukturen kan plocka upp dem och köra pg_dump och pg_upgrade mot dem. Resten av testställningen släpps som vanligt. Buggar som fångas på detta sätt (såsom en motvillig-flagga som tappas mellan deparse och åter-parse) dyker upp endast när rundresan körs från ände till ände.
4.3 rpr_nfa — körtidsmotorn
Detta är sviten som tränar varje mekanism som beskrivs i §3.5 och §3.6. Dess tester följer ett konsekvent mönster: en indatatabell vars rader är explicita booleska arrayer som deklarerar vilka DEFINE-variabler som matchar vid varje rad, parad med ett mönster som undersöker ett specifikt körtidsbeteende. Det booleska array-idiomet kopplar bort körtidstestet från DEFINE-utvärderingsmaskineriet — det som testas är "givet att dessa variabler matchar här, producerar NFA-loopen det förväntade matchningsomfånget?" snarare än "beräknar DEFINE-uttrycksutvärderaren dessa booleaner korrekt?" DEFINE-utvärderaren testas på annat håll (rpr_base för parsning, rpr för beteende från ände till ände).
En typisk testfixtur ser ut så här — en kolumn av variabelnamnsarrayer där varje post säger vilka DEFINE-variabler som aktiveras på den raden, och ett mönster vars DEFINE-satser testar för dessa namn direkt:
SELECT id, flags, first_value(id) OVER w AS ms, last_value(id) OVER w AS me FROM (VALUES (1, ARRAY['A']), (2, ARRAY['A','B']), (3, ARRAY['A','B']), (4, ARRAY['B']), (5, ARRAY['_']), (6, ARRAY['A'])) AS t(id, flags) WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL PATTERN (A+ B+) DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags));
Att läsa array-kolumnen nedåt är att läsa testscenariot direkt. Raderna 2 och 3 bär båda namnen — A och B aktiveras båda där, så NFA:n har ett verkligt val om var man ska byta från A-benet till B-benet. Den förväntade matchningen (A vid raderna 1–3 följt av B vid rad 4, under standardens giriga preferens) är fastställd av enbart dessa flaggor, utan någon uttrycksutvärdering av betydelse här — = ANY är bara det triviala skiktet som exponerar array-kolumnen för DEFINE, inte vad testet tränar. Att skriva samma scenario med ett numeriskt predikat (price > PREV(price) och liknande) skulle blanda NFA-testet med predikatutvärderarens beteende; array-idiomet håller de två renligt åtskilda, och ett fel här pekar direkt på NFA-loopen.
Absorptionstäckningen är särskilt grundlig, eftersom absorption är den optimering som med störst sannolikhet tyst producerar felaktiga svar om den aktiveras när den inte ska. Tester täcker varje form från fallanalysen i §3.2:
- enkel obegränsad variabel (
A+) — den kanoniska N-till-1-kollapsen; - grupper med fast längd (
(A B)+,(A B{2})+, tre nivåer nästlade((A (B C){2}){2})+); - ledande obegränsad variabel med ett fast suffix (
A+ B) — endast det ledandeA+bär absorptionsflaggor, suffixet inte; - per-grenabsorption inuti en alternation (
B+ C | B+ D); - flera obegränsade variabler (
A+ B+) — endast den ledande är absorberbar; - motvilliga kvantifierare (
A+?) — verifierade som uteslutna från absorption; - icke-ledande obegränsad (
A B+) — verifierad som utesluten.
Utöver absorption täcker sviten kontextens livscykel: överlappande kontexter under AFTER MATCH SKIP TO NEXT ROW, städning av misslyckade kontexter mitt i strömmen, partitionsslut-färdigställande av ofullständiga kontexter, och kontexter som påträffats efter att en kandidatmatchning redan registrerats i huvudkontexten. Var och en av dessa mappar mot en specifik beskärningsregel i §3.6, och testerna är skrivna för att misslyckas högljutt om regeln avlossas felaktigt (antingen genom att lämna redundanta kontexter vid liv eller genom att döda en kontext som skulle ha producerat utdata).
4.4 rpr_explain — utdatastabilitet
EXPLAIN-utdata är del av det användarvända kontraktet — tredjepartsverktyg (psql-autokomplettering, övervakningsdashboards, loggskrapor) parsar det, och ändringar som ser kosmetiska ut kan bryta dem. Sviten rpr_explain verifierar tre saker:
- NFA-räknare visas på rätt plats — per-WindowAgg-statistiken (peak/total/merged states, peak/total/pruned contexts, längdhistogram för matched/mismatched/absorbed/skipped, Nav Mark Lookback och — när matchningsstartrelativ navigation används — Nav Mark Lookahead) dyker alla upp under
EXPLAIN ANALYZEmed de dokumenterade etiketterna. - Mönster deparsas korrekt — varje kvantifierareform, inklusive motvilliga varianter och begränsade former; varje alternation; varje grupp; varje
AFTER MATCH SKIP-form;INITIAL; navigationsanrop med offsets; sammansatt navigation. Den deparsade texten måste rundresa tillbaka genom parsern oförändrad. - Identifierarcitering är korrekt — mönstervariabler och DEFINE-uttryck emitteras med samma citeringsregler som vanliga SQL-identifierare, så att ovanliga variabelnamn inte bryter
pg_dump-utdata.
Liksom rpr_base lämnar denna svit medvetet sina objekt på plats vid körningens slut så att pg_dump- och pg_upgrade-täckningen sträcker sig till explain-sidans artefakter också.
4.5 rpr_integration — planerareinteraktioner
PostgreSQL:s planerare har många optimeringar som arbetar på fönsterfrågor — ramkanonisering, kör-villkorspushdown, identisk-fönster-deduplicering, oanvänd-utdataprojektion, omvända övergångar för rörliga aggregat — och var och en av dem designades utan RPR i åtanke. De flesta är osäkra att tillämpa när ett fönster har en PATTERN-sats: ramen är del av matchningskontraktet, det matchade utdatat är inte längre monotont på något uppenbart sätt, och två fönster med samma spec men olika DEFINE producerar genuint olika resultat. Integrationssviten verifierar att var och en av dessa optimeringar korrekt inaktiveras eller kringgås för RPR-fönster:
- Ramkanonisering
- Planeraren skriver normalt om
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGtillROWS UNBOUNDED PRECEDINGför monotona aggregat. RPR:s ram är matchningsomfånget, inte ett aggregeringsfönster, och måste förbli ordagrann. - Kör-villkorspushdown
- Ett monotont
WHERE-filter på en fönsterfunktions utdata kan normalt skjutas in i fönsteroperatorn som ett stoppvillkor. För RPR skulle detta avsluta mönstermatchning för tidigt och möjligen kapa matchningar mitt under utvidgning. - Fönsterdeduplicering (RPR vs icke-RPR)
- Två fönster med identisk
ORDER BYoch ram skulle normalt slås samman till ett. Ett RPR-fönster och ett icke-RPR-fönster kan aldrig dela tillstånd eftersom RPR-sidan bär matchningsresultat. - Fönsterdeduplicering (olika DEFINE)
- Två RPR-fönster med samma
PATTERNmen olikaDEFINE-satser producerar olika matchningar och måste förbli åtskilda, även om deras strukturella form är identisk. - Oanvänd-utdataprojektion
- Även om den omgivande frågan aldrig läser fönstrets per-rad-utdata kan RPR-fönstret inte tas bort: mönstermatcharens bieffekter (vilka rader som ligger inom vilken matchning) matar reducerade-ram-beräkningar någon annanstans i planen.
- Omvända övergångar för rörliga aggregat
- Fönsteraggregat med en omvänd övergångsfunktion kan normalt utvärderas inkrementellt när ramen rör sig. RPR:s reducerade ram är inte ett glidande fönster; den omvända övergångsvägen skulle producera felaktiga resultat.
Mönstret över dessa tester är detsamma: var och en tillhandahåller en icke-RPR-baslinje som utlöser optimeringen (och verifierar att EXPLAIN visar att den tillämpas), kör sedan en RPR-fråga av strukturellt liknande form och verifierar att optimeringen inte tillämpas. De två halvorna tillsammans bevisar att skyddet i planeraren utför verkligt arbete och inte godkänner varje fråga utan verklig verifiering.
Denna svit är också skälet till att §3.4 är kort. "Matchningsgränser"-mekanismerna — reducerad ram, SKIP, INITIAL, begränsad-ram-avskärning — testas operationellt på annat håll; det rpr_integration verifierar är att inget annat optimerarsteg manipulerar dem på vägen genom. En passerande rpr_integration är det som låter resten av sviterna lita på att deras indata når executor oantastat.