Mysql: Display query results not containing text patterns from another table - mysql

Trying to display query result only if a text pattern from a table does not appears in the product name.
+-----------------------------------------+ +--------+
| Product Name | |pattern |
+-----------------------------------------+ +--------+
|Gangster Barbie with guns & accessories | | Gun |
|Very Safe Playdoh | | Drug |
|Star Wars Lego | | nam |
|Transformers Decepticon Druglord | | |
|GTA: Namcat Version | | |
+-----------------------------------------+ +--------+
Would like to have result:
+-----------------------------------------+
| Product Name |
+-----------------------------------------+
|Very Safe Playdoh |
|Star Wars Lego |
+-----------------------------------------+
I've tried LIKE or INSTR such as:
select `Product_Name`
from Product_table
where NOT LIKE '%'+(select `text_pattern`.`Keywords` from `text_pattern`)+'%';
but none seems to work properly. Could someone please help or point in the right direction?

Here is one method using not exists:
select p.*
from product p
where not exists (select 1
from patterns pat
where p.name like concat('%', pat.pattern, '%')
);
Note: MySQL does not use + for string concatenation. It uses the concat() function.

E.g.
DROP TABLE IF EXISTS product;
CREATE TABLE product
(name VARCHAR(50) NOT NULL PRIMARY KEY);
INSERT INTO product VALUES
('Gangster Barbie with guns & accessories'),
('Very Safe Playdoh'),
('Star Wars Lego'),
('Transformers Decepticon Druglord'),
('GTA: Namcat Version');
DROP TABLE IF EXISTS patterns;
CREATE TABLE patterns
(pattern VARCHAR(12) PRIMARY KEY);
INSERT INTO patterns VALUES
('Gun'),('Drug'),('nam');
SELECT * FROM product a LEFT JOIN patterns b ON a.name LIKE CONCAT('%',b.pattern,'%');
+-----------------------------------------+---------+
| name | pattern |
+-----------------------------------------+---------+
| Gangster Barbie with guns & accessories | Gun |
| GTA: Namcat Version | nam |
| Star Wars Lego | NULL |
| Transformers Decepticon Druglord | Drug |
| Very Safe Playdoh | NULL |
+-----------------------------------------+---------+

Related

How can I refine this query?

You might want to have a look at my previous question.
My database schema looks like this
--------------- ---------------
| candidate 1 | | candidate 2 |
--------------- \ --------------
/ \ |
------- -------- etc
|job 1| | job 2 |
------- ---------
/ \ / \
--------- --------- --------- --------
|company | | skills | |company | | skills |
--------- --------- ---------- ----------
Here's my database:
mysql> describe jobs;
+--------------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------+------+-----+---------+----------------+
| job_id | int(11) | NO | PRI | NULL | auto_increment |
| candidate_id | int(11) | NO | MUL | NULL | |
| company_id | int(11) | NO | MUL | NULL | |
| start_date | date | NO | MUL | NULL | |
| end_date | date | NO | MUL | NULL | |
+--------------+---------+------+-----+---------+----------------+
.
mysql> describe candidates;
+----------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+----------+------+-----+---------+----------------+
| candidate_id | int(11) | NO | PRI | NULL | auto_increment |
| candidate_name | char(50) | NO | MUL | NULL | |
| home_city | char(50) | NO | MUL | NULL | |
+----------------+----------+------+-----+---------+----------------+
.
mysql> describe companies;
+-------------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+---------------+------+-----+---------+----------------+
| company_id | int(11) | NO | PRI | NULL | auto_increment |
| company_name | char(50) | NO | MUL | NULL | |
| company_city | char(50) | NO | MUL | NULL | |
| company_post_code | char(50) | NO | | NULL | |
| latitude | decimal(11,8) | NO | | NULL | |
| longitude | decimal(11,8) | NO | | NULL | |
+-------------------+---------------+------+-----+---------+----------------+
.
Note that I should probably call this skill_usage, as it indicates when a skill was use don a job.
mysql> describe skills;
+----------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+---------+------+-----+---------+-------+
| skill_id | int(11) | NO | MUL | NULL | |
| job_id | int(11) | NO | MUL | NULL | |
+----------+---------+------+-----+---------+-------+
.
mysql> describe skill_names;
+------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+----------+------+-----+---------+----------------+
| skill_id | int(11) | NO | PRI | NULL | auto_increment |
| skill_name | char(32) | NO | MUL | NULL | |
+------------+----------+------+-----+---------+----------------+
So far, my MySQL query looks like this:
SELECT DISTINCT can.candidate_id,
can.candidate_name,
can.candidate_city,
j.job_id,
j.company_id,
DATE_FORMAT(j.start_date, "%b %Y") AS start_date,
DATE_FORMAT(j.end_date, "%b %Y") AS end_date,
s.skill_id
FROM candidates AS can
INNER JOIN jobs AS j ON j.candidate_id = can.candidate_id
INNER JOIN companies AS co ON j.company_id = co.company_id
INNER JOIN skills AS s ON s.job_id = j.job_id
INNER JOIN skill_names AS sn ON s.skill_id = s.skill_id
AND sn.skill_id = s.skill_id
ORDER by can.candidate_id, j.job_id
I am getting output like this, but am not satisfied with it
+--------------+----------------+---------------------+--------+------------+------------+------------+----------+
| candidate_id | candidate_name | candidate_city | job_id | company_id | start_date | end_date | skill_id |
+--------------+----------------+---------------------+--------+------------+------------+------------+----------+
| 1 | Pamela Brown | Cardiff | 1 | 3 | 2019-01-01 | 2019-08-31 | 1 |
| 1 | Pamela Brown | Cardiff | 1 | 3 | 2019-01-01 | 2019-08-31 | 2 |
| 1 | Pamela Brown | Cardiff | 1 | 3 | 2019-01-01 | 2019-08-31 | 1 |
| 1 | Pamela Brown | Cardiff | 2 | 2 | 2018-06-01 | 2019-01-31 | 3 |
| 1 | Pamela Brown | Cardiff | 3 | 1 | 2017-11-01 | 2018-06-30 | 4 |
| 1 | Pamela Brown | Cardiff | 3 | 1 | 2017-11-01 | 2018-06-30 | 5 |
| 1 | Pamela Brown | Cardiff | 3 | 1 | 2017-11-01 | 2018-06-30 | 6 |
| 1 | Pamela Brown | Cardiff | 4 | 3 | 2016-08-01 | 2017-11-30 | 1 |
| 2 | Christine Hill | Salisbury | 5 | 2 | 2018-02-01 | 2019-05-31 | 3 |
Now, I would like to restrict the search, by specifying "skill", like Python, C, C++, UML, etc and company names
The user will enter something like Python AND C++ into a skill search box (and/or Microsoft OR Google into a company name search box).
How do I feed that into my query? Please bear in mind that each skill ID has a job Id associated with it. Maybe I first need to convert the skill names from the search (in this case Python C++) into skill Ids? Even so, how do I include that in my query?
Te make a few things clearer:
both the skills & company search box can be empty, which I will interpret as "return everything"
search terms can include the keywords AND and OR, with grouping brackets (NOT is not required). I am happy enough to parse that in PHP & turn it into a MySQL query term (my difficulty is only with SQL, not PHP)
It looks like I made a start, with that INNER JOIN skills AS s ON s.job_id = j.job_id, which I think will handle a search for a single skill, given its ... name ? ... Id?
I suppose my question would be how would that query look if, for example, I wanted to restrict the results to anyone who had worked at Microsoft OR Google and has the skills Python AND C++?
If I get an example for that, I can extrapolate, but, at this point, I am unsure whether I want more INNER JOINs or WHERE clauses.
I think that I want to extend that second last line AND sn.skill_id = s.skill_id by paring the skills search string, in my example Python AND C++ and generating some SQL along the lines of AND (s.skill_id = X ), where X is the skill Id for Python, BUT I don't know how to handle Python AND C++, or something more complex, like Python AND (C OR C++) ...
Update
Just to be clear, the users are technical and expect to be able to enter complex searches. E.g for skills: (C AND kernel)OR (C++ AND realtime) OR (Doors AND (UML OR QT)).
Final update
The requirements just changed. The person that I am coding this for just told me that if a candidate matches the skill search on any job that he ever worked, then I ought to return ALL jobs for that candidate.
That sounds counter-intuitive to me, but he swears that that is what he wants. I am not sure it can even be done in a single query (I am considering multiple queries; a first t get the candidates with matching skills, then a second to get all of their jobs).
The first thing I'd say is that your original query probably needs an outer join on the skills table - as it stands, it only retrieves people whose job has a skill (which may not be all jobs). You say that "both the skills & company search box can be empty, which I will interpret as return everything" - this version of the query will not return everything.
Secondly, I'd rename your "skills" table to "job_skills", and your "skill_names" to "skills" - it's more consistent (your companies table is not called company_names).
The query you show has a duplication - AND sn.skill_id = s.skill_id duplicates the terms of your join. Is that intentional?
To answer your question: I would present the skills to your users in some kind of pre-defined list in your PHP, associated with a skill_id. You could have all skills listed with check boxes, or allow the user to start typing and use AJAX to search for skills matching the text. This solves a UI problem (what if the user tries to search for a skill that doesn't exist?), and makes the SQL slightly easier.
Your query then becomes:
SELECT DISTINCT can.candidate_id,
can.candidate_name,
can.candidate_city,
j.job_id,
j.company_id,
DATE_FORMAT(j.start_date, "%b %Y") AS start_date,
DATE_FORMAT(j.end_date, "%b %Y") AS end_date,
s.skill_id
FROM candidates AS can
INNER JOIN jobs AS j ON j.candidate_id = can.candidate_id
INNER JOIN companies AS co ON j.company_id = co.company_id
INNER JOIN skills AS s ON s.job_id = j.job_id
INNER JOIN skill_names AS sn ON s.skill_id = s.skill_id
AND skill_id in (?, ?, ?)
OR skill_id in (?)
ORDER by can.candidate_id, j.job_id
You need to substitute the question marks for the input your users have entered.
EDIT
The problem with allowing users to enter the skills as free text is that you then have to deal with case conversion, white space and typos. For instance, is "python " the same as "Python"? Your user probably intends it to be, but you can't do a simple comparison with skill_name. If you want to allow free text, one solution might be to add a "normalized" skill_name column in which you store the name in a consistent format (e.g. "all upper case, stripped of whitespace"), and you normalize your input values in the same way, then compare to that normalized column. In that case, the "in clause" becomes something like:
AND skill_id in (select skill_id from skill_name where skill_name_normalized in (?, ?, ?))
The boolean logic you mention - (C OR C++) AND (Agile) - gets pretty tricky. You end up writing a "visual query builder". You may want to Google this term - there are some good examples.
You've narrowed down your requirements somewhat (I may misunderstand). I believe your requirements are
I want to be able to specify zero or more filters.
A filter consists of one or more ANDed skill groups.
A skill group consists of one or more skills.
Filters are ORed together to create a query.
To make this concrete, let's use your example - (A and (B OR C)) OR (D AND (E OR F)). There are two filters: (A and (B OR C)) and (D AND (E OR F)). The first filter has two skill groups: A and (B OR C).
It's hard to explain the suggestion in text, but you could create a UI that allows users to specify individual "filters". Each "filter" would allow the user to specify one or more "in clauses", joined with an "and". You could then convert this into SQL - again, using your example, the SQL query becomes
SELECT DISTINCT can.candidate_id,
can.candidate_name,
can.candidate_city,
j.job_id,
j.company_id,
DATE_FORMAT(j.start_date, "%b %Y") AS start_date,
DATE_FORMAT(j.end_date, "%b %Y") AS end_date,
s.skill_id
FROM candidates AS can
INNER JOIN jobs AS j ON j.candidate_id = can.candidate_id
INNER JOIN companies AS co ON j.company_id = co.company_id
INNER JOIN skills AS s ON s.job_id = j.job_id
INNER JOIN skill_names AS sn ON s.skill_id = s.skill_id
AND
(skill_id in (A) and skil_id in (B, C))
OR
(skill_id in (D) and skil_id in (E, F))
ORDER by can.candidate_id, j.job_id
Building a bit off previous comments and answers... if handling input like
(A and (B OR C)) OR (D AND (E OR F))
is the blocker you could try moving some of the conditional logic out of the joins and filter instead.
WHERE (
((sn.skill_id LIKE 'A') AND ((sn.skill_id LIKE ('B')) OR (sn.skill_id LIKE('C'))))
AND ((co.company_id IN (1,2,3)) AND ((can.city = 'Springfield') OR (j.city LIKE('Mordor'))))
)
You can build your query string based off used input, search out Id's for selected values and put them into the string and conditionally build as many filters as you like. Think about setting up add_and_filter and add_or_filter functions to construct the <db>.<field> <CONDITION> <VALUE> statements.
$qs = "";
$qs .= "select val from table";
...
$qs .= " WHERE ";
if($userinput){ $qs += add_and_filter($userinput); }
alternately, look at a map/reduce pattern rather than trying to do it all in SQL?

How to calculate count of each value in MySQL JSON array?

I have a MySQL table with the following definition:
mysql> desc person;
+--------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | text | YES | | NULL | |
| fruits | json | YES | | NULL | |
+--------+---------+------+-----+---------+-------+
The table has some sample data as follows:
mysql> select * from person;
+----+------+----------------------------------+
| id | name | fruits |
+----+------+----------------------------------+
| 1 | Tom | ["apple", "orange"] |
| 2 | John | ["apple", "mango"] |
| 3 | Tony | ["apple", "mango", "strawberry"] |
+----+------+----------------------------------+
How can I calculate the total number of occurrences for each fruit? For example:
+------------+-------+
| fruit | count |
+------------+-------+
| apple | 3 |
| orange | 1 |
| mango | 2 |
| strawberry | 1 |
+------------+-------+
Some research shows that the JSON_LENGTH function can be used but I cannot find an example similar to my scenario.
You can use JSON_EXTRACT() function to extract each value ("apple", "mango", "strawberry" and "orange") of all three components of the arrays, and then then apply UNION ALL to combine all such queries:
SELECT comp, count(*)
FROM
(
SELECT JSON_EXTRACT(fruit, '$[0]') as comp FROM person UNION ALL
SELECT JSON_EXTRACT(fruit, '$[1]') as comp FROM person UNION ALL
SELECT JSON_EXTRACT(fruit, '$[2]') as comp FROM person
) q
WHERE comp is not null
GROUP BY comp
Indeed If your DB's version is 8, then you can also use JSON_TABLE() function :
SELECT j.fruit, count(*)
FROM person p
JOIN JSON_TABLE(
p.fruits,
'$[*]' columns (fruit varchar(50) path '$')
) j
GROUP BY j.fruit;
Demo
You can't do it without first creating a table with one row per fruit.
CREATE TABLE allfruits (fruit VARCHAR(10) PRIMARY KEY);
INSERT INTO allfruits VALUES ('apple'), ('orange'), ('mango'), ('strawberry');
There is not a good way to generate this from the JSON.
Once you have that table, you can join it to the JSON and then use GROUP BY to count the occurrences.
SELECT fruit, COUNT(*) AS count
FROM allfruits
JOIN person ON JSON_SEARCH(person.fruits, 'one', fruit) IS NOT NULL
GROUP BY fruit;
Output:
+------------+-------+
| fruit | count |
+------------+-------+
| apple | 3 |
| mango | 2 |
| orange | 1 |
| strawberry | 1 |
+------------+-------+
Note that it will do a table-scan on the person table to find each fruit. This is pretty inefficient, and as your person table gets larger, it will become a performance problem.
If you want to optimize for this type of query, then you shouldn't use JSON to store an array of fruits. You should store data in a normalized way, representing the many-to-many relationship between persons and fruits with another table.
This is related to my answer to Is storing a delimited list in a database column really that bad?
I think the simplest solution would be to use JSON_TABLE function.
The query you need is
select ft.fruit, count(ft.fruit) from person,
json_table(
fruits,
'$[*]' columns(
fruit varchar(128) path '$'
)
) as ft
group by ft.fruit
;
You can find working example in this dbfiddle
Fruit demo

mysql update and insert statements based on another table

I need some help with the mysql statements for inserting and updating rows in a new table based on the contents of another table. I am going to use this in automated perl code, but the mysql statements themselves are what I am having trouble with.
My first table named PROFILE looks something like this:
+----------+---------------------------+
| ID | NAME |
+----------+---------------------------+
| 0 | Default profile |
| 04731470 | Development profile |
| 87645420 | Core Base |
| a41401a0 | Core Test |
| ba0e3000 | Development profile child |
| e37fe780 | Test2 |
+----------+---------------------------+
The second called DEPLOYMENT has these columns (and no rows yet):
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| PROF_ID | char(36) | NO | PRI | NULL | |
| NAME | varchar(60) | NO | | NULL | |
| ID | tinyint(4) | NO | MUL | NULL | |
+------------+-------------+------+-----+---------+-------+
ID.PROFILE is the foreign key for PROF_ID.DEPLOYMENT and I want all of the values for ID.PROFILE to go in PROF_ID.DEPLOYMENT. Then I want the NAME.DEPLOYMENT and ID.DEPLOYMENT fields to be set based on the words found in the NAME.PROFILE field.
The following shows what I want to do as far as the insert statements goes, but these failed due to "ERROR 1242 (21000): Subquery returns more than 1 row":
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID) VALUES((select ID from PROFILE where NAME like '%core%'),'Core','2');
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID) VALUES((select ID from PROFILE where NAME like '%development%'),'Dev','3');
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID) VALUES((select ID from PROFILE where NAME not like '%development%' and not like '%core%'),'Default','1');
I'm not sure where to start on the update part of this but the ID.DEPLOYMENT and NAME.DEPLOYMENT fields should change as above if the text in the NAME.PROFILE fields changes with any of the words above.
This is the resulting DEVELOPMENT table I am looking for.
+----------+---------------+----+
| PROF_ID | NAME | ID |
+----------+---------------+----+
| 0 | Default | 1 |
| 04731470 | Dev | 3 |
| 87645420 | Core | 2 |
| a41401a0 | Core | 2 |
| ba0e3000 | Dev | 3 |
| e37fe780 | Default | 1 |
+----------+---------------+----+
Then I want statements to update if any of the NAME.PROFILE information changes.
Sorry if this is confusing, I wasn't sure how to explain and I am still learning mysql. Any help is appreciated.
Just get rid of the values keyword, basically:
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID)
select ID, 'Core','2'
from PROFILE
where NAME like '%core%';
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID)
select ID, 'Dev', '3'
from PROFILE
where NAME like '%development%';
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID)
select ID, 'Default', '1'
from PROFILE
where NAME not like '%development%' and not like '%core%';
By the way, you could combine these into one statement, using conditional expressions:
INSERT INTO DEPLOYMENT(PROF_ID,NAME,ID)
select ID,
(case when NAME like '%core%' then 'Core'
when NAME like '%development%' then 'Dev'
else 'Default'
end)
(case when NAME like '%core%' then '2'
when NAME like '%development%' then '3'
else '1'
end)
from PROFILE;

Specification in like query

I can't figure out what's wrong with this query. I just want to search in a specific genre, but this query gives me results of other genres. Would really like some help. This code is for my first site built in php.
SELECT *
FROM news
WHERE (titel LIKE '%keyword%') AND genre='politics'
First, I'd suggest reading up on pattern matching in MySQL. The documentation link is: http://dev.mysql.com/doc/refman/5.0/en/pattern-matching.html
SQL pattern matching enables you to use “_” to match any single character and “%” to match an arbitrary number of characters (including zero characters). In MySQL, SQL patterns are case-insensitive by default. Some examples are shown here. You do not use = or <> when you use SQL patterns; use the LIKE or NOT LIKE comparison operators instead.
Examples
To find names beginning with “b”:
mysql> SELECT * FROM pet WHERE name LIKE 'b%';
+--------+--------+---------+------+------------+------------+
| name | owner | species | sex | birth | death |
+--------+--------+---------+------+------------+------------+
| Buffy | Harold | dog | f | 1989-05-13 | NULL |
| Bowser | Diane | dog | m | 1989-08-31 | 1995-07-29 |
+--------+--------+---------+------+------------+------------+
To find names ending with “fy”:
mysql> SELECT * FROM pet WHERE name LIKE '%fy';
+--------+--------+---------+------+------------+-------+
| name | owner | species | sex | birth | death |
+--------+--------+---------+------+------------+-------+
| Fluffy | Harold | cat | f | 1993-02-04 | NULL |
| Buffy | Harold | dog | f | 1989-05-13 | NULL |
+--------+--------+---------+------+------------+-------+
To find names containing a “w”:
mysql> SELECT * FROM pet WHERE name LIKE '%w%';
+----------+-------+---------+------+------------+------------+
| name | owner | species | sex | birth | death |
+----------+-------+---------+------+------------+------------+
| Claws | Gwen | cat | m | 1994-03-17 | NULL |
| Bowser | Diane | dog | m | 1989-08-31 | 1995-07-29 |
| Whistler | Gwen | bird | NULL | 1997-12-09 | NULL |
+----------+-------+---------+------+------------+------------+
To find names containing exactly five characters, use five instances of the “_” pattern character:
mysql> SELECT * FROM pet WHERE name LIKE '_____';
+-------+--------+---------+------+------------+-------+
| name | owner | species | sex | birth | death |
+-------+--------+---------+------+------------+-------+
| Claws | Gwen | cat | m | 1994-03-17 | NULL |
| Buffy | Harold | dog | f | 1989-05-13 | NULL |
+-------+--------+---------+------+------------+-------+
Your modified query
So, you want your query to select rows from the NEWS table and select everything where the TITLE is like a keyword and GENRE is equal to a particular string.
Following is how I would structure your query. Performance considerations are not present in this query, but you may want to investigate indexes and other performance enhancers if this is a frequent query on a large table.
Originally, you are using:
SELECT * FROM news WHERE (title LIKE '%keyword%') AND genre='politics'
Option 1:
SELECT *
FROM news
WHERE
title LIKE '%keyword%'
AND
genre = 'politics';
Option 2:
You should think about where your input is coming from and if you need to parameterize the fields before you insert them into your query. Using parameters can lead to cleaner, more flexible code as well providing security and type checking.
Try something like this. This untested code, so there might be some syntax issues, but it should be close.
private string title = "%title%";
private string genre = "genre";
using (MySqlConnection con = new MySqlConnection(connectionString))
{
using (MySqlCommand cmd = new MySqlCommand())
{
cmd.Connection = con;
cmd.CommandText
= "SELECT * FROM NEWS WHERE TITLE LIKE '#title' AND GENRE = #genre; ";
cmd.Parameters.AddWithValue("#title", title);
cmd.Parameters.AddWithValue("#genre", genre);
using (MySqlDataReader drd = cmd.ExecuteReader())
{
while (drd.Read())
{
// Read from data reader
}
}
}
}

MySQL: how to convert to EAV - Part 3?

Previous Related Posts:
MySQL: how to convert to EAV?
MySQL: how to convert to EAV - Part 2?
Given a table:
TABLE: foo
===============================
| id | first_name | last_name |
===============================
| 1 | John | Doe |
| 2 | Jane | Smith |
| 3 | Ronald | McDonald |
-------------------------------
How do I take this table and convert it to these tables (an EAV implementation)?:
TABLE: attribute
===========================
| id | fk_id | attribute |
===========================
| 1 | 100 | first_name |
| 2 | 100 | last_name |
---------------------------
TABLE: value
=========================================
| id | attribute_id | row_id | value |
=========================================
| 1 | 1 | 1 | John |
| 2 | 2 | 1 | Doe |
| 3 | 1 | 2 | Jane |
| 4 | 2 | 2 | Smith |
| 5 | 1 | 3 | Ronald |
| 6 | 2 | 3 | McDonald |
-----------------------------------------
NOTES:
attribute.fk_id will be provided.
value.row_id is used to identify how the values are grouped as records in the original table.
UPDATE: Also, how do I query the EAV tables so that I can make it look like table foo again.
I give +1 to #Phil's solution for populating the EAV table. Insert one attribute at a time.
Here's another solution to reverse an EAV transformation:
SELECT v.row_id AS id,
MAX(IF(a.attribute='first_name',v.value,NULL)) AS first_name,
MAX(IF(a.attribute='last_name',v.value,NULL)) AS last_name
FROM value v INNER JOIN attribute a
ON v.attribute_id = a.id
GROUP BY v.row_id
Except that by using EAV, you've put all your values into a column of VARCHAR(255) or whatever, so you have lost information about the respective data types of the original columns.
There's really no way to do it "dynamically" without hard-coding the attribute names, either as joins as #Phil shows, or as columns as I show. It's essentially the same problem as trying to write dynamic pivot queries.
I have written more about EAV in my presentation Practical Object-Oriented Models in SQL and in my book, SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming.
I think your only hope is if you use the foo table. bar is essentially useless without the ID.
Try something like this (assuming attribute.id is an auto-increment primary key)
INSERT INTO `attribute` (`fk_id`, `attribute`)
VALUES (100, 'first_name');
INSERT INTO `value` (`attribute_id`, `row_id`, `value`)
SELECT LAST_INSERT_ID(), `id`, `first_name`
FROM `foo`;
INSERT INTO `attribute` (`fk_id`, `attribute`)
VALUES (100, 'last_name');
INSERT INTO `value` (`attribute_id`, `row_id`, `value`)
SELECT LAST_INSERT_ID(), `id`, `last_name`
FROM `foo`;
To reconstruct the foo table, try this
SELECT
`fn`.`row_id` AS `id`,
`fn`.`value` AS `first_name`,
`ln`.`value` AS `last_name`
FROM `value` `fn`
INNER JOIN `attribute` `fn_a`
ON `fn`.`attribute_id` = `fn_a`.`id`
AND `fn_a`.`attribute` = 'first_name'
INNER JOIN `value` `ln`
ON `fn`.`row_id` = `ln`.`row_id`
INNER JOIN `attribute` `ln_a`
ON `ln`.`attribute_id` = `ln_a`.`id`
AND `ln_a`.`attribute` = 'last_name'
Ergh, thanks for reminding me why I hate this pattern