You might as well timestamp it (2021)
changelog.com
changelog.com
For example, let's start with an object, which has a boolean flag "active".
boolean is_active;
We will always want to get a bit more advanced, and get into sub-states as it is brought up. We can't do that with a boolean. So we go to an enum.
Enum current_state { not_configured; being_provisioned; active; disabled; deleted; }
current_state state;
We have an enum in hiding. Converting the boolean to a timestamp stops the conversion to an enum - we can't overload the presence of a timestamp into the multiple values in the enum.
The timestamp is "what happened, when", an audit log/change history function. That should be kept separate from the rest of the database.
Sometimes, there is a business reason to track a timestamp - charge the customer 30days after the item became "active". How to model that depends on your database and what is required - history table, convert the enum to struct, time_became_active, time_of_last_state_change, etc.
A change history is a better solution, which would track:
* transaction requestor (user/job/etc)
* transaction timestamp
* diff encoding before/after state
Then if someone asks "why were these customer's disabled", we can answer "the XYZ cronjob went rabid and tore them down." and then hopefully use the diffs to reverse the transactions.
Where is that change history stored? How much data are the diffs generating? Are we deriving the final state from the log, or can the log the and record disagree?
Basically - this is now back in the "What is this history buying us?" realm, not in the "easy win" realm.
In some cases it can be absolutely worth it. But probably not all.
If you're currently using a boolean, switch it to a timestamp. There are no real world downsides.
For your example, however, I'd be tempted to have an enum as you suggest and associated history/audit table that has timestamps, mainly because there's no sense in setting timestamps into the future for the first two enum values. Still, I'd keep a timestamp for inactivation for the reason I gave above.
What should the state be vs what is the current state vs when should the state change.
For expiration it's essential that it be able to happen at the scheduled time (when it is scheduled anyways) without having to execute a transaction at that time.
A digital rental works as a pure time stamp in a DB, a car rental however can’t be rented out to the next person simply because the last persons rental period ended as you might not physically have the car etc.
It doesn't mean you shouldn't have a an expiration field.
It’s very tempting to tie various bits of business logic directly to that specific time stamp, but doing so is almost always a mistake instead you might want say a report for in_customers_hands and over 4 hours late or whatever.
The system directly doing stuff based on time stamps is a great way to get sued from regular data entry errors. One such case recently cost Hertz a $168 million payout and a lot of bad publicity.
*Of course it should also be calculated rather than being a numeric value in a table somewhere, but that’s another story.
Immediately should one think about a proper tool to handle it, which usually is a finite state machine. FSMs are somehow underappreciated outside of comm protocols and GUIs. They are an adequate tool for quite a few "backend" or "data representation" tasks though.
What the original post suggests is to timestamp the latest state transition. The approach is trivially obvious if you explicitly track state transitions as a part of the logic of your FSM.
Tracking FSM transitions is the change history/transaction log, isn't it?
It would be:
{timestamp, old_state, event, new_state}
If we convert booleans to timestamps for the FSM, I end up with:
timestamp not_configured;
timestamp being_provisioned;
timestamp active;
timestamp disabled;
timestamp deleted;
Which would cause confusion when there are multiple FSMs in the same database record.
We could isolate each FSM into its own column.
It still makes it kind-of hard to decide what's the current state. The set of timestamps needs to be considered and sorted, going from field to {field_name, timestamp} and then sorting by timestamp and extracting the max. It would make "select count(*) from customers where current_state = 'active'" brittle - the addition of a new state would require all existing queries to change.
What's the motivation for having an enum then holding in some other system a history of when that enum changed?
If you can't add a separate field, just add a log table.
Don't optimize for fictional requirements that haven't surfaced, and don't add shortcuts.
EDIT: Let me clarify my point here. A timestamp is just a timestamp. Giving a timestamp a role on signifying the state of an entity is a semantic shortcut, and semantic shortcuts lead to problems. I believe that DB structures should be as clear as possible. For example, it's impossible to know whether a timestamp is the authority on an entity's state by looking at a table structure. But a boolean field like "is_deleted" or "deleted" doesn't have that problem.
An audit log table isn't a bad idea, though. You can capture rich entity edit histories, attribution, etc.
You can derive a boolean "is it deleted" flag from that without adding another column to (mis)manage.
If you need a full record of state changes then just upgrade to an audit log, otherwise a nullable timestamp field is perfectly fine.
A NULL `deleted_at` timestamp implies that it hasn't been deleted. That's far more simple and logical then 2 fields possibly conflicting or having an "optional" timestamp.
I don't know how much of a performance improvement I'd get if I had a separate indexed bool column for tracking just the truthiness, but I do know that it probably wouldn't be enough to matter, even if SQL was a meaningful bottleneck.
I really enjoy working with these as nullable DateTime in .NET as well:
if (!record.CompletedUtc.HasValue) { /* completion logic */ }
One source of truth also means only 1 thing to update each time. It makes many kinds of problems impossible.On the other hand, on postgres & maybe others, one could perhaps index an expression deleted_at is not null, which would let one have the data shape they want while letting the index shape suit the typical access pattern.
If you start generating too much data, add a cron job to purge older items or scale back the logging.
An example from our e-commerce platform, the "order/cart" tables have corresponding log table that log all changes, user interactions, and all requests/responses from Stripe during the checkout process.
It's super useful both for tracking down bugs, but also confirming what happed when customers contact support.
1. What happens when the record is "un-deleted?" Now the timestamp is null again and history has been lost. It's not the end of the world but if you're asked to create an account history report it's an awkward conversation. "I thought we tracked when records were deleted..."
2. Similar to the end of the last problem, it creates an expectation that the times that changes are made are tracked but it doesn't quite live up to that expectation. I've learned that business people love a feature that works well (like a log table), they can deal with a feature not existing at all (like a boolean and no logging), but a half-working feature causes a lot of suffering.
Just do logging right from the start if you are writing business software where the data and transaction volume are small compared to the value of the data.
This is just a matter of convention—if your codebase is consistent about it and there's no other option for "is this record deleted?", then yes it will be absolutely clear that "deleted_at" is the right column to use.
What's actually worse for making the database structure clear is to have "vestigial" columns that store old data, like you proposed for "deleted" and "deleted_at". Now, if they disagree, which one should you trust? Maybe some code is using one, while some code is using the other. Having a single source-of-truth column—no matter what the name is—is always going to be better then having two columns that could get out of sync or disagree.
anyways i prefer to have nullable timeshamp columns instead of two like you suggested.
You might as well timestamp it - https://news.ycombinator.com/item?id=26922759 - April 2021 (198 comments)
I can also use the timestamp later if I want to move deleted records to a different table or database to improve performance depending on requirements.
Pagni's?
Go further and create an integer `version` vector clock that gets set to its value plus one. You can see the edit frequency of individual entities and construct a simple key for cache invalidation.
Whether it's in a programming language or in a database... what is false? Is it false or null or 1970-01-01 00:00:00 UTC? What if different parts of the program/table use different versions of false? What happens when I combine two truthy values with a boolean operator -- for all combinations of values, what is the resulting type and is it the value even correct? (E.g. in SQL, NULL XOR 1672254005 is NULL, which is flat-out wrong if you're using NULL in place of FALSE, and it's easy to think of further examples.)
There's so much research I'd have to do to determine that this would always be bug-free for a particular language/platform, and then I'd need to remember which languages/platforms it is safe for and which ones aren't.
If your language has a tough or flexible time with falsy, I found your problem. You shouldn't even be in that headspace, and languages that put you there introduce so much mental overhead that they're not worth it.
You shouldn't need to do research here. This should be a one and done solution from your toolbox that you can pull out at a moment's notice: "How do I handle record soft deletion and store some state around when it happened?" - zero effort, tried and true recipe borne of experience.
The database and data stores drive your product. They're the heart and soul.
If you let the SQL query predicates handle which records to select, I'm not sure I see the problem you originally outlined. Why do you care what value the timestamp has when the question is whether or not a record experienced an event.
But it's not really that bad in the worst case. Failing docs or the ability to read your db connector's source code enough to ascertain, you can always just test. Both in the sense of, try it and see what happens, and also, writing automated tests that ensure you're getting the expected behavior. If your line of logic is that hope is not a strategy, I agree. However, there are alternatives to hope that don't involve vacating best practices out of fear of hitting edge cases, and you probably want those automated tests anyways.
- True
- False
- Null/Nil/unset/error
Depending on how the application handle the third case it can be more than 3 return values.
timestamp require ad-hoc and potentially breakable workaround to emulate this behavior.
It's unclear what general problem it solves. If the database row is actually corrupted, I want the query to fail violently, and the database to fail all operations around it to avoid further corruption. If I really need to handle it, I'll handle it in my app layer.
If it's not, I don't want it to be set to some invalid state. I don't want every piece of code to need to deal with this invalid state that shouldn't be allowed to begin with. I'd rather gate it at the door and stop it from getting in there in the first place.
If you use a nullable timestamp as a boolean, this is fine; there's only two states w.r.t. set or not: NULL or not NULL, from the SQL perspective. How this is encoded in your language and tools of choice depends. If there's some database value not encodable to your language's native types, the db connector will probably decide what to do: truncate data, fail the whole query, use a type with additional entropy (like Go's NullString, for example.) In no way do I want there to be an error state for that field alone. If I did, though, for example because I need to handle partial data as an application concern, I'd want union typing. In that case you could have Error/Unset/Timestamp just fine. I don't believe SQL has this sort of union type natively, but it can be represented in many programming languages intuitively (like unsafely as a union in C, safely as an enum in Rust, safely as a discriminated union in TypeScript, and mostly safely in Go abusing interfaces.) However, that is only something I want to do if my application specifically needs it. Otherwise? Data that goes into the database must meet the constraints first.
Usually it's used with the following logic for database oriented workflow.
True = true
False = false
Null = unknow (This which might be a real-world case that you can model using null)
Of course if there is an error the engine will thrown an exception Which is a whole other kind of signal.
When using Go and sqlc, what would happen is I would use a NOT NULL field for the boolean, because I'm modelling a boolean, not a tri-state. If I was modelling something where I had three states I wanted to handle, I would use an enum instead. But even if I wanted to use NULL instead of an enum that explicitly specifies the meaning of the third state, I'd rather start as NOT NULL. Why? Because then, if I ever want to remove the constraint, I can. And when I do, sqlc will helpfully change the type of the field from a raw boolean to a nullable boolean wrapper. And doing this will break the existing code. This is good. Why? Because in every place where I assumed there were only two possible states, I can now check and see what I'd want to do in my new third case. This is better than starting with nullability, because if I don't know what NULL means or why I'd have it, it's really difficult to know what my code should do when it encounters it. You could always throw errors, but if you do that you have to ensure you clean all of that up before you start actually using NULL.
This is fundamentally about data modelling though. Talking in this abstract sense is not useful. What I feel you're failing to make an adequate case for is why a field like the one described in the linked article needs a state other than "set" or "unset". Just putting a nullable timestamp is a nearly free upgrade from putting a non nullable boolean, which is what I would've done anyways.
If I want to add more states, I want explicit data model changes, that result in explicit test and type checking errors, that highlight wherever I need to update my code's state management to account for new states.
Additional note: particular to SQL, removing not NULL constraints is basically free from the database end as far as I can tell. In addition, NULL has specific behaviors, especially w.r.t. JOINs, so I would NOT want to use NULL to model a tri-state.
I guess it is a valid take on the specific use case of publication date he mention. The problem is generalizing from that.
For user inputed data you might NOT want to coerce a default state when the user genuinely can't answer otherwise you might collect garbage because i can guarantee that the user will input random answer to unblock itself.
Of course you can define enum, but the NULL value already cover 80% of use case when used properly (as far as database are concerned).
NOT null might be a useful DB constraint (especially for ids participating columns). But you just have to read PostgreSQL release notes to notice that null handling is actually a hot topic which always call for more fine tuned settings to accommodate different workflow. People use theses features a lot. See : https://news.ycombinator.com/item?id=32053293
As far as JOIN are concerned bad NULL handling is almost always coming from a bad understanding of OUTER JOIN mechanics.
Anyhow it's an endless debate and I guess we won't be able to settle the NULL Schism tonight! See : https://news.ycombinator.com/item?id=18588239
So maybe let us end with this beautiful quote from last link top comment by @porpoisely
NULL exists to solve particular problems in computer science. It can also cause a lot of problems. You can argue it's the best solution and worst mistake depending on the situation.
Not possible with a timestamp field.
> 00:00:00 UTC
I remember a bug around this in Python regarding times (only) a decade or two ago, it was fixed a long time ago. I assume this is not an issue for modern stacks, but I would double check under say PHP which has a known history. Not normally an issue I'd say.
> What happens when I combine two truthy values with a boolean operator
Type or Runtime error. Who is adding booleans and dates together? Such egregious mistakes don't take long to surface and are impossible if planned against beforehand.
I don't think these are things to be worried about (if you're paying attention even a tiny bit) to be honest.