MySQL filter query with relation - mysql

I'm having the following problem with 2 MySQL tables that have a relation:
I can easily query table 1 (address) when I want a full list or filter the result by name or email or such. But now I need to query table 1 and filter it based on the relational content of table 2 (interests). So, I need to find a row (usually many rows) in table 1 only if a (or more) conditions are met in table 2.
Here are the tables:
CREATE TABLE IF NOT EXISTS `address` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`email` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`countryCode` char(2) COLLATE utf8_unicode_ci DEFAULT NULL,
`languageCode` char(2) COLLATE utf8_unicode_ci DEFAULT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `emailUnique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
INSERT INTO `address` (`id`, `name`, `email`, `countryCode`, `languageCode`, `timestamp`) VALUES
(1, '', 'dummy#test.com', 'BE', 'nl', '2010-07-16 14:07:00'),
(2, '', 'test#somewhere.com', 'BE', 'fr', '2010-07-16 14:10:25');
CREATE TABLE IF NOT EXISTS `interests` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`address_id` int(11) unsigned NOT NULL,
`cat` char(2) COLLATE utf8_unicode_ci NOT NULL,
`subcat` char(2) COLLATE utf8_unicode_ci NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `address_id` (`address_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
INSERT INTO `interests` (`id`, `address_id`, `cat`, `subcat`, `timestamp`) VALUES
(1, 1, 'aa', 'xx', '2010-07-16 14:07:00'),
(2, 1, 'aa', 'yy', '2010-07-16 14:07:00'),
(3, 2, 'aa', 'xx', '2010-07-16 14:07:00'),
(4, 2, 'bb', 'zz', '2010-07-16 14:07:00')
(5, 2, 'aa', 'yy', '2010-07-16 14:07:00');
ALTER TABLE `interests`
ADD CONSTRAINT `interests_ibfk_1` FOREIGN KEY (`address_id`) REFERENCES `address` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
For example, I need to find the address(es) that has (have) as interest cat=aa and subcat=xx. Or, another example, I need the address(es) with as interest both cat=aa and subcat=xx AND cat=aa and subcat=yy. Specially the latter is important and one has to keep in mind that both the address and the interest tables will be long lists and that the amount of cat/subcat combinations will vary. I'm working with reference queries through Zend_Db_Table (findDependentRowset) at the moment but that solution is way to slow for address lists numbering 100s and even 1000s of hits.
Thank you for your help.

SELECT a.name FROM address a
INNER JOIN interests i ON (a.id = i.address_id)
WHERE i.cat = "aa" AND i.subcat IN ('xx', 'yy')

I added another row in your interests table, to demonstrate a different result set between the two examples:
INSERT INTO interests VALUES (6, 2, 'aa', 'vv', '2010-07-16 14:07:00');
Then you may want to try using correlated subqueries as follows:
SELECT *
FROM address a
WHERE EXISTS (SELECT id
FROM interests
WHERE address_id = a.id AND
(cat = 'aa' and subcat = 'xx'));
Result:
+----+------+--------------------+-------------+--------------+---------------------+
| id | name | email | countryCode | languageCode | timestamp |
+----+------+--------------------+-------------+--------------+---------------------+
| 1 | | dummy#test.com | BE | nl | 2010-07-16 14:07:00 |
| 2 | | test#somewhere.com | BE | fr | 2010-07-16 14:10:25 |
+----+------+--------------------+-------------+--------------+---------------------+
2 rows in set (0.00 sec)
For the second example, we're testing for the new row added previously in order not to have the same result as above:
SELECT *
FROM address a
WHERE EXISTS (SELECT id
FROM interests
WHERE address_id = a.id AND
(cat = 'aa' and subcat = 'xx')) AND
EXISTS (SELECT id
FROM interests
WHERE address_id = a.id AND
(cat = 'aa' and subcat = 'vv'));
Result:
+----+------+--------------------+-------------+--------------+---------------------+
| id | name | email | countryCode | languageCode | timestamp |
+----+------+--------------------+-------------+--------------+---------------------+
| 2 | | test#somewhere.com | BE | fr | 2010-07-16 14:10:25 |
+----+------+--------------------+-------------+--------------+---------------------+
1 row in set (0.00 sec)
Using correlated subqueries is easy and straightforward. However keep in mind that it might not be the best in terms of performance, because the correlated subqueries will be executed once for each address in the outer query.

Related

Count clause -> incorrect count value

We have an issue using a counting combination with inner/left join that we cannot figure out how to solve.
We would appreciate any help on the matter!
We have 4 tables in the example:
1: providers: Including 2 providers
2: providers_categories: Including 2 categories. 1 provider can be in multiple categories (this seems to be causing the issue)
3: connections_providers: connecting the providers to the categories
4: reviews_providers: currently we have included 1 rating per provider
Goal: to output the review count from the table reviews_providers.
Issue: Provider 2 is included in 2 categories. The review count is doubled: 1 count for each provider category: A total of 2 reviews are printed even though only 1 entry exists.
Thank you!
Code:
SELECT prov.id, prov.title, prov_cat.title AS category, AVG(reviews.rating) AS rating, COUNT(reviews.rating) AS count
FROM connections_providers_categories conn
INNER JOIN providers_categories prov_cat
ON prov_cat.id = conn.category_id
LEFT JOIN reviews_providers reviews
ON reviews.provider_id = conn.provider_id
INNER JOIN providers prov
ON prov.id = conn.provider_id
GROUP BY prov.id
ORDER BY prov.title ASC
CREATE TABLE `connections_providers_categories` (
`provider_id` int(4) UNSIGNED NOT NULL,
`category_id` int(4) UNSIGNED NOT NULL
) ENGINE=MyISAM DEFAULT;
INSERT INTO `connections_providers_categories` (`provider_id`, `category_id`) VALUES
(1, 1),
(2, 1),
(2, 2);
CREATE TABLE `providers` (
`id` int(4) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT;
INSERT INTO `providers` (`id`, `title`) VALUES
(1, 'Provider 1'),
(2, 'Provider 2');
CREATE TABLE `providers_categories` (
`id` int(4) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` varchar(60) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT;
INSERT INTO `providers_categories` (`id`, `title`) VALUES
(1, 'Category 1'),
(2, 'Category 2');
CREATE TABLE `reviews_providers` (
`id` int(4) UNSIGNED NOT NULL AUTO_INCREMENT,
`provider_id` int(4) UNSIGNED NOT NULL,
`rating` enum('1','2','3','4','5') DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT;
INSERT INTO `reviews_providers` (`id`, `provider_id`, `rating`) VALUES
(1, 2, '5'),
(2, 1, '3');
Our question might resemble the following question, but we do not find the answer / see that it is the same case even thought both questions include multiple counts: count is multiplied after adding left join
It seems we might need a subquery, but we are not sure how to do this.
Any suggestions?
Thanks!
you can use subquery top get your result
SELECT prov.id, prov.title, GROUP_CONCAT(prov_cat.title) AS category, reviews.rating , reviews.count
FROM connections_providers_categories conn
INNER JOIN providers_categories prov_cat
ON prov_cat.id = conn.category_id
LEFT JOIN (SELECT provider_id, AVG(rating) AS rating, COUNT(provider_id) AS count FROM reviews_providers GROUP BY provider_id) reviews
ON reviews.provider_id = conn.provider_id
INNER JOIN providers prov
ON prov.id = conn.provider_id
GROUP BY prov.id,prov.title
ORDER BY prov.title ASC
id | title | category | rating | count
-: | :--------- | :-------------------- | -----: | ----:
1 | Provider 1 | Category 1 | 3 | 1
2 | Provider 2 | Category 2,Category 1 | 5 | 1
db<>fiddle here

mysql GROUB BY idea

I have the following scenario: there are 1 table with books and two couples of tables (HD/IT) with Sales Order and Purchase Order transactions connecting through Sales Order id.
The table structure follows:
CREATE TABLE `books` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`isbn` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`it_id` int(11) NOT NULL,
`kind` tinyint(4) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `books` (`id`, `isbn`, `it_id`, `kind`) VALUES
(1, '12345', 1, 1),
(2, '12345', 1, 2),
(3, '67890', 2, 1),
(4, '1111111', 2, 2);
CREATE TABLE `porders_hd` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dt` date NOT NULL,
`so_id` int(11) DEFAULT NULL,
`customer` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `porders_hd` (`id`, `dt`, `so_id`, `customer`) VALUES
(1, '2017-07-02', 1, 1),
(2, '2017-08-03', NULL, 3);
CREATE TABLE `porders_it` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hd_id` int(11) NOT NULL,
`isbn` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`dscr` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`qty` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `porders_it` (`id`, `hd_id`, `isbn`, `dscr`, `qty`) VALUES
(1, 1, '12345', 'Book 1', 1),
(2, 2, '1111111', 'Book 2', 1);
CREATE TABLE `sorders_hd` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dt` date NOT NULL,
`customer` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `sorders_hd` (`id`, `dt`, `customer`) VALUES
(1, '2017-07-01', 1),
(2, '2017-08-01', 2);
CREATE TABLE `sorders_it` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`hd_id` int(11) NOT NULL,
`isbn` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`dscr` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`qty` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `sorders_it` (`id`, `hd_id`, `isbn`, `dscr`, `qty`) VALUES
(1, 1, '12345', 'Book 1', 1),
(2, 2, '67890', 'Book 2', 1);
In summary there are:
* 1 Sales Order (#1) also existing in the Purchase Order (#1)
* 1 Sales Order (#2) still pending
* 1 Purchase Order (#2) created without a Sales Order
I want to be able to grab all Sales and Purchases Order per book's isbn and the connected SO and PO must be in the same line. The output must be like the one below:
so_id so_date po_id po_date isbn dscr
NULL NULL 2 2017-08-03 1111111 Book 2
1 2017-07-01 1 2017-07-02 12345 Book 1
2 2017-08-01 NULL NULL 67890 Book 3
I tried to grab the rows using a query like the one below:
SELECT
GROUP_CONCAT(so_id) so_id,
GROUP_CONCAT(so_date) so_date,
GROUP_CONCAT(po_id) po_id,
GROUP_CONCAT(po_date) po_date,
isbn,
dscr
FROM (
SELECT
hd.so_id so_id,
NULL so_date,
hd.id po_id,
hd.dt po_date,
bk.isbn,
it.dscr
FROM porders_hd hd,
porders_it it,
books bk
WHERE it.hd_id = hd.id
AND bk.isbn = it.isbn
AND kind = 2
UNION
SELECT
hd.id so_id,
hd.dt so_date,
NULL po_id,
NULL po_date,
bk.isbn,
it.dscr
FROM sorders_hd hd,
sorders_it it,
books bk
WHERE it.hd_id = hd.id
AND bk.isbn = it.isbn
AND kind = 1
) as table1
GROUP BY isbn, so_id, po_id
but since there is info missing I get the following result:
so_id so_date po_id po_date isbn dscr
NULL NULL 2 2017-08-03 1111111 Book 2
1 2017-07-01 NULL NULL 12345 Book 1
1 NULL 1 2017-07-02 12345 Book 1
2 2017-08-01 NULL NULL 67890 Book 3
Any ideas how can I achieve this ?
I think this is what you're after, but I can;t figure out the role of kind from your code. But here is a query that for each books, gets the associated po line item, finds the corresponding so line item and joins the header rows so the dates are available. Note my assumption that a sales order can't exist with a corresponding PO.
SELECT books.isbn, books.descr, sorders_hd.id, sorders_hd.dt, porders_hd.id, porders_hd.dt
FROM book
join porders_it on porders_it.isbn = books.isbn
join porders_hd on porders_hd.id = porders_it.hd_id
left outer join sorders_it on sorders_it.hd_id=porders_hd.so_id and sorders_it.isbn = porders_it.isbn
left outer join sorders_hd on sorders_hd.id = sorders_it.hd_it
You could normalize your tables so that descr need not be repeated, and also use the book.id in the other tables rather than isbn.
I'm adding a new answer because the previous one and the comments are illustrative. Based on that discussion, this requires a FULL OUTER JOIN which must be emulated by UNION ALL in mysql (which may be what OP was attempting originally).
Here is my new code, taking that into account:
SELECT sorders_hd.id as so_id, sorders_hd.dt as so_dt,
porders_hd.id as po_id, porders_hd.dt as po_dt,
books.isbn, porders_it.dscr
from books
left outer join porders_it on porders_it.isbn=books.isbn
join porders_hd on porders_hd.id=porders_it.hd_id
left outer join sorders_it on sorders_it.isbn=books.isbn and sorders_it.hd_id=porders_hd.so_id
left outer join sorders_hd on sorders_hd.id=sorders_it.hd_id
where books.kind=2
UNION ALL
SELECT sorders_hd.id as so_id, sorders_hd.dt as so_dt,
porders_hd.id as po_id, porders_hd.dt as po_dt,
books.isbn, sorders_it.dscr
from books
left outer join sorders_it on sorders_it.isbn=books.isbn
join sorders_hd on sorders_hd.id=sorders_it.hd_id
left outer join porders_it on porders_it.isbn=books.isbn
left outer join porders_hd on porders_hd.id=porders_it.hd_id and porders_hd.so_id=sorders_hd.id
where porders_hd.id is null and books.kind=1;
The output result is:
so_id so_dt po_id po_dt isbn dscr
1 2017-07-01 1 2017-07-02 12345 Book 1
(null) (null) 2 2017-08-03 1111111 Book 2
2 2017-08-01 (null) (null) 67890 Book 2
See SqlFiddle
The "trick" is to use union all with one of the two queries excluding records that linked both sides (to get the 'right' side of the FULL OUTER JOIN)
+1 to OP for providing the DDL and sample data!
I agree that the data model could be reworked, and could be normalized. The existing model still has at least the problem of a duplicate book record when a sales order and purchase order match (one of them is ignored). It seems to me that one improvement would be to have a master book list and include the id (or isbn if that is the primary key) from that table in porders_it and sorders_it, and eliminate the current books table.

MySQL JOIN Three Tables Using Row Values of A Table

This got complicated really quickly and I'm beginning to question the database design.
The basic concept of the application is:
User accounts
Features
Access levels
So, users have different access levels for each of the features. Fairly basic and common application I would think.
Schema:
CREATE TABLE `user_accounts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_login` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`user_password` varchar(60) COLLATE utf8_unicode_ci NOT NULL,
`user_fname` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`user_lname` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`user_group` varchar(32) COLLATE utf8_unicode_ci NOT NULL DEFAULT 'Default',
PRIMARY KEY (`id`),
UNIQUE KEY `user_login` (`user_login`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;
INSERT INTO `user_accounts` VALUES(1, 'email#example.com', 'secret', 'Example', 'Name', 'Admin');
INSERT INTO `user_accounts` VALUES(2, 'john#example.com', 'secret', 'John', 'Doe', 'Trainer');
INSERT INTO `user_accounts` VALUES(3, 'jane#example.com', 'secret', 'Jane', 'Doe', 'Default');
CREATE TABLE `user_access_meta` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `user_access_meta` VALUES(1, 'type_1');
INSERT INTO `user_access_meta` VALUES(2, 'type_2');
INSERT INTO `user_access_meta` VALUES(3, 'type_3');
INSERT INTO `user_access_meta` VALUES(4, 'type_4');
INSERT INTO `user_access_meta` VALUES(5, 'type_5');
INSERT INTO `user_access_meta` VALUES(6, 'type_6');
CREATE TABLE `user_access_levels` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_login` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`type` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
`level` int(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_login_2` (`user_login`,`type`),
KEY `user_login` (`user_login`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;
INSERT INTO `user_access_levels` VALUES(1, 'email#example.com', 'type_1', 1);
INSERT INTO `user_access_levels` VALUES(2, 'email#example.com', 'type_2', 1);
INSERT INTO `user_access_levels` VALUES(3, 'email#example.com', 'type_3', 0);
INSERT INTO `user_access_levels` VALUES(4, 'email#example.com', 'type_5', 2);
INSERT INTO `user_access_levels` VALUES(5, 'john#example.com', 'type_2', 1);
INSERT INTO `user_access_levels` VALUES(6, 'john#example.com', 'type_3', 1);
INSERT INTO `user_access_levels` VALUES(7, 'john#example.com', 'type_5', 3);
INSERT INTO `user_access_levels` VALUES(8, 'jane#example.com', 'type_4', 1);
These tables actually have a lot more fields and have foreign key constraints between them, but I've striped them down for this example. They are also used individually for other purposes.
I've successfully been able to join all three tables together for a single user with this:
SELECT
ua.`user_fname`,
uam.`type`,
ual.`level`
FROM `user_access_meta` uam
LEFT JOIN `user_access_levels` ual
ON ual.`user_login` = 'email#example.com'
AND uam.`type` = ual.`type`
JOIN `user_accounts` ua
ON ua.`user_login` = 'email#example.com';
Output:
| USER_FNAME | TYPE | LEVEL |
--------------------------------
| Example | type_1 | 1 |
| Example | type_2 | 1 |
| Example | type_3 | 0 |
| Example | type_4 | (null) |
| Example | type_5 | 2 |
| Example | type_6 | (null) |
Even this isn't ideal, but It's all I could come up with and it serves it's purpose.
Now, what I need to do is select all users including their access levels. It would look something like this:
| USER_FNAME | type_1 | type_2 | type_3 | type_4 | type_5 | type_6 |
--------------------------------------------------------------------------
| Example | 1 | 1 | 0 | (null) | 2 | (null) |
| John | (null) | 1 | 1 | (null) | 3 | (null) |
| Jane | (null) | (null) | (null) | 1 | (null) | (null) |
I feel this may not have been the best design, but the reason I went with this design is so that I can easily add and remove features or even temporarily disable them individually.
Should the design be rethought? Is it even possible to get the results I'm looking for with this design?
I've put this up on SQL Fiddle. http://sqlfiddle.com/#!2/bb313/2/0
I have a few suggestions on both your table design and then how to get the data in the format that you want.
First on the database design, the change I would advise is in the table user_access_levels. Alter you table to the following:
CREATE TABLE `user_access_levels` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`level` int(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id_2` (`user_id`,`type_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;
There is no need to store the user_login and type in this table when you can just store the user_id and the type_id. Use both of these as foreign keys to their respective tables.
Then to get the data in format that you want. MySQL does not have a PIVOT function so you will want to use a CASE statement with an aggregate function.
select ua.user_fname,
MIN(CASE WHEN uam.type = 'type_1' THEN ual.level END) type_1,
MIN(CASE WHEN uam.type = 'type_2' THEN ual.level END) type_2,
MIN(CASE WHEN uam.type = 'type_3' THEN ual.level END) type_3,
MIN(CASE WHEN uam.type = 'type_4' THEN ual.level END) type_4,
MIN(CASE WHEN uam.type = 'type_5' THEN ual.level END) type_5,
MIN(CASE WHEN uam.type = 'type_6' THEN ual.level END) type_6
FROM user_accounts ua
LEFT JOIN user_access_levels ual
ON ua.id = ual.user_id
LEFT JOIN user_access_meta uam
ON ual.type_id = uam.id
group by ua.user_fname
See a SQL Fiddle with a Demo
This version will work if you know ahead of time the type columns that you want to get the values for. But if it is unknown, then you can use prepared statements to generate this dynamically.
Here is a version of the query using prepared statements:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'MIN(case when type = ''',
type,
''' then level end) AS ',
replace(type, ' ', '')
)
) INTO #sql
FROM user_access_meta;
SET #sql = CONCAT('SELECT ua.user_fname, ', #sql, ' FROM user_accounts ua
LEFT JOIN user_access_levels ual
ON ua.id = ual.user_id
LEFT JOIN user_access_meta uam
ON ual.type_id = uam.id
group by ua.user_fname');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See a SQL Fiddle with Demo
While I'm not familiar with the specifics of MySQL, it seems to me you are describing a pretty fundamental example of a pivot table query. What you're looking for seems reasonable to me, so I don't think based on what you've shown here I'd get too concerned about revisiting the data model. You may find putting the "level" back with the "type" table, based on the ol' saw "Normalize til hit hurts, denormalize til it works :)"
Just my $0.02. Good luck.
I typically use a BIGINT column and use bit masking to set the values.
For example level1 = 2, level2=4, level3=8, level4=16, etc..
Give someone level1 and level2 access:
update user set access_level = 2 & 4
does someone have level2 access?
select 1 from user where access_level | 2 AND user_id = ?

how to match in mysql

I have two tables, one for candidates and their skills, and the other for jobs and the skills required for the job, such as the ones shown below:
CSID = candidate skill ID (PK)
CID = candidate ID (FK)
S_CODE = skill code (FK)
Here is the candidate_skill table
+------------+---------+---------+
| CSID | CID | S_CODE |
+------------+---------+---------+
| 1 | 1 | 5 |
| 2 | 1 | 9 |
| 3 | 2 | 5 |
| 4 | 2 | 10 |
+------------+---------+---------+
SJID = skill job ID (PK)
JID = job ID (FK)
S_CODE = skill code (FK)
Here is the skill_job table:
+------------+---------+---------+
| SJID | JID | S_CODE |
+------------+---------+---------+
| 12 | 50 | 5 |
| 13 | 50 | 9 |
| 14 | 51 | 1 |
| 15 | 52 | 10 |
+------------+---------+---------+
So in this example, the only candidate would be 1 for the job 50, since the skill codes (S_CODE) are 5 and 9 for both, but i also would like candidate 2 to be a match to 52, since it has the required job skill and an extra one.
I have tried several ways to match the job and candidate but I'm failing, an example of such a way is the code below:
SELECT * , COUNT(skill_job.S_CODE) AS cnt FROM candidate_skill,
skill_job WHERE candidate_skill.S_CODE = skill_job.S_CODE HAVING (cnt >=2)
the problem is, that only limits the candidate found to one and if i remove the COUNT clause, it lists all candidates that match with only one skill so candidate 2 would also be matched to the job 50.
Here is the mysql code for the tables from phpmyadmin:
-- phpMyAdmin SQL Dump
-- version 3.4.5
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Jul 16, 2012 at 09:27 PM
-- Server version: 5.5.16
-- PHP Version: 5.3.8
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET #OLD_CHARACTER_SET_CLIENT=##CHARACTER_SET_CLIENT */;
/*!40101 SET #OLD_CHARACTER_SET_RESULTS=##CHARACTER_SET_RESULTS */;
/*!40101 SET #OLD_COLLATION_CONNECTION=##COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
--
-- Database: `employment`
--
-- --------------------------------------------------------
--
-- Table structure for table `candidate`
--
CREATE TABLE IF NOT EXISTS `candidate` (
`CID` int(4) NOT NULL AUTO_INCREMENT,
`title` varchar(5) NOT NULL,
`fname` varchar(30) NOT NULL,
`lname` varchar(30) NOT NULL,
`dob` date NOT NULL,
`email` varchar(50) NOT NULL,
`address` varchar(255) NOT NULL,
`city` varchar(50) NOT NULL,
`postcode` varchar(10) NOT NULL,
`phone_num` varchar(11) NOT NULL,
`username` varchar(40) NOT NULL,
`password` varchar(40) NOT NULL,
`regdate` datetime NOT NULL,
`acc_type` enum('c','s') NOT NULL DEFAULT 'c',
`emailactivate` enum('0','1') NOT NULL DEFAULT '0',
`cv_name` varchar(60) NOT NULL,
`cv` blob NOT NULL,
PRIMARY KEY (`CID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='// this is the table for the candidates' AUTO_INCREMENT=175 ;
--
-- Dumping data for table `candidate`
--
INSERT INTO `candidate` (`CID`, `title`, `fname`, `lname`, `dob`, `email`, `address`, `city`, `postcode`, `phone_num`, `username`, `password`, `regdate`, `acc_type`, `emailactivate`, `cv_name`, `cv`) VALUES
(128, 'Mr', 'clement', 'Chilingulo', '0000-00-00', 'chlngl#yahoo.com', '28 oakfield Road', 'london', 'E6 1LW', '07771611873', 'casante', 'b59c67bf196a4758191e42f76670ceba', '0000-00-00 00:00:00', 'c', '0', '', '');
INSERT INTO `candidate` (`CID`, `title`, `fname`, `lname`, `dob`, `email`, `address`, `city`, `postcode`, `phone_num`, `username`, `password`, `regdate`, `acc_type`, `emailactivate`, `cv_name`, `cv`) VALUES
(134, 'Mr', 'rverv', 'revrb', '0000-00-00', 'tdbsdrt', 'trsbrtd', 'trbtrtrb', 'tbrfbgrts', 'trfbtrgbrfg', 'clement', 'b59c67bf196a4758191e42f76670ceba', '0000-00-00 00:00:00', 's', '0', '', '');
INSERT INTO `candidate` (`CID`, `title`, `fname`, `lname`, `dob`, `email`, `address`, `city`, `postcode`, `phone_num`, `username`, `password`, `regdate`, `acc_type`, `emailactivate`, `cv_name`, `cv`) VALUES
(165, 'Mr', 'oinInINOioni', 'ioin', '0000-00-00', 'inioimn', 'in', 'oin', 'oni', 'io', 'k', '7b8b965ad4bca0e41ab51de7b31363a1', '0000-00-00 00:00:00', 'c', '0', '', ''),
(166, 'Mr', 'pjINoNlkinoinoi', 'ino', '0000-00-00', 'oimpnponi', 'inoi', 'no', 'nj', 'nio', 'nio', 'eb5bc837d01b911029ae378e8a1c9f5d', '0000-00-00 00:00:00', 'c', '0', '', ''),
(167, 'Mr', 'vrae', 'ergvaer', '0000-00-00', 'aerbg', 'aergvera', 'aergvrea', 'aergvear', 'aergarev', 'grebvarvf', '609c1b136ec8d0d2dfdf9a2105fb605f', '0000-00-00 00:00:00', 'c', '0', '', ''),
(168, 'Mr', 'vrae', 'ergvaer', '0000-00-00', 'aerbg', 'aergvera', 'aergvrea', 'aergvear', 'aergarev', 'grebvarvf', '609c1b136ec8d0d2dfdf9a2105fb605f', '0000-00-00 00:00:00', 'c', '0', '', ''),
(169, 'Mr', 'ubp', 'bu', '0000-00-00', 'ubip', ';ub', 'ubi', 'ubo', 'buo', 'ubiipbu', '9f44ce1389a3e7372834ed730b559a5e', '0000-00-00 00:00:00', 'c', '0', '', ''),
(170, 'Mr', 'rvev', 'ferhbgetb', '0000-00-00', 'tsbtrb', 'trbstrb', 'trbsfb ', 'stb stb', 'vvfs', 'csdcdsvarewdcv', 'd41d8cd98f00b204e9800998ecf8427e', '0000-00-00 00:00:00', 'c', '0', '', ''),
(171, 'Mr', 'rvev', 'ferhbgetb', '0000-00-00', 'tsbtrb', 'trbstrb', 'trbsfb ', 'stb stb', 'vvfs', 'csdcdsvarewdcv', 'd41d8cd98f00b204e9800998ecf8427e', '0000-00-00 00:00:00', 'c', '0', '', ''),
(172, '', '', '', '0000-00-00', '', '', '', '', '', '', 'd41d8cd98f00b204e9800998ecf8427e', '0000-00-00 00:00:00', 'c', '0', '', ''),
(173, '', 'dds', 'vrv av', '0000-00-00', 'rvear', 'vreverav', 'rvedfsdv', 'frvdvf', 'fveavd', 'vdrareavdv', '84c71ac76340092398a1ed4bc7d6fe19', '0000-00-00 00:00:00', 'c', '0', '', ''),
(174, '', 'dds', 'vrv av', '0000-00-00', 'rvedwear', 'vreverav', 'rvedfsdv', 'frvdvf', 'fveavd', 'vdraredfcavdv', 'd41d8cd98f00b204e9800998ecf8427e', '0000-00-00 00:00:00', 'c', '0', '', '');
-- --------------------------------------------------------
--
-- Table structure for table `candidate_skill`
--
CREATE TABLE IF NOT EXISTS `candidate_skill` (
`CSID` int(4) NOT NULL AUTO_INCREMENT,
`CID` int(4) NOT NULL,
`S_CODE` int(4) NOT NULL,
PRIMARY KEY (`CSID`),
KEY `CID` (`CID`),
KEY `S_CODE` (`S_CODE`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='//match candidate and skill' AUTO_INCREMENT=131 ;
--
-- Dumping data for table `candidate_skill`
--
INSERT INTO `candidate_skill` (`CSID`, `CID`, `S_CODE`) VALUES
(114, 134, 2),
(116, 134, 9),
(121, 134, 5),
(126, 128, 1);
-- --------------------------------------------------------
--
-- Table structure for table `job`
--
CREATE TABLE IF NOT EXISTS `job` (
`JID` int(4) NOT NULL AUTO_INCREMENT,
`job_title` varchar(40) NOT NULL,
`job_desc` varchar(255) NOT NULL,
`start_date` date NOT NULL,
`end_date` date NOT NULL,
PRIMARY KEY (`JID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='// this is the table for the job vacancies' AUTO_INCREMENT=10 ;
--
-- Dumping data for table `job`
--
INSERT INTO `job` (`JID`, `job_title`, `job_desc`, `start_date`, `end_date`) VALUES
(1, 'engineer', ' fix engineering ish that is messed ', '0000-00-00', '0000-00-00'),
(5, 'pilot', '', '0000-00-00', '0000-00-00'),
(8, 'Charity Helper', ' ', '0000-00-00', '0000-00-00'),
(9, 'accountant', ' ', '0000-00-00', '0000-00-00');
-- --------------------------------------------------------
--
-- Table structure for table `skill`
--
CREATE TABLE IF NOT EXISTS `skill` (
`S_CODE` int(4) NOT NULL AUTO_INCREMENT COMMENT '// this is the skill primary key',
`skill_name` varchar(40) NOT NULL,
`skill_desc` varchar(255) NOT NULL,
PRIMARY KEY (`S_CODE`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=11 ;
--
-- Dumping data for table `skill`
--
INSERT INTO `skill` (`S_CODE`, `skill_name`, `skill_desc`) VALUES
(1, 'speaking', 'English is your first language'),
(2, ' writing', 'You can write English very well'),
(3, ' public', ''),
(5, 'initiative', ''),
(6, 'interviewing', ''),
(7, 'negotiating', ''),
(8, 'leading', ''),
(9, ' energy', ''),
(10, ' organisation', '');
-- --------------------------------------------------------
--
-- Table structure for table `skill_job`
--
CREATE TABLE IF NOT EXISTS `skill_job` (
`SJID` int(4) NOT NULL AUTO_INCREMENT,
`JID` int(4) NOT NULL,
`S_CODE` int(4) NOT NULL,
PRIMARY KEY (`SJID`),
KEY `S_CODE` (`S_CODE`),
KEY `JID` (`JID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=106 ;
--
-- Dumping data for table `skill_job`
--
INSERT INTO `skill_job` (`SJID`, `JID`, `S_CODE`) VALUES
(91, 5, 2),
(94, 5, 5),
(95, 5, 9),
(98, 1, 1),
(102, 1, 8),
(105, 8, 8);
--
-- Constraints for dumped tables
--
--
-- Constraints for table `candidate_skill`
--
ALTER TABLE `candidate_skill`
ADD CONSTRAINT `candidate_skill_ibfk_3` FOREIGN KEY (`CID`) REFERENCES `candidate` (`CID`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `candidate_skill_ibfk_4` FOREIGN KEY (`S_CODE`) REFERENCES `skill` (`S_CODE`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `skill_job`
--
ALTER TABLE `skill_job`
ADD CONSTRAINT `skill_job_ibfk_4` FOREIGN KEY (`JID`) REFERENCES `job` (`JID`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `skill_job_ibfk_5` FOREIGN KEY (`S_CODE`) REFERENCES `skill` (`S_CODE`) ON DELETE CASCADE ON UPDATE CASCADE;
/*!40101 SET CHARACTER_SET_CLIENT=#OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=#OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=#OLD_COLLATION_CONNECTION */;
Double negation is your friend (though I don't know MySQL well enough to tell whether it supports it):
SELECT DISTINCT CS.CID, SJ.JID
FROM CANDIDATE_SKILL CS
JOIN SKILL_JOB SJ ON CS.S_CODE = SJ.S_CODE
WHERE NOT EXISTS(SELECT 1
FROM SKILL_JOB SJ2
WHERE SJ.JID = SJ2.JID
AND NOT EXISTS(SELECT 1
FROM CANDIDATE_SKILL CS2
WHERE CS.CID = CS2.CID
AND SJ2.S_CODE = CS2.S_CODE))
A 'human translation' of this is to say that there should not exists any skill required for the job that the candidate doesn't have.
It seems like you'll need to use an INNER JOIN to combine your two tables and then compare their row values.
Should be something like this..
SELECT
CID
FROM
candidate_skill
WHERE
S_CODE IN (
SELECT S_CODE FROM skill_jobs where JID = 52
)
GROUP BY CID
CORRECTION: Previous query will fail to match all required S_CODE
SELECT
CID, COUNT(CSID) as cnt
FROM
candidate_skill
INNER JOIN skill_jobs
WHERE
S_CODE IN (
SELECT S_CODE FROM skill_jobs where JID = 52
)
GROUP BY CID
HAVING cnt = (
SELECT count(*) from skill_jobs where JID = 52
)
You will need a bit more complicated JOIN, and I see no easy way of simultaneously answering your request and the obvious ones "How is candidate X matched for job Y?" and "HOW MUCH does candidate X fit job Y?".
Anyway, here goes.
SELECT candidate_skill.CID, skill_job.JID, COUNT(*) AS has, sjtot.needed
FROM skill_job JOIN candidate_skill ON (candidate_skill.S_CODE = skill_job.S_CODE)
LEFT JOIN ( SELECT JID, COUNT(*) AS needed FROM skill_job GROUP BY JID ) AS sjtot
ON ( skill_job.JID = sjtot.JID )
GROUP BY CID, JID
HAVING has >= needed;
In practice I first group the required skills on skill_job, and this tells me that job 50 requires two skills.
Armed with this, I LEFT JOIN required and candidate skills; some candidates will join on all skills, some won't. It is then just a matter of counting how many do match.
+------+------+-----+--------+
| CID | JID | has | needed |
+------+------+-----+--------+
| 1 | 50 | 2 | 2 |
| 2 | 52 | 1 | 1 |
+------+------+-----+--------+
On second thought, required skill level and offered skill levels might be selected, and CASE WHEN used to extract a match (1 if offered >= required, offered/required otherwise). The average of the resulting values might then be used as a "skill match" index.
As for the "How is X matched for Y?" question, once you know that candidate CID 1 is good for job JID 50, you can run a LEFT JOIN between the candidates and the jobs. You already know that whatever the jobs asks, the candidate has, but this way you retrieve what unasked-for skills the candidate has, and the other relevant values as well.
A nested query joining the query above and this last one would perhaps tell you everything in one fell swoop, but it would be a costly swoop, I think :-)
If you don't care about cross-dbms support, you can use mysql specific group_concat to reduce the two s_code sets to strings, one per candidate/job and just compare them like this:
select * from
(select cs.cid, group_concat(cs.s_code order by cs.s_code) skills
from candidate_skill cs group by cs.cid) cs,
(select sj.jid, group_concat(sj.s_code order by sj.s_code) skills
from skill_job sj group by sj.jid) sj
where sj.skills = cs.skills
Be aware of that when your tables grown the the final where will be slower and slower and you can't create indexes on the group_concat() created fields.

How to generate a dynamic sequence table in MySQL?

I'm trying to generate a sequence table in MySQL, so that I can get unique ids from last_insert_id.
The problem is that I need multiple sequences dynamically.
At the first, I created a table:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL DEFAULT '0',
PRIMARY KEY (`label`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
And then tried to get the number, using example from http://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id
UPDATE sequence SET id = LAST_INSERT_ID(id + 1) WHERE label = 'test';
SELECT LAST_INSERT_ID();
After a while I realized that I also need to generate rows for new labels safely.
So I changed this schema into:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`label`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
And I simply gave up using WHERE clause to update its id.
INSERT INTO sequence (label) values ( ? )
SELECT LAST_INSERT_ID()
Is this a proper way? I want to know if there is a better solution.
The MyISAM engine will do it for you -
Table definition:
CREATE TABLE `sequence` (
`label` char(30) CHARACTER SET latin1 NOT NULL,
`id` mediumint(9) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`label`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Populate table:
INSERT INTO sequence VALUES ('a', NULL); -- add some 'a' labels
INSERT INTO sequence VALUES ('a', NULL);
INSERT INTO sequence VALUES ('a', NULL);
INSERT INTO sequence VALUES ('b', NULL); -- add another labels 'b'
INSERT INTO sequence VALUES ('b', NULL);
INSERT INTO sequence VALUES ('a', NULL); -- add some 'a' labels
INSERT INTO sequence VALUES ('a', NULL);
Show result:
SELECT * FROM sequence;
+-------+----+
| label | id |
+-------+----+
| a | 1 |
| a | 2 |
| a | 3 |
| a | 4 |
| a | 5 |
| a | 6 |
| b | 1 |
| b | 2 |
+-------+----+