A terrible schema from a clueless programmer
rachelbythebay.com
rachelbythebay.com
This was the first time regular people could go buy tickets for events & they had been lining up overnight at Bank of China locations through the country. We were down for over a day before we called it off. Apparently this led to minor upheaval at several locations in Beijing & riot police were called in.
We were pretty puzzled as we had an index and had load tested extensively. We had Oracle support working directly with us & couldn't figure out why queries had started to become table scans.
The culprit? A point upgrade to DBD::Oracle (something like X.Y.3 to X.Y.4) introduced subtle but in character sets. So the index required using a particular Unicode character set, and we were specifying it, but when it was translated into the actually query, it wasn't exactly the right one, so the DB assumed it couldn't use the index. Then, when all the banks opened & a large portion of very populous country tried to buy tickets at the same time, things just melted.
Not a fun day.
Writing the pre-ANSI-JOIN-support SQL dialect code was pretty good fun though.
Near as I can tell, we assume there is some bit of magic built into the foreign key concept that handles this for us, but that is not the case.
But it seems like I am missing your point: care to expand on it so I can learn about the gotcha as well? What are you attempting to do, and what's making the performance slow?
Or in the case of Silicon Valley, leave everything in tact with a disabled flag and then spam you for the next decade or so.
Which is more or less what the GP says as well, deletes are not implemented because doing it properly requires proper design.
If it causes an issue with our own systems it also gives me a very good justification for doing it.
And then many other times I honestly don't have the knowledge to be able to contribute into large third party systems and it seems very rare to get an issue resolved without submitting a pr.
And lastly sometimes it's hard to judge if unexpected behavior is really a 'bug'. Similar to the last point, it can honestly be hard to tell the difference sometimes.
http://gallium.inria.fr/blog/intel-skylake-bug/
"More experienced programmers know very well that the bug is generally in their code: occasionally in third-party libraries; very rarely in system libraries; exceedingly rarely in the compiler; and never in the processor."
Given that you mention China in your story, did GB 18030 have anything to do with your problems?
load tests usually only happens on staging env, not production, that is the policy.
Oracle was called in, "en masse". Keep also in mind that Oracle Portal was basically running on top of lots of dedicated tables and stored procedures and you could not access the data directly, you interacted to the contents only through vistas, aliases etc.
It was indeed due to a smallish version bump. But we had a pretty good Team Leader who had made Oracle declare that the two different version were absolutely compatible and allowing us to write code on the older version.
He made them state this in written form, before starting the development so it was later impossible to blame the developers for the problem.
Also is helo field even needed?
Even better, use a database with a proper inet datatype. That way you get correctness, space efficiency, and ability to intelligently index.
In 2021 I’d recommend just turning off the IPv6 allocation or deleting it from DNS like this site does.
This is the funniest IPv6 excuse I've heard on HN.
Also remember indexes take space, and if you index all your text columns you'll balloon your DB size; and this was 2002, when that mattered a lot more even for text. Indexes also add write and compute burden for inserts/updates as now the DB engine has to compute and insert new index entries in addition to the row itself.
Finally, normalizing your data while using the file-per-table setting (also not the default back then) can additionally provide better write & mixed throughout due to the writes being spread across multiple file descriptors and not fighting over as many shared locks. (The locking semantics for InnoDB have also improved massively in the last 19 years.)
* Assuming the author used InnoDB and not MyISAM. The latter is a garbage engine that doesn't even provide basic crash/reboot durability, and was the default for years; using MyISAM was considered the #1 newbie MySQL mistake back then, and it happened all the time.
Indexes take space, except for the clustered index.
An important distinction.
If the point of this table is always selecting based on IP, m_from, m_to, then clustering on those columns in that order would make sense.
Of course, if it's expected that a lot of other query patterns will exist then it might make sense to only cluster on one of those columns and build indexes on the others.
Indexes didn’t go away with these extra tables, they live in the ‘id’ field of each one. They also probably had UNIQUE constraint on the ‘value’ field, spending time on what you describe in the second half of my citation.
I mean, that should have saved some space for non-unique strings, but all other machinery is still there. And there are 4 extra unique constraints (also an index) in addition to 4 primary keys. Unless these strings are long, the space savings may turn out to be pretty marginal.
In general, Indexes alone can't save you from a normalization problems. From certain points of view an Index is itself another form of denormalization and while it is very handy form of denormalization, heavily relying on big composite indexes (and the "natural keys" they represent) makes normalization worse and you have to keep such trade-offs in mind just as you would any other normalization planning.
It is theoretically possible a database could do something much smarter and semi-normalized with for instance Trie-based "search" indexes, but specifically in 2002 so far as I'm aware of most of the databases at the time such indexes were never the default and didn't have great multi-column support and would have been expensive to compute if you did turn them own. Even such "smart" Indexes would likely still have suggested you normalize first.
The author mentions that these events took place in 2002. At that time the site was likely still running MySQL 3 and InnoDB was very new and considered experimental. Sure MySQL 4.0 had shipped in 2002 and InnoDB was a first class citizen, but back then upgrades from one version of MySQL to another weren't trivial tasks. You also tended to wait until the .1 release before making that leap.
So in fairness for the folks who originally set up that database MyISAM was likely their only realistic option.
I looked after and managed a fleet of MySQL servers from back then and for many years afterwards and even then it wasn't until MySQL 5 that we fully put our trust in InnoDB.
It may be that the second solution wasn’t the best, or that it was better for write performance or memory usage—this was 2002 after all. It may also be the case that the cardinal it’s of some columns was low in such a way that the four table solution was better, but maybe getting the columns in the right order for a multi column index could do that too.
Either way, I don’t think it really matters that much and doesn’t affect the point of the article.
However, normalizing the fields that contain repetitive data into separate tables could create significant space savings since the full text for each column would not need to stored for each row. Instead of 20-30 bytes per email address, a 4 byte (assuming 32-bit era) OID is stored in its place.
It's pretty easy to imagine how quickly the savings would add up, and that it would be very helpful in the era before SSDs or even 1TB HDDs existed.
> Now, what do you suppose happened to that clueless programmer who didn't know anything about foreign key relationships?
> Well, that's easy. She just wrote this post for you. That's right, I was that clueless newbie who came up with a completely ridiculous abuse of a SQL database that was slow, bloated, and obviously wrong at a glance to anyone who had a clue.
> My point is: EVERYONE goes through this, particularly if operating in a vacuum with no mentorship, guidance, or reference points. Considering that we as an industry tend to chase off anyone who makes it to the age of 35, is it any surprise that we have a giant flock of people roaming around trying anything that'll work?
I don't know the actual numbers, but it's been pointed out that at any given time something like half of all programmers have been doing it less than five years, for decades now.
That, plus the strident ignorance of past art and practice, seem to me to bring on a lot of issues.
that is an excellent term.
I could have jumped on it and played it but I figured just another toy craze, it'll be over before long.
Maybe a bit of both.
^H^H^H^H^Hitic
There, FTFY.
The narrative should lead to the conclusion. If I told you the story of the tortoise and the hare in which the hare gets shot by a hunter, then said the moral is "perseverance wins", you'd be rightfully confused.
They then went on to list the errors that this brand new entry level employee had made when writing ... an authentication system ...
I was more than a little shocked when I realized they were serious and hadn't realized the issue was sending the new entry level guy to do that job alone.
I've always seen authn. Where'd you pick this usage up?
For my part, I have not encountered "authn", "authz", or "authk" before. But prior discussion did not even mention the word "authorization", and that seemed worth bringing out in the open.
So, not nonresponsive, just responsive to things you weren't personally interested in.
They way I see it is that it was a case of something that wasn't working, then replaced with something that was working.
I don't know what your metrics about "good" or "bad" are but eventually they got a solution that covered their use cases and the solution was good enough for stakeholders and that is "good".
With yes the added confounding factors of how often our industry seems to prefer to hire youth over experience, and subsequently how often experience "drains" to other fields/management roles/"dark matter".
The thing is, the first implementation was a perfectly fine "straight line" approach to solve the problem at hand. One table, a few columns, computers are pretty fast at searching for stuff... why not? In many scenarios, one would never see a problem with that schema.
Unfortunately, "operating in a vacuum with no mentorship, guidance, or reference points", is normal for many folks. She's talking about the trenches of SV, it's even worse outside of that where the only help you might get is smug smackdowns on stackoverflow (or worse, the DBA stackexchange) for daring to ask about such a "basic problem".
It’s not clear from the article whether the author spent any time studying the problem before implementing it. If she failed to do so, it is both problematic and way more common than it ought to be.
“Ready, fire, aim”
But you know what they say: good judgment comes from experiences and experience comes from bad judgment.
Compounding the problem here is that the author now has much more experience and in a reflective blog post, still got the wrong answer.
IMO the better lesson to take away here would have been to take the time getting to know the technology before putting it into production instead of jumping head first into it. That would be bar raising advice. The current advice doesn’t advise caution; instead it perpetuates the status quo and gives “feel good” advice.
The challenge of writing good software is not about knowing and focusing on the perfection every gory detail, it's about developing the judgement to focus on the details that actually matter. Junior engineers who follow your advice will be scared to make mistakes and their development will languish compared to those who dive in and learn from their mistakes. As one gains experience it's easy to become arrogant and dismissive of mistakes that junior engineers make, but this can be extremely poisonous to their development.
Honestly, I think your comment is okay minus the fact that you're trying to highlight such hard disagreement with a sentiment that people should read the fucking manual. Really, you couldn't disagree MORE?
There is definitely value in RTFM. There are cases where making mistakes in production is not acceptable, and progressing by "making mistakes" is a mistake on its own. I don't think the case in this article sounds like one of those, but they do exist (e.g. financial systems (think about people losing money on crypto exchanges), healthcare, or, say, sending rockets into space). In many cases, making mistakes is fine in test systems, but completely, absolutely, catastrophically unacceptable in production. Although, I refuse to blame the junior engineer for such mistakes, I blame management that "sent a boy to do a man's job" (apologies for a dated idiom here).
(As an aside, overall, I disagree with many of the comments here nitpicking the way the author solved the problem, calling her "clueless", etc. I really don't care about that level of detail, and while I agree the solution does not seem ideal, it worked for them better than the previous solution.)
Categorically wrong.
Mistakes, failure, and trial and error are very much a part of developing skills. If you're not making mistakes, you're also not taking enough risks and thus missing out on opportunities for growth.
Everyone - new and experienced developers alike - should have a healthy fear of causing pain to customers. Serving customers - indirectly or otherwise - is what makes businesses run. As a principal engineer today my number one focus is on the customer experience, and I try to instill this same focus in every mentee I have.
Does that mean that junior developers should live in constant fear and decision paralysis? Of course not.
That's where the mentor - the more experienced and seasoned engineer - comes in. The mentor is roughly analogous to the teacher. And the teaching process uses a combination of a mentor, reference materials, and the lab process. The reference materials provide the details; the lab process provides a safe environment in which to learn; and the mentor provides the boundaries to prevent the experiments from causing damage, feedback to the mentee, and wisdom to fill in the gaps between them all. If any of these are absent, the new learner's development will suffer.
Outstanding educational systems were built over the last 3 centuries in the Western tradition using this technique, and other societies not steeped in this tradition have sent their children to us (especially in the last half-century) to do better than they ever could at home. It is a testament to the quality of the approach.
So no, I'm not advocating that junior developers should do nothing until they have read all the books front to back. They'd never gain any experience at all or make essential mistakes they could learn from if they did that. But junior developers should not work in a vacuum - more senior developers who both provide guidance and refer them to reference materials to study and try again, in my experience, lead to stronger, more capable engineers in the long term. "Learning to learn" and "respect what came before" are skills as important as the engineering process itself.
The thing is, specifically, that the OP did NOT have a mentor. They had a serious problem to solve, pronto, and knew enough to take a crack at it. OK, the initial implementation was suboptimal. So what? It's totally normal, learn and try again. Repeat.
It would be nice if every workplace had a orderly hierarchy of talent where everyone takes care to nurture and support everyone else (especially new folks). And where it's OK to ask questions and receive guidance even across organizational silos, where there are guardrails to mitigate accidents and terrible mistakes. If you work in such an environment, you are lucky.
It is far more common to have sharp-elbow/blamestorm workplaces which pretend to value "accountability" but then don't lift a finger to support anyone who takes initiative. I suspect that at the time Rachelbythebay worked in exactly this kind of environment, where it simply wasn't possible for an OPS person to go see the resident database expert and ask for advice.
And when it comes to taking production risks, measure twice and cut once, just like a good carpenter.
[1] Of course you should accept yourself. But that alone won’t advance the profession or one’s career.
Being able to do that is a luxury that many do not enjoy in nose-to-the-grindstone workplaces. You make an estimate (or someone makes it for you), then you gotta deliver mentor or no mentor, and whether you know the finer points of database design/care-and-feeding or not.
There's something good to be said for taking action. Rachelbythebay did just fine. No one died, the company didn't suffer, it was just a problem, it got corrected later. Big whoop.
I learned probably 80% of what I know about DB optimization from reading the Percona blog and S.O. The other 20% came from splashing around and making mistakes, and testing tons of different things with EXPLAIN. That's basically learning in a vacuum.
I've experienced this just by switching languages. C# had many articles dedicated to how much better StringBuilder was compared to String.Concat and yet, other languages would do the right thing by default. I would give advice in a totally different language about a problem that the target language did not have.
As the song goes:
"Be careful whose advice you buy but be patient with those who supply it
Advice is a form of nostalgia, dispensing it is a way of fishing the past
From the disposal, wiping it off, painting over the ugly parts
And recycling it for more than it's worth"
Do you have a specific example that would help in the case described in Rachel's blog post?
I am still making (and finding) occasional indexing and 3NF mistakes. In my experience, it is always humans finding and fixing these issues.
https://docs.microsoft.com/troubleshoot/dotnet/csharp/string...
Some languages have mutable strings, but you would still need to allocate a sufficiently larger buffer if you want to add strings in a loop.
There's a lot of interesting conflations of "right" versus "wrong" in this this thread. There's the C# "best practices" "right" versus "wrong" of knowing when to choose things like StringBuilder over string.Concat or string's + operator overloading (which does just call string.Concat under the hood). There's the "does that right/wrong" apply to other languages question? (The answer is far more complex than just "right" and "wrong".) There's the VM guarantees of .NET CLR's hard line "no string is mutable" setting a somewhat hard "right" versus "wrong" and other language's VMs/interpreters not needing to make such guarantees to themselves or to others. None of these are really ever "right" versus "wrong", almost all of them are trade-offs described as "best practices" and we take the word "best" too strongly especially when we don't know/examine/explore the trade-off context it originated from (and sometimes wrongly assuming that "best" means "universally the best").
Using a StringBuilder, the strings are only copied once when copied into the buffer. If the buffer need to grow, the whole buffer is copied into a larger buffer, but if the buffer grows by say doubling the size, then this cost is amortized, so each string on average is still only copied twice at worst.
Faster yet is concatenating an array of strings in a single concat operation. This avoids the buffer resize issue, since the necessary buffer size is known up front. But this leads to the subtle issue where a single concat is faster than using a StringBuilder, while the cost for multiple appends grows exponentially for concat but only lineary for StringBuilder.
https://github.com/python/cpython/blob/main/Objects/unicodeo...
Rust's ownership system ensures that there cannot exist mutable and immutable references to the same object at the same time. So if you have a mutable reference to a string it is ok to modify in place.
I have not confirmed either the Python example or the Rust example too deeply, since I'm replying to 5 day old comment. But the general principal holds. GC languages like C# and Java don't have easy ways to check ownership so they always copy strings when mutating them. Maybe I'll write a blogpost on this in the future.
This also nicely shows one of the fun things with table scans. They look decent at first then perf goes crappy. Then you get to learn something. In this case it looks like she used normalization to scrunch out the main table size (win on the scan rate, as it would not be hitting the disk as much with faster row reads). It probably also helped just on the 'to' lookups. Think about how many people are in an org, even a large company has a finite number of people but they get thousands of emails a week. That table is going to be massively smaller than keeping it in every row copied over and over. Just doing the 'to' clause alone would dump out huge amounts of that scan it was doing. That is even before considering an index.
The trick with most SQL instances is do less work. Grab less rows when you can. Use smaller tables if you can. Throw out unnecessary data from your rows if you can. Reduce round trips to the disk if you can (which usually conflicts with the previous rules). Pick your data ordering well. See if you can get your lookups to be ints instead of other datatypes. Indexes usually are a good first tool to grab when speeding up a query. But they do have a cost, on insert/update/delete and disk. Usually that cost is less than your lookup time, but not always. But you stick all of that together and you can have a DB that is really performant.
For me the fun one was one DB I worked in they used a GUID as the primary key, and therefore FK into other tables. Which also was the default ordering key on the disk. Took me awhile to make them understand why the perf on look up was so bad. I know why they did it and it made sense at the time to do it that way. But long term it was holding things back and ballooning the data size and crushing the lookup times.
How did I come by all of this? Lots of reading and stubbing my toe on things and watching other do the same. Most of the time you do not get a 'mentor'. :( But I sure try to teach others.
I also find my own team is usually strapped for resources, normally, people. (Usually politely phrased as "time".) Yes, one has to be wary of mythical man-month'ing it, but like my at my last employ we had essentially 2 of us on a project that could have easily used at least one, if not two more people. Repeat across every project and that employer was understaffed by 50-100%, essentially.
Some company just went for S-1, and they were bleeding cash. But they weren't bleeding it into new ventures: they were bleeding it into marketing. Sure, that might win you one or two customers, but I think you'd make much stronger gains with new products, or less buggy products that don't drive the existing customers away.
Also there's an obsession with "NIT" — not invented there — that really ties my hands as an engineer. Like, everything has to be out sourced to some cloud vendor provider whose product only fits some of the needs and barely, and whose "support" department appears to be unaware of what a computer is. I'm a SWE, let me do my thing, once in a while? (Yes, where there's a good fit for an external product, yes, by all means. But these days my job is 100% support tickets, and like 3% actual engineering.)
Now, imagine a new engineer, just getting started trying to make sense of it all, yet being met with harsh criticism and impatience.
Maybe that was exactly her point--to get everyone debating over such a relatively trivial problem to communicate the messages: Good engineering is hard. Everyone is learning. Opinions can vary. Show some humility and grace.
I know I've also had these encounters (specifically in DBs), and I'm sure there are plenty more to come.
I would not expect her to be like this from what I've read on her blog so I was surprised. Well written!
That's definitely not true anymore, if it ever was. Senior/staff level engs with 20+ years of experience are sought after and paid a ton of money.
A lot of the basic issues beginners go through can be mitigated by paying attention in class (and/or having formal education in the first place).
I’ll grant that there are other, similar, things that everyone will go through though.
My own was manually writing an ‘implode’ function in Javascript because I didn’t know ‘join’ existed.
1. Me from six months ago. Clueless about a lot of stuff, made some odd technical choices for obscure reasons, left a lot of messes for me to deal with.
2. Me six months from now. Anything I don't have time for now, I can leave for him. If I was being considerate I'd write better docs for him to work with, but I don't have the time for that either.
The table schema isn't terrible, it's just not great. A good first-pass to be optimized when it's discovered to be overly large.
It depends - if this was the full use-case then maybe a single table is actually a pretty good solution.
while main table did not have any such guarantees, so, there will be a lot of indexing and duplication in atleast 3 (of 4 sub tables)
Moreover, indexing would be much faster and efficient as there are only unique values in sub tables
For eg: if there are multiple emails from same domain name, then domain name column will have same entry multiple times.
When domain is moved to sub table, main table still has duplicate entries, in form of foreign keys, while domain table has unique entry for each domain name
Edit: I guess concatenate with a delimiter if you're worried about false positives with the concat. But it does read like a cache of "I've seen this before". Doing it this way would be compact and indexed well. MD5 was fast in 2002, and you could just use a CRC instead if it weren't. I suppose you lose some operational visibility to what's going on.
My guess is that in 2002 there were some issues making those options unappealing to the Engineering team.
When we do this in the realm of huge traffic then we run the data through a log stream and it ends up in either a KV store, Parquet with SQL/Query layer on top of it, or hashed and rolled into a database (and all of the above of there are a lot of disparate consumers. Weee Data Lakes).
This is also the sort of thing I’d imagine Elastic would love you to use their search engine for.
> A real mail server which did SMTP properly would retry at some point, typically 15 minutes to an hour later. If it did retry and enough time had elapsed, we would allow it through.
There's lots of stuff that I'm bad at. I'm also good at a fair bit of stuff. I got that way, by being bad at it, making mistakes, asking "dumb" questions, and seeing how others did it.
Sounds familiar. And sometimes that other someone is just a later version of yourself :)
I am very grateful when my past self thought to add comments for my future self.
Normalization does not mean "the same string can only appear once". Mapping the string "address1@foo.bar" to a new value "id1" has no effect on the relations in your table. Now instead of "address1@foo.bar" in 20 different locations, you have "id1" in 20 different locations. There's been no actual "deduplication", but that's again not the point. Creating the extra tables has no impact on the normalization of the data.
Of course the main point still stands, that the two schemas are exactly as normalized as each other.
Edit: rereading the original post, the author mentions that "they forged...the HELO"--so perhaps there was indeed no relationship between HELO and IP here. But again, I don't know anything about SMTP, so this could be wrong.
Yep, and that explains the "foobar" rows - those should have resolved to the same IP, except because there's no authentication that blocks it you could put gibberish here and the SMTP server would accept it.
> so correlating based on the hostname in HELO was questionable at best
Eh, spambots from two different IPs could have both hardcoded "foobar" because of the lack of authentication, so I could see this working to filter legitimate/illegitimate emails from a compromised IP.
It can, if you want to easily update an email address, or easily remove all references to an email address because it is PII.
I thought perhaps swapping the strings to integers might make it easier to index, or perhaps it did indeed help with dedupilcation in that the implementation didn't "compress" identical strings in a column—saving space and perhaps help performance. But both issues appeared to be implementation issues with an unsophisticated Mysql circa 2000, rather than a fundamentally wrong schema.
I agreed with her comment at the end about not valuing experience, but proper databasing should be taught to every developer at the undergraduate level, and somehow to the self-taught. Looking at comptia... they don't seem to have a db/sql test, only an "IT-Fundamentals" which touches on it.
To add: Not sure about MySQL, but `varchar`/`text` in PostgreSQL for short strings like those in the article is very efficient. It basically just takes up space equaling the length of the string on disk, plus one byte [1].
[1] https://www.postgresql.org/docs/current/datatype-character.h...
How do you mean? If id1 is unique on table A, and table B has a foreign key dependency on A.id, then yeah you still have id1 in twenty locations but it's normalized in that altering the referenced table once will alter the joined value in all twenty cases.
This might not be important in the spam-graylisting use case, and very narrowly it might be 3NF as originally written, but it certainly wouldn't be if there were any other data attached to each email address, such as a weighting value.
>How do you mean? If id1 is unique on table A, and table B has a foreign key dependency on A.id, then yeah you still have id1 in twenty locations but it's normalized in that altering the referenced table once will alter the joined value in all twenty cases.
Ok, but that's not what normalization means.
If you have a table as described in the article that looks like:
foo | bar | baz
------------------
foo1 | bar1 | baz1
foo2 | bar2 | baz2
foo3 | bar3 | baz3
foo4 | bar1 | baz2
foo3 | bar1 | baz4
and then you say "ok, bar1 is now actually called id1", you now have a table
foo | bar | baz
------------------
foo1 | id1 | baz1
foo2 | bar2 | baz2
foo3 | bar3 | baz3
foo4 | id1 | baz2
foo3 | id1 | baz4
you haven't actually changed anything about the relationships in this data. You've just renamed one of your values. This is really a form of compression, not normalization.
Normalization is fundamentally about the constraints on data and how they are codified. If you took the author's schema and added a new column for the country where the ip address is located in (so the columns are now ip, helo, from, to, and country), then the table is no longer normalized because there is an implicit relationship between ip and country--if ip 1.2.3.4 is located in the USA, every row with 1.2.3.4 as the ip must have USA as the country. If you know the IP for a row, you know its country. This is what 3NF is about. Here you'd be able to represent invalid data by inserting a row with 1.2.3.4 and a non-US country, and you normalize this schema by adding a new table mapping IP to country.
But none of that is what's going on in the article. The author described several fields that have no relationship at all between them--IP is assumed to be completely independent of helo, from address, and to address. And the second schema they propose is in no way "normalizing" anything. The four new tables don't establish any relationship between any of the data.
>This might not be important in the spam-graylisting use case, and very narrowly it might be 3NF as originally written, but it certainly wouldn't be if there were any other data attached to each email address, such as a weighting value.
It's not "very narrowly" 3NF. It's pretty clear-cut! A lot of commenters here are referring to the second schema as the "normalized" one and to me that betrays a fundamental misunderstanding of what the term even means. And sure, if you had a different schema that wasn't normalized, then it wouldn't be normalized, but that's not what's in the article.
While you may technically be correct, I think there’s few people that think of anything else when talking database normalization.
That said, I cannot quickly figure out if it’s actually true here.
This schema is also in Boyce-Codd normal form. It's normalized in every usual sense of the word. Trivially so, even. It's not a question of being "technically" correct. If you think the second schema is more normalized than the first one, you need to re-evaluate your mental model of what normalization means. That's all there is to it.
First of all, the index hashes of the emails depend on the email strings, hence the indexed original schema is not normalised.
Secondly, it would not effectively be compression unless there were in fact dependencies in the data. But we can make many fair assumptions about the statistical dependencies. For example, certain emails/ips occur together more often than others, and so on. In so far as our assumptions of these dependencies are correct, normalisation gives us all the usual benefits.
But databases are just magic. You try things out, usually involving a CREATE INDEX at some point, and sometime it gets faster, so you keep it.
Rachel, in he blog post is a good example of that thought process. She used a "best practice", added an index (there is always an index) and it made her queries faster, cool. I don't blame her, it works, and it is a good reminder of the 3NF principle. But work like that on procedural code and I'm sure we will get plenty of reactions like "why no profiler?".
Many, many programmers write SQL, but very few seem to know about query plans and the way the underlying data structures work. It almost looks like secret knowledge of the DBA caste or something. I know it is all public knowledge of course, but it is rarely taught, and the little I know about is is all personal curiosity.
100%!!!
I hate how developers talk about a "database" as a monolithic concept. It's an abstract concept with countless implementations built off of competing philosophies of that abstract concept. SQL is only slightly more concrete, but there's as many variants and special quirks of SQL dialects out there as databases.
While I agree that how a query planner works is one of the most ‘magic’ aspects, I think the output from the query planner in most databases is very approachable as well to regular common programmers and will get you quite far in solving performance issues. We know what full scans are (searching through every element of an array), etc.
The challenge is usually discovering that the jargon used in your database really maps to something you do already have a concept about and then reading the database documentation…
I cringed a little because some of those mistakes looks like stuff I would do even now. I have a ton on of front/back end experience in a huge variety of languages and platforms, but I am NOT a database engineer. That’s a specific skillset I never picked up nor, admittedly, am I passionate about.
Sorry to go off on a tangent, but it also brings to mind that even so-called experts make mistakes. I watch a lot of live concert footage from famous bands from the 60s to the 90s, and as a musician myself, I spot a lot of mistakes even among the most renowned artist…with the sole exception of Rush. As far as I can tell, that band was flawless and miraculously sounded better live than on record!
And use something other than an RDBMS. Put the hash in Redis and expire the key; your code simply does an existence check for the hash. You could probably handle gmail with a big enough cluster.
That super-normalized schema looks terrible.
> Considering that we as an industry tend to chase off anyone who makes it to the age of 35, is it any surprise that we have a giant flock of people roaming around trying anything that'll work?
Which results in the same problems but the root cause is different.
(And, yes, we had math in the early 2000s.)
Snark aside, I'm frustrated for the author. Her completely-reasonable schema wasn't "terrible" (even in archaic MySQL)—it just needed an index.
There's always more than one way to do something. It's a folly of the less experienced to think that there's only One Correct Way, and it discourages teammates when there's a threat of labeling a solution as "terrible."
Not to say there aren't infinite terrible approaches to any give problem: but the way you guide someone to detect why a give solution may not be optimal, and how you iterate to something better, is how you grow your team.
On the mysql she was using, breaking things out so it only needed to index ints was almost certainly a much better idea. On anything I'd be deploying to today, I'd start by throwing a compound index at the thing with the expectation that'd probably get me to Good Enough.
The database included several newly developed "stored procedures".
Time elapsed... and it was nearing the time to ship the code. So we tried to populate the database. But we could not. It turned out that the stored procedures would only allow a single record in the database.
Since a portion of the "business logic" depended on the stored procedures... well, things got "delayed" for quite a while... and we ended up having a major re-design of the back end.
Fun times.
Why _the hell_ is nobody mentioning that using a database that charges per row touched is absolute insanity? When has it become so normal that nobody mentions it?
This makes me think that these things are at least 100x cheaper than AWS might want to make me believe.
The reason is that the new schema adds a great deal of needless complexity, requires the overhead of foreign keys, and makes it a hassle to change things later.
It's better to stick the the original design and add a unique index with key prefix compression, which all major databases do these days. This means that the leading values gets compressed out and the resulting index will be no larger and no slower than the one with foreign keys.
If you include all of the keys in the index, then it will be a covering index and all queries will hit the index only, and not the heap table.
But strictly from a performance aspect, I agree it's a wash if both were done correctly.
Moreover, unless you can prove with experimental data that the 3rd-normal-form version of the database performs significantly better or solves some other business problem, then I would argue that refactoring it is strictly worse.
There are good reasons not to use email addresses as primary or foreign keys, but those reasons are conceptual ("business logic") and not technical.
There are always costs and benefits to decisions. It seems that are you only looking at the costs and none of the benefits?
There was a whole thread yesterday about how a dude found out that it isn't: https://briananglin.me/posts/spending-5k-to-learn-how-databa... (also mentioned in the RbtB post)
But your point is well-taken. Hardware is cheap.
In that case it’s not obvious to me that putting a key prefix index on every column is the correct thing to do, because that will get toilsome very quick in high write loads.
Given that she herself wrote the before and after systems 20 years ago and that the story was more about everyone having dumb mistakes when they are inexperienced perhaps we should assume the best about her second design?
But i don’t think the point of the post is whats right/wrong way of doing it. The point as mentioned by few here is that programmers makes mistakes. They are costly and will be costly if in tech industry, we continue to boot experienced engineers… the tacit knowledge those engineers have gained wi ll not be passed on and this means more people have to figure things out by themselves
We can debate the Correct Implementation all day long. The fact of the matter is that adding any index to the original table, even the wrong index, would lead to a massive speedup. We can debate 2x or 5x speedups from compression or from choosing a different schema or a different index, but we get 10,000x from adding any index at all.
Adding an index now increases the insert operation cost/time and adds additional storage.
If insert speed/volume is more important than reads keep the indexes away. Replicate and create an index on that copy.
that insert is happening after you've checked the table to see if the record is present. so two operations whose times we care about are "select and accept email" and "select, tell the sender to come back in 20 minutes, and then insert".
the insert time effectively doesn't matter, unless you've decided to abandon discussion of the original table entirely without mentioning it.
Are you implying that isn't the case today? Thousands of (big) companies are still like that and will continue to be like that.
I write my own SQL, design tables and stuff, submit it for a review by someone 10x more qualified than myself, and at the end of the day I'll get a message back from a DBA saying "do this, it's better".
There was a lot of pressure to relax 3NF as being too academic and not practical.
Around then, I had a friend who was using a pattern of varchar primary keys so that queries that just needed the (unique) name and not the metadata could skip the join. We all acted like he was engaging in the Dark Arts.
To my understanding, for example Firebird/Interbase had automatic key prefix compression as far back as early 2000s at the very least. I don't believe you could even turn it off.
Key thing is to use EXPLAIN and benchmark whatever you do. Then the right path will reveal itself...
Did everyone on HN miss that the database in question was whichever version of MySQL existed in 2002?
Well, you also save a space by doing this (though presumably you only need to index the emails as IPs are already 128 bits).
But other than that, I'm also not sure why the original schema was bad.
If you were to build individual indexes on all four rows, you would essentially build the four id tables implicitly.
You can calculate the intersection of hits on all four indexs that match your query to get your result. This is linear in the number of hits across all four indexes in the worst case but if you are careful about which index you look at first, you will probably be a lot more efficient, e.g. (from, ip, to, helo).
Even with a multi index on the new schema, how do you search faster than this?
I've run into this form of greylisting, it's quite annoying. My service sends one-time login links and authorization codes that expire in 15 minutes. If the email gets delayed, the user can just try again, right? Except I'm using AWS SES, so the next email may very well come from a different address and will get delayed again.
* security, an attacker has more time to intercept and use the links and codes.
* UX, making the user wait 15+ minutes to do certain actions is quite terrible.
I've had a couple support requests about this. The common theme seems to be the customer is using Mimecast, and the fix is to add my sender address in a whitelist somewhere in their Mimecast configuration.
Edit: typos
I'm sure a good chunk of tech workers are worried that when an influential blogger writes something like this a couple hundred developers will Google 3NF and start opening PRs.
And hey, what's so bad about people learning about 3NF? Are you not supposed to know what that is until you're some mythical ninth level DBA?
I certainly don't mean to criticize the author or say that they shouldn't have posted the article. If articles needed to be 100% correct then no-one has any business writing them. And it's still open to debate whether the article is even wrong or misleading.
> Are you not supposed to know what that is until you're some mythical ninth level DBA?
I think the world might be a better place if people _did_ wait until they were ninth level DBAs :p
Jokes aside, I have no problem with people learning 3NF. I'm more concerned that people can be too quick to take this sort of rule-of-thumb advice to heart (myself included). And in my opinion, database optimization is too complex to fit well into simple rules like "normalize when your query looks like this." My only 'rules' for high-frequency/critical tables and queries is "test, profile, load test, and take nothing for granted." But I know just enough to know that I don't know enough about SQL databases to intuit performance.
They are important because the present incarnation of the author is making all the wrong diagnoses about the problems with the original implementation, despite doing it with an air of "Yes, younger me was so naive and inexperienced, and present me is savvy and wise".
Or an even better argument: you don't need to actually understand the problem to fix it, often you accidentally fix the problem just by using a different approach.
I agree that we don't know, but it seems a little unfair to her to treat every unknown as definitely being the most negative of the possibilities.
7.4.2 Multiple-Column Indexes MySQL can create composite indexes (that is, indexes on multiple columns). An index may consist of up to 16 columns. For certain data types, you can index a prefix of the column (see Section 7.4.1, “Column Indexes”). A multiple-column index can be considered a sorted array containing values that are created by concatenating the values of the indexed columns. MySQL uses multiple-column indexes in such a way that queries are fast when you specify a known quantity for the first column of the index in a WHERE clause, even if you do not specify values for the other columns
To be more verbose about it - there is an important difference between "can be created" and "will perform sufficiently well on whatever (likely scavenged) hardware was assigned to the internal IT system in question."
I wouldn't be surprised if the "server" for this system was something like a repurposed Pentium 233 desktop with a cheap spinning rust IDE drive in it, and depending on just how badly the spammers were kicking the shit out of the mail system in question that's going to be a fun time.
In other words, with enough empathy and patience, a clueless rookie can grow into a clueless senior engineer!
Rachel usually makes more sense than that. That's why people are nitpicking implementation details.
And in that context, everything makes sense.
I think ageism is a valid concern but its also not a black and white issue because while I know quite a few older folks like me that are still constantly keeping up to date I've also known some people my age or just a little bit older who basically stagnated into irrelevance as experts in now discarded technology who never really moved on, so the line between what is ageism and what is people just not bringing value to the table anymore can be a bit blurry.
I'm by no means trying to suggest ageism isn't an issue in tech, I do see echoes of it when I look around and if I suddenly lost the network of people I work with, have worked with in the past, etc, who understand I'm still very technically 'flexible' for an older person I'm pretty sure it would make it difficult to find a job in the practical sense of getting pre-filtered out often just based on the age thing. So my heart really goes out to people who do find themselves in situations where they are capable but can't find work due to age primarily.
But, uh, the issue is somewhat complicated and very case by case, I think.
Which I guess is saying the article's setup story makes the exact opposite point that it's trying to?
Honestly it almost makes me feel compelled to start writing more for the industry but to be honest, my opinion is most ideas are not great, or not novel, up to and including my own. So writing about them seems likely not to be beneficial. I guess you could argue I could let the industry be the judge of that though.
Years ago someone ask one of my grandkids what I did for a living and he told them "He's a fix it guy". That's because when he'd come over I'd be working on something and he'd ask what I was doing and I'd tell him "I'm fixing" this or that.
There are a lot of "fix it" people here. It's what we do.
id | ip_address | from | to | content | updated_at | created_at
And indexes where you need them. Normalize data like IP addresses, from and to. And you are good to go.It was a revelation to me when I decided to experiment with having a indexed date-ONLY field and using that to search instead. It improved performance by almost two orders of magnitude. In hindsight, this should have been totally obvious if I had stopped to think about how indexing works. But, as is stated in this article, we have to keep relearning the same lessons. Maybe I should write an article about how useless it is to index a datetime field...
EDIT: This was something I ran into about 10 years ago. It is possible there was something else going on at the time that I didn't know about that caused the issue I was seeing. This is an anecdote from my past self. I have not had to use this technique since then, and we were using an on-prem server. It's possible that the rest of the table was not designed well and index I was trying to use was already inefficient for the resources that we had at the time.
e.g. btrees allow greater than/less than comparisons
postgres supports these and many more: https://www.postgresql.org/docs/9.5/indexes-types.html
And yes, please write an article, it would be quite interesting. With data and scripts to reproduce, of course.
Postgres as far as I know uses B-tree by default.
You can switch sort order I think for this as well, so "most recent" becomes more efficient.
Multi-column indexes also work, if you are just searching for first column postgres can still use multi-column index.
One common misstep is having a table with columns like (id, user, date), with an index on (user, id) and on (date, id), then issuing a query like "... WHERE user = 1 and date > ...". There is no optimal index for that query, so the optimizer will have to guess which one is better, or try to intersect them. In this example, it might use only the (user, id) index, and scan the date for all records with that user. A better index for this query would be (user, date, id).
I wouldn't be surprised if you hit some sort of similar strange bug.
Do you mean exact equality is rarely what you want because most of the values are different?
Or are you talking about the negative effect on index size of having so many distinct values?
I think the latter point could be quite database-dependent, eg. BTree de-duplication support was only added in Postgres 13. However, you could shave off quite a bit just from the fact that storing a date requires less space in the index than a datetime.
Furthermore, often times your query will have an "ORDER BY dateTimeCol" value on it (or, more commonly, ORDER BY dateTimeCol DESC). If your index is created correctly, it means it can return rows with a quick index scan instead of needing to implement a sort as part of the execution plan.
The whole point is that indexes (can) prevent full table scans, which are expensive. This is true even if the column(s) you're indexing have high cardinality: but it relies on performant table or index joins (which, if your rdbms is configured with sufficient memory, should be the case).
The cardinality of the index has other ramifications that aren't generally important here.
`WHERE state_date > x AND end_date < x`
You can't index two ranges at once in a b tree!
the under-35-thing is just another reason. another is the ever-so-new technologies that obliterate all previous knowledge of obvious stuff. another is the fact that 'history of computing' is not something that you need to learn to start doing shiny webpages.
then there's the reason that so many ppl bash at universities, at in-person schooling, at work-together places (a.k.a offices).
and because someone is going to ask me what I did to change it - here's what: for almost 20 years now i've been teaching introductory or intermediate classes in-person to hundreds of students, and observing what helps them learn as fast as possible. trying to speed it up, to assist the process. i cana tell you one thing - working together and understanding that you can always learn from someone that is next to your shoulder is of paramount importance.
after all - what good is gender or race diversity in the workplace, if you effectively do not know how to work along these people...? but this is too long for just a post here.
the ability, the skill, to be able to work with someone, with everyone, is what the vast majority of IT top-coders lack. and lack badly and painfully (for those around mostly). look around yourself for examples...
...
the day before I was thinking, that Larry Wall's timtoady principle is really about celebrating diversity in IT, about different approaches to the same problem. and not about celebrating diversity in Perl or any other language.
Plus MySQL kind of rocks. Fight me. There are some interesting optimizations that Percona has written about that may improve the performance of OPs schema. The learning never ends.
TBH I haven't seen a shortage of mentors. In fact most of our dev team is 35+ and are very approachable and collegial and our internal mentoring programs have been extremely successful - literally creating world experts in their specific field. I don't think we're unique in that respect.
I think it's rather unfortunate that the top comment here is a spoiler. Actually reading the post and going on the emotional rollercoaster that the author intended to take you on is the point. Not the punch line.
Agreed. I prefer Postgres for personal projects, but MySQL is a fine database. Honestly, even a relational DB I wouldn't want to use again (DB2...) is still pretty solid to me. The relational model is pretty damn neat and SQL is a pretty solid query language.
I wonder how many people disagree with that last part in particular...
Whether you choose normalized or denormalized depends very much on a) what you need to do now b) what you may need to do in the future. Both are considerations.
Which post is referenced here?
I also believe this to be a tooling issue. It's often opaque what ends up being run after I've done something in some framework (java jpa, django queries whatever). How many queries (is it n+1 issues at bay?), how the queries will behave etc. Locally with little data everything is fine, until it blows up in production. But you may not even notice it blowing up in production, because that relies on someone having instrumented the db and push logs+alarms somewhere. So it's easy to remain clueless.
1. The majority of the performance problem could've and probably should've been summarized as "you need to use an index". (Maybe there were MySQL limitations that got in the way of indexing back then? But these days there aren't.)
2. Everyone makes mistakes! New programmers make mistakes like not knowing about indexes. Experienced programmers make mistakes like knowing a ton about everything and then teaching things in a weird order. All of these things are ok and normal and part of growing, and it's important that we treat each other kindly in the meantime.
The original implementation was the 'obvious' one, and not terrible.
Then experience of the live system + MySQL demonstrated problems with that.
So a non-obvious but better-performing solution was implemented instead.
Great! Junior dev walks away with a mistrust of both long repeated strings appearing in a DB table, AND a mistrust of database internals. A fine lesson indeed.
(And remember, this was the era of the all-powerful DBA who could ruthlessly de-normalise your schema at a moments notice to improve performance).
1) hash of concat(ip & helo & m_from & m_to) 2) time
This was not an issue of being in a vacuum, it was an issue of not reading documentation beforehand, which for some reason is acceptable in the development world
I don't think there are so many people attempting to perform surgery or to build a bridge without reading a number of textbooks on the topic.
And this is why back in the day everyone said RTFM
Sure, I'll try and study to keep myself on my feet, but there is soo much to learn that I'd rather focus on the technologies needed right _now_ to get things done
Way more often than not "thing A has to be done with tech B" and the expectation of getting it done without multiple foot guns in gone
j/k, always nice to review some of this "basic" stuff (that you barely use in everyday work but it's good to have it in the back of your head when changing something in the db schema)
[1] https://shusson.info/post/postgres-experiments-deleting-tabl...
Still not sure why adding an index didn't work instead of breaking out into multiple tables indexed by IDs.
Besides, this use case screams for "just store the hashes and call it a day".
I'm not a database person, but this doesn't seem too bad if there was at least an index on IP. The performance there would probably be good enough for a long time depending on the usage.
But is 6 tables (indexed on the value) really better than 1 table with 6 indexes?
I don't really understand the text, but from what I could gather this is far worse than a forgotten index.
You should set them in most cases, but it doesn't really reduce lookup speed if you don't also add foreign keys.
A database with good string index support isn't doing string comparisons to find selection candidates - at least, not initially.
What a bizarrely confident article.
That's a perfection description of mysql as of 2002.
http://web.archive.org/web/20020610031610/http://www.mysql.c...
Of course, even my old DBase II handbook talks about indexes - all that's old hat. MySQL had them, too.
MySQL also used to have a long-earned reputation as a toy database though, and MySQL in 2002 was right within the timeframe where it established that reputation. So yeah, you could add indexes, but did they speed things up (as in them being an "optimization feature")? Public opinion was rather torn on that.
https://www.databasejournal.com/features/mysql/article.php/2... (to pick a contemporary example using that exact word) suggests that the 2003 release of mysql4 ends the "toy status". Others might have had different thresholds, but that reputation was hard earned and back then still pretty apropos (to a declining degree over time because MySQL caught up). I lost data to MySQL more than once (if memory serves but it's been a while: MyISAM was a mess, the InnoDB integration not ready yet)
To handwave two extreme positions of those days (because I'm too lazy to look up ancient forums where such stuff has been discussed at length), it's very possible that different groups approached things differently:
Members of a PHP User Group probably didn't consider MySQL a toy but the best thing since sliced bread, given how closely aligned PHP was to MySQL back then (it got more varied over time, but that preference can still be felt in PHP projects)
OTOH when you had a significant amount of perl coders in your peer group, it was rather likely that some of them could go on for days about how _both_ PHP and MySQL are toys when anybody dared to talk about PHP (which ties in neatly with the original post and its reference about "The One")
[bonus: more recent example: http://www.backwardcompatible.net/145-Why-is-MySQL-still-a-t...]
Lisp interning, in database tables. A.k.a. Flygweight Pattern.
the 3NF "optimized" form seems worse and unreadable. I would advise to normalize only when it's needed(complex/evolving model), in this case it was not needed
>and still NoSQL has a place and a use case, just like anything else that exists.
What actually makes you believe that it's the NoSQL that has "some use cases" and relational databases are "default ones" instead of NoSQL/no-relational by default?
This case its important to recognize it was 2002, early days. This story just doesnt read the same in todays context.
Otherwise, how long is their data retention? Wouldn’t there be more value in cleaning out entries older than, say, two hours?
I came across something like this with an apprentice recently. I let the lack of a repeatable test pass because speed / time / crunch etc and the obvious how can that possibly fail code of course failed. The most basic test would have caught it in the same way some basic performance testing would have helped rachel however many years back.
It's hard this stuff :-)
I also ran newfs on a running Oracle database. (That did vastly less damage than you would have thought, actually.)
No, it should have had a unique index on the first five columns.
(Oooh, I just hate it so much when someone gets up on their high horse and opens a blog post with a long condescending preamble about how some blogger got it wrong, and then they get it even wronger.)
When I was still wet behind the ears I was doing what you are now proposing - but its overkill.
Will try to keep making money coding
Which is nonsense. The problem with the original DB design is that the appropriate columns weren't indexed. I don't know enough about the problem space to really know if a more normalized DB structure would be warranted, but from her description I would say it wasn't.
Some offices avoid many of the challenging patterns but not the vast majority. A lot of it is natural result of the newness of the industry and the high stakes of investment. Many of the work and organizational model assumptions mean that offering flexibility to employees complicates the already challenging management role. Given the power of those roles, this means workers largely must fit into the mold rather than designing a working relationship that meets all needs. As the needs change predictably over time a mounting pressure develops.
I'm only 17 years in but it has gotten harder and harder to find someone willing to leave me to simply code in peace and quiet and dig my brain into deep and interesting problems.
(Some subtleties on projected values and such, but the point stands.)
There may well be some subtlety to the indexing capabilities of MySQL I’m unaware of though - could easily imagine myself making rookie mistakes like assuming that it has same indexing capabilities. So, to the post’s point - if I were working on a MySQL db I would probably benefit from an old hand’s advice to warn me away from dangerous assumptions.
On the other hand I also remember an extremely experienced MS SQL Server DBA giving me some terrible advice because what he had learned as a best practice on SQL Server 7 turned out to be a great way to not get any of the benefits of a new feature in SQL Server 2005.
Basically, we’re all beginners half the time.
It's a strange role where, in order to do your job well, you kind of have to hyper-specialize in the specific version of the tech that your MegaCorp employer uses, which is usually a bit older, because upgrading databases can be extremely costly and difficult.
In most cases I'd expect just adding an index to the original table to be more efficient, but it depends on the type of the original column and if some data could be de-duplicated by the normalization.
On anything you're likely to be deploying today, just throwing a compound index at it is likely quite sufficient though.
But I think she's if anything been in the industry longer than I have and the versions of mysql I first ran in production were a different story.
So you could've had a indexed column of "fingerprint", like
ip1_blahblah_evil@spammer.somewhere_victim1@our.domain
And indexed this, with only single WHERE in the query.
I don't understand at all how multiple tables thing would help compared to indices, and the whole post seemed kind of crazy to me for that reason. In fact if I had to guess multiple tables would've performed worse.
That is if I'm understanding the problem correctly at all.
"If you want to efficiently fuzzy-search through a combination of firstname + lastname + (etc), it's faster to make a generated column which concatenates them and index the generated column and do a text search on that."
(Doesn't have to be fuzzy-searching, but just a search in general, as there's a single column to scan per row rather than multiple)But also yeah I think just a compound UNIQUE constraint on the original columns would have worked
I'm pretty sure that the degree of normalization given in the end goes also well beyond 3rd-Normal-Form.
I think that is 5th Normal Form/6th Normal Form or so, almost as extreme as you can get:
https://en.wikipedia.org/wiki/Database_normalization#Satisfy...
The way I remember 3rd Normal Form is "Every table can stand alone as it's own coherent entity/has no cross-cutting concerns".
So if you have a "product" record, then your table might have "product.name", "product.price", "product.description", etc.
The end schema shown could be described as a "Star Schema" too, I believe (see image on right):
https://en.wikipedia.org/wiki/Star_schema#Example
This Rachel person is also much smarter than I am, and you can make just about anything work, so we're all bikeshedding anyways!
Let's say you know the lastname (Smith) but only the first letter of the firstname (A) - in the proposed scenario only the first letter of the firstname helps you narrow down the search to records starting with the letter (WHERE combined LIKE "A%SMITH"), you will have to check all the rows where firstname starts with "A" even if their lastname is not Smith. If there are two separate indexed columns the WHERE clause will look like this:
WHERE firstname like "A%" AND lastname = "Smith"
so the search can be restricted to smaller number of rows.
Of course having 2 indexes will have its costs like increased storage and slower writes.
Overall the blog post conveys a useful message but the table example is a bit confusing, it doesn't look like there are any functional dependencies in the original table so pointing to normalization as the solution sounds a bit odd.
Given that the query from the post is always about concrete values (as opposed to ranges) it sounds like the right way to index the table is to use hash index (which might not have been available back in 2002 in MySql).
https://www.postgresql.org/docs/current/textsearch-tables.ht...
Specifically, the part starting at the below paragraph, the explanation for which continues to the bottom of the page:
"Another approach is to create a separate tsvector column to hold the output of to_tsvector. To keep this column automatically up to date with its source data, use a stored generated column. This example is a concatenation of title and body, using coalesce to ensure that one field will still be indexed when the other is NULL:"
I have been told by PG wizards this same generated, concatenated single-column approach with an index on each individual column, plus the concatenated column, is the most effective way for things like ILIKE search as well.But I couldn't explain to you why
I don't know what kind of internal representation is used to store tsvector column type in PG, but I think GIN index lookups should be very simple to parallelize and combine (I believe a GIN index would basically be a map [word -> set_of_row_ids_that_contain_the_word] so you could perform them on all indexed columns at the same time and then compute intersection of the results?). But maybe two lookups in the same index could be somehow more efficient than two lookups in different indexes, I don't know.
I'm still sceptical about the LIKE/ILIKE scenario though. "WHERE firstname LIKE 'A%' and lastname LIKE 'Smith' " can easily discard all the Joneses and whatnot before even looking at the firstname column, whereas "WHERE combined_firstname_and_lastname LIKE 'A%SMITH' " will only be able to reject "Andrew Jones" after reading the entire string.
> I think that is 5th Normal Form/6th Normal Form or so, almost as extreme as you can get
The original schema is also in 5NF, unless there are constraints we aren't privy too.
5NF means you can't decompose a table into smaller tables without loss of information, unless each smaller table has a unique key in common with the original table (in formal terms, no non-trivial join dependencies except for those implied by the candidate key(s)).
The original table appears to meet this constraint: you could break it down into, e.g., four tables, one mapping the quad id to the IP address, one mapping it to the HELO string, etc. However, each of these would share a unique constraint with the original table, the quad id; hence the original table is in 5NF.
As for 6NF, I don't think the revised schema meets that: 6NF means you can't losslessly decompose the table at all. In the revised schema, the four tables mapping IP id to IP address etc. are in 6NF, but the table mapping quad id to a unique combination of IP id, HELO id, etc. is not: it could be decomposed into four tables similarly to how the original table could be.
(Interestingly, if the original table dropped the quad ID column and just relied on a composite primary key of IP address, HELO string, FROM address and TO address, it would be in 6NF.)
It will make sure that incorrect data is detected at the time of storage rather than at some indeterminate time in the future and it will likely be more efficient than arbitrary strings.
And in case of IP addresses, when you are using Postgres, it comes with an inet type that covers both ipv4 and ipv6, so you will be save from requirement changes
In the end you have to normalize anyways. Some databases have specific types. If not you have to pick a scheme and then ideally verify.
foo@example.com other@example.net foo@example.co mother@example.net
Btw, storing just the domains, or inverting the email strings, would have speed up the comparison
Multiple tables could be a big win if long email addresses are causing you a data size problem, but for this use case, I think a hash of the email would suffice.
But this was also back in the early '00s, when "data store" meant "relational DB", and anything that wasn't a RDBMS was probably either a research project or a toy that most people wouldn't be comfortable using in production.
Indeed; her problem looks to have been pre-memcached.
Anything you are doing conditional logic or joins on in SQL usually needs some sort of index.
SELECT INET_NTOA(ip) FROM ips WHERE...
...in MySQL at least. (Which might be why MySQL has this function built in?)I guess if you ever need to match on a specific prefix storing the numeric IP might be an issue?
If you want to search for range, you can do:
SELECT INET_NTOA(ip) FROM ips WHERE ip > INET_NTOA(:startOfRange) AND ip < INET_NTOA(:endOfRange)Modern databases abstract away a lot of database complexity for things like indices. It's true that these days you'd just add an index on the text column and go with it. Depending on your index type and data, the end result might be that the database turns the table into third normal form by creating separate lookup tables for strings, but hides it from the user. It could also create a smarter index that's less wasteful, but the end result is not so dissimilar. Manually doing these kinds of optimisations these days is usually a waste of effort or can even cause performance issues (e.g. that post on the front page yesterday about someone forgetting to add an index because mysql added them automatically).
All that doesn't mean it was probably a terrible design back when it was written. We're talking database tech of two decades ago, when XP had just come out and was considered a memory hog because it required 128MB of RAM to work well.
[0]: http://download.nust.na/pub6/mysql/doc/refman/4.1/en/create-...
The issue is that her analysis of what the issue was with her original table is completely wrong, and it's very weird given that the tone her "present" self is that it's so much more experienced and wise than her "inexperienced, naive" self.
My point is that she should give her inexperience self a break, all that was missing from her original implementation were some indexes.
Being able to look up each id via a single-string unique index would've almost certainly worked much better in those days.
More importantly, given the degree of uniqueness likely to be present in many of those columns (like the email addresses), she could have gotten away with not indexing on every column.
Your second is quite possibly true, but would have required experimentation to be sure of, and at some point "doing the thing that might be overkill but will definitely work" becomes a more effective use of developer time, especially when you have a groaning production system with live users involved.
I'd say there's very little performance gain in normalizing (it usually goes the other way anyway: normalize for good design, avoiding storing multiple copies of individual columns; de-normalize for performance).
I'm a little surprised by the tone of the article - sure, there were universities that taught computer science without a database course - but it's not like there weren't practical books on dB design in the 90s and onward?
I guess it's meant as a critique of the mentioned, but not linked other article "being discussed in the usual places".
A lot of this stuff was invented in the 70s, and was quite sophisticated by 2000. It just wasn't free, rather quite expensive. MySQL was pretty braindead at the time, and my recollection is that even postgres was not that hot either. We've very lucky they've come so far.
However...
Consider the rows `a b c d e` and `f b h i j`. How many times will `b` be stored in the two formulations? 2 and 1, right? The data volume has a cost.
Consider the number of cycles to compare a string versus an integer. A string is, of course, a sequence of numbers. Given the alphabet has a small set of symbols you will necessarily have a character repeated as soon as you store more values than there are characters. Therefore the database would have to perform at least a second comparison. I imagine you're correctly objecting that in this case the string comparison happens either way but consider how this combines with the first point. Is the unique table on which the string comparisons are made smaller? Do you expect that the table with combinations of values will be larger than the table storing comparable values? Through this, does the combinations table using integer comparisons represent a larger proportion of the comparison workload? Clearly she found that to be the case. I would expect it to be the case as well.
No it didn't. Someone else put an index on the main table and that worked.
I think you're remarking about the relationship between normalized tables and indexes. That relationship does exist in some databases.
Please say more if I'm missing your point.
Therefore as long as the size data type (C language's "sizeof") used for an ID is inferior to the average size of the column contents then using an ID will very probably lead to a performance gain.
On some DB states and usage patterns (where commonly used data+index cannot fit in RAM: the caches (DB+OS) hit ratios are < to .99) this gain will be somewhat proportional (beware of diminishing returns) to the value of the ratio (total data+index size BEFORE using IDs)/(total data+index size AFTER using IDs).
Creating queries then becomes more difficult (one has to use 'JOIN'), however there are ways alleviate this: using views, "natural join"...
Some modern DB engines let you put data into an index, in order to spare an access to the data when the index is used (Postgresql: see the "INCLUDE" parameter of the "CREATE INDEX"). As far as I understand using a proper ID (<=> on average smaller than the data it represents) will also lead to a gain(?)
This is what the post is about.
And btw, if they hadn't normalized those tables, it'd instead have been pointed out to them that their "post is bizzare..."
These days, sure, the original schema plus indices would work fine on pretty much anything I can think of. Olde mysqls were a bit special though.
It seems pretty relevant to me that the author mostly writes snarky articles which directly contradict the moral of this story.
For the record, I totally agree with the moral of the story. But we can't just blindly ignore context, and in this case the context is that the author regularly writes articles where someone else is ridiculed because the author has deemed them incompetent.
In a similar vein, no one would just ignore it and praise Zuckerberg if he started discussing the importance of personal privacy on his personal blog.
To me it really clashed with the original post which concludes we need guidance/mentorship/reference points. Most of the comments in Hackernews are constructive or genuinely curious, don't just dismiss the author and instead provide things to learn.
For the author to then write a reply and dismiss all these comments as comments that are really saying "I am THE ONE. You only need to know me. I am better than all of you." is just plain rude and completely counter to their own point.
> It's a massive problem, and we're all partly responsible. I'm trying to take a bite out of it now by writing stuff like this.
suggests to me that maybe she has recently had a change of heart about such rants. I'm willing to give her the benefit of the doubt, because we all need to give each other the benefit of the doubt these days.
If you'd please review https://news.ycombinator.com/newsguidelines.html and stick to the rules when posting here, we'd appreciate it.
Edit: you've been breaking the site guidelines repeatedly. We ban accounts that do that. I don't want to ban you, so please stop doing this!
Some comments on the previous blog post raised important questions regarding minimum understanding/knowledge of technology one utilizes as part of their day job. And I would agree that indexing is a fundamental aspect while using relational databases.
But unfortunately this isn't very uncommon these days, way too often have I heard people in $bigco say, let's use X, everyone uses X without completely understanding the implications/drawbacks/benefits of that choice.
Also, how does performing joins give a performance advantage here. I'm assuming there would be queries to get at the IDs of at least one, but going up to 4, to get at the IDs of the items in the quad. Then there would be a lookup in the mapping table.
I have worked for some time in this industry, but I have never had to deal with relational databases (directly; query tuning and such were typically the domain of expert db people). It would be interesting to see an explanation of this aspect.
EDIT: To people who may want to comment, "You missed the point of the article!": no, I did not, but I want to focus on the technical things I can learn from this. I agree that ageism is a serious problem in this industry.
Joins give a performance advantage due to the fact that you aren't duplicating data unnecessarily. The slow query in question becomes five queries (4 for the data once for the final lookup) which can each be done quickly and if any one of them return nil, you can return a timeout.
It is still not clear how is it better to do up to 5 separate queries or maybe a few joins, than to store the strings and construct indices and such on them? Is the idea that the cost of possibly establishing a new socket connection for some concurrent query execution or sequentially executing 5 queries is still < possible scans of the columns (even with indexing in effect)? Also, even if you had integers, don't you need some sort of an index on integer fields to avoid full table scans anyway?
RTFD! (Read The F**in Docs!) - I only skimmed the post, but its definitely something that would have been avoided had some SQL documentation or introduction been read.
I'd argue one of the huge things that differentiates "senior" developers from all the "other" levels - we're not smarter or more more clever than anyone else - we read up on the tools we use, see how they work, read how others have used them before... I understand this was from 2002, but MySQL came out in 1995 - there was certainly at least a handful of books on the topic.
Perhaps when just starting off as an intern, you may be able to argue that you are 'operating in a vacuum' but any number of introductory SQL books or documentation could quickly reveal solutions to the problem encountered in the post. (Some of which are suggested in these comments).
Of course we all make mistakes in software - I definitely could see myself creating such a schema and forgetting to add any sort of indexing - but when running into performance issues later, the only way you'll be able to know what to do next to fix it is by having read literature about details of the tools you are using.
Operating in a vacuum? Then break out of it and inform yourself. Many people have spent many hours creating good documentation and tutorial on many many software tools - use them.
And in this case, she designed and deployed the system, and it worked and met the business needs for several months. When performance became an issue, she optimized. I'm sure she had plenty of other unrelated work to do in the meantime, especially as a lead/solo dev in the early 2000's.
Sounds like a productive developer to me.