Using SQL Regex to Classify and Filter Messy Data

Categories:
Written by:Sara Nobrega
Messy ratings, jumbled addresses, and buried keywords: SQL regex filters and classifies all of it inside the query, in seven real interview questions.
Text data rarely arrives clean. A stars column stores 4 in one row and four in the next, an address reads Pier 39 here and 39 Pier there, and a free-text field hides the word bull inside bullish. SQL regex (or regular expressions) is how we filter and classify that mess in the query, without exporting it to Python first.
A regular expression in SQL matches a value against a pattern instead of a fixed string. With it, we can keep only rows whose values are all digits, extract a year from a wine title, or count how often a specific word appears across thousands of documents.
We pulled seven interview-style questions from StrataScratch's coding platform to show SQL regex working on real messy data. The examples run on PostgreSQL, and we flag where the syntax changes on MySQL, SQL Server, and Oracle. For every question, we show the table, what one output row means, the solution built up step by step, and the output.

What Is SQL Regex?
A regular expression describes a text pattern: one or more digits, a word with a boundary on each side, an optional prefix, a set of allowed characters. SQL regex is that pattern language wired into the database so you can match, extract, replace, and split text in a query.

In PostgreSQL, the core pieces are small:
~returns true when a value matches a pattern.!~is the negated version.~*and!~*are the case-insensitive forms.regexp_matches(text, pattern, 'g')returns the matches, one row per match, when theg(global) flag is set.regexp_replace(text, pattern, replacement, 'g')rewrites every match.regexp_split_to_table(text, pattern)splits a string into rows.

The patterns themselves use a shared vocabulary.
[0-9] is a character class (any digit). ^ and $ anchor a match to the start and end of the value. + and * are quantifiers (one-or-more, zero-or-more). (plum|cherry) is alternation (this word or that one). \D matches any non-digit. In PostgreSQL, \m and \M mark the start and end of a word, and [[:punct:]] is a POSIX class for punctuation.

A single predicate shows how they combine. stars ~ '^[0-9]+$' reads as: from start (^) to end ($), the value is one or more digits ([0-9]+). Anything with a letter, a space, or a decimal point fails. That one line is the difference between a clean cast and a query that errors on the first four.
Why LIKE Breaks Down
LIKE and ILIKE match a substring with % wildcards. contents ILIKE '%optimism%' is true when optimism appears anywhere in the value, and that same simplicity is where it starts to leak on three fronts:
- False positives:
%...%has no idea where a word starts or ends, so%optimism%matchesoptimism,optimismo, andoptimism-drivenall the same. If the requirement is the exact word,LIKEcannot tell them apart. - Brittle patterns: ruling out each lookalike means adding another
NOT LIKEcondition for every near-miss you happen to spot, and the pattern still misses the one you did not think of. - Unreadable logic: a predicate that has grown three or four
NOT ILIKEexceptions buries the actual intent of the query under a pile of special cases.

On a table where no value contains a lookalike substring, a loose match returns the same rows as a stricter one would, and the question below is a case like that. The moment a value can contain a near-miss, or the requirement says "the word" instead of "mentions," a regex word boundary is the safer choice.
Keyword Filtering: LIKE vs SQL Regex
Find drafts which contains the word 'optimism'
A workplace file-storage system wants to help users quickly locate their own draft files related to a specific topic. Find all files whose name starts with "draft" (regardless of case) and whose content includes "optimism" anywhere within it, also regardless of case. Output all columns for these files.
Data View
google_file_store holds one row per stored file: a filename and the file's contents as free text.
Grain (what one output row means): one file whose name starts with draft and whose contents mention optimism.
Common Mistakes
contents ILIKE '%optimism%' is a substring match: it cannot tell optimism apart from optimismo or optimism-driven, because the substring is present either way. None of the stored files here contain a lookalike, so the loose match returns the same rows as a stricter one would.
A different table might not be so forgiving; once a lookalike substring is possible, LOWER(contents) ~ '\moptimism\M' is the safer pattern, anchoring the match to a whole word with \m and \M. The filename ILIKE 'draft%' half is a fair use of LIKE either way: an anchored prefix, which regex would write as ^draft.
For a deeper look at what LIKE and wildcards can and can't do on their own, see our guide to SQL LIKE Queries for Pattern Matching.
Solution
1) Keep draft files that mention the keyword
Output
| filename | contents |
|---|---|
| draft2.txt | The stock exchange predicts a bull market which would make many investors happy, but analysts warn of possibility of too much optimism and that in fact we are awaiting a bear market. |
Same table, higher stakes next. The next question counts exact word occurrences in the same contents column, where a substring match cannot get away with the shortcut it took here.
SQL Regex for Exact Word Matching
Counting Instances in Text
Find the number of times the exact words bull and bear appear in the contents column.
Count all occurrences, even if they appear multiple times within the same row. Matches should be case-insensitive and only count exact words, that is, exclude substrings like bullish or bearing.
Output the word (bull or bear) and the corresponding number of occurrences.
Data View
Same google_file_store table. We count how many times the exact words bull and bear appear in contents, case-insensitively, counting every occurrence even when a word repeats in one row, and excluding substrings like bullish or bearing.
Grain (what one output row means): one word (bull or bear) and its total number of occurrences across all files.
How This Shows Up In Interviews
The question hands you the failure mode: "exclude substrings like bullish or bearing." That is a direct request for word boundaries, and \m(bull)\M supplies them. A follow-up we have seen is "count every occurrence, not every row." A boolean ILIKE can only tell you whether a row contains the word. regexp_matches(..., 'g') returns one row per match, so a LATERAL join over it followed by COUNT(*) counts repeats inside the same document too.
Solution
1) Count one word with word boundaries
We match bull with a start-of-word (\m) and end-of-word (\M) escape so bullish does not count. The g flag makes regexp_matches emit a row per hit, and the LATERAL join lets us count those rows.
SELECT 'bull' AS word,
COUNT(*) AS nentry
FROM google_file_store,
LATERAL regexp_matches(LOWER(contents), '\m(bull)\M', 'g');2) Stack both word counts (final solution)
Output
| word | nentry |
|---|---|
| bull | 3 |
| bear | 2 |
Same table as the LIKE query, same word, different result. That is the whole case for regex over LIKE when a word boundary matters.
When SQL Regex Is the Right Tool
Reach for regex when the pattern needs more than a fixed substring:

- A character class or shape, like "all digits" or "a letter followed by four numbers."
- A word boundary, so
rosedoes not fire insiderosemary. - Alternation, matching any of several terms in one predicate.
- Extraction, replacement, or splitting driven by a pattern rather than a fixed position.
Stay with plain LIKE for a simple prefix, suffix, or substring: filename ILIKE 'draft%' is clearer and can use an index. Stay with string functions like split_part or substring when the position is fixed and known. And when you find yourself parsing the same field the same way in query after query, the right fix is often a modeled column, which we cover under alternatives.
How to Filter Data with SQL Regex
Filtering is the smallest, most common regex job: keep the rows whose values match a shape, drop the rest. The cleanest example is guarding a cast. A column typed as text can hold 4, four, or an empty string, and :: INTEGER throws on anything that is not a clean number. A regex predicate removes the bad rows before the cast runs.
SQL Regex for Validating Numeric Data
Cast stars column values to integer and return with all other column values
Yelp wants its review ratings stored as clean whole numbers so downstream reporting can rely on them. Convert the star rating for each review to an integer, since some ratings were stored as text and include values that aren't valid whole numbers, such as decimals or non-numeric characters. Exclude any review whose rating isn't a valid whole number.
Output all columns from the reviews table, with the star rating returned as an integer.
Data View
yelp_reviews holds one row per review. The stars column is stored as text and contains both numeric values and non-numeric junk, so a direct cast is unsafe.
Grain (what one output row means): one review whose stars value is entirely digits, with stars returned as an integer.
Validation Checks
Before trusting the cast, confirm the pattern isolates the rows you expect. Run SELECT stars, stars ~ '^[0-9]+$' FROM yelp_reviews and eyeball which values return true. The anchors matter: [0-9]+ without ^ and $ would match 4 stars because it finds digits somewhere inside the value. Anchoring to the whole string is what makes ~ '^[0-9]+$' a safeguard. Note it also rejects decimals like 4.5, since . is not in the class. If half-star ratings exist, the pattern needs to change.
Solution
1) Return all columns for rows where stars is all digits (final solution)
Output
| business_name | review_id | user_id | stars | review_date | review_text | funny | useful | cool |
|---|---|---|---|---|---|---|---|---|
| AutohausAZ | C4TSIEcazRay0qIRPeMAFg | jlbPUcCRiXlMtarzi9sW5w | 5 | 2011-06-27 | Autohaus is my main source for parts for an old Mercedes that has been in the family since new that I am restoring. The old beater is truly a labor of | 1 | 2 | 1 |
| Citizen Public House | ZZ0paqUsSX-VJbfodTp1cQ | EeCWSGwMAPzwe_c1Aumd1w | 4 | 2013-03-18 | First time in PHX. Friend recommended. Friendly waitstaff. They were understaffed and extremely busy, but we never knew. The short ribs were tende | 0 | 0 | 0 |
| Otto Pizza & Pastry | pF6W5JOPBK6kOXTB58cYrw | JG1Gd2mN2Qk7UpCqAUI-BQ | 5 | 2013-03-14 | LOVE THIS PIZZA! This is now one of my favorite pizza places in phoenix area. My husband i walked into this cute family owned business and were greet | 0 | 0 | 0 |
| Giant Hamburgers | QBddRcflAcXwE2qhsLVv7w | T90ybanuLhAr0_s99GDeeg | 3 | 2009-03-27 | ok, so I tried this place out based on other reviews. I had the cheeseburger with everything, some fries, and a chocolate shake. The burger was okay, | 0 | 1 | 1 |
| Tammie Coe Cakes | Y8UMm_Ng9oEpJbIygoGbZQ | MWt24-6bfv_OHLKhwMQ0Tw | 3 | 2008-08-25 | Overrated. The treats are tasty but certainly not the best I have ever had. I would have rated this a two star but the cakes and cookies are REALLY pr | 1 | 3 | 2 |
| Marcellino Ristorante | GTUOIBCEGGt_aGp-bRogfg | gopGuEb-ft6cHKMyCZEvJg | 1 | 2010-12-17 | This place sucks. Food was average and we had to wait an hour even though we had a reservation. My americano tasted like warm soy sauce and they blame | 2 | 3 | 0 |
| Shanghai Club | wHfxd0Bq4JYLiUEO55xe4Q | eVQ_yDkMlF62oofUwc29Kw | 5 | 2013-09-19 | This is our favorite Chinese restaurant in the area! The service is always consistent and our favorite waitress - Becky - always makes time to spend w | 0 | 0 | 0 |
| Freddys Frozen Custard & Steakburgers | NfTR_B1yW1hPVEoXlSJV-w | YnHYlN1m7jDhAH9XgR4Dlg | 4 | 2013-01-24 | Love the tiny fries. | 1 | 0 | 0 |
| Chipotle Mexican Grill | k-Oo0Gs4AC04GJAecu_iWg | HjpzhIQFRQbFmc_7CtFDmg | 4 | 2011-05-11 | When you don't feel like a full restaurant and you want more than the normal Mexican fast food, Chipolte fits the bill. The food is always fresh, the | 0 | 0 | 0 |
| Arizona Fire & Water Restoration | uvVmBnYQf8Mnt-s64D8XOg | zQP7cLujr-MJ207uuNFC1w | 5 | 2011-08-03 | This is a fantastic company. They have a very high rating with BBB and have won a ton of customer service awards. Hopefully you dont need them but i | 0 | 0 | 0 |
| Arriba Mexican Grill | 0HOrc_RX87-01dbdFMSjJw | 8m8HQtZox4vS-N-AW3mzxw | 5 | 2013-12-31 | Open Christmas Day! Their food is delicious. Especially breakfast! Mmmm, the salsa mmmmm. Hatch chills, pork, chicken...pollo asada, carne asada...you | 1 | 1 | 0 |
| Renegade Tap & Kitchen | ZaCA3v9bWUpHuwZ6NO8C1Q | iVTzpbZ6qBdFllvcJLbmeg | 4 | 2011-02-26 | Dinner on a busy Friday night. Arrived on time for 7:30 reservation. Table not ready, restaurant and bar full. Host and hostess pleasant, but not o | 0 | 0 | 1 |
| Chipotle | _gcGIGfziNkhaIlkjhjKHg | 3uU_6L8GnFOHTsO4I3oedg | 3 | 2013-09-24 | I absolutely looove Chipotle. My problem is when you make me pay almost $2 extra for guacamole... please don't be stingy with it. Hook it up, Chipotl | 1 | 2 | 1 |
| SanTan Village | LXiDBkXxcyL4IPnXbjw0VQ | Ba-tIR3a8hhwIk-y_hVzFg | 4 | 2011-03-27 | Next month when the temps skyrocket to triple digits, i will prolly not think soo highly of this place, but for now, San Tan has gotten alot of my fre | 0 | 1 | 2 |
| Love's | Er4Y-yj1JBW9cCbIf3ViKg | bC3By-saT9ylKu-dwWgtcw | 4 | 2012-11-13 | Plenty of gas pumps and convenient to get some cooked food inside. If getting gas, caution for enter and exits at pumps. | 0 | 0 | 0 |
| Dirty Drummer | tMYUWXoFuLdFecqqP60R3A | X_kPh3nt0AJPNPHye2rTlA | 4 | 2011-06-18 | I was introduced to this place by my coworker and friend. She goes here every single day at lunch and its cute because everyone knows her there! Any | 1 | 3 | 2 |
| Euro Pizza Cafe | x2atXyt-QwCTzHhglzxj3Q | JKp42Y520azWI_WBzUMxTw | 5 | 2012-04-07 | This is a nice cafe with a diverse menu. There's indoor or outdoor dining with a view of the famous fountain! I ate here with my friend Connie who als | 0 | 0 | 1 |
| US Airways | o_YetnCcK_96ueIULO84fA | Vl4k0FiMNCRzEQwOpe4hXw | 2 | 2012-07-20 | Well, they have good flights and connections from time to time. Their fleet is very archaic though, probably most of their planes are older than me (a | 0 | 1 | 0 |
| Wal-Mart Neighborhood Market | c7JHcdWo5pZ3rMIDbsDt_g | IDHrwv_RCildFvmfWTkj5Q | 2 | 2011-10-02 | I occasionally stop in this store while driving past. I will try to keep my comments on the positive side. NOTE if you don't read anything else: if | 0 | 1 | 1 |
| Flancer's Cafe | 78XeKBmSE0reBjsmqg7HNg | AYGHNy8gPxl2Q-etTT3hZw | 3 | 2012-12-01 | This place has decent food, cute atmosphere, but the service is problematic. I was stuck in Mesa for training and had lunch there on Halloween. My pal | 5 | 7 | 5 |
| Pappadeaux Seafood Kitchen | y6q-inMFFoEci-wRATp1-A | S3bvMOL50vgS_8-TtlGi4w | 1 | 2010-12-02 | $20 for a double Maker's Mark? Me thinks not T.G.I. McPappadeaux. I was not impressed by anything that was there and will not only steer clear but m | 13 | 6 | 6 |
| Lone Star Steakhouse | fp73RBYM6NAnNWii9bxZ8w | 7Ot-v89x44U_VdIPgD3qKg | 4 | 2011-10-18 | Great food and a rare bird of a honest manager when it comes to whats in the food. They have very comfy booths. Did nor care for the country music. I | 0 | 0 | 0 |
| EVO | 3D3Avu2d8Gj-HEbqqWhswg | H982l-WK1p49z9jZFNMEfQ | 4 | 2013-12-29 | Great atmosphere and decor..a welcome change from the chains and anchor restaurants in the area that all feel the same. My wife could not stop raving | 0 | 0 | 0 |
| Conocido Park | 0ESAQ8Ynk1nZPt5LayMwng | 3OelvbzNK3KSmMdL0O9nRQ | 4 | 2013-10-08 | I play this disc golf course weekly. The baskets are frequently moved to keep the course fresh. Trees provide difficulty and also shade! There are Dis | 0 | 1 | 0 |
| Nate's Barber Shop | KEMsCW33Y1ZQEGTiMKmcrA | Hm_7pViZyrp_Z62lBRopAg | 5 | 2012-12-12 | Nates barber shop is one of the best shops that I have encountered in my life. If your looking for a great inexpensive ($13) good looking haircut this | 0 | 0 | 0 |
| S & S Tire and Automotive Service Center | 99_nV5h4JHomT7cgh0V6lg | hYKjQHu2fk4nMgCWO50f-w | 5 | 2011-02-25 | My partner and I needed two new tires and an alignment. We have a Chevy Cobalt and its only two years old. Went to the dealership and they wanted almo | 1 | 0 | 0 |
| La Parrilla Suiza | ldvKeuzBSIesZEmFcr4ooQ | EXvhtd_05d1H9RlXa2CnIQ | 5 | 2012-07-27 | This food is just....fantastic. I don't know what else to say. I grew up eating at La Parrilla Suiza in Tucson. The tortilla soup and "Queso Suiza" | 0 | 1 | 0 |
| Scratch Pastries & Bistro | znMtXO5hY5XPqAMj_7VLRg | b2DKC4kC8-QeSeGZ_MF3XQ | 5 | 2012-03-16 | Yes, it is in a strip mall. Don't let that fool you. This is some downright good food with affordable prices. Even though service was very attentive | 0 | 0 | 0 |
| Joyride Taco House | uyLuLYfjs3S_8u3OkrIdmw | mY6zzvFbK0ENnQOdgtiT4Q | 2 | 2013-10-19 | Great food! But not worth the HORRIBLE SERVICE! Took about 15 mins for drinks that included a dr pepper. The server said the bartender is working hard | 2 | 3 | 2 |
| In-N-Out Burger | nxoxgQka8mTK-rLCh7sg3w | OwVB3YzcYeTRV09tpNDBSA | 5 | 2011-07-08 | Nothing beats a #1 with grilled onions, no tomatoes, well done french fries and a pink lemonade. I've been known to eat here twice to three times a we | 0 | 0 | 0 |
| Roka Akor | fpjKqP8ONJ9rT82VoUhIQQ | 4ozupHULqGyO42s3zNUzOQ | 5 | 2011-07-18 | I hate to admit it, but it had been a long while since my last visit to Roka Akor. I deserve a hand slap. But last week, I had the perfect excuse to p | 5 | 8 | 10 |
| Ruth's Chris Steak House | z3pSiipCrQM3B6i9PrnoGw | hJBOxmNREXmMGTfXgMcGug | 5 | 2010-03-30 | Best steak I have ever eaten is at Ruth's Chris steakhouse. Comes to your table sizzling hot. Sides are sold individually but are pretty good. The des | 0 | 0 | 0 |
| Yupha's Thai Kitchen | arf6Ne6h0UDXizsbMcOomQ | AkJFqLqHHAKY3H5R8p7cPQ | 5 | 2012-12-09 | Yupha Thai is definitely a "yuppy" in terms of being a great spot to go to. There are not too many places to eat near ASU Research Park, which is rig | 0 | 0 | 0 |
| Hotel Indigo Scottsdale | VDtEMw1X397ViDlP7oErTw | 8Oy9-UwJQWffS0yOwPG6Ew | 4 | 2013-07-01 | I stayed here for one night while on a recent business trip! I wish I had discovered this place sooner.... I was pleasantly surprised! I never heard | 0 | 0 | 0 |
| Da Vˆng | xbVGTBSsXmvu56FTbXp7Aw | F6mQhKLdj_PEdxLvDYOm2Q | 5 | 2011-12-15 | I haven't had better pho anywhere in az. the large pho is enormous for the price. i usually go with my family and order a regular sized pho for myself | 0 | 0 | 0 |
| Trader Joe's | HzI7nVlXJQJR3GO1KXYxlA | cMmQsFyrYBv6hIE6NffqZQ | 5 | 2011-01-03 | Trader Joe's always goes above and beyond in all of their services. The food is fresh and delicious, the prices reasonable and the people are great. | 0 | 0 | 0 |
| Beaver Choice | 18fIpXUbcm9k6Pmtkbf0aA | 3gIfcQq5KxAegwCPXc83cQ | 4 | 2011-04-21 | So I went here tonight with a friend. I was really excited and nervous to try this place after reading the reviews. So you walk in and walk up to the | 1 | 1 | 0 |
| Some Burros | Oqogqje3RKspPwVcREfsXA | GnqNc74So5Pc8C3hkA2hCg | 5 | 2009-07-10 | Came here w/ the hubster's. I've actually been craving this for some time now. The pollo fundido is great! So goooooood! Its a pretty big portion. The | 0 | 2 | 1 |
| Superstition Ranch Market | B1xnRb2j_iW2Ws0u1B0FNw | EOLRikjQxTIpXB4aV1hbPQ | 5 | 2012-01-05 | Great place to shop, buy what you will use within a few days. | 0 | 0 | 0 |
| Fuego Tacos | J6nrjjCjXc-hnRpZZPrLnQ | A99dyhEqcd_yXKPfBWeZHA | 3 | 2011-08-18 | I went to a late lunch on a Saturday and the Esplanade area was quite dead (I'm surprised because in the old days ('98-'99), I remember it to be prett | 0 | 0 | 0 |
| The Woodshed | #NAME? | lsp7p2NuC5MX4_iuch3_OA | 1 | 2012-06-17 | We only went here because it has been a traditional Father's Day event with friends. We decided to join them for the 1st time......really...we will n | 0 | 1 | 0 |
| Rosati's Pizza | Ld4Qg2Du0S3ulcdDCdm7Jg | SEDJTWEzMdqp7UsS1W3KXw | 3 | 2012-10-24 | First of all, let me just say their food is fantastic. I love their pizza. I love their salads. I love the cheesy garlic bread and their chicken parm. | 1 | 0 | 0 |
| Sekai Sushi | 8TB8vM1H_SuEK2hS-5wu7g | TDlgqAxf268QOw-OUk2Urw | 2 | 2012-05-02 | I am sorry to say people of Mesa you must have pretty low standards if this is the best sushi bar in Mesa. I went there last night and I really did no | 0 | 2 | 0 |
| D'Vine Bistro & Wine Bar | JvHH1Z84UJ1P5T9uIxEnyQ | rT4ycOjlrKefSAcjoQga5g | 4 | 2012-04-12 | Love the atmosphere and fantastic happy hour! Favorite spot in Mesa:) | 0 | 1 | 0 |
| Scottsdale Stadium | eAYq_HT_gbD_ECgIWn3GoA | Mt3dPqOlnlGyVCftCcokmg | 4 | 2012-03-27 | Ignoring the fact that Scottsdale Stadium is a bit overpriced these days for Spring Training Giants tickets, its fun. Its basically one big party (in | 0 | 3 | 0 |
| Thai House | #NAME? | fczQCSmaWF78toLEmb0Zsw | 4 | 2008-07-21 | Damn... Helen Y beat me to the punch and got the FTR for this place! Oh well, I will say that she did a great job with her review - I think I was Tha | 3 | 8 | 7 |
| Jalape–o Inferno Bistro Mexicano | VRiSQiIfUnZdp0CxNMkLWg | 4nJ5ryQTcQKs8mCrgt8-BQ | 2 | 2011-04-05 | The dinner we had here was OK... nothing to rave about. The tortilla chips were very good as others have mentioned... a mix of corn and deep fried flo | 0 | 0 | 0 |
| Caffe Boa | hi6a3fvAbtZq9jMIM8gkwQ | qa05pUVNapADHZXpHMPMeA | 3 | 2010-06-26 | Caffe Boa is an interesting jokester, so, it is interestingly hard to review it. I'll keep it short. I have gone here several times, and each time wit | 0 | 0 | 0 |
| Golden Valley | ZYEAmRpYHxJYcbIv-c7S2w | gg_OKjOAl_vVmdh5ZETuiw | 3 | 2012-02-25 | Can I tell the difference between Uzbek cuisine and other Middle Eastern or Mediterranean cuisine? Nope. For all I know it's only a matter of where | 0 | 0 | 0 |
| Canyon Cafe | gi9hLYOPk_fbvOr2mCHu7g | nH9OZEGfgseWjC5_IPGCXw | 5 | 2010-08-09 | Everything about this place is wonderful!! I love the huge windows and outdoor patio! Gorgeous! Their food is amazing everything from the chips (there | 0 | 0 | 1 |
| Panda Express | r52OE-CfRoJQyjBtn0vHIQ | fMyKbyYY9Poy9B_1QZPKcg | 1 | 2010-05-31 | My young children love Panda Express orange chicken. They eat it 2-4 times a month. We were at the mall on a Saturday afternoon and stopped into the f | 0 | 0 | 0 |
| Gordon Biersch Brewery Restaurant | pfPFWY5SXQEEnlVJbFaNqA | nKaR5Z9Qmqc4RsakLLX_7w | 3 | 2010-03-29 | It's very hard for me to enjoy a house brewed schwarzbier that, to me, tastes more like Bud Light with a hint of acrid smoke flavor than what I consid | 2 | 4 | 3 |
| Chili's Grill & Bar | 42EOZ0KMF4wU1Sz7oeLfpA | AfyzIHPy5zds_mqf2Jdc9g | 3 | 2011-02-07 | I love Chili's. My husband and I always go for the 2 for $20 deal at whatever Chili's location we may be, but this location seemed to skimp out on the | 0 | 0 | 0 |
| Beckett's Table | lNLiQx1zi-ctta6v4LLhXw | GJwbccjXgoRPbNuWcNKYXA | 5 | 2012-01-29 | Beckett's Table is a fantastic restaurant for people who want great food, great wine, and great service. My wife and I dined there with a couple of fr | 0 | 0 | 0 |
| My Big Fat Greek Restaurant | IuSys52QuyTxGv3HLFKBSw | 1gY1N3pkxTzh7kK4BxANyw | 4 | 2011-05-07 | I hated Greek food, until I tried this place. My "health nut" girlfriend had to drag me to this place, kicking and screaming. After all, my long ago | 2 | 3 | 3 |
| Goldmans Deli | 6DggWM9rgzC_mIo4THFpMA | PKZvqm3IeWiWBYoDDoEG4w | 5 | 2012-09-02 | Traveled in from the east coast so I got to Arizona very early in the day. I needed to kill some time before I could check into my hotel room so I en | 0 | 0 | 0 |
| Macayo's Depot Cantina | 98nvcyGhtHlKO8pDlOcCsA | bZFRqP7s0Vszxeu8_IwYow | 3 | 2008-03-21 | I finally ate lunch here after not being able to get decent parking for Quiznos on Mill Ave. My coworker and I were starving and needed a place to sh | 0 | 2 | 3 |
| Rayner's Chocolate & Coffee Shop | GCdNDjutQWsT-qaYwW0zxw | M28A6JPQFBJnRBCfODe8IA | 4 | 2013-05-15 | Cute bakery/coffee shop hidden in a little plaza on 51st Ave off of Thunderbird Rd. Nice selection of unique baked goods, chocolates and coffee drinks | 0 | 1 | 0 |
| Sushi Brokers | up3ueFZ1xJh_ts6dVu3_0A | hDlSSyDreM9xY4yQWPm54w | 2 | 2009-02-26 | --expensive for business lunch --servers very attentive, prompt --lacks nth-degree detail of a Japanese chef running things; rolls and standards are s | 4 | 3 | 3 |
| The Vig Uptown | rib7dXO863eL5VGUDsot8g | uQCk37gNl1bEmkjAv6_kAw | 4 | 2011-04-17 | My meal was great; the decor / layout is great also, with a lovely patio out back. The only downsides are parking, and the fact that the entrance is a | 0 | 1 | 0 |
| Lo-Lo's Chicken & Waffles | IlFoK4meMZ7Ws4enESzeTQ | EacK6XwZjsTD6QYSIRlJ7Q | 5 | 2008-07-14 | At first most of my noobie friends are very skeptical about the fusion of Chicken and Waffles. but after taking them to this place and experiencing f | 2 | 3 | 2 |
| Shoe Carnival | YhlJA_CuoZlK4FIJUHlCnw | _PzSNcfrCjeBxSLXRoMmgQ | 2 | 2010-05-17 | I had a $5 coupon in the mail so I was like what the heck. And is it next to Home Goods (one of my favie home decoration stores). I got to the store i | 2 | 1 | 0 |
| My Big Fat Greek Restaurant | Qe0FO565tGfTxb7QtNCVwg | 2vl3MXKr8iQOWTNse5kgdw | 3 | 2008-04-03 | Nice menu selection; food was tasty. Good atmosphere. Wished they had a restaurant in the LA area. | 0 | 1 | 0 |
| Casey Moore's Oyster House | R7ZJPW4qEXuqI41aaWmO0A | rLtl8ZkDX5vH5nAx9C3q5Q | 4 | 2009-04-02 | This is a fun place for appetizers and drinks if it is not too crowded and the temperature is just right outside. Otherwise the inside gets way too p | 1 | 2 | 1 |
| Q to U BBQ | IJxqQwzJjAURPBAB_-iOAA | MSgZpSWlf8T2H_46OWNgCQ | 5 | 2011-09-08 | Really enjoyed the Ribs and fries, I came with friends that really like BBQ, they agree, the ribs were terrific! I am sure we will be going back to Q | 1 | 1 | 1 |
| Lightning Lube | kezCWAz6MO1wKwXB_DK-3Q | bwmXfjwrogAaGqV33kSVpQ | 3 | 2013-08-24 | $115 for an oil change and two air filters for my civic. I must've had a really long day or she had magical powers and made me forget I could simply c | 0 | 1 | 1 |
| Athens Gyros | aAgVzZU2b0YbYGi4byeI6w | L8_GwFxxtGSYR2F_dglpSg | 4 | 2013-08-17 | Great food nice service. The girl that worked up front introduced herself to the other patrons that were there and asked them how the food was but no | 0 | 0 | 0 |
| Metro Light Rail | lG8Swugg_DQxY3NgT_BEig | LqgGgWi3FLHBViX9tmZ9sw | 3 | 2011-10-31 | I just wish that this stupid HUGE metropolis could have more LIGHT RAIL connections!! compared to the circus you have to stand in the buses, the LR is | 1 | 1 | 1 |
| Oregano's Pizza Bistro | 7QvgM_LJi6SRp_GuOXPFZQ | for16MiFS1M_8_cne6IbIw | 4 | 2013-02-04 | Big Delicious portions! | 0 | 0 | 0 |
| Gallo Blanco | gULD5qz_CQI9clPWh2FNHA | Z02XdD0muEz2FFQKPERMYQ | 5 | 2013-12-29 | Stopped in here one night right before Christmas. Short and sweet: Margaritas - very good (and huge by the way) Tacos - awesome Guacamole - exce | 2 | 2 | 2 |
| Los Dos Molinos | JcWhDcyNl3r_Tbeqiac15Q | GoymUzKqvET2QOZkIWZi9w | 4 | 2014-01-07 | We have a friend who said the salsa is way too hot. All I heard was "you must try Los Dos Molinos". We love spicy food and are always up to a challeng | 0 | 1 | 1 |
| Nancy's Nail Salon | ZGo8c57MrzQrSN6R7zO1uQ | A9g7YnTtsSV-wEIo3HI1YQ | 4 | 2011-06-09 | Came here with my sister in law she had a coupon for a 27.99 mani/ spa pedi. The staff was friendly they have a tv and plenty of magazines to look thr | 0 | 1 | 0 |
| Changing Hands Bookstore | 1xzMe1EEwhF23RNh3InKkQ | fPHLPrymsyb6WSFFKoMrTQ | 5 | 2010-10-26 | This not-so-little bookstore has it all... new and used books, a unique gift section, book signings and events, wonderful staff and a cool, organized, | 0 | 0 | 1 |
| Tortilla Fish | OmSYYxZskG9BeRMwb5Dltw | MxO7EY766jVoFEZzkpwmOQ | 2 | 2013-10-06 | My experience wasn't bad, just not up to the hype of all the other reviews. I tried the shrimp, fish, campechana and machaca tacos. The shrimp tacos w | 0 | 0 | 0 |
| Pet Club | 0LvO1yc-52fJ6vIHaFVdAw | LXOhR4ZUULSbBNztxYZ2dQ | 2 | 2013-07-16 | That awkward moment when local competitors come write negative reviews about a store and then direct traffic to their own store.... | 0 | 1 | 1 |
| Taste of Tops | J2lGBvJOcuhmauWs3rgMSg | aIAjAU-6NH583EkQ6E9KRw | 4 | 2009-10-09 | Okay, in interest of full disclosure, I literally live around the corner and across the street from Tops Liqour and have been waiting forever for this | 1 | 1 | 1 |
| Carolina's Mexican Food | N6eg6Jc_mL_XHMGmw6GElw | cbxUyCUMjkWAs1h4auYeAw | 4 | 2012-01-31 | If you're looking for real Mexican in a hurry this is your place. This is one of places ill always bring out of town guests who can't find good Mexica | 0 | 0 | 1 |
| Super L Ranch Market | cK3J7FAqruLZM_Y5J29Q8Q | z06IHGXI_ofBc2DkAbCgnA | 5 | 2011-05-09 | HOLY CRAP THEY HAVE FROZEN XIAOLONGBAO. :) These delicious little bites of porky, soupy dumpling heaven have eluded me since I first tasted them in | 2 | 2 | 1 |
| Salt Cellar | IXGX_Lk2NgCH-0OQNcGMpQ | E4HbTIHd9PVjUnEKpysaLw | 5 | 2011-10-04 | My experience here was absolutely amazing. My boyfriend and I had reservations for 7:30 pm on a Saturday night and the service was amazing and the fo | 0 | 2 | 1 |
| Rosita's Place | f_yQqlsim0S9YAIIYFvR5Q | 7nlZJW84Adt6oYn2shnn_g | 3 | 2013-05-30 | The food here is delicios, but it takes forever. From the time I ordered to when I was served 25minutes. Come only if you have time to spare to sit an | 0 | 0 | 0 |
| Matador Restaurant | x927gFqVNPSOPwNrKqPmmQ | nyHh14Vb9S269-kGKaUelg | 4 | 2011-08-09 | I've eaten at Matador almost once a month for about15 years. I won't get into the logistics of others reviews. I give the higher than average star rat | 0 | 0 | 1 |
| Hanny's | riZp_RIN28ld-U2Q5dhKhA | GRgBu4K7GOb3354esp_xkg | 4 | 2010-06-08 | Food = 4 stars Place = 5 stars Service = 4 stars I REALLY like this place. It looks super classy inside and the deco on both floors is a | 3 | 3 | 3 |
| Crust Pizza and Wine Cafe | rTc3d_GYXyHuf_tQoED80g | AOmdmYYSeLUstcN084_wMA | 1 | 2012-10-16 | I told my friend that I'd rather eat out of a vending machine, and he said.. "Yelp THAT!" I had the calamari app, the caprese salad and the eggplant | 2 | 1 | 0 |
| Ocean Air | 9gLTx4HjE-NeSa3KTfGJJQ | K0U0Hp6rgXHrYCG4jpPT8w | 5 | 2013-09-01 | Thank you! Ocean Air came highly recommended and now I know why!! Excellent, fast, friendly service!!! Reasonably price AC maintenance and FAST respon | 0 | 0 | 0 |
| Sleepy Dog Brewpub | 9gtyU7vjWUjddmFrT97sww | Fm0EXFwIfDQoIm9RgcAOKQ | 3 | 2013-04-06 | As a number of others have said, the beer is good with a good number of choices. The food is pretty good too. The biggest issue has been service. The | 0 | 0 | 0 |
| unPhogettable | yVK0x3_-o16ufBbIyqGJRw | sWh4Tjwa8ch_rziHtTN9LA | 5 | 2013-08-27 | Always amazing service! Always amazing pho! Add veggies to a meat dish! The spring rolls A1 & A6 are the best in town!!! We are now here on a weekly b | 1 | 1 | 1 |
| Paradise Valley Burger Company | bkZ67PfRlKLKl4x5mIFYSg | ff00OcqImnNYy-OvSgUZyw | 5 | 2013-11-04 | Best burgers in town! They don't skimp on quality or ingenuity. | 0 | 0 | 0 |
| Hanny's | 4n_3G2Xux0stcgOUsrzYaw | ev7D2jo5OUDeHf0dWoWlsQ | 2 | 2012-06-25 | Super disappointing, they won't take reservations and I wanted to make a reservation for 20 persons. I've been here many many times and love the food | 0 | 0 | 2 |
| Golden Panda | ikNpO72tj7uI5VTatHpoAA | 80OFMLRA0yW3sE4ciYg_vA | 1 | 2009-10-10 | Why oh why is it so hard to find good Chinese food in this town? The two behind the counter at this place are Chinese - please don't tell me you eat | 0 | 0 | 0 |
| Breakfast Club | U_oJEB166nCeBNY-wqadxw | AMYi-53cxstrCR5wqyY1KA | 5 | 2011-01-11 | O-M-Goodness! What luck to have eaten here for breakfast!! Huge portions served with fresh fruit slices or mixed berries. Great service and very nice | 0 | 0 | 0 |
| Ulta Salon Cosmetics & Fragrance | 9e3MOWg4zrq_NOqKP3fMcQ | F6QsMoJdvtohlbnST-fDyQ | 4 | 2011-03-14 | I really like this store and have been going here for as long as they have been open. 18 or 19 years. It is always exciting to be rewarded for the pro | 1 | 1 | 0 |
| Essence Bakery CafŽ | NkekoPY-4txUxkyoN_Tu4w | DrWLhrK8WMZf7Jb-Oqc7ww | 5 | 2012-09-14 | Ok, I'm not sure I ever had this pastry combination, but it was clearly a great item. It was the Chocolate almond croissant. Usually those two varieti | 0 | 1 | 0 |
| zpizza | UJEPSoO6yNnR8kdneDy0rg | fSi-yrKtBD58h2vPxjNE1A | 4 | 2010-12-01 | We ordered 4 rusticas for delivery using their buy-one-get-one promo online. They had a great deal on rustica pizzas, but somewhere in the fine print | 2 | 1 | 1 |
| Kona Grill | osYRF4FQe4cziGIXz33eQQ | gITFg65GtRDUb-0n460vNg | 4 | 2011-03-17 | I've eaten at Kona Grill twice in two days while doing business in the area. I had the Kona Burger and the Pepperoni Pizza. The burger was fantastic | 0 | 0 | 0 |
| White House | xn2LkVHBuRZ_jAg-LIiQ4Q | wFweIWhv2fREZV_dYkz_1g | 4 | 2011-07-25 | It's been a while since I took part in the nightlife in Scottsdale, it's not my scene but I was invited to White House for a party so you know how tha | 3 | 5 | 4 |
| Crowne Plaza Resort Hotel San Marcos Golf Resort | bMKW11Cf1Zeu1zWkzDbtrQ | Kt9NwDONle_mc0QHTud9jw | 1 | 2011-07-18 | Rating the golf course, horrible!! Thank goodness there was no one in front of us and we zipped around the course. They clearly stopped maintaining | 0 | 0 | 0 |
| Hon Machi | mu8Gst6LkzG5ahmolCH55g | KucBnMrhalzxnD9AWrxwYQ | 5 | 2011-06-21 | Great place for sushi and tepan - period. Not the "high-end" places but a rock solid place with lots of variety and good prices. | 0 | 1 | 0 |
| Bourbon Steak a Michael Mina Restaurant | AA6QQUFGWWkZlbpat46OfQ | cEIeuU0-4fX0Y4qCUW3PwQ | 2 | 2008-12-01 | My husband, some friends and I went to this place for restaurant week. We all sampled different dishes to experience a range of items - the multi-flav | 0 | 1 | 2 |
| Tempe's Front Porch | J71o5dOSoxoOhcR8NEo4Og | R4Ax3btoJ6qLXhqq6J50VQ | 4 | 2014-01-06 | This is the front porch of Monti's, my boyfriend and I were pretty confused looking for it. It's outdoors with a bunch of heating lamps, be sure to s | 1 | 2 | 1 |
| Joyride Taco House | pKe_ORPqaW0vfGyFkbxdHw | xv9nUSKR5RqnkgD0tufTfA | 4 | 2013-12-02 | Tried this place tonight with my boo and I am definitely a fan. He loved his carne asada burrito and my enchiladas were super tasty. They are a littl | 0 | 0 | 0 |
| Lunardis | fpjKqP8ONJ9rT8209thIQQ | 9itypHULqGyO42s3zNUzOQ | 5 | 2018-06-11 | This is the nicest grocery store in the city. I actually met my wife at this grocery store while shopping for avocados. | 6 | 7 | 10 |
The pattern ^[0-9]+$ is the workhorse of numeric filtering. It answers "is this value a plain integer?" and nothing else. We come back to it in the FAQs, and again in the classify section, where the same test decides which token in an address is the house number.
How to Classify Messy Data with SQL Regex
Filtering keeps or drops a row. Classifying labels it. Regex classifies by testing each value against a pattern and using the result to route the row, usually inside a CASE. The address problem below is the clearest example, and it shows regex doing one job while split_part and CASE handle the rest. If CASE syntax itself is rusty, our full guide to CASE WHEN statements covers the basics before you get to the address example below
SQL Regex for Classifying Inconsistent Data
Number of Streets Per Zip Code
Count the number of unique street names for each postal code in the business dataset. Use only the first word of the street name, case insensitive (e.g., "FOLSOM" and "Folsom" are the same). If the structure is reversed (e.g., "Pier 39" and "39 Pier"), count them as the same street. Output the results with postal codes, ordered by the number of streets (descending) and postal code (ascending).
Data View
sf_restaurant_health_violations holds one row per recorded health violation, including the business's business_postal_code and business_address. Addresses are inconsistent: some read 1000 Folsom St, others Pier 39, others 39 Pier.
Grain (what one output row means): one postal code and the number of distinct street names in it.
Trade-offs
The task splits into three jobs, and each tool does the one it is best at. split_part(business_address, ' ', 1) extracts a token by position. ~ '^[0-9]+$' classifies that token as numeric or not. CASE encodes the decision: if token 1 is the number, the street is token 2; if token 2 is the number (the 39 Pier case), the street is token 1; otherwise take token 1. Trying to do all three with regex alone would be harder to read and slower. Splitting the work is the senior move here.
Edge Cases
Two assumptions can break this. First, NULL postal codes: the WHERE business_postal_code IS NOT NULL guard drops them before they reach the GROUP BY, so a NULL code does not become its own bucket. Second, the "first word" rule assumes the street name is a single token, so Van Ness collapses to van. That is acceptable for a rough street count and would be wrong for exact address matching. Naming that assumption out loud is what an interviewer listens for.
Solution
1) Classify each address and extract the street token
For each row, we test which token is the house number and take the other one as the street name, lower-cased so Folsom and folsom collapse.
SELECT
business_postal_code,
business_address,
LOWER(
CASE
WHEN split_part(business_address, ' ', 1) ~ '^[0-9]+$' THEN split_part(business_address, ' ', 2)
WHEN split_part(business_address, ' ', 2) ~ '^[0-9]+$' THEN split_part(business_address, ' ', 1)
ELSE split_part(business_address, ' ', 1)
END
) AS street_name
FROM sf_restaurant_health_violations
WHERE business_postal_code IS NOT NULL;2) Count distinct streets per postal code (final solution)
Output
| business_postal_code | n_streets |
|---|---|
| 94103 | 16 |
| 94133 | 11 |
| 94102 | 10 |
| 94109 | 9 |
| 94107 | 8 |
| 94108 | 8 |
| 94110 | 8 |
| 94112 | 8 |
| 94104 | 7 |
| 94105 | 7 |
| 94114 | 6 |
| 94111 | 5 |
| 94115 | 5 |
| 94122 | 5 |
| 94118 | 4 |
| 94121 | 4 |
| 94132 | 4 |
| 94134 | 4 |
| 94117 | 3 |
| 94123 | 3 |
| 94124 | 3 |
| 94116 | 2 |
| 94127 | 2 |
| 94131 | 1 |
SQL Regex Correctness Pitfalls
Regex is precise about what you write, which means most bugs are patterns that match slightly more or slightly less than you meant. Two traps show up constantly: accidental substring hits, and the empty string that regex returns when nothing matches.
SQL Regex for Handling Word Variations
Aroma-based Winery Search.
A wine curator wants to identify producers whose wines feature specific delicate aromas. Find all wineries that produce wines whose descriptions mention plum, cherry, rose, or hazelnut in singular form. Exclude any wine whose description also contains a plural form of those words (e.g., "cherries", "plums", "roses", or "hazelnuts").
Output the distinct winery names.
Data View
winemag_p1 holds one row per wine review, including the winery and a free-text description of the wine's aromas. We want wineries whose descriptions mention plum, cherry, rose, or hazelnut in the singular, and not the plural forms.
Grain (what one output row means): one distinct winery matching the aroma rule.
Common Mistakes
Three mistakes hide in this one query.
First, no boundaries: description ~ 'rose' matches rosemary and prosecco. The escapes \m and \M pin the match to a whole word.
Second, treating "match the singular" as enough. Matching cherry does not exclude cherries, so the second predicate uses !~ to reject the plurals explicitly.
Third, case. Rather than the case-insensitive operator ~*, this solution first lowercases the column with lower(description) and matches against lowercase patterns, keeping the two predicates consistent. Either approach works; mixing them is where people trip.
Solution
1) Match any of the singular aromas as whole words
Alternation (plum|cherry|rose|hazelnut) matches any one of the four, and \m ... \M keeps each to a whole word.
SELECT DISTINCT winery
FROM winemag_p1
WHERE lower(description) ~ '\m(plum|cherry|rose|hazelnut)\M';2) Exclude the plural forms (final solution)
Output
| winery |
|---|
| Bella Piazza |
| Bodega Noemaa de Patagonia |
| Bodega Norton |
| Bodegas La Guarda |
| Caligiore |
| Camlibag |
| Catalina Sounds |
| C. Donatiello |
| Comtesse Therese |
| Dashwood |
| Geyser Peak |
| Goldeneye |
| Grandes Vinos y Vinedos |
| Hopler |
| Il Poggione |
| La Capilla |
| La Mannella |
| Les Belles Collines |
| Mannina Cellars |
| Martin Ray |
| Niebaum-Coppola |
| Pine Ridge |
| Roagna |
| Sullivan |
| Terra Valentine |
| Valiano |
| Wolffer |
The second pitfall is about what a regex returns when it finds nothing. This next question extracts a year and then has to survive titles that contain no year at all.
SQL Regex for Extracting Numbers from Text
Macedonian Vintages
Find the vintage years of all wines from the country of Macedonia. The year can be found in the 'title' column. Output the wine (i.e., the 'title') along with the year. The year should be a numeric or int data type.
Data View
winemag_p2 holds one row per wine review, including the country and a title that usually contains the vintage year, as in Château 2016 Vranec. We want the year, as a numeric type, for wines from Macedonia.
Grain (what one output row means): one Macedonian wine with its extracted vintage year.
Edge Cases
regexp_replace(title, '\D', '', 'g') deletes every non-digit, collapsing the title down to its digits. The sharp edge: if a title has no digits, the result is not NULL, it is the empty string '', and ''::NUMERIC throws an error that kills the whole query. This is the single most common regex-extraction bug. Regex functions signal "no match" with an empty string, never with SQL NULL, so you have to convert it yourself. NULLIF(..., '') turns the empty result into NULL, the cast becomes safe, and the row survives with year = NULL instead of crashing the query.
Solution
1) Strip every non-digit from the title
SELECT
title,
regexp_replace(title, '\D', '', 'g') AS digits
FROM
winemag_p2
WHERE
country = 'Macedonia';2) Convert the empty case to NULL, then cast (final solution)
Output
| title | year |
|---|---|
| Macedon 2010 Pinot Noir (Tikves) | 2010 |
| Stobi 2011 Macedon Pinot Noir (Tikves) | 2011 |
| Stobi 2011 Veritas Vranec (Tikves) | 2011 |
| Bovin 2008 Chardonnay (Tikves) | 2008 |
| Stobi 2014 uilavka (Tikves) | 2014 |
How to Test SQL Regex Patterns
A regex predicate either matches or it does not, and it will not tell you why. Test patterns before you trust them.

The fastest check is to project the match result next to the value instead of filtering on it. SELECT stars, stars ~ '^[0-9]+$' AS is_int FROM yelp_reviews shows you which rows pass and which fail, so you can spot a value the pattern wrongly keeps or drops. Run it on a sample first with a LIMIT.
For extraction and matching, regexp_matches(..., 'g') is its own test tool: it returns exactly what matched, one row per hit. We used it in the bull/bear count to see every occurrence, and it works the same way for debugging, when you want proof that \m(rose)\M is not firing on rosemary. For splitting, regexp_split_to_table shows the tokens your split actually produced, which is how you catch an empty token from a double space or a stray delimiter.
Two checks catch most bugs: compare the count of matched rows against the count of unmatched rows and confirm the split makes sense, and spot-check the boundary cases by hand (a value with no match, a value with a repeat, a NULL).
SQL Regex Performance Trade-Offs
Regex is evaluated per row, and it does not use a standard B-tree index the way an anchored LIKE 'abc%' can. On a few thousand rows, this never matters. On tens of millions, it does.
Two costs are worth naming. First, a regex in a WHERE clause scans every row and runs the engine on each value. If a cheaper condition can shrink the set first, put it before the regex, the way the aroma query already filters on lowercase and the street query guards on IS NOT NULL. Second, functions that produce rows multiply the work: regexp_split_to_table in the word-frequency query turns each document into one row per word, and regexp_matches(..., 'g') in the bull/bear count does the same per match. A table with long text fields can explode into a very large intermediate result.
When a regex filter runs constantly on a large table, the durable fix is to stop parsing at query time. Compute the parsed value once and store it, either as a generated column or during load, and index that. We return to this under alternatives.
Alternatives to SQL Regex
Regex is not always the cheapest or clearest tool. Before reaching for it, weigh the alternatives.

Plain LIKE and ILIKE win for a fixed prefix, suffix, or substring. filename ILIKE 'draft%' is readable and index-friendly, and no regex improves on it.
String functions win when the position is known. split_part, substring, left, right, position, and translate handle fixed-shape parsing without a pattern engine. The street query leans on split_part for exactly this reason and uses regex only for the numeric test.
Full-text search wins for real-word search at scale. PostgreSQL's tsvector and tsquery are built for matching words across large document sets with stemming and an index, which is a better fit than a table scan of regexp_matches when search is a core feature rather than a one-off.
Data modeling wins when you parse the same field the same way repeatedly. If every query strips a year out of a title, the year belongs in its own column, populated once, indexed, and validated on write. That turns a per-query regex into a one-time cost.
Preprocessing wins when the same cleanup runs on every read. The word-frequency query strips punctuation with regexp_replace(t.contents, '[[:punct:]]', '', 'g') every time it runs, and every other query against contents would redo that same work. Clean the text once, when the row is loaded, and later queries read already-clean text instead of paying for the regex again.
Derived columns win when the same extraction runs on every read. If every query pulls a year out of a wine title with regexp_replace and a NULLIF guard, the year belongs in its own column instead, computed once, either as a generated column or during load. Filtering on year = 2016 is also cheaper than re-evaluating NULLIF(regexp_replace(title, '\D', '', 'g'), '')::NUMERIC = 2016 on every row.
Normalization wins when one field packs more than one fact. business_address holding a house number and a street name jammed into one string is why the street query needs split_part and a regex test just to work out which token is which. Splitting that column into street_number and street_name at write time turns the three-tool question the street query answers into a plain GROUP BY on a column that already holds the right value.

Common SQL Regex Use Cases
Across the questions above, the same handful of jobs keep coming up:

- Validate: keep only values that match a shape, as
^[0-9]+$does for the star ratings. - Extract: pull a structured value out of free text, as
\Dstripping does for the vintage year. - Clean and normalize: strip or standardize characters before further work.
- Mine text: count exact words across documents, as the
bull/bearquery does. - Classify: label or route a row by which pattern it matches, as the address
CASEdoes.
Cleaning and tokenizing show up so often that they deserve their own example. The next question stacks two regex tools to turn a text column into a word-frequency table.
SQL Regex for Cleaning and Tokenizing Text
Count Occurrences Of Words In Drafts
Find the number of times each word appears in the contents column across all rows in the google_file_store dataset. Output two columns: word and occurrences.
Data View
The same google_file_store table. We count how many times each distinct word appears across all the contents values.
Grain (what one output row means): one distinct word and how many times it occurs in total.
Trade-offs
This is the "clean, then split" recipe. regexp_replace(t.contents, '[[:punct:]]', '', 'g') strips punctuation using the POSIX class [[:punct:]], so optimism, and optimism normalize to the same token. Then regexp_split_to_table(..., E'\\s+') splits the cleaned text on runs of whitespace into one row per word. The E'\\s+' is an escape-string literal: the E prefix lets \\s mean "whitespace," and + collapses multiple spaces into a single split so you do not get empty tokens. Doing the same cleanup with nested replace() calls would be longer and would still miss cases the character class covers for free.
Solution
1) Clean punctuation and split into one row per word
SELECT t.filename,
regexp_split_to_table(regexp_replace(t.contents, '[[:punct:]]', '', 'g'), E'\\s+') AS word
FROM google_file_store t;2) Group and count each word (final solution)
Output
| word | occurrences |
|---|---|
| market | 6 |
| a | 5 |
| and | 4 |
| the | 4 |
| of | 4 |
| investors | 4 |
| stock | 3 |
| make | 3 |
| many | 3 |
| happy | 3 |
| which | 3 |
| bull | 3 |
| would | 3 |
| exchange | 3 |
| predicts | 3 |
| are | 2 |
| optimism | 2 |
| but | 2 |
| much | 2 |
| possibility | 2 |
| too | 2 |
| warn | 2 |
| bear | 2 |
| fact | 2 |
| awaiting | 2 |
| analysts | 2 |
| we | 2 |
| that | 2 |
| in | 2 |
| their | 1 |
| as | 1 |
| predicting | 1 |
| always | 1 |
| is | 1 |
| best | 1 |
| game | 1 |
| uncertain | 1 |
| all | 1 |
| an | 1 |
| should | 1 |
| practices | 1 |
| follow | 1 |
| instincts | 1 |
| future | 1 |
We cover regexp_split_to_table and its string-to-array cousins in more depth in our guide to string manipulation in SQL.
SQL Regex Syntax by Database
The patterns are mostly portable. The operators and functions around them are not. The ~ operator, the regexp_* functions, and the \m/\M word boundaries used above are PostgreSQL-specific. Here is how the common jobs translate.

Two notes matter in interviews. SQL Server had no native regex operator for years; teams worked around it with limited LIKE bracket classes or CLR functions. SQL Server 2025 added REGEXP_LIKE, REGEXP_REPLACE, REGEXP_SUBSTR, and related functions, so newer patterns now match those in the table. And SQLite has a REGEXP operator only when the host application registers a regexp() function, so it is not available by default. StrataScratch runs PostgreSQL, which is why the solutions above use ~ and the regexp_* family.
LIKE vs Regex vs String Functions vs Data Modeling
Four tools, one decision. The honest answer is that each owns a range, and picking well is the skill. The decision rules follow from the table. If the pattern is a fixed substring, use LIKE.

If it is a shape or needs a boundary, use regex. If the position is fixed, string functions are clearer and faster. And if the same parse runs in query after query on a large table, model the value into its own column so you pay the cost once. A senior answer names the trade-off out loud: a regex in an ad hoc query is fine, but the same regex in a nightly job over a hundred million rows is a reason to add a parsed column.
SQL Regex Best Practices
- Anchor when you mean the whole value.
^[0-9]+$checks the entire string;[0-9]+alone matches digits anywhere inside it. - Use word boundaries for whole-word matches.
\m word \Mstopsrosefrom firing insiderosemary. - Guard casts against the empty string.
regexp_replacereturns'', notNULL, when nothing matches, so wrap it inNULLIF(..., '')before casting. - Decide case handling once. Either lowercase the column and match lowercase patterns, or use
~*throughout. Do not mix the two.

- Test with
regexp_matchesbefore filtering. Project the match result next to the value so you can see what passes and what fails. - Filter cheaply first. Put an indexable or low-cost condition ahead of the regex to reduce the row set.
- Watch functions that produce rows.
regexp_split_to_tableand globalregexp_matchesmultiply rows and can blow up an intermediate result. - Know your dialect.
~andregexp_*are PostgreSQL; MySQL, Oracle, and SQL Server 2025 use theREGEXP_*functions instead.
Conclusion
SQL regex earns its place on the jobs LIKE and string functions cannot do cleanly: matching a shape, requiring a word boundary, extracting a value from free text, or classifying a row by pattern. The seven questions here walk that range, from a one-line digit filter to a word-frequency table built out of regexp_replace and regexp_split_to_table.

The two habits that separate a working query from a fragile one are worth repeating: anchor and bound your patterns so they match exactly what you mean, and handle the empty string that regex hands back on no match. Get those right, test the pattern before you trust it, and keep an eye on cost at scale, and regex becomes a reliable way to filter and classify messy data without leaving the database.
FAQs

Is SQL Regex Slow?
It can be on large tables, because a regex predicate runs per row and cannot use a standard B-tree index the way an anchored LIKE 'abc%' can. On small and medium tables, the difference is negligible. When it matters, filter with a cheaper condition first, and if the same regex runs constantly, precompute the parsed value into an indexed column instead of matching at query time.
How Do I Match Only Numbers Using SQL Regex?
Anchor a digit class to the whole value: col ~ '^[0-9]+$' is true only when the value is one or more digits from start to end. To extract the digits from a mixed string instead of filtering, strip the non-digits with regexp_replace(col, '\D', '', 'g'). Note that ^[0-9]+$ rejects decimals and negatives, so widen the class if you need those.
How Do I Classify Rows Using Regex in SQL?
Put the regex test inside a CASE. Test each value against a pattern and use the result to assign a label or pick a branch, as the address query does: CASE WHEN split_part(addr,' ',1) ~ '^[0-9]+$' THEN ... END. The regex decides the category; the CASE records the decision.
How Do NULL Values Behave with Regex?
A regex match against NULL returns NULL, not true or false, so a row with a NULL value never passes a ~ filter. The other direction is the trap: regexp_replace and regexp_matches return an empty string, never NULL, when nothing matches. Wrap the result in NULLIF(..., '') if you need a real NULL before casting or aggregating.
Should I Use Regex to Validate Email Addresses in SQL?
For a rough check, yes: a pattern like col ~ '^[^@\s]+@[^@\s]+\.[^@\s]+$' catches obviously malformed values. A fully correct email regex is enormous and still cannot confirm the address exists. Use a simple pattern for shape validation in the database, and verify deliverability with a confirmation email rather than a longer regex.
Can Regex Replace Data Normalization?
No. Regex cleans and reshapes text at read time, as the word-frequency query does when it strips punctuation and splits on whitespace. Normalization changes how the data is stored, so every query benefits and the cost is paid once. Use regex to prototype the cleanup and handle genuinely ad hoc text, and move repeated parsing into the schema when a field is read the same way over and over.
Share