Data structure improvement

I’m currently developing a mental health app that requires users to complete three tasks daily. Each task includes a streak count for consecutive days of completion. One of the tasks involves viewing a daily quote. I’m working on expanding the database of daily quotes, but I’m struggling to develop a logical approach to displaying a new quote every day without repeating it excessively.

Additionally, I always check on the appearance of the home view if we have a new day. And when yes, I insert a new day.

Overall, this approach seems inefficient, and I’m seeking a more effective solution. Does anyone have any tips or suggestions to improve this aspect of the app?

class Day {
    var percentage: Double
    var tasksDone: [String]
    var sparkleSeen: Bool
    var startedAt: Date
    var quoteOfTheDay: Quote?
    
    init(percentage: Double = 0.0, tasksDone: [String] = [], sparkleSeen: Bool = false, startedAt: Date = .now, quoteOfTheDay: Quote? = nil) {
        self.percentage = percentage
        self.tasksDone = tasksDone
        self.sparkleSeen = sparkleSeen
        self.startedAt = startedAt
        self.quoteOfTheDay = quoteOfTheDay
    }
}

On the application code you could use something like CoreData or SQLite to create a table of Quotes with IDs and a last seen date column. Like:

struct Quote {
    let id: Int
    let quote: String
    let author: String 
    // ...
    let lastSeen: Date?
}

Then for your Day structure you'd just reference that quote via the identifier to avoid duplicating data:

struct Day {
    var percentage: Double
    var tasksDone: [String]
    var sparkleSeen: Bool
    var startedAt: Date
    var quoteOfTheDay: Int?
}

And to find a new quote you'd search for quotes that have a nil lastSeen and randomly select one. When you've run out quotes just nil the lastSeen column on the Quotes table to reset it all or just start over by searching for the oldest lastSeen.

2 Likes

Thank you for your advice.

Currently, I’m using SwiftData, but I’m not satisfied with its stability and performance, so I’ll switch to Supabase.

On Supabase, I want to configure it so that everyone has read permissions but no write permissions. What are your thoughts on how I can achieve this? I apologize for not mentioning this earlier.

Also, do you think it’s a good idea to create a Day object for every day and then edit it throughout the day?

I assume you're talking about using RLS in Supabase, and authenticating users so they have an identifier to track their data with.

For the quotes table you'd just have an RLS policy that always returns false. That'll allow you to update it on your admin side but no one else would be able to write or update that table. Rows in the day table would still be able to reference it, similar to what I mentioned earlier.

I'd also assume you still want users to be able to update their own Day data. Since you're using PostgreSQL, yes, keeping one record per day is a sensible option. Just make your Day table's primary key a combination of the (user_id and date columns) so you don't have duplicate rows per day per user.

1 Like

In this case, if I can only edit things in the quotes table, how could you imagine that the last seen property would work?

Is there a way to prefill the id with (user_id and date columns)?

You'd have to do a table relating user data to the quotes table like

CREATE TABLE (
    quote_id INT NOT NULL,
    user_id UUID NOT NULL REFERENCES users(id),
    day DATE NOT NULL
);
1 Like

So in this case the user_id would be the primary key?

Would you remove the standard id property in the table?

The usual way to do something like this is to shuffle the records, then pick them one by one in shuffled order. Once all have been picked shuffle again and start over. It's like picking cards from a deck of cards. It's impossible to pick the same Quote twice in a row until you reshuffle. If you can do this in-memory it's quite simple.

I'm not a db expert but I believe there's a way to shuffle rows in a column. Assign the shuffledIndex to each row and save your currentIndex separately. When you need a new Quote it's at currentIndex + 1.

1 Like

You can use a Linear-feedback shift register - Wikipedia to generate a sequence of distinct integers with a long period and use them to look up records by index in your quote database.

4 Likes

The primary key would be all three columns

PRIMARY KEY (quote_id, user_id, day)

And yeah I'd keep the id property on the quote table but remove it from this relation table. The reason it's a multi-column PK is because that's how your rows are unique in that table. You only want to store one copy each of the (user/day/quote) combination.

1 Like

This is true, but the other issue he was mentioning was keeping track of what quotes the user has seen. Also because he's using postgres, loading all the quotes into memory is just wasting resources. The way to combine both requirements would be to use a join query.

Postgresql provides this nice built-in, trusted, extension that is very fast for querying random samples: postgresql, benchmark

CREATE EXTENSION tsm_system_rows;

SELECT * FROM (
    (
        SELECT * FROM quotes
        LEFT JOIN (
            SELECT * FROM users_quotes 
            WHERE user_id='USER_ID_HERE') as users_quotes ON users_quotes.quote_id=quotes.id
        WHERE
            users_quotes.quote_id IS NULL -- filter out quotes that have been seen
    ) as filtered_quotes
) TABLESAMPLE SYSTEM_ROWS(1);
1 Like

That’s a great idea. I’m currently planning to build a Vapor Swift backend in addition to just using the Supabase SDK within my app. In this case, I might consider using a Redis database for caching all the indexes. This backend would then access Supabase instead of every client separately. Do you think Redis is an appropriate database for this scenario? Alternatively, would you recommend an alternative database?

Thank you for your insights. Do you know if Swift has already implemented a similar algorithm?

Thank you for your insights. If it’s possible to prevent the same quote from appearing repeatedly like @phoneyDev mentioned, I believe I won’t need to keep track of which quote the user has already seen.

In this case, I would build a Vapor Swift backend to save the indexes. However, I’m not sure if Redis is the best choice. What are your thoughts?

Redis will be very expensive for that task.

1 Like

In this case which db would you suggest?

I'd still say a SQL database like PostgreSQL is your best bet. A memory database like Redis is usually for short-lived data like a cache. What you're looking for is for users to be able to save information over the course of days and weeks, which is what a persistent SQL database is good at.

1 Like

another choice is a schema-less database like MongoDB which has a lot of users and a really great data replication feature set.

1 Like

Even if I only intend to store the index of the currently shuffled Quote array?

Do you know if MongoDB is cheaper than Redis.