SQLite की परफ़ॉर्मेंस (व्यू) के लिए सबसे सही तरीके

सिद्धांत और Jetpack Compose का इस्तेमाल

Android, SQLite के लिए बिल्ट-इन सपोर्ट उपलब्ध कराता है. यह एक असरदार एसक्यूएल डेटाबेस है. अपने ऐप्लिकेशन की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने के लिए, इन सबसे सही तरीकों का इस्तेमाल करें. इससे यह पक्का किया जा सकेगा कि आपका ऐप्लिकेशन तेज़ी से काम करता रहे और डेटा बढ़ने पर भी इसकी परफ़ॉर्मेंस में कोई बदलाव न हो. इन सबसे सही तरीकों का इस्तेमाल करने से, परफ़ॉर्मेंस से जुड़ी उन समस्याओं के होने की संभावना भी कम हो जाती है जिन्हें दोहराना और ठीक करना मुश्किल होता है.

बेहतर परफ़ॉर्मेंस के लिए, परफ़ॉर्मेंस के इन सिद्धांतों का पालन करें:

  • कम पंक्तियां और कॉलम पढ़ें: सिर्फ़ ज़रूरी डेटा पाने के लिए, अपनी क्वेरी को ऑप्टिमाइज़ करें. डेटाबेस से कम से कम डेटा पढ़ें, क्योंकि ज़्यादा डेटा पाने से परफ़ॉर्मेंस पर असर पड़ सकता है.

  • SQLite इंजन को काम सौंपना: एसक्यूएल क्वेरी में कैलकुलेशन, फ़िल्टर करने, और क्रम से लगाने के ऑपरेशन करना. SQLite के क्वेरी इंजन का इस्तेमाल करने से, परफ़ॉर्मेंस को बेहतर बनाया जा सकता है.

  • डेटाबेस स्कीमा में बदलाव करना: अपने डेटाबेस स्कीमा को इस तरह से डिज़ाइन करें कि SQLite, क्वेरी के बेहतर प्लान और डेटा प्रज़ेंटेशन बना सके. टेबल को सही तरीके से इंडेक्स करें और परफ़ॉर्मेंस को बेहतर बनाने के लिए, टेबल के स्ट्रक्चर को ऑप्टिमाइज़ करें.

इसके अलावा, समस्या हल करने के लिए उपलब्ध टूल का इस्तेमाल करके, अपने SQLite डेटाबेस की परफ़ॉर्मेंस का आकलन किया जा सकता है. इससे उन क्षेत्रों की पहचान करने में मदद मिलती है जिनमें ऑप्टिमाइज़ेशन की ज़रूरत होती है.

हमारा सुझाव है कि आप Jetpack Room लाइब्रेरी का इस्तेमाल करें.

परफ़ॉर्मेंस के लिए डेटाबेस कॉन्फ़िगर करना

SQLite में बेहतर परफ़ॉर्मेंस के लिए, अपने डेटाबेस को कॉन्फ़िगर करने के लिए, इस सेक्शन में दिया गया तरीका अपनाएं.

सिंक करने का मोड बदलना

WAL का इस्तेमाल करते समय, डिफ़ॉल्ट रूप से हर कमिट एक fsync जारी करता है. इससे यह पक्का करने में मदद मिलती है कि डेटा डिस्क तक पहुंच गया है. इससे डेटा की ड्यूरेबिलिटी बेहतर होती है, लेकिन कमिट करने की प्रोसेस धीमी हो जाती है.

SQLite में, सिंक्रोनस मोड को कंट्रोल करने का विकल्प होता है. अगर आपने WAL चालू किया है, तो सिंक्रोनस मोड को NORMAL पर सेट करें:

Kotlin

// 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");

Java

// 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");

इस सेटिंग में, डेटा को डिस्क में सेव करने से पहले ही कमिट किया जा सकता है. अगर डिवाइस बंद हो जाता है, जैसे कि बिजली चले जाने या कर्नल पैनिक की वजह से, तो हो सकता है कि कमिट किया गया डेटा मिट जाए. हालांकि, लॉगिंग की वजह से आपका डेटाबेस खराब नहीं होता.

अगर सिर्फ़ आपका ऐप्लिकेशन क्रैश होता है, तो भी आपका डेटा डिस्क तक पहुंच जाता है. ज़्यादातर ऐप्लिकेशन के लिए, इस सेटिंग से परफ़ॉर्मेंस बेहतर होती है. इसके लिए, कोई शुल्क नहीं लिया जाता.

क्वेरी की परफ़ॉर्मेंस को बेहतर बनाना

जवाब देने में लगने वाले समय को कम करके और प्रोसेसिंग की क्षमता को बढ़ाकर, SQLite में क्वेरी की परफ़ॉर्मेंस को बेहतर बनाने के लिए, इन सबसे सही तरीकों को अपनाएं.

सिर्फ़ उन लाइनों को पढ़ें जिनकी आपको ज़रूरत है

फ़िल्टर की मदद से, नतीजों को किसी खास टाइप के हिसाब से देखा जा सकता है. इसके लिए, तारीख की सीमा, जगह या नाम जैसे कुछ मानदंड तय किए जा सकते हैं. सीमाएं तय करके, यह कंट्रोल किया जा सकता है कि आपको कितने नतीजे दिखें:

Kotlin

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

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """, null)) {
  while (cursor.moveToNext()) {
    // Process cursor data
  }
}

सिर्फ़ उन कॉलम को पढ़ें जिनकी आपको ज़रूरत है

ज़रूरत न होने पर कॉलम न चुनें. इससे आपकी क्वेरी धीमी हो सकती हैं और संसाधनों का इस्तेमाल बढ़ सकता है. इसके बजाय, सिर्फ़ उन कॉलम को चुनें जिनका इस्तेमाल किया जाता है.

यहां दिए गए उदाहरण में, id, name, और phone को चुना गया है:

Kotlin

// 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
    }
}

Java

// 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 कॉलम की ज़रूरत होगी:

Kotlin

db.rawQuery("""
    SELECT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(0)
        // Further processing
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String name = cursor.getString(0);
    // Further processing
  }
}

क्वेरी में पैरामीटर जोड़ना

आपकी क्वेरी स्ट्रिंग में ऐसा पैरामीटर शामिल हो सकता है जिसके बारे में सिर्फ़ रनटाइम पर पता चलता है. जैसे, यहां दिया गया पैरामीटर:

Kotlin

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
        }
    }
}

Java

@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 से बाइंड किया जा सकता है:

Kotlin

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
        }
    }
}

Java

@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 का इस्तेमाल करें:

Kotlin

db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Only iterate over distinct names in Kotlin
        // Process distinct name
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    // Only iterate over distinct names in Java
    // Process distinct name
  }
}

जब भी हो सके, एग्रीगेट फ़ंक्शन का इस्तेमाल करें

लाइन के डेटा के बिना, एग्रीगेट किए गए नतीजों के लिए एग्रीगेट फ़ंक्शन का इस्तेमाल करें. उदाहरण के लिए, यहां दिया गया कोड यह जांच करता है कि कम से कम एक मैचिंग लाइन मौजूद है या नहीं:

Kotlin

// 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
}

Java

// 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 दिखता है:

Kotlin

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
    }
}

Java

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() फ़ंक्शन, डेटाबेस की सभी लाइनों को पढ़ता है और लाइन की सभी वैल्यू दिखाता है:

Kotlin

// 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
}

Java

// 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() का इस्तेमाल करने पर, डेटाबेस सिर्फ़ गिनती दिखाता है:

Kotlin

db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    cursor.moveToFirst()
    val count = cursor.getInt(0)
    // Use count
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """, null)) {
  cursor.moveToFirst();
  int count = cursor.getInt(0);
  // Use count
}

कोड के बजाय नेस्ट क्वेरी

SQL को कंपोज़ किया जा सकता है. साथ ही, यह सबक्वेरी, जॉइन, और फ़ॉरेन की की पाबंदियों के साथ काम करता है. ऐप्लिकेशन कोड का इस्तेमाल किए बिना, एक क्वेरी के नतीजे का इस्तेमाल दूसरी क्वेरी में किया जा सकता है. इससे SQLite से डेटा कॉपी करने की ज़रूरत कम हो जाती है. साथ ही, डेटाबेस इंजन को आपकी क्वेरी को ऑप्टिमाइज़ करने की अनुमति मिल जाती है.

यहां दिए गए उदाहरण में, यह पता लगाने के लिए क्वेरी चलाई जा सकती है कि किस शहर में सबसे ज़्यादा ग्राहक हैं. इसके बाद, उस शहर के सभी ग्राहकों का पता लगाने के लिए, क्वेरी में मिले नतीजे का इस्तेमाल किया जा सकता है:

Kotlin

// 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
            }
        }
    }
}

Java

// 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 क्वेरी का इस्तेमाल करें:

Kotlin

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
    }
}

Java

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
  }
}

एसक्यूएल में यूनीक वैल्यू की जांच करना

अगर किसी लाइन को तब तक नहीं डाला जाना चाहिए, जब तक कि टेबल में किसी कॉलम की वैल्यू यूनीक न हो, तो उस यूनीकनेस को कॉलम की शर्त के तौर पर लागू करना ज़्यादा असरदार हो सकता है.

यहां दिए गए उदाहरण में, एक क्वेरी का इस्तेमाल उस लाइन की पुष्टि करने के लिए किया गया है जिसे डालना है. वहीं, दूसरी क्वेरी का इस्तेमाल लाइन को डालने के लिए किया गया है:

Kotlin

// 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
    )
)

Java

// 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,
    });

Kotlin या Java में यूनीक कंस्ट्रेंट की जांच करने के बजाय, टेबल को तय करते समय 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 को यह जांचने दिया जा सकता है कि कॉन्स्ट्रेंट का पालन किया गया है या नहीं:

Kotlin

try {
    db.execSql(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        arrayOf(customer.id.toString(), customer.name, customer.username)
    )
} catch(e: SQLiteConstraintException) {
    throw AddCustomerException(customer, e)
}

Java

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, Kotlin या Java कोड की तुलना में, कम समय में और कम ओवरहेड के साथ कंस्ट्रेंट की पुष्टि करता है. ऐप्लिकेशन कोड के बजाय SQLite का इस्तेमाल करना सबसे सही तरीका है.

एक ही लेन-देन में कई आइटम जोड़ना

लेन-देन में कई कार्रवाइयां शामिल होती हैं. इससे न सिर्फ़ काम करने की क्षमता बढ़ती है, बल्कि डेटा भी सटीक होता है. डेटा की अनुकूलता बेहतर बनाने और परफ़ॉर्मेंस को तेज़ करने के लिए, आप एक साथ कई आइटम जोड़ सकते हैं:

Kotlin

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

Java

db.beginTransaction();
try {
  for (customer : Customers) {
    db.execSQL(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        new String[] {
          String.valueOf(customer.id),
          customer.name,
          "customerValue"
        });
  }
} finally {
  db.endTransaction()
}