تصور کنید میخواهید به خانهای که سالها پیش ساخته شده یک اتاق جدید اضافه کنید، اما هر بار که دیواری را جابهجا میکنید، سقف کل خانه فرو میریزد. این دقیقاً همان چالشی بود که تیم DuckDB در مدیریت زبان SQL با آن دستوپنجه نرم میکرد تا اینکه در نسخه ۲.۰، معماری پارسر خود را بهطور کلی تغییر داد. آیا یک پایگاهداده میتواند زبان خود را با تغییر بنیادین در معماری پارسر تکامل بخشد؟ DuckDB در نسخه ۲.۰ با جایگزینی پارسر قدیمی مشتقشده از PostgreSQL با یک سیستم جدید مبتنی بر PEG، به این پرسش پاسخ میدهد.
به نقل از گزارش فنی منتشر شده در duckdb.org، این پایگاهداده اکنون پارسر قدیمی خود را که بر پایه PostgreSQL بود، با یک سیستم مبتنی بر گرامر بیان تجزیهای (Parsing Expression Grammar یا PEG) جایگزین کرده است. برای درک اهمیت این موضوع، باید بدانیم که اکثر کاربران دیتابیس از طریق SQL با سیستم تعامل دارند، اما موتور زیرین باید ابتدا سینتکس را پیش از اجرا اعتبارسنجی کند. سالها بود که DuckDB از یک پارسر مشتقشده از PostgreSQL استفاده میکرد که اگرچه زیربنایی پخته فراهم کرده بود، اما سقف رشد فنی تیم را محدود کرده بود. همانطور که تیم قابلیتهای «SQL دوستانه» (Friendly SQL) مانند GROUP BY ALL و انتخاب ستونها با استفاده از SELECT * EXCLUDE (...) را اضافه میکرد، سیستم قدیمی بهطور فزایندهای شکننده میشد.
در سیستم قدیمی که از نوع LALR(1) بود و از ابزارهای YACC/Bison استفاده میکرد، حتی افزودن قابلیتهای ساده باعث ایجاد تداخلهای پیچیده «جابهجایی/کاهش» (shift/reduce) یا «کاهش/کاهش» (reduce/reduce) میشد. این موضوع باعث میشد تکامل زبان بدون شکستن قوانین موجود، بسیار دشوار باشد. همانطور که در تحلیلهای پیشین ما درباره بهینهسازی موتورهای پردازش داده اشاره کردیم، انعطافپذیری در لایه زبان، کلید پذیرش ابزار توسط توسعهدهندگان است.
نقش پارسر در DuckDB
برای درک تأثیر این تغییر، باید نگاهی به نحوه پردازش یک پرسوجو در DuckDB بیندازیم. سیستم از سه مرحله اولیه اصلی عبور میکند:
- توکنساز (Tokenizer): اولین گام است که رشته ورودی خام را به توکنهایی مانند KEYWORD، NUMBER یا IDENTIFIER تقسیم میکند. این مرحله همچنین کامنتهایی که با
--یا/* */مشخص شدهاند را شناسایی کرده و نادیده میگیرد. - پارسر (Parser): این مرحله تعیین میکند که آیا توکنها از گرامر DuckDB پیروی میکنند یا خیر و یک درخت
ParseResultتولید میکند. - تبدیلکننده (Transformer): نتایج کلی پارسر را به درخت نحو انتزاعی (AST) داخلی DuckDB تبدیل میکند و ساختارهایی مانند
SQLStatement،TableRefوParsedExpressionرا میسازد.
وظیفه پارسر صرفاً این است که تشخیص دهد آیا یک پرسوجو از نظر نحوی (Syntactically) معتبر است یا خیر. برای مثال، پرسوجویی مانند SELECT * WHERE true FROM range(1); باعث بروز خطای پارسر (Parser Error) میشود، زیرا ترتیب بندها (Clauses) مطابق با گرامری نیست که سیستم میپذیرد. در مقابل، پرسوجویی مانند FROM missing_table; از مرحله پارسر عبور میکند اما بعداً در مرحله Binder شکست میخورد، زیرا جدولی با این نام وجود ندارد.
گویش SQL در DuckDB
در حالی که SQL در DuckDB بهشدت از کنوانسیونهای PostgreSQL پیروی میکند، اما به گونهای متمایز تکامل یافته است که تیم آن را DuckSQL مینامد. این تمایز حیاتی است زیرا «گویش SQL» (زبانی که پذیرفته میشود) و «پیادهسازی پارسر» (کدی که زبان را میخواند) دو موجودیت مجزا هستند.
DuckSQL ویژگیهایی را از سیستمهای مختلف از جمله PostgreSQL، Oracle، GoogleSQL (برای BigQuery)، MySQL، MariaDB، SQLite و Spark SQL جذب کرده است، در حالی که بهطور عمدی برخی از رفتارهای PostgreSQL را حذف کرده است. با جایگزینی پیادهسازی پارسر در نسخه ۲.۰، DuckDB در حال بازنویسی گرامر است بدون اینکه خودِ گویش DuckSQL را تغییر دهد.
انتقال به معماری PEG
برای حل محدودیتهای سیستم LALR(1)، DuckDB به گرامرهای بیان تجزیهای (PEG) انتقال یافت. برخلاف سیستم قبلی، PEG جایگزینها را با ترتیبی سختگیرانه و صریح ارزیابی میکند. اگر قانون اول مطابقت نداشته باشد، سیستم بهسادگی قانون بعدی را امتحان میکند.
قوانین SelectFrom در گرامر جدید را در نظر بگیرید:SelectFrom <- SelectFromClause / FromSelectClauseSelectFromClause <- SelectClause FromClause?FromSelectClause <- FromClause SelectClause?
در اینجا، عملگر <- یک قانون را تعریف میکند، / یک انتخاب (Choice) را مشخص میکند و ? یک عنصر را اختیاری میسازد. این ساختار به DuckSQL اجازه میدهد هم پرسوجوهای سنتی که با SELECT شروع میشوند و هم معادلهای «SQL دوستانه» که با FROM شروع میشوند را بپذیرد. چون جایگزینها بهطور صریح مرتب شدهاند، گرامرهای PEG دچار تداخلات shift/reduce که در LALR دیده میشد، نمیشوند.
این رویکرد مشابه تصمیمی بود که سایر زبانهای بزرگ نیز گرفتند؛ برای مثال، پایتون در نسخه ۳.۹ به دلایل مشابه برای افزایش انعطافپذیری و تکامل، پارسر LL(1) خود را به یک پارسر مبتنی بر PEG تغییر داد.
حل تله عملکرد (Performance Trap)
انتقال یک دیتابیس عملیاتی به PEG بدون ریسک نیست. تیم در مرحله نمونهسازی (Prototype) با یک نقص عملکردی بحرانی مواجه شد: «بازگشت نمایی» (Exponential Backtracking). در پرسوجوهای بدساخت که پرانتزهای بازِ تطبیقنیافته زیادی داشتند، یک تطبیقدهنده PEG ساده ممکن است یک قانون گرامری را در یک موقعیت توکن، بارها و بارها در حالی که جایگزینهای مختلف را امتحان میکند، ارزیابی کند.
طبق گزارش duckdb.org، تیم این موضوع را با پرسوجوهایی که تعداد زیادی پرانتز باز داشتند (مثلاً SELECT ((((((((((((((((((;) آزمایش کرد. در پارسر آزمایشی PEG که در نسخه ۱.۵ عرضه شده بود، افزودن تنها یک پرانتز باز، زمان تجزیه را تقریباً دو برابر میکرد:
- ۱۸ پرانتز باز: ۵.۳۰۳ ثانیه
- ۱۹ پرانتز باز: ۱۰.۶۴۰ ثانیه
برای حل این بحران، تیم تکنیک Packrat Parsing را پیاده کرد که یک روش ذخیرهسازی در حافظه (Memoization) است. Packrat Parsing نتیجه اعمال یک تطبیقدهنده در یک موقعیت خاص از توکن را ذخیره میکند. اگر پارسر دوباره سعی کند همان تطبیقدهنده را در همان موقعیت اجرا کند، از نتیجه ذخیره شده در حافظه (Cache) استفاده میکند.
این بهینهسازی زمان پردازش همان پرسوجوی ۱۹ پرانتزی را از بیش از ۱۰ ثانیه به تنها ۰.۰۰۱ ثانیه کاهش داد. اگرچه این کار مستلزم حافظه اضافی در حین تجزیه است، اما برای جلوگیری از پیچیدگی زمانی نمایی در ورودیهای بدساخت، یک معامله ارزشمند است.
قابلیتهای جدید در DuckSQL
پارسر جدید بلافاصله ارتقاهای سینتکسی متعددی را در گویش DuckSQL فعال کرد. در حالی که DuckSQL همچنان تحت تأثیر PostgreSQL است، اکنون یک گویش متمایز است که میتواند مستقل از پیادهسازی پارسر تکامل یابد:
- عبارات-دستورات (Expression-Statements): کاربران اکنون میتوانند عبارات را بدون کلمه کلیدی
SELECTاجرا کنند. برای مثال،date: current_date(), time: current_localtime();اکنون معتبر است. این قابلیت با مستعارهای پیشوندی (Prefix Aliases) نیز کار میکند. - دستورات CONNECT: این قابلیت که برای Quack معرفی شده، اجازه میدهد پرسوجوها به دیتابیسهای راه دور مسیریابی شوند. کاربر میتواند
CONNECT 'postgres://localhost/mydb';را اجرا کند، سپس یکSELECT count(*) FROM orders;را روی سرور راه دور اجرا نموده و در نهایتDISCONNECT;را بزند. - مدیریت منابع خارجی: دستورات جدیدی برای مدیریت داراییهای خارج از هسته دیتابیس از طریق افزونهها اضافه شده است. این شامل دستوراتی چون
CREATE EXTERNAL RESOURCE '<resource-type>' AS <name> (...)،REGISTER EXTERNAL RESOURCE '<resource-type>' AS <name> FROM <handle>،SHOW EXTERNAL RESOURCES،CONNECT TO EXTERNAL RESOURCE <name>وDESTROY EXTERNAL RESOURCE <name>میشود. - بهبود COPY TO: سیستم اکنون از
PARTITION BYوORDER BYمستقیماً در دستور کپی پشتیبانی میکند، مانند:COPY orders TO 'orders' ( FORMAT parquet, PARTITION BY (year, month), ORDER BY (order_date) );
باز کردن پارسر برای افزونهها
مهمترین تغییر، حرکت از «پارسرهای جایگزین» (Fallback Parsers) به سمت «افزونههای یکپارچه» است. پیش از این، افزونههایی مانند psql و duckpgq به عنوان جایگزین عمل میکردند؛ DuckDB ابتدا سعی میکرد پرسوجو را تجزیه کند و تنها در صورت شکست، افزونه را فرا میخواند. این موضوع ترکیب سینتکس چندین افزونه یا افزودن سینتکس در داخل SQL موجود (بدون اینکه افزونه مجبور باشد کل SQL اطراف را خودش تجزیه کند) را غیرممکن میکرد.
اکنون، افزونهها میتوانند قوانین خود را مستقیماً به گرامر DuckDB تزریق کنند. آنها میتوانند توکنساز را گسترش دهند، قوانین گرامری اضافه کنند و تطبیقدهندههای سفارشی ثبت کنند، در حالی که از بقیه DuckSQL مجدداً استفاده میکنند.
به عنوان یک مثال عینی، تیم افزونه «Pipe SQL» را نمایش داد. این افزونه اجازه میدهد دادهها با استفاده از نماد |> از طریق توالیای از عملگرها جاری شوند. یک گرامر ساده شده برای این قابلیت شامل قوانینی مانند PipeSelectAtom <- PipeSource PipeStage+ است، که در آن + نشان میدهد یک پرسوجوی Pipe باید حداقل شامل یک عملگر Pipe باشد.
سایر قوانین در این افزونه عبارتند از:
PipeSource <- FromClause / SelectStatementType / SelectParensPipeStage <- '|>' PipeOperatorPipeOperator <- PipeAggregate / PipeAggregateGroupOnly / PipeWhere / PipeSelect / PipeExtend / PipeDistinct / PipeOrderBy / PipeLimitPipeWhere <- WhereClausePipeSelect <- 'SELECT' TargetListPipeExtend <- 'EXTEND' TargetListPipeDistinct <- 'DISTINCT'PipeOrderBy <- OrderByClausePipeLimit <- LimitClause OffsetClause?PipeAggregate <- 'AGGREGATE' TargetList GroupByClause?PipeAggregateGroupOnly <- 'AGGREGATE' GroupByClause
به دلیل ادغام با پارسر PEG، این افزونه میتواند سینتکس Pipe را با ویژگیهای استاندارد DuckSQL مانند range() و مستعارهای پیشوندی ترکیب کند. برای مثال:FROM range(6) t(i) |> WHERE i % 2 = 0 |> SELECT i, doubled: i * 2 |> ORDER BY i DESC;
جزئیات پیادهسازی فنی
این انتقال مستلزم آن بود که تبدیلکننده (Transformer) جدید PEG دقیقاً همان درخت نحو انتزاعی (AST) پارسر قدیمی PostgreSQL را تولید کند. این امر تضمین میکند که Binder و بقیه خط لوله اجرا (Execution Pipeline) بدون تغییر باقی بمانند. پارسر PEG ابتدا در نسخه ۱.۲ برای تکمیل خودکار (Autocomplete) در CLI معرفی شد، در نسخه ۱.۵ به یک ویژگی آزمایشی اختیاری تبدیل شد (و برای یک شوخی در روز اول آوریل برای اینکه DuckDB به زبان هلندی صحبت کند استفاده شد) و اکنون در نسخه ۲.۰ به پیشفرض تبدیل شده است.
تیم بر چندین نیاز در سطح تولید تمرکز کرد:
- اولویت عملگرها و تلازم (Precedence and Associativity): تضمین اینکه پرسوجویی مانند
SELECT true OR true AND false;بهدرستی به صورت(true OR (true AND false))تفسیر شود، زیراANDاولویت بیشتری نسبت بهORدارد. - طبقهبندی کلمات کلیدی: تمایز بین کلمات کلیدی رزرو شده (RESERVED) مانند
SELECTکه نمیتوانند به عنوان نامهای بدون کوتیشن استفاده شوند، و سایر کلمات کلیدی که بسته به متن میتوانند به عنوان شناسه (Identifier) به کار روند. - گزارش خطا: ارائه زمینه (Context) مفید و نشان دادن اینکه در پرسوجوهای نامعتبر چه اتفاقی افتاده است، بدون اینکه کاربر مجبور به مراجعه به دفترچه راهنما شود.
- سازگاری AST: تبدیلکننده PEG باید همان ساختارهایی را تولید کند که گرههای پارسر مشتقشده از PostgreSQL تولید میکردند، هر جا که رفتار زبان قرار است بدون تغییر بماند.
- عملکرد در ورودیهای غیرمعمول: تضمین اینکه پرسوجوهای بدساخت بهطور ناگهانی زمان تجزیه طولانی پیدا نکنند، فراتر از مسائل بازگشت نمایی که با Packrat Parsing حل شد.
ثبت و تبدیل افزونهها
برای ادغام سینتکس جدید، یک افزونه باید قانون گرامری موجود را که میخواهد گسترش دهد و قوانین تبدیلکننده برای تبدیل آن سینتکس به AST را مشخص کند. برای مثال، Pipe SQL قانون PipeSelectAtom را به عنوان یک جایگزین برای SelectAtom ثبت میکند و کلمات کلیدی aggregate و extend را به عنوان RESERVED معرفی میکند.
گرامر حاصل بهطور مؤثر به این شکل در میآید: SelectAtom <- PipeSelectAtom / SelectParens / SelectStatementType. ابتدا جایگزین افزونه امتحان میشود؛ اگر بدون مصرف توکنها شکست بخورد، جایگزینهای داخلی استفاده میشوند.
سپس تبدیلکننده، ParseResult را به یک SelectStatement تبدیل میکند. چون افزونه میتواند از قوانین گرامری موجود DuckDB (مانند GroupByClause) استفاده کند، میتواند از توابع تبدیل مربوطه نیز بهره ببرد. این یعنی افزونهها دیگر نیازی ندارند که عبارات، ارجاعات به جدول یا بندهای GROUP BY را از ابتدا پیادهسازی کنند.
این بازنگری معماری، پارسر را از یک «دربان استاتیک» به یک «رابط برنامهپذیر» تبدیل میکند. با جداسازی گویش SQL از پیادهسازی پارسر، DuckDB تضمین میکند که افزودن یک ویژگی جدید دیگر نیازمند رقص خطرناک با تداخلات LALR نیست.
برای توسعهدهندگان، این بدان معناست که مانع ایجاد افزونههای SQL دامنه-ویژه (Domain-Specific) از بین رفته است. توانایی افزودن سینتکس قابل گسترش در زمان اجرا نشان میدهد که DuckDB خود را نه فقط به عنوان یک دیتابیس، بلکه به عنوان یک موتور منعطف برای زبانهای دستکاری دادهها (Data Manipulation Languages) جایگاهبندی میکند. در دنیای ابزارهای خودکار، دقت در تعریف این ساختارها حیاتی است؛ برای مثال، Truss 1.8 با هدف کاهش توهمات عاملهای برنامهنویس در نامگذاری ستونها تلاش میکند تا تعامل بین کد و دیتابیس را دقیقتر کند.
کاربران باید مخزن گیتهاب را برای API نهایی افزونه گرامر زمان اجرا زیر نظر داشته باشند، زیرا پیادهسازی فعلی پیش از انتشار کامل نسخه ۲.۰ در حالت پیشنمایش قرار دارد.
گام بعدی شما
- اگر از DuckDB برای تحلیلهای محلی استفاده میکنید، نسخه ۲.۰ را برای تجربه سینتکسهای کوتاهتر و سریعتر نصب کنید.
- توسعهدهندگان افزونهها باید مستندات جدید API گرامر زمان اجرا را در گیتهاب دنبال کنند.
- قابلیتهای جدید
COPY TOرا برای بهینهسازی خروجیهای Parquet در پروژههای خود به کار بگیرید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما درباره تراشههای Blackwell مراجعه کنید.




گفتگو