1

We have an multi-user C# application, each user has the app files locally. This app connects to a central SQLite database file in the network, shared to any user.

We know SQLite is not made for concurrent user access, but we can't install a dbms, and there is a low amount of writing made to the db.

The problem is as soon as we have 2 users reading the db (not writing), the app will become very slow in getting the value and show it to the users.

To help, i will show a minimized part of the class that handle connections to the db:

    public class clsDBConnectionManager
{
    private static clsDBConnectionManager _instance;
    private string _gblConn = "";

    private SQLiteConnection _gblCon;
    public SQLiteCommand _gblCmd;
    private SQLiteTransaction _Trans;

    private clsDBConnectionManager()
    {
        try
        {
            _gblConn = Session.Instance.SqliteConnectionString;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    public static clsDBConnectionManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new clsDBConnectionManager();
            }
            return _instance;
        }
    }
    public SQLiteConnection GetConnection()
    {
        try
        {
            if (_gblCon == null)
            {
                _gblCon = new SQLiteConnection(_gblConn);
            }

            if (_gblCon.State == ConnectionState.Closed)
            {
                _gblCon.Open();
                if (_gblCmd == null)
                {
                    _gblCmd = new SQLiteCommand(_gblCon);
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show("Connexion Impossible on " + _gblConn + Environment.NewLine + "Command :" + _gblCon + Environment.NewLine + ex.Message);
            throw;
        }

        return _gblCon;
    }

    public void CloseConnection(bool withTransaction = false)
    {

        if (withTransaction || _Trans == null)
        {
            if (_Trans != null)
            {
                _Trans.Commit();
            }

            if (_gblCmd != null)
            {
                _gblCmd.Dispose();
                _gblCmd = null;
            }

            if (_Trans != null)
            {
                _Trans.Dispose();
                _Trans = null;
            }

            if (_gblCon != null)
            {
                try
                {
                    _gblCon.Close();

                }
                catch (SQLiteException ex)
                {

                    Console.WriteLine("Closing connection failed.");
                    Console.WriteLine("Error: {0}", ex.ToString());

                }
                finally
                {
                    _gblCon.Dispose();
                    _gblCon = null;
                }
            }
        }
    }

    public DataTable GetSQLData_byReader(string commandtext)
    {
        DataTable dt = new DataTable();
        try
        {
            GetConnection();

            _gblCmd.CommandText = commandtext;

            using (SQLiteDataReader objDr = _gblCmd.ExecuteReader())
            {
                dt.Load(objDr);
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }

        return dt;
    }
}

Then, in another class we call the previous class to read the db and retrieve data:

public DataTable GetHolidayList(string _Year)
    {


        string _Sql = "SELECT * from t_PUBLICHOL WHERE  substr(strftime(PUBLICHOL_DATE), 7, 4)='" + _Year + "'";

        DataTable table = new DataTable();

        try
        {
            table = _sqliteManager.GetSQLData_byReader(_Sql);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
            throw;
        }
        finally
        {
            _sqliteManager.CloseConnection();
        }

        return table;
    }

Does someone see something incorrect?

  • We really need help on this, as the current app speed is not acceptable, even if it is to say that the code is correct, since we are not sure... Thanks in advance. – Nicolas Daumalle Feb 06 '18 at 14:14
  • Why can't you put a proper DBMS somewhere? MySQL, Postgress, SQL Server etc. – mason Feb 06 '18 at 14:47
  • @mason We can't for legal and business reasons. We can't install anything in the client network. – Nicolas Daumalle Feb 06 '18 at 14:54
  • Are you sure you can't? You're installing an application on their network - why can't you install a DBMS as well? Or use a cloud hosted database? You are shooting yourself in the foot by not using a DBMS. Rather than trying to come up with crazy workarounds, you should address the actual issue head on. – mason Feb 06 '18 at 14:58
  • You don't specify which wrapper you are using to access SQLite from C#; also, please specify the connection string you are using. – Mark Benningfield Feb 06 '18 at 15:00
  • @mason I totally agree with your statement. But we can't install nothing. Even our application is portable, they just copy the files in their folder and launch the app. – Nicolas Daumalle Feb 06 '18 at 15:13
  • So why not use a cloud hosted database? – mason Feb 06 '18 at 15:14
  • @MarkBenningfield we use System.Data.SQLite. Is this answering your question? – Nicolas Daumalle Feb 06 '18 at 15:14
  • Why not use a DBMS that doesn't require an install? You can run MySQL as a [portable application](https://stackoverflow.com/questions/42045494/running-starting-mysql-without-installation-on-windows). – mason Feb 06 '18 at 15:15
  • @NicolasDaumalle You haven't shown your connection string. – mason Feb 06 '18 at 15:16
  • @NicolasDaumalle: That's part of it. Edit your question to show the connection string you are using, or show the code that constructs it from a `ConnectionStringBuilder`. – Mark Benningfield Feb 06 '18 at 15:17
  • @NicolasDaumalle you should probably re-evaluate your requirements/ SQLite is an *embedded* database. By default *only* one thread can open it for modification at a time. You can set it in WAL mode but performance will suffer and modification may not appear for several seconds. – Panagiotis Kanavos Feb 06 '18 at 15:36
  • @NicolasDaumalle even with WAL SQLite is *not* optimized for multi-user network access. MS Access is. – Panagiotis Kanavos Feb 06 '18 at 15:38
  • @mason my connection string is "Data Source='[Database path]';Version=3;New=False;datetimeformat=CurrentCulture;Regional=false;DateTimeKind='yyyy-MM-dd HH:mm:ss';Compress=True;" – Nicolas Daumalle Feb 06 '18 at 15:41
  • @PanagiotisKanavos I know we shouldn't use sqlite but the app was already made with it when we took over it, we are working on creating improved versions of it. – Nicolas Daumalle Feb 06 '18 at 15:45
  • @NicolasDaumalle you probably can't do anything more than use WAL mode then, and *probably* cache things locally, or in memory. At least, SQLite can work as an in-memory db – Panagiotis Kanavos Feb 06 '18 at 15:47
  • @NicolasDaumalle *and* avoid long-running connections and transactions. NO global connections, transactions, no waiting until disposal to close the connection. No transactions unless you *really* need them. The code posted here violates all of these – Panagiotis Kanavos Feb 06 '18 at 15:48
  • @PanagiotisKanavos I'm very interested to understand what is wrong with the code, i always open the connection and close it as soon as i can.. – Nicolas Daumalle Feb 06 '18 at 15:53
  • @NicolasDaumalle: Unless you have a custom build of the library that supports compression (like CEROD, etc.), the `compress=true` option is a no-op. If you do actually have compression support, you should turn it off. – Mark Benningfield Feb 06 '18 at 16:10
  • @NicolasDaumalle: WAL won't help you here, since WAL doesn't work over network file systems. The default `IsolationLevel` is `Serializable`, which gives you clean reads and locked writes, so that's about the best you can do there. – Mark Benningfield Feb 06 '18 at 16:12
  • @NicolasDaumalle: Ultimately, you're going to be limited by the ping time of the network, which you can't do anything about. Unless you have measured how much of the delay is due to ping time, there's not much else to go on at this point. – Mark Benningfield Feb 06 '18 at 16:14
  • @MarkBenningfield Thank you very much for your help! I will remove compress=true. Anyway it seems to do nothing actually. – Nicolas Daumalle Feb 07 '18 at 08:32
  • Anyone sees something incorrect in the code? like @PanagiotisKanavos seems to suggest (without saying what...) – Nicolas Daumalle Feb 07 '18 at 08:35
  • @NicolasDaumalle I told you what's wrong. Don't use global variables. SqlManager here *does* hold global variables. *Remove* all this code and create, open the connection explicitly inside a `using` block. You can reuse a command with different connections, parameters, so the SqlManager isn't necessary and actually *prevents* you from defining commands in advance and reusing them. And finally, SQLite is NOT meant for such use. You can't get around that. If you only want to read the data, copy the file locally – Panagiotis Kanavos Feb 07 '18 at 08:39
  • @PanagiotisKanavos I clearly stated multiple times that I know SQLite is not meant for such use. Are the global variables you are talking about are _gblCon, _gblCmd and _Trans? But we reinitialize them for every query to the db. The only object that persists is the manager. How could I define commands in advance and what is the advantage? I appreciate your help. – Nicolas Daumalle Feb 07 '18 at 09:17

2 Answers2

1

Accessing an SQLite file over a network tends not to work very well, there are a lot of posts about this problem. Even if you just read from the Db there can be issues. So the problem is not really the number of clients, or the code but the fact that you have a network accessible SQLite db file with > 1 client.

I think you need to consider a different approach to the problem.

If you have so few changes and you mostly read data, could you not cache the table data in local memory when the application starts? Then you just have a slow start, and the rest of the time it is OK. This assumes you don't have too much data to cache, and that it does not change that often.

Another approach is a server client approach. Here, you build a "server" application which controls the SQLite db file. It then gets requests from "clients" and serves them.

If the request come as REST commands then you also don't need to maintain a constant connection to your "server" which might make things easier and less complex.
But you will need a centralised location to run the server. This of course could also be a cloud based solution.

jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
0

With multiple clients accessing the same file in read/write mode, the file server can no longer risk to allow any client to cache the file data locally.

You could try to open the connection in read-only mode:

...;Read Only=True

and to open a read/write connection only when actually needed. But there is no guarantee that this will result in better caching, or that it will keep working after OS updates.

CL.
  • 173,858
  • 17
  • 217
  • 259