11 ms·
Implementing State Machines in PostgreSQL
- bluejekyll 9y agoCool example. But, doing this will create a tight coupling between business logic , the state machine, and storage/persistence, Postgres. If ever you decide you want to migrate away from Postgres, you'll need to rewrite all the business logic into some other out of process language. If ever you want to scale this, such that you want to calculate states in batches, you'd want to have the business logic somewhere else...
- ericfrederich 9y agoMy thoughts exactly. I don't do a ton of DB programming, but I've only ever written one thing in a non-agnostic way. We got a requirement that users wanted to copy an entire "project" which was the top level of a hierarchy. I wrote an Oracle routine to do the deep copy. I did it because I imagined what the PL/SQL would look like (clean) vs. what the Java code would look like (considering Hello World in Java is ugly, you can see where I'm going ;-)
- devy 9y agoMine too! Having logic embedded into SQL functions seems to be an anti-pattern to me (it's harder to maintain and harder to do release management). While it's great that Postgres can do this (btw, I love Postgres), I suspect there aren't many people will use this feature in production environment.
- liotier 9y agoYou should see what large Oracle shops in the enterprise segment do with PL/SQL, though whether they have gone too far is open for debate...
- bluejekyll 9y agoYes they have. And then it's full on vendor lock-in.
- _jal 9y agoI used to think this, but have relaxed that view over time. For constraints, validity checking, things like adding/updating timestamps, and other things that are about data integrity, about the only time I don't do that in the DB is when outside information is involved such that it can't be. Otherwise, to the extent possible, I want the datastore to only accept valid data. This goes to the notion that fixing code is easier than fixing data, so the store can not only defend itself, but also help catch bugs. There are also times when dealing with huge amounts of data that doing whatever you're doing in an SP is the only way to get decent performance. Dragging enormous tables over the network to process is sometimes really wasteful. If you're tuning indexes and whatnot against that model, you're already changing the DB, and doing so at a level that is implicit rather than explicit, and in ways that can change out from under you (if the statistics change).
- cat199 9y ago> it's harder to maintain and harder to do release management How is this any different than anything else where DB data & application logic have to be kept in sync? version control + loader/change scripts should handle it, no?
- pilif 9y ago> If ever you decide you want to migrate away from Postgres[…] it's one of the things to keep in mind. Depending on your application, migrating away from postgres will be more or less painful. If you're already deeply invested in postgres features, this is just one more problem to solve. > If ever you want to scale this, such that you want to calculate states in batches, you'd want to have the business logic somewhere else yes. This has the all the usual issues of putting (some) business logic into the database. On the other hand, by using this, you're basically just creating a data integrity constraint similar to a foreign key, just one not as widely supported. Still. If you ever plan to move to a database that doesn't support foreign key constraints, you will have to implement the business logic somewhere else. For me, data integrity is paramount. If ever I can put a data integrity check directly into the database, I will do it because bugs in the application logic can exist and when the database itself enforces integrity constraint, I'm protected from those. I don't want to have to deal with, to stay in the framework of the article, a shipped, but unpaid order. Was this a bug in the application? Did it actually ship? Did the payment fail? If the database blows up on any attempts to store invalid data, I'm protected from having to ask these questions.
- _jal 9y ago> If the database blows up on any attempts to store invalid data, I'm protected from having to ask these questions. I'm always surprised when people fight using constraints. During dev, I see them as a godsend for spotting problems - I don't know how many bugs having self-enforcing data structures has caught. I will say that I have, under protest, turned them off in production once for performance. A particular heavily-used flow was annoying due to a large number of FK checks on an intermediate step. But never during development, and given the number of FK-violation errors I've seen in production code, preferably not even then. Fixing code is almost always so much easier than fixing data.
- jjawssd 9y agoThis implies that a need to migrate away from Postgres will arise. For most companies it is very likely that such a need will never materialize. Meanwhile Postgres keeps getting better and better. https://wiki.postgresql.org/wiki/New_in_postgres_10 https://wiki.postgresql.org/wiki/New_in_postgres_10
- bluejekyll 9y agoIt's not just about migration. There are deployment issues as well. I like to think of each layer in a tiered system as having differing deployment requirements and time tables. Front-end systems will be very frequent, backend systems possibly less so, though not necessarily, and then DBs ideally infrequent and they generally take longer. At a minimum, keeping them decoupled is freeing for patching bugs and releasing features independently. It does raise the bar for keeping all changes compatible with existing systems.
- aeorgnoieang 9y agoThere are good deployment tools for databases, e.g. ones for which all of the database 'code' objects (or just all of the objects period) are maintained in source control and updates are either automatic or scripted. Of course updating a database is much harder than overwriting executable or library files, but a lot of the objects in a database should be safely updatable by simply dropping and recreating them. The tricky changes are of course things like, e.g. splitting one column into two or merging two columns in two tables into one. But those changes are even harder to do the 'dumber' your database is, i.e. the less logic there is in it that enforces a certain level of quality in its data.
- agentultra 9y agoA great one is http://sqitch.org http://sqitch.org
- aeorgnoieang 9y agoYeah, I've seen that before and it looks promising. My favorite, and the only one I've used extensively, is [DB Ghost](http://www.dbghost.com/ http://www.dbghost.com/). What I like about it compared to all others I've run across is that it, by default, will automatically sync your target DB (e.g. a production DB) with a model source DB that it also builds automatically. So instead of scripting out every schema change as explicit SQL statements and queries you just maintain the scripts to build the model source DB, e.g. to add a column to a table, instead of creating a SQL script file to `ALTER TABLE Foo ADD COLUMN Bar ...` you just update the existing SQL script file with the `CREATE TABLE Foo ...` statement. When you deploy changes – 'sync' a target DB in the DB Ghost terminology – it automatically detects differences and modifies the target to match the source. The benefit being that neither you nor the DB Ghost program needs to explicitly perform every single migration since the beginning of time. Only changes that need to be explicitly handled as migrations, but, with a little customization, doing that is pretty easy too. The bar now for me working with databases is whether I can create a new 'empty' database (with test data) in a minute or two and whether I can automatically deploy changes (or, as I'm doing now, generate a deployment script automatically). Given that, I can actually do something like TDD for databases, which is really nice, especially if there's significant business logic in the database (which there almost always is in my experience to-date).
- jsjohnst 9y agoI hear the argument about switching storage layers a lot, and I don't completely disagree with it, but in my 20+ years writing code, I've never found a case where switching storage engines didn't cause a massive rewrite even when the storage layer was used in an agnostic way. I'm sure there are examples where it has worked, but saying it as if it's a maxim just feels wrong to me.
- mercer 9y agoCould you give some specifics as to what kind of stuff typically needs refactoring if you switch storage layers?
- edoceo 9y agoStored Procedures, any use of the RDBMS SQL extensions (eg: BETWEEN)
- jsjohnst 9y agoI partly answered your question in a comment above. If you'd like me to go more in depth, happy too!
- dspillett 9y agoEven if you stick to the SQL standard as much as possible engine specific syntax almost always creeps in unless you routinely test against all target systems from the start (which you might not be able to justify the time to do on many projects where being platform agnostic is not initially a core high-priority requirement). And even where there are no feature or syntax issues there may well be optimisation differences. For instance going from MSSQL to postgres you might hit a significant difference with CTEs because MSSQL can perform predicate optimisations through them but postgres doesn't - this might mean core queries need to be refactored significantly for performance reasons (to avoid extra index or table scans) if not functional ones. (not intending to pick on postgres here, I'm sure there are similar examples in the other direction and between other engines, but this is the most significant example that immediately springs to mind).
- bluejekyll 9y ago
- felixge 9y agoAuthor here. > If ever you decide you want to migrate away from Postgres, you'll need to rewrite all the business logic into some other out of process language. Yeah, but I don't consider this a bad thing. IMO migrating between databases should always require a lot of rewriting. If it doesn't, you're most likely underutilizing the features provided by your database. > If ever you want to scale this, such that you want to calculate states in batches, you'd want to have the business logic somewhere else... I'm not sure I follow. The application I'm using this in has > 1 billion rows and we frequently re-compute the state of all our entities across the entire data set in batches after we make changes to our logic. Having the code that does this in the database avoids having to move large amounts of data between our db and the application.
- bluejekyll 9y ago> IMO migrating between databases should always require a lot of rewriting. If it doesn't, you're most likely underutilizing the features provided by your database. Very likely true. But when you get locked into Oracle and it makes it nearly impossible to move because of the features your relying on, has a big effect on shaping your perspective on this. > The application I'm using this in has > 1 billion rows and we frequently re-compute the state of all our entities across the entire data set in batches after we make changes to our logic. That is very impressive. Mind sharing the rate of change across all of those rows? Also, I'm curious. Is this all that DB instance does? Or is it responsible for other data as well?
- cwyers 9y agoThe only thing worse than paying through the nose for Oracle is paying through the nose for Oracle and treating it like a really expensive version of MySQL/Postgres. If you're paying for it, might as well get some value for your money and use those features.
- felixge 9y ago> But when you get locked into Oracle and it makes it nearly impossible to move because of the features your relying on, has a big effect on shaping your perspective on this. I hear you. Picking a database is major decision and shouldn't be made lightly. That being said, I hear a lot of companies are successfully migrating from Oracle to Postgres these days. And in fact, this application was actually migrated from Couchbase to Postgres, but that's a story for another day perhaps :). > That is very impressive. Mind sharing the rate of change across all of those rows? Nowadays its about 5 million inserts / day with peak rates of 150 inserts / sec. Not too crazy, but it adds up over time :). > Also, I'm curious. Is this all that DB instance does? Or is it responsible for other data as well? It does a few other things, but this is the main workload.
- bamdadd 9y agoI agree but have you read BoiledCarrot from Martin Fowler? https://martinfowler.com/bliki/BoiledCarrot.html https://martinfowler.com/bliki/BoiledCarrot.html
- oelmekki 9y agoActually, I'm under the impression it's way more common to migrate language or framework than data storage. I externalize most of my business logic to postgres for that reason (plus, it's incredibly performant, especially since you don't keep requesting connections from connection pool for each single part of the computation). My backend app handles request sanitizing, providing endpoints, etc. All data processing is made in the database. Love it.
- gaius 9y agoIf ever you decide you want to migrate away from Postgres, you'll need to rewrite all the business logic into some other out of process languag You'll rewrite your front end 20 times in 20 different "frameworks", and your app 5 times in 5 different languages, for everytime you actually change databases
- ris 9y ago> If ever you decide you want to migrate away from Postgres, you'll need to rewrite all the business logic into some other out of process language. Of you follow this rule you'll never be able to use the most powerful features of postgres. > If ever you want to scale this, such that you want to calculate states in batches, you'd want to have the business logic somewhere else... Not due I'm totally on board with the premise here, but the need to calculate state outside the database is a generally valid one and so it might be a good idea to write the core of the transition function in JavaScript and implement the postgres functions in PLv8.
- greggyb 9y agoIf you ever decide you want to migrate away from Postgres, it's because there's another platform that supports significant functionality that Postgres doesn't. Changing out the data tier is not a decision made on a whim. If there is significant functionality we want to take advantage of in this new platform, then that means that we have to refactor anyway to use this, because by definition we either don't do it today, or we do it with an insufficient workaround in Postgres. The migration necessitates large refactoring by its very justification.
- zitterbewegung 9y agoSince FSMs seem to make sense in some cases while implementing logic in both the server / database / client it would be interesting to create a language that would be a DSL which outputs transducers (Finite State Transducers) for each part. Using ideas from Functional Reactive Programming would be useful. Adding a schema for data within the transducers would be helpful. You would end up with something like react where logic would be shared and scaffolding could be created after making the server. This could possibly be augmented by CQRS and or Event Sourcing. I think functional programming would help (Clojure or F# would be my choices for implementation). This would also help with vendor lock in.
- jononor 9y agoThis was basically the background for http://github.com/jonnor/finito http://github.com/jonnor/finito - still experimental.
- staticassertion 9y agoCheck out P?
- deleted 9y ago[deleted]
- zmonx 9y agoThank you for sharing! My impression is that database systems are increasingly gravitating towards Prolog, with various extensions such as logical rules, more expressive aggregation, state machines, constraints, Turing completeness, ... All these sound very familiar to Prolog programmers. Only recently, there was a post on GRAKN.AI which seemed heavily inspired by Prolog. This is good news for Prolog: Modern Prolog systems provide many features that are important in the domain of databases, such as JIT indexing, transactions, and dedicated mechanisms for semantic data.
- harperlee 9y agoCan you recommend a good in depth introduction to "productive" (in contrast to a more academic approach) modern prolog for someone very superficially familiar to it (think uni course, long time ago)? I heard that e.g. Constraint Logic Programming functionalities makes some old approaches obsolete, and thus going through old material as starting point is very ineffective.
- zmonx 9y agoPlease see my profile page: It contains several links to material that I recommend for learning modern Prolog. You can quite often apply Prolog in actual practice if you know it. For instance, see how often "logic" is mentioned just in the context of the present discussion. It's nice to know a logic programming language that can elegantly express business logic and business rules!
- justQuestion2 9y agoI found it hard to use entities with more than 4 Attributes when defining them as simple compounds like person(Firstname, Lastname, Gender, Age, State, Country). Accessing the attributes by position makes the code hard to read because one has to remember the position and mistakes are not catched because there is no type system. Would you recommend using the libray record or using dicts in swi-prolog? This would make the code non portable. Moreover, do you recommend dedicated strings or list of atoms to represent strings?
- deleted 9y ago[deleted]
- ysleepy 9y agoCool trick. Should be an enum instead of text though. Also the transition table might be an actual SQL table as well instead of switch-cases in a function. - That way it remains a bit more declarative and you can do some meta-queries.
- aidos 9y agoI disagree about enum. I've tried using it but I found that it's too hard to manage/migrate in postgres. Obviously there are a few different ways of achieving the enumish behaviour in postgres (or other dbs) — nowadays I just start with text with a constraint and upgrade to a real table if I need more detail. YMMV but I don't think a blanket "just use enum" is the correct approach.
- emidln 9y agoThis is too hard to put into a sql file and run with your migrations tool? BEGIN; ALTER TYPE my_enum ADD VALUE 'bar' AFTER 'foo'; COMMIT;
- aidos 9y agoFrom the docs (as I suspect you already know): ALTER TYPE ... ADD VALUE (the form that adds a new value to an enum type) cannot be executed inside a transaction block Also, removing a value is a pain (though, that may have changed now). I know you can do it — but I've found after trying a bunch of approaches that text with constraints is a better starting point. Just wanted to point it out because it's something I researched a fair amount. Many guides say that enum can be a pain but I decided that I'd use them to be pure, then discovered they were a pain, and now don't use them as a starting point.
- felixge 9y agoYeah, this doesn't actually work. You'll get this error: ERROR: ALTER TYPE ... ADD cannot run inside a transaction block
- 9y ago
- mrkgnao 9y agoFor people interested in Prolog-like DB systems: https://github.com/agentm/project-m36 https://github.com/agentm/project-m36 > Unlike most database management systems (DBMS), Project:M36 is opinionated software which adheres strictly to the mathematics of the relational algebra. The purpose of this adherence is to prove that software which implements mathematically-sound design principles reaps benefits in the form of code clarity, consistency, performance, and future-proofing.
- felixge 9y agoAuthor here: If the article above has you excited, come and join my team at Apple. We're hiring Go and PostgreSQL developers in Shanghai, China right now. Relocation is possible, just send me an e-mail to find out more about this role. My e-mail is in my profile.
- mixmastamyk 9y agoHow is living in Shanghai?
- felixge 9y agoGood question. I'm working remotely myself and can't relocate easily because my wife is a doctor and it's very difficult for her to move between medical systems. I was lucky to join this team when this wasn't considered a problem. That being said, I'm in Shanghai frequently to sync with my colleagues and it's an amazing city. I've found pretty much most conveniences available to me in Berlin and to some extend even better. There is an active expat community, and a fascinating vibe. My manager (from SF, USA) has been there for over a year now and has enjoyed the experience a lot and extended his stay. YMMV, but I'm fairly well traveled and would say that it's a pretty great spot if you're willing to emerge in a foreign language and culture.
- dizzystar 9y agoI've built inventory systems in a similar fashion. On one hand, using a bunch of PL/pgSQL makes sense for transaction isolation and faster execution. I'm not sure how much of the arguments about business logic matter in this case, but a strong argument for using PL/pgSQL is that these queries written in Python (for example) are going to be significantly slower, and that really matters when there are 100 concurrent users hitting the database every 10 seconds. No one likes waiting for their system to update... they'll just switch over to Excel. I think that PL/pgSQL is a great use-case for situations where preoptimizing for speed isn't a mistake, though the trade off is that you need to find the rare expert on processing languages (what order to triggers fire and how do you prevent this from being an issue?), and who can work through the complex logic of the system. I wonder why the author didn't use windowing instead the lateral example. You can window and sum over composite rows, and it goes pretty fast. I never did a direct comparison between lateral and windowing, but windowing would be much cleaner and not require you to generate a series.
- felixge 9y agoDespite being pretty familiar with window functions, I couldn't figure out how to do it for that example in the time I was willing to spend on this post :). If you have a better query, I'm happy to update the article and give credit.
- zac123 9y agoI have done very similar things in code rather in SQL. just a bit of curious, I understood it is not possible many to many connections, but why don't you allow from cancel->started again? The business logic is always tend to be changed. I think this is a bad example using FSM here. IMHO, I will only want to apply stable, constant and (code)internal FSM to pgSQL. (Thought about a joke how to kill a programmer just needs change the requirements three times. lol) Good article anyway.
- cr0sh 9y agoI love seeing SQL being "abused" in this manner. It's almost as crazy as the Excel spreadsheet I found to simulate a neural network. I've always loved seeing how far you could push SQL and databases to do things that (on the surface) would seem extremely difficult or impossible. What it usually turns out to be is possible - but in the long-term unmaintainable. This FSM is not that - not yet (if it gained even more states, it could get there). But I have seen (and I have unfortunately written) queries in SQL that could make your hair stand up. Insane extreme monstrosities that I am both proud and ashamed of (fortunately, I don't work for that company any longer). On a different but related note, I do recall one query that a friend of mine wrote to allow the querying of a zip code database (which had lat/lon columns for each zip code), to be able to calculate distances from a given address - using SQL. It was a simple form of geo lookup he had to do for a particular project. I later used the same code as a part of a lookup process for placing markers on a google map (markers would indicate whatever was needed for "show me locations near my address within N miles"). It was a pretty interesting piece of SQL for the reason of doing the specialized distance calculation (can't remember which calc, but one of the simplest ones that didn't take into account certain things about the earth's "roundness" to make things super-accurate - that wasn't needed for short distances and purpose).
- felixge 9y agoHah, I hear you :). Pushing this much logic into the DB can feel weird at times. And the application this is part of definitely has a lot of very complicated and large SQL queries. That being said, I think things can be somewhat tamed by applying the same best practices to your SQL as you apply to your regular code: I.e. lots of tests, good comments and documentation, extracting logic into either set returning functions or functions suitable for lateral joins (both can be inlined if done right [1]), keeping things in version control, etc. But yes, applying those practices can be harder in SQL than it is in your application layer language for various reasons. So I'll always recommend avoiding going too crazy. You can often get the best of both worlds by making pragmatic choices about what should be done at which layer. No need to enslave yourself to a false dichotomy. [1]: https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions
- cryptonector 9y agoI do this sort of thing, and I highly recommend it, especially with PG. Another thing I like to do is to use queries against the pg_* tables to generate code/metadata for components of the application running outside SQL.
- cryptonector 9y agoAnother useful thing to do is to take advantage of record types. PG SQL is approaching something that one might call higher-order SQL. When you can query the SQL schema using SQL queries (though I admit that the pg_* tables kinda suck) and when you have things like record types, you really do have a very powerful SQL.
- kingdomcome50 9y agoOh no... I commend your effort, but this is totally ill-conceived. What's even more disheartening is the number of people who looked at this and also thought it was a good idea. There is just so much wrong here... let's start with the basics. If this is truly a FSM then why on earth are you using a transaction table (state is a value)? An accumulating snapshot table (state is a field), besides being EXACTLY for this purpose, will be far more efficient in almost every way. Your "analytic" queries would be simple select statements, and your trigger (shudder) could be reduced to simple constraints. The sheer amount of over-engineering here is staggering. Lastly, what is the purpose of putting the transition logic in the database? It's simply redundant upon actual implementation. Somewhere, somehow, another program has to actually carry out your actions (paying/shipping/cancelling) on the order and make a call to your database with the appropriate event inputs. So why not just put the entire state machine with the rest of your business logic? As many have pointed out, this is where it should be anyway.
- felixge 9y agoTBH I'm not sure if I follow your argument. You can certainly apply the main idea of this article (modeling a FSM as a user defined aggregate) to automatically materialize the latest state of each order. In fact, that's what we're doing in the app we're using this technique in. Anyway, YMMV and I'm not recommending to apply the ideas in this article in every situation. But after having had all of this logic in the application layer before migrating to Postgres, I find this approach more maintainable.
- kingdomcome50 9y agoWhat I'm getting at is that the author went ahead and implemented a bad solution to a simple problem because it seemed "cool". This is a great example of what NOT to do. A simple accumulating snapshot table with a bit field along with a timestamp for each state and some simple constraints would do the exact same thing more efficiently. Furthermore, I'm challenging the utility of putting the transaction logic in the database. It doesn't make sense other than to seem elegant. His application needs to actually respond to the events so it's just silly to separate the transitions. This is a complete redo.
- moojah 9y ago".. come and join my team at Apple. We're hiring Go and PostgreSQL developers in Shanghai, China.." Woot? Apple uses Go?
- felixge 9y agoYeah :).
- omi 9y agoThis just doesn't belong in the database.
- cryptonector 9y agoAnother interesting idea is to implement applications in SQLite3 (all local). IIRC libgda (GNOME GDA) uses SQLite3 to drive LDAP and other protocols. This will get much easier to do now with the new "pointer passing interface" in SQLite3 3.20.0 (http://sqlite.org/bindptr.html http://sqlite.org/bindptr.html). I'm not recommend this, nor against this. Just observing that it's a legitimate design pattern.
- ris 9y agoThis is cool however I think it may exhibit performance problems when used in the wild if the trigger constraint were to be used. (Could possibly gain some performance replacing a UNION with UNION ALL). Also your colleagues may curse you down the line when they need to change the state machine's behaviour. Would of course be possible to keep note of a "state machine version number" on each row but you would end up with a transition function that had to know about every historic version of the state machine...