But what you should not do in Postgres, so please welcome Jimmy Angelacos.
Thanks very much.
I'm a Senior Solutions Architect at EDB and I am grateful to EDB for allowing me to make
Postgres my day job because it is an excellent database, it is an excellent community and
thank you all for attending a talk with such a clickbaity title.
And thank you to the guys at home for clicking.
So why this title?
I didn't come up with it.
So this title is the title of a Postgres Wiki page that's called Don't Do This.
And I got all the content from there.
So that's the end of the talk.
But no, anyway, so this talk is not all inclusive, right?
I'm not going to tell you all the mistakes you can make with Postgres.
Who can?
I mean, there is literally nothing that you cannot mess up with no matter which database
you use.
You can always find a way to mess up.
But these are some of the things that we've noticed that people are doing wrong in general
with Postgres.
So some of them are misconceptions.
Like I believe this thing works this way, but it doesn't.
Some things are confusing because of the way they're implemented in Postgres, especially
things that are not part of the SQL standard, but Postgres extensions to the SQL standard.
So to be fair, Postgres is the most SQL standard compliant database.
It just has some things on top of it.
Other databases implement a subset of the SQL standard and also confusing things.
So we're a bit better from that respect.
And some common mistakes that people make that usually have a significant impact in
production environments.
So we'll be looking at some bad examples of SQL that you can write in Postgres.
We'll be looking at some improper data types for storing certain things.
Andreas had a good talk this morning about this, covering many of the same topics.
We will be looking at wrong ways to use Postgres features.
And also some things that affect your performance and affect the security of the server that
you need to be aware of.
So let's start off with some bad SQL.
First and foremost, not in.
As in the Boolean, not in, right?
It doesn't work the way you expect it to.
So when you're writing, select something where something else is not in this subquery.
You have to keep in mind that SQL and Postgres by extension is not Python and it's not Ruby.
So it doesn't behave the way you expect it to if you're used to writing not in Booleans
in programming languages.
So select A from table one where A not in one constant, right?
So it's always true.
And null returns nothing because if you perform a not in and there's even one null, the result
is null.
Not false, null.
So equally, select A from table one, a more real world scenario where A is not in, select
B from table two.
Even if one B is null, then the whole result is null.
So it's not doing what you're expecting it to.
Let's say that table two has no null Bs, right?
B is not null.
Why is this still bad?
And you should not use it because it doesn't optimize well in the Postgres query planner.
And instead of performing what is known as an anti-join, so it's the complete opposite
of a join.
Show me the rows you cannot join from this table.
So the Postgres query planner chooses a sub-plan and if that's a hashed sub-plan, that's kind
of okay.
If it's a simple sub-plan, then the performance of this thing is disastrous.
So even if you don't have nulls, you don't want to use it.
What should you use instead?
You should use an anti-join, as we just said, which looks something like this.
The column from table one where not exists is a better way to write not in.
So wherever column from table two does not exist where table one column equals table
two column.
So you want the rows that table two doesn't, can't match up to table one.
So that's an anti-join.
Or another way you could write this is select column from table one and use a left join.
So left join, table two, using the column, using this Postgres shorthand for join on
column equals column, but in this case I'm using column because it's the same name in
both tables.
So left join where table two dot call is null.
What does that do?
If it cannot find matches on the left-hand side to the right-hand side, then the right-hand
side, the result from table two is a null.
And that's how you get your anti-join.
To be fair, not in is okay.
If you know that there are no nulls and you cannot know that for a table, and as we said
it has performance implications, but when you're excluding constants that's fine, right?
Because if you have an index and you're able to tell that none of this is in the index,
then you're fine to use not in.
But generally speaking, try to prefer not exists or anti-joins.
Another thing is that we've seen people use the wrong way without knowing is between,
especially when you write a query with a where clause that specifies between timestamp one
and timestamp two.
Why is that?
Because between A and B is inclusive.
It's a closed interval.
So when you're saying between one and 100, you're saying include one and also include
100 in the results.
When is this bad?
This is bad when you're a bank, let's say, and you want to sum up the transactions for
the day, right?
The amounts from all transactions from the day.
And your DBA has written the following query, select some of the amounts from transactions
where transaction timestamp is between the end of the previous day and the end of the
current day, right?
So it should be fine.
No, it's not.
Because if a transaction has happened exactly at midnight, you'll get it twice.
Because when you run that query tomorrow, it's going to return the same row because you've
included midnight in both queries, right?
So that's a bad thing.
So it's better to be explicit instead and use select some amount from transactions where
transaction timestamp is greater or equal than and transaction timestamp is less than,
excluding the equality with midnight, right?
So that is very, very safe.
And there's no way to read it wrong.
It's very explicit, very clear.
Another thing, using uppercase in identifiers.
Many people like to do this because it looks very professional because they're used to
some database that was out there in the 80s that only could support uppercase table names.
And that database can now use lowercase, but the habit is still there.
Now why is that a bad thing in Postgres?
So if you use table or column names that are all capitals or mixed case, Postgres will
just ignore you and make everything lowercase unless you use double quotes around the names.
So create table plurp and create table quacks.
What are the consequences of issuing these two DDLs?
It creates a table named plurp, lowercase, and a table named quacks with a capital Q.
Why is that a problem?
So table here is shorthand for select star from plurp.
So table plurp works because it's not quoted, so Postgres ignores the case.
Table plurp quoted, even if it's exactly the same way we specified it when we were creating
the table, will fail and it will say there's no such table.
Equally table quacks fails because there's no lowercase table quacks.
Table quacks in double quotes works fine.
So you can see how you can mess up your schema with this.
If you give your schema to a developer and they're not aware that there's a difference
between double quoted and unquoted table names, then you get in trouble.
I think.NET by default, even if you don't do anything, double quotes everything.
So if you make the mistake of including capitals there, then they're not going to work in Postgres.
So unless you create the tables from within.NET, that is.
So the same goes for column names.
If you want pretty column names in your output and your reports, then just use select call
as pretty name.
Double quote the pretty name.
It can have spaces, it can have emoji, whatever you want.
And Postgres will just return exactly that name and you don't have to change your column
name on your table to make accounting happy.
Now moving on from SQL, let's look at the wrong use of some of Postgres' built in data
types.
Again timestamps.
So if you create a column that is type timestamp, that means timestamp without time zone.
So these are naive timestamps and they represent a local time somewhere.
But you don't know where.
It stores a date and a time with no time zone information.
There's no way to retrieve the time zone where this row was inserted.
And why is that a bad thing?
Because arithmetic breaks down totally.
You cannot add and subtract dates and intervals and anything else because you can't calculate,
you can't make computations on what the time would be because of things such as time zone
changes and daylight savings times.
So it's meaningless, it will give you the wrong results.
So instead please use timestamp TZ or TZ if you're British.
Timestamp with time zone is the equivalent.
Timestamp TZ is the shorthand and that stores a moment in time.
A moment in time means the number of seconds that have passed from midnight at the beginning
of the first of January 2000.
So it's absolute, it's definite and you know exactly which moment in time you're specifying.
The arithmetic works correctly as you would expect.
And this by default displays in your time zone but you can also choose to display it
at time zone.
So if you've inserted something which is midnight UTC and you want it in Eastern time
that would automatically convert it.
If you said at time zone Eastern it would automatically convert it to minus five hours
or minus six hours if there's a DST difference between the two time zones.
So you don't have to worry about the conversions.
Just use timestamp with time zone and you won't have to worry about it.
Even if you don't need time zone calculations and all of your operations and all of your
queries are coming from within the same time zone it's better to use this.
Because then when you have to export your data and give it to someone else they know
exactly what this means even if they don't know your time zone.
So also if you've decided to only use UTC throughout your organization then don't use
timestamp to store UTC because Postgres doesn't know it is UTC.
It just sees a local time and doesn't know where it is so it can't convert it.
Now something less frequently used is the type time TZ or time with time zone.
That is a quirk of SQL.
It is there because the standard specifies it and that's the only way Postgres implements.
That's the only reason why Postgres has implemented this.
So time with time zone has questionable usefulness.
Because time zones in the real world have little meaning without dates.
It can be the middle of the day in Australia and the previous day here.
So it will be times in some time zone but the date is different and you don't know it.
So the offset can vary with daily savings time and that's a bad thing because time TZ
has a fixed offset and that makes it impossible to do date calculations across daily savings
times boundaries.
So just use timestamp TZ instead.
There's also a space saving.
For some reason this thing is 12 bytes.
I don't know why.
A timestamp is 8 bytes.
So just use timestamp TZ or timestamp with time zone instead.
Current underscore time is another favorite.
Current time is timestamp TZ.
So we just said don't use timestamp TZ.
Just use current timestamp or the function now to get the current time with the time
zone and local timestamp that returns the timestamp if you just want to know what time
it is here in your local time zone.
Equally you can use current date for date and local time for the local time.
These are not timestamps.
These are dates sometimes.
This is one of my favorites.
This morning Andres showed that many people when they want to store a string they just
create car 255.
That should take care of it.
What is the problem with that?
It's that this is padded with white space up to n.
So if you create a car 255 and you insert a single character to store then that inserts
254 blank spaces after it in the database for no reason.
The padding spaces are useless because they are ignored when comparing but equally they
create a problem because they don't work for like expressions and they don't work for
regular expressions because a regex will see the spaces.
So it's inconsistent.
So just don't use it.
And anyway you're not gaining anything by specifying a limit in the number of characters
because it's not even stored as a fixed width field in Postgres.
The storage is exactly the same.
You're just wasting space by adding white space.
Once wise it's even worse because Postgres is spending the extra time discarding those
zeros when you're requesting a result that it's supposed to ignore those zeros.
So also another consequence of car n is that an index created for a character of n length
may not work with a query that accepts a text parameter or a varchar parameter with no limit.
The index is created for a different data type therefore it does not apply to that query.
So also limits are bad always.
Limits on strings are bad.
If you create a company name and you think 50 characters are enough I don't know any
company name that is more than 50 characters and then you get a customer that's called
Petersons and Sons and Friends Bits and Parts Limited which is 54.
And then you have to go and change the column width in the database and your DBA starts
swearing even though they selected the character length themselves because they were told to.
Also it's useless for restricting length.
It throws an error okay but it doesn't make sure that the length is exactly what you want.
So if you want a four digit pin and you enter it as car four that is not enforced if someone
enters a three digit pin.
You need an extra check so it doesn't guarantee anything.
So to restrict length and make sure that the length of what everyone enters is consistent
then use a check and strain and enforce it.
So bottom line is just use text.
Text is the same as the confusingly named Varkar with no parentheses.
So text.
Money get away from the type money because it's useless.
It's fixed point which means that it doesn't handle fractions of a cent.
So for finance that's very bad because you usually have subdivisions of the lowest denomination
of currency whether it's a cent or a penny or whatever else.
So the rounding may be off and that is a bad thing in finance.
Another bad thing is that it doesn't know which currency it's storing the values for.
So it assumes that the currency is what you specified in LC monetary.
And if you don't know what LC monetary is it's just going to assume whatever it finds
in your UNIX configuration or Linux.
Even worse it accepts garbage input.
So if you select that thing and convert it to money it casts it to whatever it believes
is right.
And because my laptop was set up for UK pounds it assumed that that's UK pounds.
So just use numeric and store the currency in another column for that row with a foreign
key so you know which currency that is.
Serial how many people here use serial and like it.
So I will explain why you shouldn't like it.
It used to be useful shorthand it is still useful shorthand but it's now less useful
than it used to be because it's non-SQL standard and it messes up the permissions when you
use it.
So permissions for sequences created using serial automatically created using the serial
keyword when creating a table they need to be managed separately from the table.
So a consequence of this disconnect is that create table like another table with a table
that uses serial will use the same sequence from the other table.
And you don't want that usually.
So instead we've come up with identity columns that are more verbose but much clearer in
what they do because they're attached to the table that created them.
So create table ID begin generated by default as identity and also primary key.
With an identity column you don't need to know the name of the sequence.
So when you alter table tab, alter column ID, restart a thousand you don't need to know
what the sequence is called.
It's attached to the table so it will just restart the sequence from a thousand.
A side note here if your application is depending on a serial sequence to generate things like
receipt IDs, receipt numbers that is something you should generally generate in your application
to make sure that there are no gaps because there's no guarantees whatsoever that a sequence
in Postgres will have no gaps.
If you try to insert something and there's an error and you're all back, you've skipped
over that sequence number.
Never goes back.
Cool.
So now let's look at improper usage of Postgres features.
Character encoding SQL underscore ASCII.
It is not a database encoding that you should be using unless you know exactly what you're
doing.
So things like storing text from the 1960s where no character sets other than ASCII.
When you specify that your database is encoding is SQL ASCII, you are skipping all encoding
conversion and all encoding validation.
So it will accept just anything and it will assume that if your character has a byte value
from 0 to 127 that it's ASCII and if it's over 127 to 255, then it will not even try.
It will just store it and not interpret it as anything.
So it doesn't behave the same way as a character set setting and it's very bad that this is
the default.
Fortunately, most distributions, the packages that Devin makes for distributions have UTF-8
as the default.
So that's a safer choice.
Also when you use SQL ASCII, you can end up storing a mixture of encodings because it
doesn't check and validate anything.
So once you've done that, there's no going back.
There's no way to recover the original strings because you don't know which encoding they
came from.
Rules.
Rules are a thing that predates SQL in Postgres.
When it was just Postgres, not Postgres SQL.
It's a very old thing that has its specific purpose and its purpose is not to work like
a trigger.
Rules do not apply conditional logic.
They rewrite your queries to modify them or add extra queries on top of them.
So any rule that's non-trivial, so any rule that's not like a select or an update into
a view is going to have unintended consequences because it's going to execute the original
query if it's an insert and then apply the rule and then generate another row potentially
or change the value of the row you inserted.
So also, as we said, it's older than SQL in Postgres and it's non-SQL standard.
So unless you're using rules to create views that you can write to, use a trigger instead.
That's what you want to use.
There's an exhaustive blog post by Depeche that you can read.
You will find the link in the slides afterwards.
Table inheritance.
Table inheritance is a relic of the time of object-oriented databases.
If you remember, up on our website, we used to say that Postgres is an object-relational
database.
Maybe we still do.
Okay.
But everything in Postgres is an object.
Fine.
That doesn't mean that table inheritance applies to tables because it seemed like a good idea
before ORMs that you would have some sort of inheritance from a table type to another
table type.
And the way you would write that was create table events, let's say, with an ID and some
columns and then create a table meetings.
Meetings are events, right?
And they have a scheduled time, but all the other characteristics of an event.
So why not create table inherits the other table?
It's also used to implement partitioning in Postgres before Postgres 10, but is now incompatible
with the new way of partitioning after Postgres 10.
So you cannot inherit from a partition's table, and you cannot add inheritance to a table
that's partitioned.
So if you've got it in your database, there is a way to undo it, and I will just skim
over it.
You can replace it with a foreign key relationship between the two tables.
And it works exactly the same way.
So create table new meetings, like meetings.
Table inheritance is scary.
I apologize.
It's not for young guys.
So create table new meetings, like meetings, creates it in exactly the same way.
Alter table to add another column to store the foreign key relationship.
So that should have been event ID, excuse me.
Anyway.
So you copy the data from the old table into the new table.
So insert into new meetings, select everything from meetings, including the ID.
You create the required constraints, triggers, et cetera, everything you need for the table,
new meetings.
And if you have a very large table, you can apply a very dirty hack that says that because
I know that the data in the other table is valid, I don't need to validate it again.
So I add the constraint, the foreign key constraint, as not valid.
If you're doing this in a live system that needs to be online while you're making this
change, create a trigger so that changes coming into meetings can go into new meetings as
well.
And the dirtiness of the hack comes in the fact that you should really not be touching
PG catalog at all, but if you do know that your constraint is valid because the data
in your existing table is valid, you just go ahead and update PG constraint set, constraint
validated equals true for that foreign key constraint we just created.
And then finally, in order not to do lengthy locking when you're doing this, begin a transaction
in a code block, an anonymous code block.
You alter table meetings, rename to old meetings.
Then you change new meetings that has exactly the same content now with an additional column.
You rename it to meetings, you drop the old table, and then you commit.
Be careful, also create a trigger to insert update delete items in events as they get
changed in meetings.
And that's about it.
You've gotten rid of your table inheritance.
Another very confusing thing, if you look at the Postgres documentation, it explains
very well how to do this, but this is probably not what you want to do.
So partitioning by multiple keys is not partitioning on multiple levels, right?
So let's say we create a table transactions, and it has a location code and a timestamp
among other columns.
And I want to partition it by timestamp and also location code, because I want a separate
table for each time period for each location code, right?
So I create table transactions, 202302A for values from timestamp 2023, so the first
of February to the first of March, and for location codes AAA to BAA.
Then I create the second partition, and 202302B is a partition of transactions for values from
the same time period, but different locations, okay?
So I'm using locations BAA to BZZ, error, partition transactions 202302B would overlap.
Why is that? because you're specifying limits for the keys within each partition.
So it will accept values that satisfy those keys, but this is not subpartitioning.
What you do want is subpartitioning.
You want to partition by one key, and then partition those tables by another key.
That is the way to do it correctly.
So you create table transactions, location type, et cetera, et cetera, partition by
range of timestamp first, okay?
Because we want the first level of partitioning to be timestamp based.
Then you create table partitions as transactions, excuse me, as a partition of transactions
for values from the first of February to the first of March, and we choose hash partitioning
within those partitions for the location code.
And all that means over there is that when I create the first partition, it's for values
with modulus four remainder, zero means just divided by four equal parts.
And that creates a partition, a table that is partitioned by both things, subpartitions.
Now let's talk a little bit about performance.
One thing we see people doing all the time is using many more connections than they should
be, accepting many more connections into their Postgres server than they should be.
The default is very sensible, it's at 100 connections.
We see things like 5,000 connections in production.
And a server with 32 CPUs, a server with 32 CPUs, there's no way on Earth it's going
to do more than 32 things at the same time, right?
It's common sense, okay?
You may accept up to 100 things with 32 CPUs and interleave and overlap, that's fine.
Or one of the connections may be idle and you take advantage of that to serve the other
connections but 5,000 is excessive and we'll see why.
Because Postgres is process-based and for every new client connection it spawns a new
process.
And a new process comes with inter-process communication through semaphores and shared
memory and that has an overhead.
So every process you add to the system adds to that overhead and you run at the risk of
your CPU spending most of its time doing context switching between one process and the other.
Also accessing the same objects from multiple connections may cause many lightweight locks
to appear, what are called latches in other databases.
And if you're trying to access the same objects from many client connections, then that lock
even if it's not explicit it becomes heavily contented and the other connections trying
to access that object will slow each other down.
So instead of opening one connection that does 400 times the work, you open 400 connections
that do one 400th the amount of work and that doesn't perform the same, that performs worse
because it's making your data hotter for no reason because they compete for access to
that data.
And also there's no fair queuing, it's more or less random, so lightweight locks don't
have queuing so you don't know who will get priority and there's no guaranteed quality
of service.
Now mitigation strategy is also you need to be aware that before Postgres 13 there's
the issue of snapshot contention.
So each transaction keeps an MVCC snapshot even if it's idle and so you can end up using
server resources even for idle connections and slow everything else down.
So this is contention that is caused by too much concurrency.
So instead of opening 5,000 connections just put a PG Bouncer in front of your database
or another connection pooler and just allow fewer connections into the database while
accepting the client connections from the connection pooler.
That way you throttle or you introduce latency on the application side but that's not always
bad because in some cases it can protect your server's performance which is more important
than making let's say a non-interactive client wait for a few milliseconds more.
It sounds counterintuitive but it leads to higher performance overall.
High transaction rate is also a problem when you're burning through transactions very quickly
because there's a lot of detail here about the way transaction IDs work in Postgres but
the bottom line is that there's 4.2 billion transaction IDs.
The future for you is 2.1 billion transactions in the future and the past is another 2.1 billion
transactions.
So if you are writing with a huge data rate with let's say an OLTP workload that can go
through 2.1 billion transactions in a week that will overrun the last transaction and
you will no longer know whether that transaction is in the past or in the future and that's
a problem.
Postgres won't let you do that, it will shut down to avoid doing that and the solution that
we came up with is called freezing where you go through the table and you mark each row
as you know to be old as frozen and you know that that row is always in the past even if
it has a transaction ID from another time.
So the problem is you need to make sure that Postgres has the chance to freeze those rows
before the wrap around.
So what can you do?
You can reduce the number of transactions, you can use batching.
Instead of committing 100 things, just batch them or 1,000 things and that automatically
uses 1,000 transactions less, sorry 1,000 the transaction rate that you would have and
that helps.
Also it helps to bump up the effectiveness of auto vacuum and that takes care of freezing.
Another favorite is people that turn off auto vacuum, so the thing that actually makes multi
view concurrency control work, so don't turn it off.
Its work is removing dead tuples, freezing things, among other things, it does have overhead
because it scans tables and indexes and acquires locks and gives them up voluntarily and that's
why it has limited capacity by default.
But the defaults are not suitable for production workload.
So if you're concerned about the overhead of auto vacuum then turning it off is not
the solution because the alternative is worse.
You can risk shutting down your database or accumulating bloat because there's no way
to avoid the vacuum in Postgres yet.
And when you outrun vacuum by writing faster than your database can auto vacuum it then
you may come up with a bloat runaway that requires a vacuum full and that takes a total
lock on the table and nobody can use it.
So instead of turning off auto vacuum, actually make it work harder and you can find in the
Postgres documentation how to make it work harder in order to avoid bloat and transaction
ID wraparound.
There's some standard stuff here about explicit locking.
If your application needs to lock things to make sure that concurrency, oops, out of power,
can I use something else?
I have a copy.
Okay, so we're only like two or three slides.
If you're really interested in knowing them you can talk to Jimmy afterwards but you can
ask now questions about what he already talked about.
So if we have like five minutes for questions, so if you have a question please raise your
hand and we are going to bring the microphone to you so you can ask questions.
There is a question there.
Good.
Thanks, it's on the website.
Is there any difference on how, is there any difference in how VARCAR and VARCAR N are
stored on the disk?
Sorry I didn't hear your question.
If there is any difference in how VARCAR and VARCAR N and text are stored on the disk?
No, VARCAR is exactly the same as text.
It's the same type.
Okay.
So it doesn't matter like also for indexes like I know in my SQR.
No, no, it doesn't make a difference but VARCAR with a limit is a different type.
Got it.
Thank you.
Thanks.
Another question?
Just one of the browser.
Questions, questions.
Jimmy, I have a question.
So you were talking about money and why does money is actually implemented?
Is it SQL standard or?
Connected.
Sorry, what was the question?
If money is so bad as a data type, why is it implemented in Postgres?
Because it was actually deprecated because of those bad things that we talked about.
Twice.
As Andreas pointed out this morning and people requested it so we reinstated it twice.
Oops.
There you go.
So people wanted it.
Okay.
People wants money.
So.
Different kind of money, exactly.
Any other questions?
Okay we have another question here.
Quick question about table inheritance.
So I know I've read the Postgres documentation about all its flaws and why you shouldn't
use it especially now that there's partitioning.
But overall I think the idea of having tables that have some common columns but then diverge
on some others is an interesting idea.
There's other ways to solve it.
Like in previous jobs I've implemented one table that had all the common columns and
then one separate table for each variation.
But are there other solutions that you implement for those types of?
ORMs.
Why not use ORMs to make as complicated the data model as you like but not store the complexity
as inheritance relationships on the database.
But doesn't that create larger tables that you'll have to read no matter if the data
is sparse?
No all you need to link them is a foreign key relationship.
So you're just storing an extra identifier I guess.
Yeah.
Thank you.
Okay.
Here.
Never mind.
So anyway, before the last thing I wanted to tell you, right, it was the security slides.
They're important.
Never use trust over TCPIP in your PGHBA conf.
That was the most important thing I had to say in the remainder of the slides.
Do not trust anything coming from TCPIP.
Always use password, MD5 certificate, scram authentication.
That was the last thing.
Sorry.
I'll take your question now.
Thanks.
I'm curious as to why outer left join isn't implemented, it's just left join, is that
of course it's the same thing as using the anti-join you used earlier.
I'm just curious why it isn't implemented.
It's the same thing.
Outer left join is the same thing as left join in Postgres.
Yeah, I know.
But outer left join should be, according to my old books of SQL89 or something, just
the anti-join left side.
So you do not take the center part where the rings meet.
You remove the intersection, just take the left part.
Right.
So yeah, the way Postgres supplements it is it just enters null for the things that
don't exist that don't correspond.
And the right join would put the nulls on the other side.
That's the difference.
There was another question here before.
So you mentioned about the date and the time handling.
Is there any way in Postgres that doesn't involve an awful lot of hackery to deal with
partial dates?
E.g., for example, if I said I'm going to take the train tomorrow morning or I'm going
on holiday in August.
So you want to store like August?
Well, August 24.
Right.
So you can use a date with no context.
You can use a date that says August 24.
Well no, not as in August 24, as in August 2024.
Okay, so you can just use extract from that date or truncate and lose all of the other
context with that date and only store August 2024.
Thank you.
We have time for the very last question here, somebody who is ready.
Hi.
When you write V2 of this presentation, what do the other don't do is that you would add?
Other don't do is to involve like, I don't know, like foreign data wrappers or well or
I guess the more exotic parts of Postgres that you would say.
Yeah, as I said, this talk couldn't be all-inclusive.
It was the top things that we see people doing wrong every day.
Fair enough.
Right.
So thanks everybody for staying until the very last talk.
Excellent.
And remember, you can now get out here on the front because there are no more talks.
You can pick up your stickers here.
And once again, thank you, Jimmy, for your presentation.
Cheers.