Monday, August 24, 2020

What does Postgres do better than MySQL?

Here's a bit of an essay I wrote, originally on Reddit, on what does Postgres do that MySQL doesn't? It's not comprehensive - just some of the features and issues I've encountered that I tried to pour out into that post.

I tried to give MySQL a few props for what they've done better but sometimes it felt like stretching, and most of it is just speculation (like, I've HEARD that managing its replication is easier, but I haven't tried it personally...) Some of my criticisms are outdated, so I've tried to edit and update as needed. 

Anyway, here it is:

I could probably rant for hours on this subject. My company uses MySQL, and while I love the place, I miss Postgres (and other DBs). I feel like I'm about 15-20 years backwards w/ MySQL. Some major points:

  • MySQL lags behind by FAR in keeping up with the times. MySQL 8.0 (released in 2018) addresses some major points, for example the standard features Window Functions (from SQL:2003) and CTEs (from SQL:99) which are incredibly useful. Just read that again. It took them until 2018 to release very useful (not useless junk), standard features that are from 2003 and 1999, respectively. That said, you might ask, well if they've caught up now then what's the problem? The problem is that it's a repeated trend I've seen since the early 2000s (MySQL devs were reluctant to implement features we take for granted now like transactions.) Furthermore, discussions like this lead me to believe I shouldn't have faith in the correctness of newer MySQL features. Furthermore they actually haven't caught up. There are some great SQL:92(!) (3 decades old!) features such as deferrable constraints, available in most major DBMSs that MySQL still lacks.
  • Generally, when MySQL does catch up with a feature that Postgres has had for years, it has major limitations. Examples: recursive CTEs do not have breadth-first or depth-first options as Postgres does, and does not have built-in cycle detection to avoid infinite loops (the MySQL blog does show you how to hack together your own solution, at least). Another example is you can't use user-defined stored functions in a MySQL functional index (called expression indexes in Postgres-ese). Once in a while, MySQL does win in this area - i.e. Postgres has the more limited feature - but that is far less frequent in my experience.
  • MySQL has a less flexible and less smart query planner.
  • Postgres has better explain/query plan analyzing features. For example, Postgres lets me view a "before the query is executed" plan estimate, as well as an "after execution" plan. The latter gives me detailed info of how the query actually ran, how long each specific step in the query took, indexes used, and how much memory each step consumed. E.g., I see three joins and a where clause: among those, the WHERE filter was quick and so were two of the joins, but one join was slow. This allows me to focus on the specific pain point. MySQL's EXPLAIN tool only gives me a "before" (much less valuable IMO) plan estimate, and the info it provides can be cryptic and shallow. So when I'm tuning MySQL queries, there's more experimentation and guesswork. (Edit: MySQL does have EXPLAIN ANALYZE now, as of 8.0.18 released on Oct. 2019; Postgres apparently introduced it in PG 7.2, released way back in 2002)
  • Transactional DDL is a huge advantage of PG, though this is more of a plus of PG than a negative of MySQL, as many other DBMSs lack this as well. I can create a table, insert some data, create an index, alter the table, roll it all back atomically if I want, or back to a savepoint in the middle. Flexible and it makes schema/data change management much cleaner (in the context of tools like Liquibase, or plain sql scripts). In MySQL, each DDL statement (such as that alter table) forces a commit.
  • MySQL has arbitrary index key limits - 767 bytes in that row format, 3k in this one, etc., that can be a pain sometimes. (Edit: I learned PG does have this too; but when using a row format in MySQL that had the low limit of 767 bytes, I found myself hitting that too often.)
  • PG has more indexing options: expression indexes, partial indexes, BRIN, GIST, etc. Without getting into specifics, each is useful wrt different kinds of data sets and kinds of queries. (Edit: I since learned that MySQL 8 does have something similar to expression indexes, i.e. functional indexes, but they have more limitations.)
  • Postgres lets you use heap tables, MySQL does not (assuming InnoDB storage engine). <TODO: add info>
  • Postgres has nice array handling. So you can store array types such as an array of ints or an array of varchars in your table. There are also various array functions and operators to read the arrays, manipulate them, and so on.
  • Postgres has materialized views, albeit limited ones. These basically let you cache query results and refresh them on demand. Great for reports, summary tables and such. Not as advanced as the MVs Oracle offers, but better than the nothing that MySQL offers.
  • MySQL has wacky settings like STRICT_MODE (and others) that deal with default behaviors, many designed to sweep errors under the rug. Auto-conversions of numeric types to strings and vice versa - doesn't sound so bad but can result in bad data and really weird stuff happening, like this that I once wrote about: my old blog While they can often be disabled, in my experience, few devs ever do so. Other weird behaviors include missing numbers and dates being generated as 0 or 0000-00-00 (I forget the exact circumstances), and others. Special meanings assigned to 0.. Headache inducing. :)
  • For nearly every advanced feature in MySQL, you dig into it and it has a bunch of weird, arbitrary limitations and quirks. It feels like corners were cut to rush a MVP feature out the door. Postgres - things generally work cleanly and completely; limitations do, of course exist, but silly limitations are far less common. They're usually sensible. <TODO: examples>
  • UTF-8. Part of my job is to review my coworkers' DDL changes. Sometimes they create new VARCHAR columns with the character set "utf8". Unfortunately, "utf8" in MySQL is not standard UTF-8 - it is their now-deprecated, broken first attempt at UTF-8. The newer encoding, "utf8mb4" is the right one. So I have to advise devs repeatedly "No, utf8 is not actually UTF-8, utf8mb4 is. Use that instead." There is an overarching issue here, too. Generally, PG has well thought-out designs, from features down to the naming conventions. I follow the pg-hackers newsgroup, in which the devs discuss all things related to PG development. They would bang on each others' ideas, constructively criticize, ensure solutions are consistent with the PG Way. There's probably some of that in MySQL, but I can't imagine the PG devs making a broken UTF-8 implementation.
  • TEXT types in MySQL come with a slew of baggage, See here. [link no longer works, but a major point is indexing limitations] Your average dev leans towards TEXT when they can't think of a limit for their VARCHAR. (MySQL forces you to specify a max length, such as VARCHAR(50).) In PG, you can define a VARCHAR without a length, or TEXT if you want, with zero baggage. As basic as this point is, I find it to be hugely valuable!
  • MySQL allows non-standard SQL styles that are "not good" IMO, like selecting columns not in the GROUP BY and quoting string literals with either single or double quotes. (The GROUP BY variant can be valid, but often has unexpected side effects that most devs don't realize.) Non-standard backticks around identifiers...
  • MySQL is only starting to implement parallel querying - it supports exactly two, almost useless niche cases e.g. SELECT COUNT(*) FROM my_table(no WHERE clause allowed), while PG's have been solid in many kinds of queries for years. (Parallel querying meaning multiple threads each execute part of your query simultaneously.)
  • Overall PG is cleaner, with better, well-thought-out design, and fewer wonky "gotchas". (how about that MySQL 5.5 and earlier timestamp madness? Oh the headaches that's caused!)
  • Hash joins: an efficient algorithm in some situations used to execute your joins, was only just released in MySQL 8, but to quote the MySQL Blog: "MySQL only supports inner hash join, meaning that anti, semi and outer joins are still executed using block-nested loop." In PG, it works in more situations. Again, a common theme is more limitations with advanced features in MySQL.
  • Postgres supports many procedural languages such as pl/pgsql, pl/java, pl/python. So you can actually write code that strongly resembles Java or Python in your PG stored procedures. Also you can use such procedural code in (see next bullet point)...
  • Anonymous blocks: A stored procedure has to be created and it lives on the database. What if you just want a one-off proc? You could create the proc, execute it, then drop it. Worry about naming collisions, permissions to create and drop a proc, etc. But with an anonymous block: send a "script" over to your database and it runs, without creating any db object. This can be handy.

Those are some major points.

What does MySQL do better? You can tell I'm biased, but I'll do my best to be fair:

  • It was more popular for a while - it may still have an edge in that area though I think the margin has narrowed considerably. Popularity has the advantage of your skillset in the more popular tool being more valuable in the job market, and cloud services and the like are likely to support more popular ones first. E.g., back when I started using Amazon RDS years ago, it supported MySQL but not PG, so I reluctantly chose MySQL for a project. Now it supports both.
  • I've heard, though don't have firsthand experience with this - that MySQL's replication is easier to manage.
  • When you update a row in PG, it is marked as a "dead tuple", a new row is written, and dead tuples are cleaned up asynchronously by a process called the autovacuum daemon. It's like a garbage collector. This is not necessarily bad, and is often good. But if you have an extremely high rate of updates (such as thousands of update transactions - not to be confused with rows updated - on a single table per second), your table might perpetually bloat as the daemon cannot keep up. This can in turn lead to an issue: all 32-bit values of transaction IDs are assigned, crashing PG.
    • Worth noting: this is talked about to a disproportionately high degree as a red flag, because it sounds so bad, but in practice this is extremely rare. You would have to use up all 32-bit transaction IDs - FOUR BILLION - AND it must be the case that the daemon can't keep up. Keep in mind that Postgres provides you with configurable parameters for autovacuum daemon's behavior, so even in such rare circumstances it may be manageable.
  • Postgres connections are more heavyweight, as each uses a server process. Some think this means PG is "not scalable". They may be right to some degree, but this shouldn't turn you off to PG. (There are different dimensions to scalability.) You may need to tighten connection management if you need beyond, say, thousands of simultaneous connections. This is being reviewed by PG devs last I heard (2023).

Let's keep it going:

  • Want to add a column to your 200-million row table? Let's say it has columns a, b and c and you want to add d.
    • Here's what Postgres does under the hood:
      • Add column d. The end.
    • Here's what MySQL does under the hood (EDIT: this one is less true, i.e. not true in all circumstances, as of MySQL 8.0, since the INSTANT DDL algorithm was added):
      • Create new table with columns a, b, c and d.
      • Copy all the 200 million rows over to new table.
      • Recreate indexes and whatever else depends on the table. Not sure what else really, but I wonder how views interact with this.
      • Drop the old table.
      • Rename the new table

I'm probably oversimplifying the Postgres case a little bit, as it too may have to update views and such, as well as allocate disk, but the key point is it certainly doesn't have to move potentially vast quantities of data around for no reason. Now it MAY be wise to rebuild tables/indexes occasionally anyway, but being forced to do that with every change - no thanks.

  • Want to work with data dumps?
    • Postgres's tool pg_dump lets you dump data to a variety of formats: a plain-text SQL script, a custom binary format, a directory or a tar. The "custom" format is great because it's nice and flexible. Say I dump an entire schema of tables a,b,c and d, and all dependent objects (indexes, views, etc.). Later, if I want to restore just tables b and d, I tell pg_restore something like --tables=b,d. Voila, it works.
    • MySQL's mysqldump only lets me write the plain-text SQL. It is a script, full of commands like CREATE TABLE t, INSERT INTO t, etc. At my job, we generate these every night and they are tens of gigabytes. Of plain-text SQL commands. Good luck if you only want to restore tables b and d. You have to edit the text file to comment out a, and c, all the dependent objects and all the insert statements. Maybe use tools like awk or sed, I guess, if the file is so huge that you can't open it in your favorite text editor. Really fun times if your db is in the GB, TB or bigger range. I actually thought about writing a tool to automatically handle this, but lost the motivation.
  • Postgres has the hierarchy: cluster (an instance of PG essentially) => database => schema => table (and other objects). In MySQL databases and schemas are synonymous. Why? I don't know. Maybe not the biggest deal technically, but I find it creates confusion in a cultural sense. For example I often hear colleagues say things like, "Well THAT's a separate database, we shouldn't have a foreign key from table x to table y if they are in separate databases! That's dangerous because they might reside on different servers one day." They also might write database creation scripts operating on the idea that one schema == one siloed "database" even when that's not ideal. Nomenclature matters. But maybe I designed table x and y to reside in different SCHEMAS in the SAME DATABASE, it's just that MySQL makes no distinction. So in Postgres I might just put x and y in different schemas under the same database, it is clear that they are roommates who live together, and that's much less confusing. The PG way also opens the door to logical grouping of db objects other than tables. For example if I have a lot of stored procs, once you get beyond 20 or 30 of them you use your GUI client of choice to view them all, and what you see is this massive, mind-boggling, flat list of them all in one place. With the concept of separate schemas in the same database, I could make one schema to encapsulate all of, say, billing procedures, another for database administrative tasks, another for order management. It's a great way to divide up procedures (and other objects) logically without conflating with the concept of databases (which "everyone knows" are distinct entities).
  • JSON types - MySQL's JSON stuff is getting pretty good, but Postgres's JSON/JSONB types and related functions are more robust, with better indexing features.

Editing much later to add another point:

MySQL has mandatory indexes on foreign key columns: OK I will first admit this could be good or bad depending on your point of view. I find it bad. To briefly summarize the context, it is sometimes, but not always, a good idea to build indexes on your foreign key columns. If you never look up child rows given a parent row, and never use cascading effects like ON DELETE CASCADE, then you don’t need to index a foreign key. But MySQL forces you to create these indexes even when they’re not necessary. Of course, indexes take time to build, take up disk space, slow down updates/inserts etc. On larger tables you could have indexes that are hundreds of MB or even GB, and if you don’t need them, that’s really silly to force us to build them.

Furthermore it’s just annoying for them to be mandatory, when it blocks other operations for little reason. Just recently I was expanding a MySQL table’s unique key from columns a,b to a,b,c. So my first idea was to drop the unique key then recreate it on columns a,b,c. Simple enough. BUT, b was a foreign key column so I got an error on the drop unique key command. So instead I had to:

  1. drop the foreign key
  2. drop the unique constraint
  3. create the new unique constraint
  4. recreate the foreign key

I can, however, see the “good” point of view being that careless developers need indexing on all foreign keys to be enforced, or else they’ll never bother to make them when they ARE necessary, lest they cause performance/locking issues. I just don’t agree with this POV, though. I would prefer to just educate the developers working on schema design to create indexes judiciously rather than just blanket-enforce all FKs must be indexed everywhere.


SQL here != SQL there.

Can we please stop thinking some SQL command you used on database server x is universal? As you may be aware, I love answering questions on reddit, r/sql, to help people out with their SQL questions. But unfortunately I'm constantly finding myself having to ask, "Which database are you using?" as well as correcting others who suggest solutions that only work on a different database than what the person having the problem is using. This is because people think SQL works the same everywhere, I guess. 

There are extensions to SQL on all the major database systems. None of them implements every feature in all of the SQL specs. Even if they did, the SQL specs have many optional features. So for one feature, it might say, "This feature works like THIS, but OPTIONALLY it works like THAT." So even if we had two SQL databases that fully implement SQL (we don't), they could vary in behavior.

This morning I was frustrated by getting downvoted when correcting a commenter who gave a MS SQL Server-specific solution to a poster who didn't even say he's on MS SQL Server! 

So I made this meme to vent my frustrations.


SQL here != SQL there.

Friday, January 03, 2020

Stackoverflow answer on what Postgres has that MySQL lacks

The user u/truilus on Reddit (r/SQL) pointed out this Stackoverflow answer. It's really good: https://stackoverflow.com/a/8182996

It would be even better if it pointed out the various other quirks and oddities that MySQL has that Postgres doesn't, like this one I talked about in my previous post here: http://mwrynn.blogspot.com/2018/09/my-coworker-recently-came-up-to-me.html

Or, about how the MySQL query planner is pretty much inferior.

Or, if it talked about WHY each of these features in this huge list are useful things. I thought I'd write here about one, single Postgres feature that it is incredibly useful:

Transactional DDL: This is a really big one for me, particularly with respect to SQL scripts and rolling out incremental schema changes. Let's say I want to apply the following changes to an existing schema, to upgrade it from schema version i to schema version i+1:

   0. begin;
   1. insert into table x;
   2. create table y;
   3. insert into y;
   4. alter table z add column abc;
   5. update z set abc = ...;
   6. commit;

In Postgres I can roll out this whole set of statements in an all or nothing manner. In MySQL (and to be fair, some other databases as well), if say statement #5 fails for ANY reason - could be a bad foreign key reference, lack of disk space, or anything else - errors can and will happen - I'm stuck with a partially applied set of changes. What if the insert (statement #1) only makes business sense in conjunction with the update in the last statement? Well too bad, because statement 3 (alter table) forced a commit. So now there's "bad data" on prod until I have a chance to manually revert everything.

In Postgres it goes like this: Uh oh, statement 5 failed? Roll it all back! We're back at schema version i, not schema version i + random mess, where random mess != 1. Ta-da!

Thursday, October 03, 2019

PostgreSQL 12 released today!

PostgreSQL 12 was released today, and among many cool features, we have the much-awaited removal of the CTE optimization fence! Let's take a quick, hands-on look...

Please review my old post that described how it worked in previous versions: https://mwrynn.blogspot.com/2016/04/common-table-expressions-postgres-vs.html

In short, when you used a CTE, it materialized the results in memory, then ran the subsequent query against that.

You can still do that if you want in Postgres 12 with the "materialized" keyword, but you're not forced to.

So let's repeat the test I did 3 and a half years ago! Postgres 12 only - I won't repeat on Oracle, nor will I make fun of MySQL this time.

Here's what I've got:


mwrynn=# EXPLAIN ANALYZE
WITH peeps_by_state AS NOT MATERIALIZED                               (SELECT COUNT(*) AS cnt, state FROM person_location GROUP BY state)
SELECT cnt                                                          FROM peeps_by_state                                                 WHERE state=13;
                                                                         QUERY PLAN                                                                         
------------------------------------------------------------------------------------------------------------------------------------------------------------
 Subquery Scan on peeps_by_state  (cost=1000.00..484733.72 rows=50 width=8) (actual time=3687.870..3687.871 rows=1 loops=1)
   ->  Finalize GroupAggregate  (cost=1000.00..484733.22 rows=50 width=12) (actual time=3687.869..3687.869 rows=1 loops=1)
         Group Key: person_location.state
         ->  Gather  (cost=1000.00..484732.22 rows=100 width=12) (actual time=3687.732..3694.247 rows=3 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ->  Partial GroupAggregate  (cost=0.00..483722.22 rows=50 width=12) (actual time=3682.099..3682.099 rows=1 loops=3)
                     Group Key: person_location.state
                     ->  Parallel Seq Scan on person_location  (cost=0.00..481655.75 rows=413195 width=4) (actual time=0.448..3600.027 rows=333333 loops=3)
                           Filter: (state = 13)
                           Rows Removed by Filter: 16333333
 Planning Time: 0.159 ms
 Execution Time: 3694.318 ms
(13 rows)

Now let's use the "MATERIALIZED" keyword and compare.


mwrynn=# EXPLAIN ANALYZE
WITH peeps_by_state AS MATERIALIZED                                   (SELECT COUNT(*) AS cnt, state FROM person_location GROUP BY state)
SELECT cnt                                                          FROM peeps_by_state                                                 WHERE state=13;
                                                                               QUERY PLAN                                                                               
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 CTE Scan on peeps_by_state  (cost=534753.70..534754.83 rows=1 width=8) (actual time=11828.277..11828.392 rows=1 loops=1)
   Filter: (state = 13)
   Rows Removed by Filter: 49
   CTE peeps_by_state
     ->  Finalize GroupAggregate  (cost=534741.03..534753.70 rows=50 width=12) (actual time=11828.191..11828.328 rows=50 loops=1)
           Group Key: person_location.state
           ->  Gather Merge  (cost=534741.03..534752.70 rows=100 width=12) (actual time=11828.181..11835.081 rows=150 loops=1)
                 Workers Planned: 2
                 Workers Launched: 2
                 ->  Sort  (cost=533741.01..533741.14 rows=50 width=12) (actual time=11818.697..11818.703 rows=50 loops=3)
                       Sort Key: person_location.state
                       Sort Method: quicksort  Memory: 27kB
                       Worker 0:  Sort Method: quicksort  Memory: 27kB
                       Worker 1:  Sort Method: quicksort  Memory: 27kB
                       ->  Partial HashAggregate  (cost=533739.10..533739.60 rows=50 width=12) (actual time=11818.610..11818.624 rows=50 loops=3)
                             Group Key: person_location.state
                             ->  Parallel Seq Scan on person_location  (cost=0.00..429572.40 rows=20833340 width=4) (actual time=0.521..3374.912 rows=16666667 loops=3)
 Planning Time: 0.300 ms
 Execution Time: 11835.398 ms
(19 rows)

So there we have it - a significant difference! (Note that the times here cannot be compared to the times in the previous post, as it's a different server, different config, etc.)

I'll end the post here, but I may go over the steps in this execution plan and try to explain what's going on - maybe in a subsequent post or maybe I'll just edit this one.

Thursday, June 13, 2019

Clusters, Clusters Everywhere!


Clusters: what an overloaded term in the database world! Whenever someone talks to me about a database cluster, or a clustered index or clustered table, I have to stop and think for a moment to figure out what they mean. I am going to attempt to list all of the contexts in which the term “cluster” or “clustered” apply that I can think of, in no particular (or logical) order:

PostgreSQL Cluster: In Postgres, a cluster is simply your database storage area on disk. It is a collection of databases. The hierarchy is that a cluster has many databases that have many schemas that have many relations and other objects. A Postgres Cluster is more or less synonymous with a specific installation of Postgres.

Catalog Cluster: The SQL standard definition. Essentially the same as the PostgreSQL Cluster but generic. (I hadn’t actually heard of this one before except in the Postgres documentation in which they define a Postgres Cluster.)

Clustered Index and Non-clustered Index (MS SQL Server): In SQL Server, a clustered index refers to the table itself being physically shaped like an index. The index stores the entire row, sorted by key, in this index structure. A non-clustered index in Microsoft Land contains the key values in the index, but the rest of the row resides elsewhere in a heap or clustered table (see below)

MySQL (InnoDB) has something much like this too, but you don’t have a choice to use heap tables as an alternative. In Oracle they are called Index-Organized Tables and heap tables are the default.

Advantages: looking up by primary key is faster.

Disadvantages: inserts are slower.

There are more nuances, both pros and cons, but that's the gist of it. (One thing to consider is if your Primary Key is a UUID vs. a standard auto-increment integer, but I'll let you Google that. :))
 
Clustered Table (MS SQL Server): A table that has a clustered index (above) is called a clustered table.

Clustered Index (Postgres): An index that indicates the sort order of the table on which it is built. You first create your table as usual. Then create a clustered index on the columns you want to sort by. Populate the table. Then run the CLUSTER command to sort the table. Note that unlike the Microsoft clustered index above, a Postgres clustered index does NOT store the entire row's data in the index structure.

Advantages: this may speed up queries that use table scans with the sorted columns in the where clause. Check out the top answer on this stackexchange question - it talks about looking up by a date range on such a sorted table and the performance benefits - https://dba.stackexchange.com/questions/39589/optimizing-queries-on-a-range-of-timestamps-two-columns

Some more info from the PG documentation: "In cases where you are accessing single rows randomly within a table, the actual order of the data in the table is unimportant. However, if you tend to access some data more than others, and there is an index that groups them together, you will benefit from using CLUSTER. If you are requesting a range of indexed values from a table, or a single indexed value that has multiple rows that match, CLUSTER will help because once the index identifies the table page for the first row that matches, all other rows that match are probably already on the same table page, and so you save disk accesses and speed up the query."

Disadvantages: You need to maintain your sort order by periodically re-running the CLUSTER command, and a table lock is held on the table during the process.

I once had a summary table that was completely rebuilt every night during a maintenance period, and the CLUSTER command was run afterward. So in a case like this having to run the CLUSTER command is not much of an issue.

The CLUSTER command (also Postgres): This pairs up with the Clustered Index for Postgres above. It is simply the command you issue to perform the sort - i.e. to re-order the table according to the Clustered Index.

Table Clusters (Oracle): A group of tables that share common columns and store related data in the same blocks on disk. Essentially this is a way to physically colocate tables that are joined frequently, while maintaining the logical distinction between the tables. Table clusters can be used to reduce disk I/O and improve access times for joins. They can reduce storage requirements too, as the cluster key value is not stored repeatedly for each row.

You can also put just a single table in an Oracle Table Cluster - so all the data would be grouped by key in its physical home on disk. This may allow some queries to perform faster. See this old AskTom post.

Cluster Index (Oracle): A Cluster Index is used to support a Table Cluster (above). "To locate a row in a cluster, the cluster index is used to find the cluster key value, which points to the data block associated with that cluster key value. Therefore, Oracle accesses a given row with a minimum of two I/Os--possibly more, depending on the number of levels that must be traversed in the index." -Oracle docs

Hash Clusters (Oracle): (Oh dear lord, it keeps going with Oracle, doesn't it?) I'm just going to quote Tom Kyte on this one:

Hash clusters are useful when you always access the data by primary key. For example, say you have some data in a table T and you ALWAYS query:

select * from T where id = :x;

That might be a good candidate for a hash cluster since there will be no index needed. Oracle will hash the value of :x into a physical address and go right to the data. No index range scan, just a table access by the hash value

The “common sense” definition of a Cluster: this is what I think most people are talking about when they mention a “mysql cluster” or a “database cluster." They are talking about a group of several servers, each running a database service, probably with some sort of replication set up, working in tandem to achieve load balancing and high availability. This is more a general concept than a specific feature/technology.

Real Application Cluster (aka RAC): A trademark of the Oracle Corporation, is an Oracle-specific implementation of the “common sense” definition of a cluster. This feature lets you use many Oracle instances (an instance is a set of processes and memory areas) together in a “shared everything” architecture. Each server has its own copy of the database (database referring to the data files, essentially) and they are kept in sync automatically.

Confused yet?? :) Actually for me, writing out these definitions helps me become a little less confused. Hope it helps you, too! A question for my vast readership out there -- is there any other "cluster" I missed? I am sure there are many, many implementations of the "common sense" cluster, but that's ok, we don't need to list every one. :) Also I realize some of these features are supported by databases I didn't mention such as Sybase. Again, no need to list every one.

Ok, until next time, keep clustering! (Or something...) 




Tuesday, June 11, 2019

My favorite subject...in the wild!

My friend Scott pointed out that the site boxofficemojo.com most likely does not use bind variables for its search feature - shame shame!!!

Here's an example of a "good" search. Let's look up The Godfather:


And it works! No surprises there... (Although they could probably work on their relevancy scores.)


Now let's look up the movie Breakin' (note the apostrophe):


...And click "Search"...

Doh!!! Guess the apostrophe breaks the query because they're concatenating user inputs.

Just bind!

Thursday, June 06, 2019

Yikes

There was a post where somebody was writing code that concatenated user inputs from a form into SQL...



:(

A recent SQL question


--*--BEGIN QUESTION--*--
In lieu of our internal guru being available I was wondering if anyone would be able to help me figure out how to query the data I need.

table1 - contract data

contractId subTaskId column1 column2 column_N
1 1 meta1 meta2 metaN
1 2 meta1 meta2 metaN
1 3 meta1 meta2 metaN
1 4 meta1 meta2 metaN
2 1 meta1 meta2 metaN
2 2 meta1 meta2 metaN
2 3 meta1 meta2 metaN
table2 - workflow tracking for each contract subTask

contractId subTaskId taskStep processStep column1 column_N
1 1 1 Processing meta1 metaN
1 1 2 Review meta1 metaN
1 1 3 Routing meta1 metaN
1 2 1 Processing meta1 metaN
1 2 2 Routing meta1 metaN
1 3 1 Processing meta1 metaN
1 3 2 Review meta1 metaN
1 3 3 Routing meta1 metaN
1 3 4 Final meta1 metaN
1 4 1 Processing meta1 metaN
1 4 2 Final meta1 metaN
2 1 1 Final meta1 metaN
2 2 1 Review meta1 metaN
2 3 1 Final meta1 metaN

Results

contractId subTaskId processStep table1.columns table2.columns
1 1 Routing table1.columns table2.columns
1 2 Routing table1.columns table2.columns
1 3 Final table1.columns table2.columns
1 4 Final table1.columns table2.columns
2 1 Final table1.columns table2.columns
2 2 Review table1.columns table2.columns
2 3 Final table1.columns table2.columns

The column1/2/N is just to represent that each table has a bunch of additional columns of metadata, most of which I'd like to be present in my results.

The following is about as close as I was able to get(newbie), but I couldn't figure out how to also bring in the data columns from table b.

SELECT
    a.*
FROM
    table1 a
    INNER JOIN
        (SELECT contractId, subTaskId, MAX(taskStep) AS taskStep
         FROM table2 GROUP BY contractId, subTaskId) AS b ON
        a.contractId = b.contractId
        AND a.subTaskId = b.subTaskId

Thank you!

--*--END QUESTION--*--

--*--BEGIN ANSWER--*--
Which database are you using? I'll assume Postgres because why not. :)

Others are asking what you're looking for because it isn't clear. My best guess is you want, per each contractId/subTaskId grouping, the row with the maximum taskStep within that grouping. The other columns come along for the ride.

In that case I'd say you're on the right track. Let's ignore the join for starters, to simplify the problem. I'll only look at table2.

So again, what you've started writing looks pretty good:

mwdb=# SELECT contractId, subTaskId, MAX(taskStep) AS taskStep
mwdb-#          FROM table2 GROUP BY contractId, subTaskId
mwdb-# ORDER BY contractId, subTaskId; --I added an ORDER BY so it looks nicer
 contractid | subtaskid | taskstep
------------+-----------+----------
          1 |         1 |        3
          1 |         2 |        2
          1 |         3 |        4
          1 |         4 |        2
          2 |         1 |        1
          2 |         2 |        1
          2 |         3 |        1
(7 rows)

Eyeballing these results and comparing to your expected output, so far looks good! It's the right number of rows and taskstep corresponds to the processStep you want. But how to pull in processStep?

I'll let you in on a little trick. When you use aggregate functions with a group by, you can't just (except in MySQL - but they cheat :)) pull in any arbitrary other column you want. See below:

mwdb=# SELECT contractId, subTaskId, MAX(taskStep) AS taskStep, processstep
mwdb-# FROM table2 GROUP BY contractId, subTaskId;
ERROR:  column "table2.processstep" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...contractId, subTaskId, MAX(taskStep) AS taskStep, processste...

But what if we slap an arbitrary aggregate function to bring it along for the ride? This is a trick I often use if the "along for the ride" column is the same for each combination of grouping columns. A quick demo:

--here's my test table
mwdb=# SELECT * FROM delete_me;
 a | b | c
---+---+---
 1 | 1 | 1
 1 | 2 | 1
 1 | 3 | 1
 2 | 1 | 1
 2 | 2 | 1
(5 rows)

--and here's my query that brings column c along for the ride
mwdb=# SELECT a, MAX(b) AS max_b, MIN(c) AS c FROM delete_me GROUP BY a;
 a | max_b | c
---+-------+---
 1 |     3 | 1
 2 |     2 | 1
(2 rows)

I could have easily used MAX(c) instead of MIN(c) - it doesn't really matter. BUT we cannot use this trick in our original problem. This is because processStep is not unique for a given combination of contractId, subTaskId. If we try this trick we will essentially be grabbing an arbitrary value of processStep, like so:

--WRONG
mwdb=# SELECT contractId, subTaskId, MAX(taskStep) AS taskStep, MIN(processstep) AS processStep
FROM table2 GROUP BY contractId, subTaskId
mwdb-# ORDER BY contractId, subTaskId;
 contractid | subtaskid | taskstep | processstep
------------+-----------+----------+-------------
          1 |         1 |        3 | Processing
          1 |         2 |        2 | Processing
          1 |         3 |        4 | Final
          1 |         4 |        2 | Final
          2 |         1 |        1 | Final
          2 |         2 |        1 | Review
          2 |         3 |        1 | Final
(7 rows)

Comparing this to your expected results, we can see that processStep is not always correct. So now what?

Often I've seen coders solve this problem by writing a query that scans the table twice like so:

mwdb=# SELECT table2.contractId, table2.subTaskId, table2.taskstep, table2.processstep                                                                                          FROM table2 JOIN (SELECT contractId, subTaskId, MAX(taskstep) max_taskstep FROM table2 GROUP BY contractId, subTaskId) max_table2 ON table2.contractId=max_table2.contractId AND table2.subTaskId=max_table2.subTaskId AND table2.taskstep = max_table2.max_taskstep
ORDER BY table2.contractId, table2.subTaskId, table2.taskstep, table2.processstep
mwdb-# ;
 contractid | subtaskid | taskstep | processstep
------------+-----------+----------+-------------
          1 |         1 |        3 | Routing
          1 |         2 |        2 | Routing
          1 |         3 |        4 | Final
          1 |         4 |        2 | Final
          2 |         1 |        1 | Final
          2 |         2 |        1 | Review
          2 |         3 |        1 | Final
(7 rows)

TA-DA!!! However, this is not the best solution. Like I said before, we have to scan the table twice, which you should avoid in general if possible. Besides it's not the most elegant solution out there. However it's perfectly acceptable if it meets your performance criteria. Maybe you don't care about performance at all for your purposes.

If we EXPLAIN ANALYZE this query, we can see proof that the Postgres query planner decided to scan table2 twice. In our tiny table it doesn't really matter, but what if we had a billion rows in the table?

mwdb=# EXPLAIN ANALYZE SELECT table2.contractId, table2.subTaskId, table2.taskstep, table2.processstep FROM table2 JOIN (SELECT contractId, subTaskId, MAX(taskstep) max_taskstep FROM table2 GROUP BY contractId, subTaskId) max_table2 ON table2.contractId=max_table2.contractId AND table2.subTaskId=max_table2.subTaskId AND table2.taskstep = max_table2.max_taskstep
mwdb-# ORDER BY table2.contractId, table2.subTaskId, table2.taskstep, table2.processstep;
                                                                          QUERY PLAN                                                                       
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=100.38..100.39 rows=1 width=44) (actual time=0.137..0.138 rows=7 loops=1)
   Sort Key: table2.contractid, table2.subtaskid, table2.taskstep, table2.processstep
   Sort Method: quicksort  Memory: 25kB
   ->  Merge Join  (cost=90.56..100.37 rows=1 width=44) (actual time=0.107..0.120 rows=7 loops=1)
         Merge Cond: ((table2.contractid = table2_1.contractid) AND (table2.subtaskid = table2_1.subtaskid) AND (table2.taskstep = (max(table2_1.taskstep))))
         ->  Sort  (cost=55.27..57.22 rows=780 width=44) (actual time=0.071..0.071 rows=14 loops=1)
               Sort Key: table2.contractid, table2.subtaskid, table2.taskstep
               Sort Method: quicksort  Memory: 26kB
               ->  Seq Scan on table2  (cost=0.00..17.80 rows=780 width=44) (actual time=0.049..0.057 rows=14 loops=1)
         ->  Sort  (cost=35.29..35.79 rows=200 width=12) (actual time=0.032..0.032 rows=7 loops=1)
               Sort Key: table2_1.contractid, table2_1.subtaskid, (max(table2_1.taskstep))
               Sort Method: quicksort  Memory: 25kB
               ->  HashAggregate  (cost=23.65..25.65 rows=200 width=12) (actual time=0.017..0.017 rows=7 loops=1)
                     Group Key: table2_1.contractid, table2_1.subtaskid
                     ->  Seq Scan on table2 table2_1  (cost=0.00..17.80 rows=780 width=12) (actual time=0.002..0.004 rows=14 loops=1)
 Planning time: 0.388 ms
 Execution time: 0.503 ms
(17 rows)

So how do we avoid scanning the table twice? Enter our savior, window functions!!!

Let's throw away the group by, and instead use the row_number() over (partition by contractid, subtaskid order by taskstep desc) window function. That probably looks like random gibberish if you haven't seen it before, but it all it means is: let's generate a row number (1, 2, 3, 4, etc.) that resets to one for every combination of contractid, subtaskid, and start counting from the largest taskstep within that combo. Why do we want this number? Well if we start with the largest taskstep for every contractid, subtaskid combo, then we know that #1 is the one we want to keep. We can throw out the rest!

Let's see it in action:

mwdb=# SELECT contractid, subtaskid, taskstep, processstep, ROW_NUMBER() OVER(PARTITION BY contractid, subtaskid ORDER BY taskstep DESC) AS rn
FROM table2
ORDER BY contractId, subTaskId;

 contractid | subtaskid | taskstep | processstep | rn
------------+-----------+----------+-------------+----
          1 |         1 |        3 | Routing     |  1
          1 |         1 |        2 | Review      |  2
          1 |         1 |        1 | Processing  |  3
          1 |         2 |        2 | Routing     |  1
          1 |         2 |        1 | Processing  |  2
          1 |         3 |        4 | Final       |  1
          1 |         3 |        3 | Routing     |  2
          1 |         3 |        2 | Review      |  3
          1 |         3 |        1 | Processing  |  4
          1 |         4 |        2 | Final       |  1
          1 |         4 |        1 | Processing  |  2
          2 |         1 |        1 | Final       |  1
          2 |         2 |        1 | Review      |  1
          2 |         3 |        1 | Final       |  1
(14 rows)

Now the task from here is simple. Keep only the rows with rn=1!

mwdb=# SELECT contractid, subtaskid, taskstep, processstep FROM (
mwdb(#   SELECT contractid, subtaskid, taskstep, processstep, ROW_NUMBER() OVER(PARTITION BY contractid, subtaskid ORDER BY taskstep DESC) AS rn
mwdb(#   FROM table2
mwdb(# ) sub
mwdb-# WHERE rn=1
mwdb-# ORDER BY contractId, subTaskId;
 contractid | subtaskid | taskstep | processstep
------------+-----------+----------+-------------
          1 |         1 |        3 | Routing
          1 |         2 |        2 | Routing
          1 |         3 |        4 | Final
          1 |         4 |        2 | Final
          2 |         1 |        1 | Final
          2 |         2 |        1 | Review
          2 |         3 |        1 | Final
(7 rows)

And that's your final answer! But before we go, let's prove that it's not scanning the table twice:

mwdb=# EXPLAIN ANALYZE SELECT contractid, subtaskid, taskstep, processstep FROM (SELECT contractid, subtaskid, taskstep, processstep, ROW_NUMBER() OVER(PARTITION BY contractid, subtaskid ORDER BY taskstep DESC) AS rn FROM table2) sub WHERE rn=1 ORDER BY contractId, subTaskId;

                                                      QUERY PLAN                                                     
-----------------------------------------------------------------------------------------------------------------------
 Subquery Scan on sub  (cost=55.27..82.57 rows=4 width=44) (actual time=0.069..0.096 rows=7 loops=1)
   Filter: (sub.rn = 1)
   Rows Removed by Filter: 7
   ->  WindowAgg  (cost=55.27..72.82 rows=780 width=44) (actual time=0.064..0.086 rows=14 loops=1)
         ->  Sort  (cost=55.27..57.22 rows=780 width=44) (actual time=0.054..0.055 rows=14 loops=1)
               Sort Key: table2.contractid, table2.subtaskid, table2.taskstep DESC
               Sort Method: quicksort  Memory: 26kB
               ->  Seq Scan on table2  (cost=0.00..17.80 rows=780 width=44) (actual time=0.008..0.013 rows=14 loops=1)
 Planning time: 0.197 ms
 Execution time: 0.168 ms
(10 rows)

I'll clean the formatting up on this post tomorrow perhaps. It's late and I have to go to bed. :)

Last-second note: To be fair, it did do a "subquery scan on sub" to get just the rn=1 rows, in addition to the "seq scan" on table2. The first query did two "seq scans", so in a sense, our new query did scan every row twice. So is this any better? Well in most real world cases, the window function query is still a better bet. You would often have a where clause on the "sub" query, and best to avoid having to filter the data twice. (Imagine a billion rows in the table and a complicated where clause...)

--*--END ANSWER--*--

Friday, May 17, 2019

I was very pleased to see the above in the documentation for Psychopg (a PostgreSQL adapter for Python). I'm thinking of doing a presentation on the subject at my company, which shall remain nameless, because time and time again I see a lack of bind variables. I see it on my team; I see it on other teams. We have countless Jira tickets about various applications and other projects breaking due to complications from apostrophes and single quotes as input. (I'd post screenshots - it's really quite impressive - but I can't divulge that information.) Someone please make it stop! I guess that someone has to be me.

Thursday, March 14, 2019

Happy Pi Day!

(Borrowed from Connor McDonald's Twitter account.)

SQL> select
  2    sum((
  3    4 / (8*(level-1)+1) -
  4    2 / (8*(level-1)+4) -
  5    1 / (8*(level-1)+5) -
  6    1 / (8*(level-1)+6)
  7    ) / power(16,level-1)) pi
  8  from dual connect by level <= 10;

        PI
----------
3.14159265

Sunday, February 17, 2019

"CTEs to no longer be an optimization fence (COMMITED) (git.postgresql.org)"

Saw this linked to on Reddit with the title I quoted in this post's title...

Great news in the world of Postgres: no longer will Common Table Expressions (CTE) be shackled to the evil CTE Optimization Fence! Linky

As I previously blogged about, the optimization fence is a drawback to CTEs in Postgres that in short potentially impacts performance when you use them. So you may use them to make a query neater but you can pay a penalty. If the new commits are all that they promise, this performance hit will be no more! Here's the link to my old post about them - linky - note that they should perform about as well Oracle now!