CodeCrafters
750 subscribers
94 photos
50 videos
42 files
171 links
Download Telegram
🔰Sqliteviz

یه ابزارکاربردی برای مصورسازی دیتاستهای CSV و کاربا sqlite برخی از قابلیتها:

+کوئریهای SQL را در SQLite اجرا کنید و چارتهای Plotly و پیوت تیبل را بر اساس مجموعه نتایج ایجاد کنید.
+یک فایل CSV را به SQLite ایمپورت کرده و گرافشو تحویل بگیرید.
+خروجی دیتاهای خودرا بصورت CSV اکسپورت بگیرید.
+ساپورت فایلای JSON
+تهیه خروجی بصورت SQLITE
نمونه:
https://sqliteviz.com/app
گیتهاب:
https://github.com/lana-k/sqliteviz


#postgresql

@code_crafters
3👍3
تراکنش در دنیا پایگاه داده ها: قهرمان‌های گمنامِ پشت صحنه 💪

در دنیای پایگاه داده، تراکنش (Transaction) مثل یه گروه سربازه که یا با هم تا تهِ خط می‌رن، یا همه با هم برمی‌گردن. وقتی حرف از تراکنش ها می‌شه، خیلی‌ها یاد تراکنش حساب‌های بانکی میفتن و با خودشون می‌گن: «وا، من که به این پیچیدگی‌ها نیاز ندارم!» اما حقیقت اینه که تراکنش ها فقط مال پایگاه‌های داده‌ی بانک‌ها نیستن. خیلی وقت‌ها مردم گیر یه سری مثال‌های بانکی می‌افتن و فراموش می‌کنن که تو دنیای واقعی، تراکنش ها چقدر کاربرد دارن.

با استفاده از تراکنشها برنامه‌نویس‌ها نه تنها وقتِ خودشون رو ذخیره می‌کنن، بلکه کد مورد نیازشون برای برنامه‌هایی که به پایگاه داده وابسته هستن رو هم کم می‌کنن. اگه پایگاه داده خودش وضعیت تراکنش رو مدیریت نمی‌کرد، باید خود برنامه‌نویس این کار رو انجام می‌داد. به نظر ساده میاد، ولی وقتی با داده‌های مرتبط به هم سروکار داشته باشیم، خیلی زود پیچیده می‌شه.

مثلاً به مراحل ثبت‌نام ساده‌ی یه کاربر فکر کنین. این مراحل باید یه رکورد کاربر و یه رکورد حساب بسازن و بعد اون‌ها رو به هم وصل کنن. بدون تراکنش باید تک تک مراحل رو تو برنامه بنویسیم:

کاربر رو بساز
اگه موفق نبود، خارج شو و پیام خطا بفرست
حساب رو بساز
اگه موفق نبود، کاربر رو حذف کن، خارج شو و پیام خطا بفرست
کاربر رو به حساب وصل کن
اگه موفق نبود، کاربر، حساب رو حذف کن، خارج شو و پیام خطا بفرست

اما با تراکنش، می‌تونیم راحت‌تر و تمیزتر عمل کنیم:

تراکنش رو شروع کن
حساب رو بساز
کاربر رو بساز
کاربر رو به حساب وصل کن
تراکنش موفقیت‌آمیز بود؟
اگه آره، پیام موفقیت رو برگردون
اگه نه، پیام خطا رو برگردون

حالا دیگه نگران این نیستیم که بخاطر یه دستوری که موفق نشده، بقیه رو پاک کنیم. یا همه با هم درست انجام می‌شن، یا همه با هم شکست می‌خورن. به همین سادگی!

خب، حالا می‌بینیم که تراکنش ها نه تنها پیچیده نیستن، بلکه تو خیلی از کارها می‌تونن دست چپ و راست برنامه‌نویس‌ها باشن. دیگه این قهرمان‌های گمنام دنیای پایگاه داده رو دست‌کم نگیرین!

#postgresql
@Code_Crafters
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥1👏1
تراکنش های ابتدایی

ساده‌ترین تراکنش در زیر آمده است. با استفاده از BEGIN تراکنش را شروع کرده و با COMMIT آن را پایان می‌دهد.
sql 

BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id) VALUES ('Elizabeth', 'Christensen', current_date, 1, 'Master of the Highwire', 2, 2) RETURNING employee_id;
COMMIT;


مثل آب خوردن کار کرد، درسته؟ شبیه اینه که یه دستور معمولی رو اجرا کنیم ( داریم از دستور RETURNING استفاده می‌کنیم تا بعداً چیزی رو ثابت کنیم). برای اینکه مطمئن بشید داده‌ها ثبت شدن، حالا این دستور رو بزنید:


SELECT    * FROM employees  WHERE    first_name = 'Elizabeth' AND    last_name = 'Christensen';


اما اگر تصمیم بگیرید به جای COMMIT، تراکنش را ROLLBACK کنید، نتیجه‌ای متفاوت خواهید داشت:

BEGIN;

INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id) VALUES ('Chris', 'Winslett', current_date, 1, 'Jr Director of the Highwire', 2, 2) RETURNING employee_id;

ROLLBACK;


وقتی از دستور ROLLBACK استفاده می‌کنی، مثل این میمونه که داری به پایگاه داده میگی: «بیخیال این تراکنش شو، انگار نه انگار اتفاقی افتاده.» با این کار، هر تغییری که توی تراکنش انجام شده بود، پاک میشه و توی تاریخچه پایگاه داده ذخیره نمیشه. اما یه نکته جالب اینجاست: با اینکه تراکنش ثبت نمیشه، اما پایگاه داده باز هم یه شناسه برای اون تراکنش در نظر می‌گیره. ولی اگه بعدا بخوای اون رکورد رو با دستور select پیدا کنی، هیچی پیدا نمیشه، چون از اول ثبت نشده بوده!!
SELECT * FROM employees WHERE first_name = 'Chris' AND last_name = 'Winslett';

می‌دونی چرا وقتی یک تراکنش رو شروع می‌کنی، حتی قبل از اینکه اون رو نهایی کنی، یک مقدار شناسه (ID) بهت برمی‌گردونه؟

ببین، تراکنش‌ها گروهی از عملیات‌های پایگاه داده هستن که با هم انجام می‌شن. گاهی اوقات، لازمه که یک مقدار شناسه یکتا برای هر رکوردی که وارد پایگاه داده می‌کنیم، داشته باشیم. برای این کار، از دنباله‌ها (sequences) استفاده می‌کنیم. دنباله‌ها، مقدار شناسه بعدی رو به صورت خودکار تولید می‌کنن.

حالا، فرض کن که دو تراکنش همزمان دارن داده وارد پایگاه داده می‌کنن. اگه هر دو تراکنش از دنباله‌ای استفاده کنن که هنوز نهایی نشده، ممکنه هر دو تراکنش مقدار شناسه یکسانی رو دریافت کنن. این باعث می‌شه که دو رکورد با یک شناسه یکسان در پایگاه داده وجود داشته باشن. این کار درست نیست و می‌تونه باعث مشکلاتی بشه.

برای جلوگیری از این مشکل، دنباله‌ها حتی قبل از اینکه تراکنش نهایی بشه، مقدار شناسه بعدی رو تولید می‌کنن. این کار باعث می‌شه که هر دو تراکنش مقدار شناسه متفاوتی رو دریافت کنن.

حالا، اگه تراکنش رو نهایی نکنی، مقدار شناسه‌ای که بهت برمی‌گردونه، فقط یک مقدار موقتی هست. این مقدار تا زمانی که تراکنش نهایی بشه، در پایگاه داده ذخیره نمی‌شه.

اگه تراکنش رو با دستور ROLLBACK لغو کنی، مقدار شناسه‌ای که بهت برمی‌گردونده، از بین می‌ره.

برای اینکه مطمئن بشی که مقدار شناسه‌ای که دریافت می‌کنی، در پایگاه داده ذخیره شده، باید تراکنش رو با دستور COMMIT نهایی کنی.

اگه در حین انجام تراکنش، خطا رخ بده، نمی‌تونی اون رو نهایی کنی. در این صورت، مقدار شناسه‌ای که دریافت می‌کنی، در پایگاه داده ذخیره نمی‌شه.

BEGIN;
INSERT INTO employees (first_name, last_name) VALUES ('Tom', 'Jones') RETURNING employee_id;
COMMIT;

بعد از اجرای دستور بالا، یک دستور ROLLBACK نشان داده خواهد شد. بنابراین، دستور COMMIT ناموفق بوده است زیرا آخرین وضعیت تراکنش یک خطا است.

#postgresql
@Code_Crafters
👍2
رازهای مخفی تراکنش‌های پایگاه داده: هرچی توی تراکنش هست، همونجا میمونه! (Transaction Scope)

تصور کن یه صندوق داری داری که توش کلی کارای عجیب غریب میشه، ولی هیچکس اجازه نداره سرک بکشه تا وقتی کار تموم نشده! همینه داستان تراکنش‌های پایگاه داده.

فرض کن داری تو دوتا کامپیوتر جدا جدا با یه پایگاه داده کار می‌کنی. کامپیوتر اول یه سری دستورات میده، مثلاً یه اطلاعاتی رو عوض می‌کنه یا یه چیزی اضافه می‌کنه، ولی هنوز کارش تموم نشده. حالا کامپیوتر دوم بخواد همون اطلاعات رو ببینه، چی میشه؟ خب، تا وقتی که کارِ اون یکی کامپیوتر تموم نشده و همه چی تایید نشده، کامپیوتر دوم چیزی نمیبینه!

مثل همون صندوقه، اطلاعات تغییرات مخفیه تا کار تموم نشده، بعدش همه می‌بینن چی به چیه. اینجوری مطمئن می‌شیم که همه اطلاعات با هم هماهنگه و هیچ قاطی‌بازی‌ای نمیشه.

پس یادت باشه، تراکنش‌ها مثل یه سری کارای مخفی تو صندوق می‌مونن تا همه چی جمع و جور بشه. هیچکس زودتر از موعد حق نداره سرک بکشه!

#postgresql
@Code_Crafters
2👍1
تراکنش ها در دنیای واقعی

تراکنش‌ها در دنیای واقعی خیلی پیچیده‌تر از مثال‌های ساده‌ای هستند که دیدیم. مثلاً، فرض کن می‌خوایم یه کارمند جدید در شرکتمون اضافه کنیم. این دستورات ساده هستند و به راحتی اجرا می‌شن. ولی اگه در اجرای یکی از این دستورات خطا رخ بده، تمام تغییراتی که انجام شده‌اند، از بین می‌رن:
BEGIN; 
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id) VALUES ('Chris', 'Winslett', current_date, 1, 'Jr Director of the Highwire', 2, 2);
INSERT INTO dependents (first_name, last_name, employee_id) VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Chris' AND last_name = 'Winslett'));
INSERT INTO dependents (first_name, last_name, employee_id) VALUES ('youngest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Chris' AND last_name = 'Winslett'));
COMMIT;




اگه دوباره همون کد بالا رو اجرا کنید، بازم با خطا مواجه می‌شید. دلیلش اینه که دوتا مقدار برای employee_id برمی‌گرده. همچنین، کارمند تکراری هم ایجاد نمی‌شه.

وای! چقدر تراکنش‌ها عالین!

حالا بیایید یه کاری کنیم که ببینیم چطوری تراکنش‌ها با خطا مواجه می‌شن.

اول، سعی می‌کنیم مقدار null رو برای فیلد first_name در دستور دوم قرار بدیم. این فیلد توی جدول اجباریه، پس تراکنش با شکست مواجه می‌شه.

بعد، حقوق (salary) رو حذف می‌کنیم. این کار باعث لغو تراکنش قبل از تاییدش می‌شه.

BEGIN;   
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id) VALUES ('Bob', 'Young', current_date, 1, 'Jr Director of the Highwire', 2, 2);
INSERT INTO dependents (first_name, last_name, employee_id) VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
INSERT INTO dependents (first_name, last_name, employee_id) VALUES (null, 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
COMMIT;


حواست باشه! اجرای کدی که دیدی، با خطا مواجه شده و به عقب برگشته. یعنی تغییراتی که قرار بود اعمال بشه، ذخیره نشده. نگران نباش، اینجوری اطلاعاتت سالم می‌مونه. برای اطمینان بیشتر، می‌تونی یه سرچ بزنی و ببینی که اسم "Bob Young" توی لیست کارمندا نیست. خیالت راحت!
SELECT *  FROM employees  WHERE  first_name = 'Bob' AND last_name = 'Young';




دلیل این امر این است که تراکنش به طور کامل انجام نشد. این یک حالت "همه یا هیچ" است.

#postgresql
@Code_Crafters
2
تغییرات ساختار در تراکنش ها

شاید تعجب کنید، اما PostgreSQL اونقدر باحال هست که هیچ تغییری توی ساختار پایگاه داده رو ثبت نمی‌کنه تا زمانی که کل تراکنش با موفقیت انجام بشه! این ویژگی توی دنیای پایگاه‌های داده خیلی خاصه و خیالمون رو راحت می‌کنه.

بذارید با یه مثال براتون توضیح بدم. فرض کنید می‌خوایم یه ستون جدید به اسم "middle_name" به جدول "employees" اضافه کنیم. برای این کار، از دستورات زیر استفاده می‌کنیم:
BEGIN;
ALTER TABLE employees ADD COLUMN middle_name VARCHAR(50) DEFAULT NULL;
COMMIT;


دستور "BEGIN" شروع تراکنش رو مشخص می‌کنه و دستور "COMMIT" هم پایان تراکنش رو نشون می‌ده. هر تغییری که بین این دو دستور انجام بشه، تا زمانی که دستور "COMMIT" اجرا نشه، ثبت نمی‌شه.

حالا اگه از دستور \d employees استفاده کنیم، می‌بینیم که ستون "middle_name" به جدول اضافه شده.

اما اگه یه تغییر پیچیده‌تر بخوایم انجام بدیم و یه اشتباه کوچولو توش داشته باشیم، چی می‌شه؟ مثلاً فرض کنید می‌خوایم چند تا ستون جدید برای آدرس به جدول اضافه کنیم، اما یادمون بره که برای ستون "postal_code" یه مقدار پیش‌فرض تعیین کنیم. دستورات زیر رو ببینید:
BEGIN;
ALTER TABLE employees ADD COLUMN address_line_1 VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN address_line_2 VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN city VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN province VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN postal_code VARCHAR(50) NOT NULL;
COMMIT;


به خاطر اینکه کل این تغییرات داخل یه تراکنش قرار گرفتن، و چون برای ستون "postal_code" مقدار پیش‌فرض تعیین نکردیم، هیچ کدوم از تغییرات ثبت نمی‌شن! یعنی اگه دوباره از دستور \d employees استفاده کنیم، می‌بینیم که ستون‌های آدرس اضافه نشدن.

به این ویژگی فوق‌العاده می‌گن "Transactional DDL". توی پایگاه‌های داده دیگه که این ویژگی رو ندارن، ممکنه تغییرات نصفه و نیمه انجام بشن و همه چیز بهم بریزه. اما توی PostgreSQL، یا همه تغییرات با موفقیت انجام می‌شن، یا هیچ تغییری ثبت نمی‌شه.

#postgresql
@Code_Crafters
2
تراکنش‌های پیشرفته: نقطه‌ی نجات (SAVEPOINT)

یه ویژگی توپ دیگه از Postgres می‌خوام بهتون معرفی کنم که کار با تراکنش‌ها رو توی شرایط پیچیده خیلی راحت‌تر می‌کنه. اسمش "نقطه‌ی نجات" یا SAVEPOINT هست.

تصور کنید یه تراکنش دارید و چند تا دستور داخلش انجام می‌دید. شاید بخواهید یه جایی وسط کار، یه نقطه‌ی امن داشته باشید که اگه هر اتفاقی افتاد، بتونید به اونجا برگردید و همه چی رو از اول انجام بدید. SAVEPOINT دقیقا همینه!

بذار یه مثال بزنم. فرض کنید می‌خوایم یه کارمند جدید به اسم Bob Young اضافه کنیم، بهش چندتا وابسته اضافه کنیم و بعد تراکنش رو تموم کنیم. ولی ممکنه یه جا تو کار اشتباهی پیش بیاد. برای همین از SAVEPOINT استفاده می‌کنیم:
BEGIN;

INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id) VALUES ('Bob', 'Young', current_date, 1, 'Jr Director of the Highwire', 2, 2);

SAVEPOINT saved_employee; // این نقطه‌ی نجاته!

INSERT INTO dependents (first_name, last_name, employee_id) VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));

INSERT INTO dependents (first_name, last_name, employee_id) VALUES (null, 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));

ROLLBACK TO SAVEPOINT saved_employee; // اگه یه مشکلی پیش بیاد، به نقطه‌ی نجات برمی‌گردیم!

COMMIT;


حالا اگه اسم Bob Young رو توی کارمندها Search کنیم، پیداش می‌کنیم، ولی وابسته‌هایش رو نمی‌بینیم. چرا؟ چون وقتی توی اضافه کردن وابسته‌ها یه خطایی پیش اومده، تراکنش رو به نقطه‌ی نجات برگردوندیم و فقط اضافه‌کردن Bob Young انجام شده.

می‌دونم این مثال یه کم مصنوعیه، ولی SAVEPOINT توی شرایط پیچیده‌تری که معمولاً برنامه‌نویس هم باهاش درگیره، خیلی کارآمد میشه. فعلاً شاید لازم نباشه ازش استفاده کنی، ولی خوبه بدونی همچین ویژگی‌ای وجود داره!

#postgresql
@Code_Crafters
3👍1
محاصره در تراکنش ها

یه چیزی هست که شاید خیلی‌هاتون متوجهش نشده باشید. اونم اینه که توی PostgreSQL، حتی ساده‌ترین دستورات هم توی یه تراکنش انجام می‌شن! عجیبه نه؟

شاید بگید "خب چطور می‌شه فهمید؟ ما که تراکنشی شروع نکردیم!". ولی یه خورده صبر کنید... می‌تونیم یه دستوری اجرا کنیم که برامون نشون بده الان داخل کدوم تراکنشیم. بفرما:

SELECT txid_current();


حالا چی شد؟ یه عددی بهتون نشون داد، درسته؟ ولی ما که چیزی شروع نکردیم! چون اون عددی که دیدید، شناسه‌ی تراکنشیه که همین الان توش هستید.

حالا بازم همون دستور رو اجرا کنید. چی می‌بینید؟ اون عدد، یه واحد زیادتر شده! آره، این یعنی حتی همین یه دستور کوچیک، یه تراکنش جدید برا خودش باز کرده.

شاید بپرسید چرا انقدر ریزه‌کاری؟ خب، اینجوری PostgreSQL، خیالش راحته که هر تغییری که انجام می‌شه، یا کاملاً انجام می‌شه، یا اصلاً انجام نمی‌شه. اگه توی یه دستور مشکلی پیش بیاد، کل تراکنش باطل می‌شه و هیچ تغییری اعمال نمی‌شه.

پس یادتون باشه، توی PostgreSQL، همیشه تو یه تراکنش هستید، چه بخواهید، چه نخواهید! این یه جور مراقبت اضافه‌ست که داده‌هاتون رو سالم نگه می‌داره.

#postgresql
@Code_Crafters
👍52
پارتیشن‌بندی | Partitioning

فکر کن یه انبار بزرگ داری پر از وسایل. بعضی هر روز استفاده میشن، بعضی یه ماه یه بار، بعضی سال به سال یه نگاه بندازیشون می‌کنی، و یه سری هم هستن که دیگه اصلا به کار نمیان. اگه همشون رو یه جا و به یه شکل نگه داری، انبارت هم شلوغ میشه، هم پیدا کردن سخته، هم نگهداری هزینه‌بر میشه.

پارتیشن‌بندی دقیقا همون چوب جادویی برای انبار داده‌هایت محسوب میشه! با پارتیشن‌بندی، داده‌هات رو بر اساس استفاده و اهمیتشون طبقه‌بندی می‌کنی. به این ترتیب، اون اطلاعاتی که زیاد به کار نمیان یا دیگه قدیمی شدن رو توی بخش‌های کم‌هزینه‌تر و دورتر انبار (مثلا آرشیو) می‌ذاری، درحالیکه چیزایی که همش دم دستت هستن رو جلوی دست نگه می‌داری (مثل دیتابیس اصلی).

حالا چطور این تقسیم و بخش‌بندی هزینه رو کم می‌کنه؟ یه مثال بزنیم: فرض کن انبارت پر از لباس‌های قدیمی باشه. خیلی از این لباس‌ها رو دیگه نمی‌پوشی. خب اگه همشون رو توی کمد رختخواب نگه داری، هم جای بیشتری می‌گیرن، هم هر وقت بخوای یه لباس جدید بذاری، باید همه رو جابه‌جا کنی و هم تمیز کردنشون سخته. ولی اگه اونایی که دیگه استفاده نمیشه رو ببری توی چمدون و زیر تخت بذاری، هم کمدت خلوت و مرتب‌تر می‌شه، هم دیگه لازم نیست هر دفعه همه رو جابه‌جا کنی و گردگیریشون کنی. پارتیشن‌بندی داده‌ها هم دقیقا همینطوره. اطلاعات قدیمی و کم‌کاربرد رو از دیتابیس اصلی درمیاری و به یه جای دیگه‌ای منتقل می‌کنی، مثلا یه هارددیسک دیگه‌ای یا یه سیستم آرشیو. اینجوری هم دیتابیس اصلی سریع‌تر و کوچیک‌تر می‌شه، هم هزینه‌ی نگهداری کم‌تر می‌شه.

پس پارتیشن‌بندی یه راه بی‌نظیره برای اینکه هم انبار اطلاعاتی منظمی داشته باشی، هم هزینه نگهداری رو مدیریت کنی. دیگه لازم نیست نگران انبار شلوغ و هزینه‌های اضافی باشی!

👩‍💻 #postgresql
@Code_Crafters
Please open Telegram to view this post
VIEW IN TELEGRAM
2
سرعتِ جت با پارتیشن‌بندی: وقتی انبارت مرتب باشه، پیدا کردن وسایل هم سریع‌تره! 🚀

یکی از دلایلی که خیلی‌ها عاشق پارتیشن‌بندی هستن، افزایش سرعت جستجو توی داده‌هاست. تصور کن انبار وسایل‌ت مرتب و تفکیک‌ شده‌ باشه. اگه بخوای یه چیز خاص رو پیدا کنی، خیلی سریع‌تر پیداش می‌کنی، نه؟ پارتیشن‌بندی هم دقیقا همین کار رو با داده‌هات می‌کنه.

وقتی داده‌هات رو بر اساس تاریخ یا یه کلید خاص پارتیشن‌بندی می‌کنی، جستجوها به جای اینکه کل انبار رو زیر و رو کنن، مستقیم به بخش مربوطه می‌رن. مثل اینه که تو انبارت، برای هر دسته از وسایل یه قفسه جدا داشته باشی. حالا اگه دنبال کلاه زمستونی می‌گردی، مستقیم به قفسه‌ی لباس‌های زمستونی می‌ری، نه اینکه تک‌تک سبدهای خونه رو بگردی!

اینجوری جستجو خیلی سریع‌تر انجام می‌شه، مخصوصا وقتی از ایندکس‌ها یا همون برچسب‌های راهنما هم استفاده کنی. دیگه خبری از انتظارای طولانی برای پیدا کردن یه تیکه اطلاعات توی یه انبار داده‌های به‌هم‌ریخته نیست. همه چی مرتب و منظم، با دسترسی سریع و یه کلیک!

👩‍💻 #postgresql
@Code_Crafters
Please open Telegram to view this post
VIEW IN TELEGRAM
2