Android پشتیبانی داخلی برای SQLite، یک پایگاه داده SQL کارآمد، ارائه میدهد. این روالهای مطلوب را دنبال کنید تا عملکرد برنامهتان را بهینهسازی کنید و مطمئن شوید که با افزایش دادههایتان، سریع و بهطور قابلپیشبینی سریع باقی میماند. بااستفاده از این روالهای مطلوب، احتمال مواجه شدن با مشکلات عملکردی را که بازتولید و عیبیابی آنها دشوار است نیز کاهش میدهید.
برای دستیابی به عملکرد سریعتر، این اصول عملکرد را دنبال کنید:
خواندن ردیفها و ستونهای کمتر: پُرسمانهایتان را بهینهسازی کنید تا فقط دادههای ضروری را بازیابی کنید. مقدار دادههای خواندهشده از پایگاه داده را بهحداقل برسانید، زیرا بازیابی دادههای اضافی میتواند بر عملکرد تأثیر بگذارد.
انتقال کار به موتور SQLite: عملیات محاسباتی، فیلتر کردن، و مرتبسازی را در پُرسمانهای SQL انجام دهید. استفاده از موتور پرسمان SQLite میتواند عملکرد را بهطور قابلتوجهی بهبود بخشد.
اصلاح طرحواره پایگاه داده: طرحواره پایگاه دادهتان را بهگونهای طراحی کنید که به SQLite کمک کند طرحهای پُرسمان و نمایشهای داده کارآمدی بسازد. نمایهسازی صحیح جدولها و بهینهسازی ساختار جدولها برای بهبود عملکرد.
علاوهبراین، میتوانید از ابزارهای عیبیابی موجود برای اندازهگیری عملکرد پایگاه داده SQLite خود استفاده کنید تا به شناسایی بخشهایی که نیاز به بهینهسازی دارند کمک کنید.
توصیه میکنیم از کتابخانه Jetpack Room استفاده کنید.
پیکربندی پایگاه داده برای عملکرد
مراحل این بخش را دنبال کنید تا پایگاه دادهتان را برای عملکرد بهینه در SQLite پیکربندی کنید.
فعال کردن گزارشگیری پیشنویس
SQLite جهشها را با افزودن آنها به گزارش اجرا میکند و گاهی اوقات آنها را در پایگاه داده فشرده میکند. به این کار ثبت پیشنویس (WAL) میگویند.
WAL را فعال کنید
مگر اینکه از ATTACH DATABASE استفاده کنید.
بهجای اجرای PRAGMA journal_mode=WAL توسط خودتان، WAL را ازطریق چارچوب فعال کنید. Room بهطور پیشفرض WAL را در دستگاههایی که حافظه دسترسی تصادفی کمی ندارند فعال میکند.
بدون Room، از یکی از موارد زیر استفاده کنید:
- روش
enableWriteAheadLogging - پرچم باز
ENABLE_WRITE_AHEAD_LOGGING - روش
setWriteAheadLoggingEnabledمربوط بهSQLiteOpenHelper
وقتی چارچوب میداند که پایگاه داده از WAL استفاده میکند، مجموعهای از
اتصالها را باز میکند تا خواندنها بتوانند همزمان با نوشتن اجرا شوند، و
حالت همگام WAL را که در بخش بعدی شرح داده شده است اعمال میکند. اگر خودتان
PRAGMA journal_mode=WAL را اجرا کنید، چارچوب همچنان با پایگاه داده بهعنوان
پایگاه داده دفتر روزنامه بازگشت رفتار میکند. این روش از یک اتصال استفاده میکند و تراکنشها حالت همگام پیشفرض FULL را حفظ میکنند.
آسانتر کردن حالت همگامسازی
SQLite گزینهای برای کنترل حالت همگام دارد. در حالت FULL، هر
تعهد یک fsync صادر میکند تا اطمینان حاصل شود که دادهها به دیسک میرسند. این کار
دوام دادهها را بهبود میبخشد اما سرعت ثبت را کاهش میدهد. با WAL، حالت NORMAL
در هر ثبت همگامسازی نمیشود و همچنان از پایگاه داده دربرابر
خرابی محافظت میکند.
وقتی WAL را ازطریق چارچوب فعال میکنید، Android حالت همگام WAL پیشفرض دستگاه را اعمال میکند که در AOSP برای Android نسخه ۹ و بالاتر NORMAL است.
سازندگان دستگاه میتوانند این پیشفرض را تغییر دهند و پایگاههای دادهای که از WAL استفاده نمیکنند
بهطور پیشفرض روی FULL تنظیم میشوند. برای اطمینان از اینکه پایگاه دادهتان از NORMAL استفاده میکند، آن را هنگام
باز کردن پایگاه داده تنظیم کنید:
// When opening the database
val params = SQLiteDatabase.OpenParams.Builder()
.addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
// SYNC_MODE_NORMAL is available in API level 33 and higher.
// On API levels 28 to 32, pass "NORMAL" instead.
.setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
.build()
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL")
در این تنظیم، پیشاز ذخیره شدن دادهها در دیسک، ممکن است یک commit برگردانده شود. اگر دستگاه خاموش شود، مثلاً بهدلیل قطع برق یا کرنل پنیک، ممکن است دادههای ثبتشده ازدست برود. بااینحال، بهدلیل ثبت گزارش، پایگاه داده شما خراب نشده است.
اگر فقط برنامهتان ازکار بیفتد، دادههایتان همچنان به دیسک میرسد. برای اکثر برنامهها، این تنظیمات باعث بهبود عملکرد بدون هزینه مادی میشود.
مدیریت کردن اتصالها و حافظه
هر اتصال پایگاه داده باز حافظه SQLite را نگه میدارد: حافظه نهان صفحه، تخصیصدهنده lookaside، و حافظه نهان عبارتهای کامپایلشده. صفحههای فایل پایگاه داده نیز در حافظه نهان صفحه هسته باقی میمانند. برای کنترل این حافظه بدون کاهش سرعت پُرسمانهایتان، رویههای این بخش را دنبال کنید.
هر پایگاه داده را یکبار باز کنید
برای کل فرایندتان، از یک نمونه SQLiteOpenHelper یا RoomDatabase برای هر فایل پایگاه داده استفاده کنید،
برای مثال با تبدیل آن به یک نمونه تکی. هر نمونه
مجموعه اتصال، حافظه پنهان صفحه، و حافظه پنهان عبارت خودش را دارد و دو نمونه
که یک فایل را باز میکنند نمیتوانند آنها را همرسانی کنند. در یک محک، ادغام دو کمککننده که یک فایل را باز میکردند در یک فایل، حافظه پنهان صفحه SQLite را تقریباً به نصف کاهش داد و تأخیر پُرسمان p95 را به بیشاز نصف کاهش داد.
برای پیدا کردن نمونههای تکراری، dumpsys dbinfo را برای
مسیر پایگاه داده یکسان در زیر بیشاز یک عنوان Connection pool for بررسی کنید.
تغییرات طرحواره را خارج از فراخوانی باز نگه دارید
فراخوانی متقابل onOpen مربوط به SQLiteOpenHelper هربار که پایگاه داده باز میشود اجرا میشود، و فراخوانی متقابل onOpen مربوط به RoomDatabase.Callback نیز همینطور است. عبارتهایی مثل CREATE TABLE IF NOT EXISTS و CREATE INDEX IF NOT EXISTS را در onCreate و onUpgrade یا در انتقالهای اتاق قرار دهید. setVersion را در onOpen فراخوانی نکنید، زیرا SQLiteOpenHelper ازقبل نسخه را بعداز onCreate و onUpgrade تنظیم میکند. حتی وقتی طرحواره ازقبل وجود دارد، این
عبارتها همچنان در هر باز شدن اجرا میشوند و setVersion در فایل پایگاه داده
مینویسد.
اندازه حافظه نهان بیانیه آماده
Android دستورات کامپایلشده را برای هر اتصال ذخیره میکند. حافظه نهان بهطور پیشفرض ۲۵ عبارت را نگه میدارد و میتوانید آن را با setMaxSqlCacheSize تا MAX_SQL_CACHE_SIZE (۱۰۰) افزایش دهید. اگر پایگاه دادهای
بیشاز ۲۵ عبارت متمایز را در مسیرهای داغ خود اجرا کند، حافظه پنهان دچار کوبش میشود و
SQLite همان عبارتها را بارها و بارها کامپایل میکند.
ابتدا، پُرسمانهایتان را پارامتربندی کنید. هیچ اندازه حافظه نهانی نمیتواند به SQL ساختهشده با الحاق رشته کمک کند، زیرا هر رشته یک عبارت جدید است. سپس، موارد موفق و ناموفق حافظه نهان عبارت را در بخش POOL STATS در
dumpsys meminfo بررسی کنید. برای مثال، پایگاه داده phenotype.db در خروجی نمونه آن بخش ۱۴۷٬۹۲۱ مورد موفق و ۸۹٬۶۱۶ مورد ناموفق دارد که نرخ ناموفق ۳۸٪ است. افزایش اندازه حافظه نهان در پایگاه دادهای مثل این میتواند
تقریباً همه موارد ناموفق را حذف کند. هر عبارت ذخیرهشده در حافظه پنهان چند کیلوبایت حافظه مصرف میکند.
برای افزایش اندازه حافظه نهان، onConfigure را در زیرکلاس SQLiteOpenHelper لغو کنید:
override fun onConfigure(db: SQLiteDatabase) {
db.setMaxSqlCacheSize(SQLiteDatabase.MAX_SQL_CACHE_SIZE)
}
اگر از Room استفاده میکنید، همان روش را در SupportSQLiteDatabase در
onOpen فراخوانی برگشتی RoomDatabase.Callback فراخوانی کنید.
پایگاههای داده غیرفعال را ببندید، اما پایگاههای داده فعال را باز نگه دارید
پایگاه داده باز، اتصالات، حافظههای پنهان، و توصیفگرهای فایل خود را حتی در زمان بیکاری حفظ میکند. در یک محک، پایگاه داده Room غیرفعال در حالت WAL که باز مانده بود حدود ۶۷۸ کیلوبایت حافظه پنهان صفحه SQLite و ۵ توصیفگر فایل را نگه داشت. اگر فقط گاهی از پایگاه داده استفاده میکنید، مثلاً درطول همگامسازی روزانه، آن را پساز اتمام کار ببندید.
برای بستن ایمن پایگاههای داده غیرفعال، این دستورالعملها را دنبال کنید:
- Room میتواند پایگاه داده غیرفعال را با
setAutoCloseTimeoutبرای شما ببندد، که یک میانای برنامهسازی کاربردی آزمایشی است. Room در دسترسی بعدی پایگاه داده را دوباره باز میکند وonOpencallback شما را دوباره اجرا میکند، بنابراین هرگونه پیکربندی اتصال را در آنجا قرار دهید. - از روشهای
setIdleConnectionTimeoutمربوط بهSQLiteOpenHelperوSQLiteDatabase.OpenParams.Builderاجتناب کنید. این روشها در سطح API ۲۹ منسوخ شدهاند زیرا اتصالها را بدون بازیابی تنظیماتی که با عبارتهایPRAGMAاعمال کردهاید میبندند. - پایگاه دادهای را که هر چند ثانیه یکبار پُرسمان میکنید نبندید. وقفه زمانیای که کوتاهتر از فاصله بین پرسمانهای شما باشد باعث میشود پایگاه داده بهطور مکرر بسته و دوباره باز شود، که این امر هربار حافظه پنهان صفحه و حافظه پنهان عبارت را دور میاندازد و پرسمان بعدی را کندتر میکند.
حساب برای حافظه SQLite
تشخیص حافظه SQLite میتواند دشوار باشد. حافظه نهان صفحه SQLite و حافظه نهان عبارت
تخصیصهای پشته بومی هستند. اما Android بهطور پیشفرض ورودی/خروجی نگاشتشده در حافظه SQLite را فعال نمیکند، بنابراین SQLite فایلهای پایگاه داده را با خواندن فایل معمولی میخواند و محتوای فایلی که هسته ذخیره میکند صفحههای فایل نگاشتنشده است. این موارد در PSS برنامهتان در dumpsys meminfo لحاظ نمیشوند. در دستگاههایی که از حسابداری حافظه cgroup v2 استفاده میکنند، هسته همچنان این صفحهها را به cgroup حافظه برنامهتان اختصاص میدهد. برای کاهش این ردپای پنهان، بااستفاده از نمایهها و خواندن فقط ردیفها و ستونهای موردنیازتان، صفحههای کمتری را بخوانید. بستن پایگاههای دادهای که نیاز ندارید
حافظه نهان SQLite آنها را نیز آزاد میکند.
تعریف کردن طرحوارههای جدول کارآمد
برای بهینهسازی عملکرد و بهحداقل رساندن مصرف داده، طرحواره جدول کارآمدی تعریف کنید. SQLite طرحها و دادههای پُرسمان کارآمدی میسازد که منجر به بازیابی سریعتر دادهها میشود. این بخش روالهای مطلوب برای ایجاد طرحوارههای جدول را ارائه میدهد.
INTEGER PRIMARY KEY را درنظر بگیرید
برای این مثال، جدولی را بهصورت زیر تعریف و پر کنید:
CREATE TABLE Customers(
id INTEGER,
name TEXT,
city TEXT
);
INSERT INTO Customers Values(456, 'John Lennon', 'Liverpool, England');
INSERT INTO Customers Values(123, 'Michael Jackson', 'Gary, IN');
INSERT INTO Customers Values(789, 'Dolly Parton', 'Sevier County, TN');
برونداد جدول بهصورت زیر است:
| rowid | id | نام | شهر |
|---|---|---|---|
| 1 | 456 | جان لنون | لیورپول، انگلستان |
| 2 | 123 | مایکل جکسون | گری، ایندیانا |
| 3 | 789 | دالی پارتن | شهرستان سویر، تنسی |
ستون rowid
نمایهای است که ترتیب درج را حفظ میکند. پرسمانهایی که
براساس rowid فیلتر میکنند بهعنوان جستجوی سریع B-tree اجرا میشوند، اما پرسمانهایی که
براساس id فیلتر میکنند اسکن جدول کندی هستند.
اگر قصد دارید جستجوها را براساس id انجام دهید، میتوانید از ذخیره کردن ستون
rowid اجتناب کنید تا دادههای کمتری در فضای ذخیرهسازی داشته باشید و
پایگاه داده بهطور کلی سریعتر شود:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
اکنون جدول شما بهصورت زیر است:
| id | نام | شهر |
|---|---|---|
| 123 | مایکل جکسون | گری، ایندیانا |
| 456 | جان لنون | لیورپول، انگلستان |
| 789 | دالی پارتن | شهرستان سویر، تنسی |
چون نیازی به ذخیره کردن ستون rowid ندارید، پُرسمانهای id سریع هستند. توجه داشته باشید
که جدول اکنون براساس id بهجای ترتیب درج مرتب شده است.
تسریع پُرسمانها با نمایهها
SQLite از
نمایهها
برای تسریع پُرسمانها استفاده میکند. هنگام فیلتر کردن (WHERE)، مرتبسازی (ORDER BY)، یا
انبوهش (GROUP BY) ستون، اگر جدول نمایهای برای ستون داشته باشد،
پرسوجو تسریع میشود.
در مثال قبلی، فیلتر کردن براساس city مستلزم اسکن کردن کل جدول است:
SELECT id, name
WHERE city = 'London, England';
برای برنامهای با تعداد زیادی پرسمان شهر، میتوانید آن پرسمانها را با نمایه تسریع کنید:
CREATE INDEX city_index ON Customers(city);
نمایه بهعنوان جدول اضافی پیادهسازی میشود، براساس ستون نمایه مرتب میشود، و
به rowid نگاشت میشود:
| شهر | rowid |
|---|---|
| گری، ایندیانا | 2 |
| لیورپول، انگلستان | 1 |
| شهرستان سویر، تنسی | 3 |
توجه داشته باشید که هزینه فضای ذخیرهسازی ستون city اکنون دوبرابر شده است، زیرا
اکنون هم در جدول اصلی و هم در نمایه وجود دارد. ازآنجاییکه از نمایه استفاده میکنید، هزینه فضای ذخیرهسازی اضافهشده ارزش مزیت پُرسمانهای سریعتر را دارد.
بااینحال، نمایهای را که استفاده نمیکنید نگه ندارید تا از پرداخت هزینه فضای ذخیرهسازی بدون بهرهمندی از بهبود عملکرد پُرسمان جلوگیری کنید.
ایجاد نمایههای چندستونی
اگر پُرسمانهایتان چندین ستون را ترکیب میکند، میتوانید نمایههای چندستونی ایجاد کنید تا پُرسمان را کاملاً تسریع کنید. همچنین میتوانید از نمایه در ستون بیرونی استفاده کنید و اجازه دهید جستجوی داخلی بهعنوان اسکن خطی انجام شود.
برای مثال، با فرض پُرسمان زیر:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
میتوانید پُرسمان را با نمایه چندستونی به همان ترتیبی که در پُرسمان مشخص شده است تسریع کنید:
CREATE INDEX city_name_index ON Customers(city, name);
اما اگر فقط یک نمایه در city داشته باشید، ترتیب بیرونی همچنان تسریع میشود، درحالیکه ترتیب داخلی به اسکن خطی نیاز دارد.
این ویژگی با پرسوجوهای پیشوندی نیز کار میکند. برای مثال، نمایه
ON Customers (city, name) همچنین فیلتر کردن، ترتیببندی، و گروهبندی
براساس city را تسریع میکند، زیرا جدول نمایه برای نمایه چندستونی براساس
نمایههای دادهشده در ترتیب دادهشده مرتب میشود.
WITHOUT ROWID را درنظر بگیرید
بهطور پیشفرض، SQLite ستون rowid را برای جدول شما ایجاد میکند، که در آن rowid یک
INTEGER PRIMARY KEY AUTOINCREMENT ضمنی است. اگر ازقبل ستونی دارید که
INTEGER PRIMARY KEY است، این ستون نام مستعار rowid میشود.
برای جدولهایی که کلید اصلی بهغیراز INTEGER یا ترکیبی از ستونها دارند،
WITHOUT ROWID را درنظر بگیرید.
ذخیره دادههای کوچک بهعنوان BLOB و دادههای بزرگ بهعنوان فایل
اگر میخواهید دادههای بزرگ را با یک ردیف مرتبط کنید، مانند تصویر کوچک یک تصویر
یا عکس برای یک مخاطب، میتوانید دادهها را در یک ستون BLOB یا در
یک فایل ذخیره کنید و سپس مسیر را در ستون ذخیره کنید.
فایلها معمولاً به مضارب ۴ کیلوبایت گرد میشوند. برای فایلهای بسیار کوچک که
خطای گرد کردن قابلتوجه است، ذخیره کردن آنها در
پایگاه داده بهعنوان BLOB کارآمدتر است. SQLite تماسهای سیستم فایل را بهحداقل میرساند و در برخی موارد
سریعتر از سیستم فایل زیربنایی است.
بهبود عملکرد پُرسمان
این روالهای مطلوب را دنبال کنید تا عملکرد پُرسمان را در SQLite با بهحداقل رساندن زمان پاسخ و بهحداکثر رساندن کارایی پردازش بهبود دهید.
فقط ردیفهای موردنیازتان را بخوانید
فیلترها به شما امکان میدهند نتایج را با مشخص کردن معیارهای خاصی، مانند محدوده تاریخ، مکان، یا نام، محدود کنید. محدودیتها به شما امکان میدهد تعداد نتایجی را که میبینید کنترل کنید:
db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Process cursor data
}
}
فقط ستونهای موردنیازتان را بخوانید
از انتخاب ستونهای غیرضروری که میتواند پُرسمانهایتان را کند کند و منابع را هدر دهد اجتناب کنید. درعوض، فقط ستونهایی را انتخاب کنید که استفاده میشوند.
در مثال زیر، id، name، و phone را انتخاب میکنید:
// This is not the most efficient way of doing this.
// See the following example for a better approach.
db.rawQuery(
"""
SELECT id, name, phone
FROM customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(1)
// Further processing
}
}
بااینحال، فقط به ستون name نیاز دارید:
db.rawQuery("""
SELECT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
// Further processing
}
}
پارامتربندی پُرسمانها
رشته پُرسمان شما ممکن است شامل پارامتری باشد که فقط در زمان اجرا شناخته میشود، مانند موارد زیر:
fun getNameById(id: Long): String?
db.rawQuery(
"SELECT name FROM customers WHERE id=$id", null
).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getString(0)
} else {
null
}
}
}
در کد بالا، هر پُرسمان رشته متفاوتی میسازد و بنابراین
از حافظه نهان عبارت بهره نمیبرد. هر تماس به SQLite نیاز دارد تا آن را قبلاز اجرا کامپایل کند. درعوض، میتوانید متغیر مستقل id را با
پارامتر جایگزین کنید و
مقدار را با selectionArgs مقید کنید:
fun getNameById(id: Long): String? {
db.rawQuery(
"""
SELECT name
FROM customers
WHERE id=?
""".trimIndent(), arrayOf(id.toString())
).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getString(0)
} else {
null
}
}
}
اکنون پُرسمان میتواند یکبار کامپایل و ذخیره شود. پرسمان کامپایلشده بین فراخوانیهای مختلف getNameById(long) دوباره استفاده میشود.
تکرار در SQL، نه در کد
از یک پُرسمان واحد استفاده کنید که همه نتایج هدفیابیشده را برمیگرداند، بهجای اینکه از یک حلقه برنامهنویسیشده برای تکرار پُرسمانهای SQL برای برگرداندن نتایج فردی استفاده کنید. حلقه برنامهنویسی حدود ۱۰۰۰ برابر کندتر از یک پُرسمان SQL است.
از DISTINCT برای مقادیر منحصربهفرد استفاده کنید
استفاده از کلیدواژه DISTINCT میتواند عملکرد پُرسمانهایتان را با کاهش مقدار دادههایی که باید پردازش شوند بهبود بخشد. برای مثال، اگر میخواهید
فقط مقادیر یکتا را از ستونی برگردانید، از DISTINCT استفاده کنید:
db.rawQuery("""
SELECT DISTINCT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Only iterate over distinct names in Kotlin
// Process distinct name
}
}
هرزمان ممکن بود از توابع تجمیعی استفاده کنید
برای نتایج تجمیعی بدون دادههای ردیف، از توابع تجمیعی استفاده کنید. برای مثال، کد زیر بررسی میکند که آیا حداقل یک ردیف منطبق وجود دارد یا نه:
// This is not the most efficient way of doing this.
// See the following example for a better approach.
db.rawQuery("""
SELECT id, name
FROM Customers
WHERE city = 'Paris';
""".trimIndent(),
null
).use { cursor ->
if (cursor.moveToFirst()) {
// At least one customer from Paris
// Handle found
} else {
// No customers from Paris
// Handle not found
}
برای واکشی فقط ردیف اول، میتوانید از EXISTS() برای برگرداندن 0 درصورت عدم وجود ردیف منطبق و 1 درصورت وجود یک یا چند ردیف منطبق استفاده کنید:
db.rawQuery("""
SELECT EXISTS (
SELECT null
FROM Customers
WHERE city = 'Paris';
);
""".trimIndent(),
null
).use { cursor ->
if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
// At least one customer from Paris
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
استفاده از توابع تجمیعی SQLite در برنامه کد:
-
COUNT: تعداد ردیفهای موجود در ستون را میشمارد. SUM: همه مقادیر عددی را در ستون اضافه میکند.MINیاMAX: کمترین یا بیشترین مقدار را تعیین میکند. برای ستونهای عددی، انواعDATE، و انواع نوشتاری کار میکند.-
AVG: مقدار عددی میانگین را پیدا میکند. -
GROUP_CONCAT: رشتهها را با جداکننده اختیاری بههم متصل میکند.
استفاده از COUNT() بهجای Cursor.getCount()
در
مثال زیر، تابع
Cursor.getCount()
همه ردیفها را از پایگاه داده میخواند و همه مقادیر ردیف را برمیگرداند:
// This is not the most efficient way of doing this.
// See the following example for a better approach.
db.rawQuery("""
SELECT id
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
val count = cursor.getCount()
// Use count
}
اما بااستفاده از COUNT()، پایگاه داده فقط تعداد را برمیگرداند:
db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
cursor.moveToFirst()
val count = cursor.getInt(0)
// Use count
}
پرسوجوهای تودرتو بهجای کد
SQL قابلترکیب است و از زیرپرسشها، پیوندها، و محدودیتهای کلید خارجی پشتیبانی میکند. میتوانید از نتیجه یک پُرسمان در پُرسمان دیگری استفاده کنید بدون اینکه لازم باشد از کد برنامه استفاده کنید. این کار نیاز به کپی کردن دادهها از SQLite را کاهش میدهد و به موتور پایگاه داده اجازه میدهد پُرسمان شما را بهینهسازی کند.
در مثال زیر، میتوانید پُرسمانی را اجرا کنید تا ببینید کدام شهر بیشترین مشتری را دارد، سپس از نتیجه در پُرسمان دیگری استفاده کنید تا همه مشتریان آن شهر را پیدا کنید:
// This is not the most efficient way of doing this.
// See the following example for a better approach.
db.rawQuery("""
SELECT city
FROM Customers
GROUP BY city
ORDER BY COUNT(*) DESC
LIMIT 1;
""".trimIndent(),
null
).use { cursor ->
if (cursor.moveToFirst()) {
val topCity = cursor.getString(0)
db.rawQuery("""
SELECT name, city
FROM Customers
WHERE city = ?;
""".trimIndent(),
arrayOf(topCity)).use { innerCursor ->
while (innerCursor.moveToNext()) {
// Process inner cursor data
}
}
}
}
برای دریافت نتیجه در نصف زمان مثال قبلی، از یک پُرسمان SQL با عبارتهای تودرتو استفاده کنید:
db.rawQuery("""
SELECT name, city
FROM Customers
WHERE city IN (
SELECT city
FROM Customers
GROUP BY city
ORDER BY COUNT (*) DESC
LIMIT 1;
);
""".trimIndent(),
null
).use { cursor ->
if (cursor.moveToNext()) {
// Process cursor data
}
}
بررسی یکتایی در SQL
اگر ردیفی نباید درج شود مگر اینکه مقدار ستون خاصی در جدول یکتا باشد، ممکن است اعمال آن یکتایی بهعنوان محدودیت ستون کارآمدتر باشد.
در مثال زیر، یک پُرسمان برای اعتبارسنجی ردیفی که باید درج شود اجرا میشود و پُرسمان دیگری برای درج واقعی:
// This is not the most efficient way of doing this.
// See the following example for a better approach.
db.rawQuery(
"""
SELECT EXISTS (
SELECT null
FROM customers
WHERE username = ?
);
""".trimIndent(),
arrayOf(customer.username)
).use { cursor ->
if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
throw AddCustomerException(customer)
}
}
db.execSQL(
"INSERT INTO customers VALUES (?, ?, ?)",
arrayOf(
customer.id.toString(),
customer.name,
customer.username
)
)
بهجای بررسی محدودیت یکتا در Kotlin، میتوانید آن را در SQL هنگام تعریف جدول بررسی کنید:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
SQLite همانند موارد زیر عمل میکند:
CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);
اکنون میتوانید ردیفی را درج کنید و به SQLite اجازه دهید محدودیت را بررسی کند:
try {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, customer.username)
)
} catch(e: SQLiteConstraintException) {
throw AddCustomerException(customer, e)
}
SQLite از نمایههای یکتا با چند ستون پشتیبانی میکند:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
SQLite محدودیتها را سریعتر و با سربار کمتری نسبتبه کد Kotlin اعتبارسنجی میکند. بهترین روش این است که بهجای کد برنامه از SQLite استفاده کنید.
دستهبندی چند درج در یک تراکنش
تراکنش چندین عملیات را انجام میدهد که نهتنها کارایی بلکه درستی را نیز بهبود میبخشد. برای بهبود سازگاری دادهها و تسریع عملکرد، میتوانید درجها را دستهبندی کنید:
db.beginTransaction()
try {
customers.forEach { customer ->
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, "customerValue")
)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
خارج از یک تراکنش صریح، هر نوشتن در تراکنش ضمنی خود اجرا میشود و هزینه کامل یک commit را پرداخت میکند، ازجمله یک fsync در حالت همگام FULL. دستهبندی این هزینه را یکبار پرداخت میکند، که میتواند دستهای از
نوشتنها را یک مرتبه یا بیشتر سریعتر کند.
استفاده از ابزارهای عیبیابی
SQLite ابزارهای عیبیابی زیر را برای کمک به اندازهگیری عملکرد ارائه میدهد.
استفاده از پیامواره تعاملی SQLite
SQLite را در دستگاهتان اجرا کنید تا پُرسمانها را اجرا کنید و یاد بگیرید.
نسخههای مختلف پلاتفرم Android از بازبینیهای مختلف SQLite استفاده میکنند. برای استفاده از همان موتوری که در دستگاه Android وجود دارد، از adb shell استفاده کنید و sqlite3 را در دستگاه هدف اجرا کنید.
میتوانید از SQLite بخواهید که زمان پُرسمانها را اندازهگیری کند:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
بااستفاده از EXPLAIN QUERY PLAN میتوانید از SQLite بخواهید توضیح دهد که چگونه قصد دارد به پُرسمان پاسخ دهد:
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers
مثال قبلی برای پیدا کردن همه مشتریان اهل پاریس به اسکن کامل جدول بدون نمایه نیاز دارد. به این حالت پیچیدگی خطی میگویند. SQLite باید همه ردیفها را بخواند و فقط ردیفهایی را نگه دارد که با مشتریان پاریس مطابقت دارند. برای رفع این مشکل، میتوانید نمایه اضافه کنید:
sqlite> CREATE INDEX Idx1 ON Customers(city);
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SEARCH test USING INDEX Idx1 (city=?
اگر از پوسته تعاملی استفاده میکنید، میتوانید از SQLite بخواهید که همیشه طرحهای پُرسمان را توضیح دهد:
sqlite> .eqp on
برای اطلاعات بیشتر، به برنامهریزی پُرسمان مراجعه کنید.
SQLite Analyzer
SQLite میانای خط فرمان (CLI) sqlite3_analyzer را برای تخلیه اطلاعات اضافی ارائه میدهد که میتوان از آن برای عیبیابی عملکرد استفاده کرد. برای نصب،
به صفحه بارگیری SQLite بروید.
میتوانید از adb pull برای بارگیری فایل پایگاه داده از دستگاه هدف به ایستگاه کاریتان برای تجزیهوتحلیل استفاده کنید:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
مرورگر SQLite
همچنین میتوانید ابزار GUI SQLite Browser را در صفحه بارگیریهای SQLite نصب کنید.
ثبت گزارش Android
Android پُرسمانهای SQLite را زمانبندی میکند و آنها را برای شما ثبت میکند:
# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR
در Android 13 و نسخههای بالاتر، میتوانید فقط عملیاتهای کند را ثبت کنید. آستانهای را برحسب میلیثانیه تنظیم کنید، سپس برنامهتان را بازراهاندازی کنید تا برچسب گزارش را بخواند:
$ adb shell setprop log.tag.SQLiteSlowQueries VERBOSE
$ adb shell setprop db.log.slow_query_threshold 100
$ adb shell am force-stop <package-name>
عملیات آهسته در logcat تحت برچسب SQLiteConnection با مدتزمان، SQL، و مسیر پایگاه داده آنها ظاهر میشوند. برای تنظیم آستانه برای یک برنامه، از
db.log.slow_query_threshold.<uid> استفاده کنید، که در آن <uid> شناسه کاربری برنامه است.
ردیابی Perfetto
هنگام پیکربندی Perfetto، دسته database atrace را اضافه کنید تا
عملیات SQLite در ردیابی گنجانده شود. برای دیدن اینکه هر رشته هنگام اجرای SQL چه کاری انجام میدهد،
رویدادهای زمانبند را نیز اضافه کنید:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_blocked_reason"
atrace_categories: "database"
}
}
}
Android هر عملیات پایگاه داده را بهعنوان یک برش ردیابی میکند که نام آن از عملیات و SQL آن گرفته شده است، برای مثال
executeForCursorWindow SELECT * FROM message WHERE body LIKE ?. SQL
? جایبان خود را حفظ میکند و به ۲۵۶ نویسه کوتاه میشود. متغیرهای مستقل Bind گنجانده نشدهاند. عملیات شامل open، close، prepare، execute، و
متغیرهای executeFor است. برش prepare به این معنی نیست که حافظه نهان عبارت
ازدست رفته است.
چیدمان قطعه به نسخه Android بستگی دارد:
- در Android 17 و نسخههای بالاتر، هر اتصال یک قطعه ناهمگام در پردازش برنامه دارد که نام آن
SQLiteConnection-است و بهدنبال آن شماره اتصال میآید. تراکنشها بهصورت برشهایTRANSACTION-EXCLUSIVE،TRANSACTION-IMMEDIATE، یاTRANSACTION-DEFERREDنشان داده میشوند و عبارتهایی که در آنها اجرا شدهاند در زیر آنها بهصورت تودرتو قرار میگیرند. رشتهای که عملیات را اجرا میکند، برای هر تراکنش و برای هر عبارت خارج از تراکنش، رویدادهای فوریSTART-وEND-را نیز در قطعه رشته خود دریافت میکند. نام این رویدادها با نام قطعه اتصال تمام میشود، برای مثال-SQLiteConnection-0. - در Android 16 و نسخههای پایینتر، هر عبارت SQL متمایز قطعه ناهمگام خودش را در فرایند برنامه دارد و تراکنشها ردیابی نمیشوند.
شمارههای اتصال برای هر پایگاه داده از ۰ شروع میشود، بنابراین اتصالات اصلی
همه پایگاههای داده در یک فرایند، قطعه SQLiteConnection-0 را بهطور مشترک استفاده میکنند. اگر
برنامهتان بهطور همزمان از بیشاز یک پایگاه داده استفاده میکند، مدتزمانها را با
رویدادهای START- و END- در قطعههای رشته اندازهگیری کنید.
شکل ۱. اسکن کامل جدول در رشته اصلی در Android 17.
میتوانید این برشها را با PerfettoSQL در میانای کاربری Perfetto یا با
trace_processor تحلیل کنید. پُرسمان زیر رایجترین عملیات SQLite را در هر فرایند پیدا میکند. این پُرسمان در همه نسخههای Android کار میکند:
INCLUDE PERFETTO MODULE slices.with_context;
SELECT process_name, name AS operation, COUNT(*) AS count
FROM process_slice
WHERE name GLOB 'open *' OR name GLOB 'close *'
OR name GLOB 'prepare *' OR name GLOB 'execute*'
OR name GLOB 'TRANSACTION-*'
GROUP BY process_name, name
ORDER BY count DESC
LIMIT 10;
پُرسمان زیر عملیات SQLite را که بیشاز ۱ میلیثانیه در رشته اصلی در Android 17 و بالاتر طول کشیده است فهرست میکند:
INCLUDE PERFETTO MODULE slices.with_context;
WITH marks AS (
SELECT process_name, utid, is_main_thread, ts, name,
LEAD(ts) OVER (PARTITION BY utid ORDER BY ts) AS end_ts,
LEAD(name) OVER (PARTITION BY utid ORDER BY ts) AS end_name
FROM thread_slice
WHERE name GLOB 'START-*' OR name GLOB 'END-*'
)
SELECT process_name, ts, ROUND((end_ts - ts) / 1e6, 1) AS dur_ms,
substr(name, 7) AS operation
FROM marks
WHERE name GLOB 'START-*'
AND end_name = 'END-' || substr(name, 7)
AND is_main_thread
AND end_ts - ts > 1e6
ORDER BY dur_ms DESC
LIMIT 10;
شکل ۲. عملیات SQLite طولانیتر از ۱ میلیثانیه در رشته اصلی.
پرسمان زیر رشتههایی را پیدا میکند که خارج از تراکنش صریح مینویسند، جایی که هر نوشتن هزینه commit خود را در Android 17 و بالاتر پرداخت میکند:
INCLUDE PERFETTO MODULE slices.with_context;
WITH ops AS (
SELECT process_name, thread_name, utid, name,
upper(ltrim(substr(name, instr(name, ' ') + 1))) AS sql
FROM thread_slice
WHERE name GLOB 'START-*'
)
SELECT process_name, thread_name,
SUM(name GLOB 'START-TRANSACTION-*') AS transactions,
SUM(name GLOB 'START-execute*' AND (sql GLOB 'INSERT *'
OR sql GLOB 'UPDATE *' OR sql GLOB 'DELETE *' OR sql GLOB 'REPLACE *'))
AS autocommit_writes
FROM ops
GROUP BY utid
HAVING autocommit_writes > 0
ORDER BY autocommit_writes DESC
LIMIT 10;
dumpsys meminfo
adb shell dumpsys meminfo <package-name> آمار مربوط به استفاده از حافظه برنامه را چاپ میکند، ازجمله جزئیات مربوط به SQLite در بخش DATABASES. برای مثال، این مورد از برونداد adb shell dumpsys meminfo com.google.android.gms.persistent در دستگاه توسعهدهنده گرفته شده است:
DATABASES
pgsz dbsz Lookaside(b) cache hits cache misses cache size Dbname
PER CONNECTION STATS
4 52 45 8 41 6 /data/user/10/com.google.android.gms/databases/gaia-discovery
4 8 0 0 0 (attached) temp
4 52 56 5 23 6 /data/user/10/com.google.android.gms/databases/gaia-discovery (1)
4 252 95 233 124 12 /data/user_de/10/com.google.android.gms/databases/phenotype.db
4 8 0 0 0 (attached) temp
4 252 17 0 17 1 /data/user_de/10/com.google.android.gms/databases/phenotype.db (1)
4 9280 105 103169 69805 25 /data/user/10/com.google.android.gms/databases/phenotype.db
4 20 0 0 0 (attached) temp
4 9280 108 13877 6394 25 /data/user/10/com.google.android.gms/databases/phenotype.db (2)
4 8 0 0 0 (attached) temp
4 9280 105 12548 5519 25 /data/user/10/com.google.android.gms/databases/phenotype.db (3)
4 8 0 0 0 (attached) temp
4 9280 107 18328 7886 25 /data/user/10/com.google.android.gms/databases/phenotype.db (1)
4 8 0 0 0 (attached) temp
4 36 51 156 29 5 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
4 36 97 47 27 10 /data/user/10/com.google.android.gms/databases/context_feature_default.db
4 36 56 3 16 4 /data/user/10/com.google.android.gms/databases/context_feature_default.db (2)
4 300 40 2111 24 5 /data/user/10/com.google.android.gms/databases/gservices.db
4 300 39 3 17 4 /data/user/10/com.google.android.gms/databases/gservices.db (1)
4 20 17 0 14 1 /data/user/10/com.google.android.gms/databases/gms.notifications.db
4 20 33 1 15 2 /data/user/10/com.google.android.gms/databases/gms.notifications.db (1)
4 120 40 143 163 4 /data/user/10/com.google.android.gms/databases/android_pay
4 120 123 86 32 19 /data/user/10/com.google.android.gms/databases/android_pay (1)
4 28 33 4 17 3 /data/user/10/com.google.android.gms/databases/googlesettings.db
POOL STATS
cache hits cache misses cache size Dbname
13 68 81 /data/user/10/com.google.android.gms/databases/gaia-discovery
233 145 378 /data/user_de/10/com.google.android.gms/databases/phenotype.db
147921 89616 237537 /data/user/10/com.google.android.gms/databases/phenotype.db
156 30 186 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
50 57 107 /data/user/10/com.google.android.gms/databases/context_feature_default.db
2114 43 2157 /data/user/10/com.google.android.gms/databases/gservices.db
1 31 32 /data/user/10/com.google.android.gms/databases/gms.notifications.db
229 197 426 /data/user/10/com.google.android.gms/databases/android_pay
4 18 22 /data/user/10/com.google.android.gms/databases/googlesettings.db
Android 14 و بالاتر جدولهای PER CONNECTION STATS و POOL STATS
نشاندادهشده در مثال را چاپ میکند. Android 13 و نسخههای پایینتر فقط جدول مربوط به هر اتصال را چاپ میکنند،
و آمار حافظه پنهان را در یک ستون cache بهصورت
hits/misses/size ترکیب میکنند.
در بخش PER CONNECTION STATS، موارد زیر را خواهید دید:
-
pgsz: اندازه یک صفحه پایگاه داده، به کیلوبایت. dbsz: اندازه کل فایل پایگاه داده، به کیلوبایت.Lookaside(b): تعداد جایگاههای حافظه جانبی که اتصال استفاده میکند. باوجود(b)در سرصفحه، این تعداد جایگاهها است، نه بایتها.-
cache hits،cache misses، وcache size: آمار حافظه نهان عبارت آمادهشده اتصال. این مقادیر عبارتهای SQL کامپایلشده را میشمارند، نه صفحههای پایگاه داده را. -
Dbname: مسیر فایل پایگاه داده. اتصالهای اضافی به همان پایگاه داده دارای عددی در پرانتز هستند، مثل(1). با WAL، یک پایگاه داده میتواند چندین اتصال داشته باشد تا خواندنها بتوانند بهصورت موازی اجرا شوند.
در بخش POOL STATS، آمار حافظه پنهان دستور آمادهشده را برای هر
مجموعه اتصال پیدا خواهید کرد:
-
cache hits: SQLite دستورات آمادهشده را ذخیره میکند و تلاش میکند هنگام اجرای پُرسمانها از آنها مجدداً استفاده کند تا در تلاش و حافظه برای کامپایل کردن دستورات SQL صرفهجویی کند. این تعداد دفعات دسترسی به حافظه نهان عبارت است. cache misses: تعداد موارد عدم تطابق حافظه نهان عبارت.-
cache size: در Android 16 QPR2 و بالاتر، تعداد دستورات آمادهشده در حافظه نهان، که در اتصالات غیرفعال استخر جمع شدهاند. در Android 14 تا Android 16 QPR1، این مقدار مجموع موفقیتها و شکستها است و اندازه حافظه نهان را نشان نمیدهد.
از این آمار برای تصمیمگیری درباره اینکه چه چیزی را اصلاح کنید استفاده کنید:
- نرخ بالای عدم تطابق به این معنی است که SQLite همان عبارتها را بهطور مکرر کامپایل میکند.
ابتدا، SQL ساختهشده با الحاق رشته را پیدا کنید و
آن را پارامتری کنید. اگر پُرسمانهایتان ازقبل پارامتری شده است و
cache sizeهر اتصال در حد ۲۵ است، اندازه حافظه نهان را افزایش دهید. - مسیر پایگاه دادهای که بیشاز یکبار بدون پسوند سبک
(1)ظاهر میشود به این معنی است که برنامه شما همان فایل را دو بار باز کرده است. هر پایگاه داده را یکبار باز کنید. - پایگاه دادهای که برنامهتان بهندرت از آن استفاده میکند، تا زمانی که باز است همچنان حافظه را اشغال میکند. وقتی کارتان تمام شد، آن را ببندید.
dumpsys dbinfo
adb shell dumpsys dbinfo <package-name> وضعیت هر مخزن اتصال را در برنامهتان چاپ میکند. -v را اضافه کنید تا دستورات موجود در حافظه نهان دستورات آمادهشده هر اتصال نیز فهرست شود. خروجی نمونه زیر از Android 17 است،
با مسیرها و خطوط طولانی کوتاه شده:
** Database info for pid 2705 [com.example.app] **
Connection pool for /data/user/0/com.example.app/databases/settings.db:
Open: true
Max connections: 4
Total execution time (ms): 4963
Total statements executed: 231239
Average time per statement (ms): 0
Configuration: openFlags=805306368, ..., journalMode=WAL, syncMode=NORMAL
IsReadOnlyDatabase: false
Available primary connection:
<none>
Available non-primary connections:
<none>
Acquired connections:
<none>
Connection waiters:
<none>
Connection pool for /data/user/0/com.example.app/databases/events.db:
Open: true
Max connections: 1
Total execution time (ms): 842
Total statements executed: 840
Average time per statement (ms): 1
Configuration: openFlags=268435456, ..., journalMode=TRUNCATE, syncMode=FULL
IsReadOnlyDatabase: false
Available primary connection:
Connection #0:
isPrimaryConnection: true
onlyAllowReadOnlyOperations: false
totalLongOperations: 0
Most recently executed operations:
0: [10-26 04:22:32.133] executeForChangedRowCount took 0ms -
succeeded, sql="DELETE FROM params WHERE app_id=?", path=...
1: [10-26 04:22:32.133] prepare took 0ms - succeeded,
sql="DELETE FROM params WHERE app_id=?", path=...
...
بهدنبال موارد زیر بگردید:
Configuration: حالت ژورنال و حالت همگامسازی استخر. در Android 13 و بالاتر، این خط حالتهایی را نشان میدهد که چارچوب اعمال کرده است، ازجمله پیشفرضهای پلاتفرم. در این مثال،events.dbاز دفتر روزنامه بازگشت با حالت همگامسازیFULLاستفاده میکند، بنابراین هر ثبت منتظر همگامسازی کامل میماند. این خطPRAGMAعبارتهایی را که خودتان اجرا میکنید منعکس نمیکند.Max connections: مقدار ۱ برای پایگاه داده قابلنوشتن به این معنی است که چارچوب پایگاه داده را در حالت WAL درنظر نمیگیرد، بنابراین خواندن و نوشتن از یک اتصال مشترک استفاده میکنند.Total execution time،Total statements executed، وAverage time per statement: در Android 13 و بالاتر دردسترس است. از آنها برای پیدا کردن شلوغترین پایگاههای دادهتان استفاده کنید.-
Most recently executed operations: ۲۰ عملیات آخر در هر اتصال، با مدت زمان و SQL آنها. بهدنبال الگوهایی مثل اجرای همان پُرسمان در یک حلقه یا نوشتنهای متعدد پشتسرهم خارج از تراکنش باشید. در Android 15 QPR1 و نسخههای بالاتر، این فهرست شامل تراکنشهای صریح نیز میشود. totalLongOperationsوOperations exceeding 2000ms: دردسترس در Android 15 QPR1 و بالاتر. این بخش نشان میدهد چند عملیات و تراکنش بیشتر از ۲ ثانیه طول کشیده است و ۱۰ مورد آخر را فهرست میکند.Acquired connectionsوConnection waiters: اتصالهایی که درحال استفاده هستند و رشتههایی که درانتظار اتصال هستند. منتظران نشاندهنده رقابت هستند، مثلاً تراکنش نوشتن طولانی که نوشتنهای دیگر را مسدود میکند.-
Prepared statement cache: با-vچاپ شد. بسیاری از ورودیهایی که فقط در مقادیر دقیق متفاوت هستند به SQL اشاره میکنند که با الحاق رشته ساخته شده است. - وجود مسیر یکسان در زیر بیشاز یک عنوان
Connection pool forبه این معنی است که برنامهتان فایل یکسانی را دو بار باز کرده است.
منابع بیشتر
محتوا را مشاهده میکند
توصیهشده برای شما
- توجه: وقتی جاوا اسکریپت خاموش باشد، نوشتار پیوند نمایش داده میشود
- اجرای محکزنی در «ادغام پیوسته»
- قابهای ثابت
- ایجاد و اندازهگیری «نمایههای پایه» بدون Macrobenchmark