بهترین شیوه‌ها برای عملکرد SQLite (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

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

برای دستیابی به عملکرد سریع‌تر، این اصول عملکرد را دنبال کنید:

  • خواندن سطرها و ستون‌های کمتر : کوئری‌های خود را بهینه کنید تا فقط داده‌های ضروری بازیابی شوند. میزان داده‌های خوانده شده از پایگاه داده را به حداقل برسانید، زیرا بازیابی داده‌های اضافی می‌تواند بر عملکرد تأثیر بگذارد.

  • ارسال کار به موتور SQLite : انجام محاسبات، فیلتر کردن و مرتب‌سازی عملیات درون کوئری‌های SQL. استفاده از موتور کوئری SQLite می‌تواند عملکرد را به میزان قابل توجهی بهبود بخشد.

  • اصلاح طرحواره پایگاه داده : طرحواره پایگاه داده خود را طوری طراحی کنید که به SQLite در ساخت طرح‌های پرس‌وجو و نمایش داده‌های کارآمد کمک کند. جداول را به درستی فهرست‌بندی کنید و ساختارهای جدول را برای افزایش عملکرد بهینه کنید.

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

توصیه می‌کنیم از کتابخانه Jetpack Room استفاده کنید.

پیکربندی پایگاه داده برای عملکرد بهتر

برای پیکربندی پایگاه داده خود برای عملکرد بهینه در SQLite، مراحل این بخش را دنبال کنید.

حالت همگام‌سازی را آرام کنید

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

SQLite گزینه‌ای برای کنترل حالت همگام‌سازی دارد. اگر WAL را فعال کنید، حالت همگام‌سازی را روی NORMAL تنظیم کنید:

کاتلین

// When opening the database
val paramsBuilder: SQLiteDatabase.OpenParams.Builder = SQLiteDatabase.OpenParams.Builder()
paramsBuilder.journalMode = SQLiteDatabase.SYNC_MODE_NORMAL

// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");

جاوا

// When opening the database
SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
paramsBuilder.setJournalMode(SQLiteDatabase.SYNC_MODE_NORMAL);

// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");

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

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

بهبود عملکرد پرس و جو

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

فقط ردیف‌هایی را که نیاز دارید بخوانید

فیلترها به شما امکان می‌دهند نتایج خود را با مشخص کردن معیارهای خاصی مانند محدوده تاریخ، مکان یا نام، محدود کنید. محدودیت‌ها به شما امکان می‌دهند تعداد نتایجی را که می‌بینید کنترل کنید:

کاتلین

db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Process cursor data
    }
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """, null)) {
  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
    }
}

جاوا

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id, name, phone
    FROM customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String 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
    }
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String 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
        }
    }
}

جاوا

@Nullable
public String getNameById(long id) {
  try (Cursor cursor = db.rawQuery(
      "SELECT name FROM customers WHERE id=" + id, null)) {
    if (cursor.moveToFirst()) {
      return cursor.getString(0);
    } else {
      return 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
        }
    }
}

جاوا

@Nullable
public String getNameById(long id) {
  try (Cursor cursor = db.rawQuery("""
          SELECT name
          FROM customers
          WHERE id=?
      """, new String[] {String.valueOf(id)})) {
    if (cursor.moveToFirst()) {
      return cursor.getString(0);
    } else {
      return null;
    }
  }
}

حالا می‌توان کوئری را یک بار کامپایل و ذخیره کرد. کوئری کامپایل شده بین فراخوانی‌های مختلف getNameById(long) دوباره استفاده می‌شود.

استفاده 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
    }
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    // Only iterate over distinct names in Java
    // 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
}

جاوا

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id, name
    FROM Customers
    WHERE city = 'Paris';
    """, null)) {
  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
    }
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT EXISTS (
      SELECT null
      FROM Customers
      WHERE city = 'Paris'
    );
    """, null)) {
  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 : رشته‌ها را با استفاده از یک جداکننده اختیاری به هم متصل می‌کند.

به جای Cursor.getCount() از COUNT() استفاده کنید.

در مثال زیر، تابع 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
}

جاوا

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id
    FROM Customers;
    """, null)) {
  int 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
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """, null)) {
  cursor.moveToFirst();
  int 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
            }
        }
    }
}

جاوا

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT city
    FROM Customers
    GROUP BY city
    ORDER BY COUNT(*) DESC
    LIMIT 1;
    """, null)) {
  if (cursor.moveToFirst()) {
    String topCity = cursor.getString(0);
    try (Cursor innerCursor = db.rawQuery("""
        SELECT name, city
        FROM Customers
        WHERE city = ?;
        """, new String[] {topCity})) {
        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
    }
}

جاوا

try (Cursor cursor = db.rawQuery("""
    SELECT name, city
    FROM Customers
    WHERE city IN (
      SELECT city
      FROM Customers
      GROUP BY city
      ORDER BY COUNT(*) DESC
      LIMIT 1
    );
    """, null)) {
  while(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
    )
)

جاوا

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT EXISTS (
      SELECT null
      FROM customers
      WHERE username = ?
    );
    """, new String[] { customer.username })) {
  if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
    throw new AddCustomerException(customer);
  }
}
db.execSQL(
    "INSERT INTO customers VALUES (?, ?, ?)",
    new String[] {
      String.valueOf(customer.id),
      customer.name,
      customer.username,
    });

به جای بررسی محدودیت منحصر به فرد در کاتلین یا جاوا، می‌توانید آن را در 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)
}

جاوا

try {
  db.execSQL(
      "INSERT INTO Customers VALUES (?, ?, ?)",
      new String[] {
        String.valueOf(customer.id),
        customer.name,
        customer.username,
      });
} catch (SQLiteConstraintException e) {
  throw new AddCustomerException(customer, e);
}

SQLite از ایندکس‌های منحصر به فرد با چندین ستون پشتیبانی می‌کند:

CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);

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

درج دسته‌ای چندین مورد در یک تراکنش واحد

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

کاتلین

db.beginTransaction()
try {
    customers.forEach { customer ->
        db.execSql(
            "INSERT INTO Customers VALUES (?, ?, ?)",
            arrayOf(customer.id.toString(), customer.name, "customerValue")
        )
    }
} finally {
    db.endTransaction()
}

جاوا

db.beginTransaction();
try {
  for (customer : Customers) {
    db.execSQL(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        new String[] {
          String.valueOf(customer.id),
          customer.name,
          "customerValue"
        });
  }
} finally {
  db.endTransaction()
}
{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}