Most efficient way to count items under a nested tree - mysql

So I'm looking to provide a category (x) style product count on my website.
I'm currently using MySQL.
My categories table looks like:
Categories (Id, TreeLeft, TreeRight, Level, Name) - with 'Level' being the node depth.
Categories entered look like this:
My Item > Categories relation table looks like:
ItemCategories (ItemId, CategoryId)
Assuming I have:
1 item under 'TUBE'
2 items under 'LCD'
1 item under 'FLASH'
1 item under '2 WAY-RADIOs'
How can I most efficiently query my items (large db) + categories (4000 in db), to produce:
Electronics (5)
- Televisions (3)
- Tube (1)
- LCD (2)
- Portable Electronics (2)
- MP3 Players (1)
- Flash (1)
- 2 Way Radios (1)
Taking note to only return those categories which have products in them and also correctly counts them up the tree.
Any help most appreciated.
Edit: DB Code to recreate environment locally:
CREATE TABLE IF NOT EXISTS `Categories` (
`Id` int(11) NOT NULL auto_increment,
`TreeLeft` mediumint(7) NOT NULL,
`TreeRight` mediumint(7) NOT NULL,
`Level` tinyint(3) NOT NULL,
`Name` varchar(255) NOT NULL,
UNIQUE KEY `Id` (`Id`),
KEY `TreeLeft` (`TreeLeft`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=11 ;
INSERT INTO `Categories` (`Id`, `TreeLeft`, `TreeRight`, `Level`, `Name`) VALUES
(1, 1, 20, 1, 'Electronics'),
(2, 2, 9, 2, 'Television'),
(3, 10, 19, 2, 'Portable Electronics'),
(4, 3, 4, 3, 'Tube'),
(5, 5, 6, 3, 'LCD'),
(6, 7, 8, 3, 'Plasma'),
(7, 11, 14, 3, 'MP3 Players'),
(8, 15, 16, 3, 'CD Players'),
(9, 11, 14, 3, '2 Way Radios'),
(10, 12, 13, 4, 'Flash');
CREATE TABLE IF NOT EXISTS `ItemCategories` (
`CategoryId` int(11) NOT NULL,
`ItemId` int(11) NOT NULL,
KEY `CategoryId` (`CategoryId`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `ItemCategories` (`CategoryId`, `ItemId`) VALUES
(4, 3442),
(5, 3441),
(5, 3456),
(9, 5343),
(10, 5423);

Please see the subsection, entitled Aggregate Functions in a Nested Set, of Mike Hillyer's classic article on this subject at Managing Hierarchical Data in MySQL

Related

Mapping AppUsers to a Customer by AppUser information

I have database with ERD:
And sample data:
CREATE TABLE `AppUser` (
`AppUser_ID` bigint(20) NOT NULL,
`SomeFields` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`AppUser_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `AppUser` (`AppUser_ID`, `SomeFields`) VALUES
(1, 'values'),
(2, 'values'),
(3, 'values'),
(4, 'values'),
(5, 'values');
CREATE TABLE `IdpUser` (
`IdpUser_ID` bigint(20) NOT NULL,
`SomeFields` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
ADD PRIMARY KEY (`IdpUser_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `IdpUser` (`IdpUser_ID`, `SomeFields`) VALUES
(1, 'values'),
(2, 'values'),
(3, 'values'),
(4, 'values'),
(5, 'values');
CREATE TABLE `UserClaim` (
`Attribute_ID` bigint(20) NOT NULL,
`IdpUser_ID` bigint(20) DEFAULT NULL,
`AttributeKey` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
`AttributeValue` bigint(20) DEFAULT NULL,
PRIMARY KEY (`Attribute_ID`),
FOREIGN KEY (`IdpUser_ID`) REFERENCES `IdpUser` (`IdpUser_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `UserClaim` (`Attribute_ID`, `IdpUser_ID`, `AttributeKey`, `AttributeValue`) VALUES
(1, 1, 'Email_ID', 1),
(2, 2, 'Email_ID', 2),
(3, 3, 'Email_ID', 3),
(4, 4, 'Email_ID', 4),
(5, 5, 'Email_ID', 5),
(6, 2, 'Phone_ID', 4),
(7, 3, 'Phone_ID', 2),
(8, 4, 'Phone_ID', 3),
(9, 5, 'Phone_ID', 5),
(10, 2, 'PublicKey_ID', 1),
(11, 3, 'PublicKey_ID', 2),
(12, 1, 'PublicKey_ID', 1);
CREATE TABLE `UserInfo` (
`Attribute_ID` bigint(20) NOT NULL,
`AppUser_ID` bigint(20) DEFAULT NULL,
`AttributeKey` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
`AttributeValue` bigint(20) DEFAULT NULL,
PRIMARY KEY (`Attribute_ID`),
FOREIGN KEY (`AppUser_ID`) REFERENCES `AppUser` (`AppUser_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `UserInfo` (`Attribute_ID`, `AppUser_ID`, `AttributeKey`, `AttributeValue`) VALUES
(1, 1, 'Email_ID', 1),
(2, 2, 'Email_ID', 2),
(3, 3, 'Email_ID', 3),
(4, 4, 'Email_ID', 4),
(5, 5, 'Email_ID', 2),
(6, 2, 'Phone_ID', 1),
(7, 3, 'Phone_ID', 2),
(8, 4, 'Phone_ID', 3),
(9, 5, 'Phone_ID', 4);
CREATE TABLE `UserMapping` (
`Mapping_ID` int(11) NOT NULL,
`AppUser_ID` bigint(20) NOT NULL,
`IdpUser_ID` bigint(20) NOT NULL,
PRIMARY KEY (`Mapping_ID`),
FOREIGN KEY (`AppUser_ID`) REFERENCES `AppUser` (`AppUser_ID`),
FOREIGN KEY (`IdpUser_ID`) REFERENCES `IdpUser` (`IdpUser_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `UserMapping` (`Mapping_ID`, `AppUser_ID`, `IdpUser_ID`) VALUES
(1, 2, 2),
(2, 3, 3),
(3, 1, 1),
(4, 4, 5),
(5, 5, 4);
UserInfo holds internal User info, UserClaim holds external User info, my application has multiple login type. Internal User and External User is mapped.
I use both internal and external User attribute for mapping Users to a Customer. If Users have the same AttributeKey and AttributeValue will be grouped in a Customer and the following attributes of those Users must be added to the Customer
All User with the same AttributeKey and AttributeValue is
grouped into a group.
The Customer will have all attribute of the Users belong to that Customer.
For example - with above data we have Mapping_ID = 2 belongs to Customer1 with list of attributes :
Email_ID = [2]
Phone_ID = [1,4]
PublicKey_ID = [1]
And we also have Mapping_ID = 5 with list of attributes:
Email_ID = [2,5]
Phone_ID = [4,5]
PublicKey_ID = []
And we also have Mapping_ID = 1 with list of attributes:
Email_ID = [1]
Phone_ID = []
PublicKey_ID = [1]
Because Email_ID = 2, Phone_ID = 4 belong to Mapping_ID = [2,5] so Mapping_ID = [2,5] is mapped to Customer1.
Because PublicKey_ID = 1 belong to Mapping_ID = [2,1] so Mapping_ID = [2,1] is mapped to Customer1.
=> Mapping_ID = [1,2,5] are mapped to Customer1 and Customer1 attributes :
Email_ID = [1,2,5]
Phone_ID = [1,4,5]
PublicKey_ID = [1]
My approach is to group by AttributeKey and AttributeValue in each UserInfo and UserClaim to get temporary Customers and their attributes, then join their attributes together and group to a Customer with the same AttributeKey and AttributeValue. I saw my approach made thing difficult to intersect list of attributes to group the Customer and how to store Customer attributes for effectively add or remove attributes.
I am looking for an idea for this problem or any better approach to solve this puzzle.
UserInfo has approximately 100m rows.
UserClaim has approximately 300m rows.
I can use MySql, PDI (Pentaho Data Integration) tools to design and maintain ETL to get my result.

MySQL - Computing the AVG(DISTINCT) of a relation

Column B of a relation has the following list of values in the five rows of the table:
3, NULL, 2, 3, 5
Which of the following is the correct value of AVG(DISTINCT B)?
I have tried creating a relational table here with the specified rows and then executed AVG(DISTINCT A): http://ideone.com/3ItE01
CREATE TABLE A(a int(8), b int(8), c int(8), d int(8), e int(8));
INSERT INTO A VALUES (3, NULL, 2, 3, 5);
INSERT INTO A VALUES (3, NULL, 2, 3, 5);
INSERT INTO A VALUES (3, NULL, 2, 3, 5);
INSERT INTO A VALUES (3, NULL, 2, 3, 5);
INSERT INTO A VALUES (3, NULL, 2, 3, 5);
But my SQL Query of "AVG(DISTINCT A)" is not valid. I am new to SQL and looking for documentation. Any ideas?
Create table like this
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1;
Insert values with query
insert into `test`(`id`,`value`) values (1,'3'),(2,'2'),(3,'3'),(4,NULL),(5,'5');
now use query
SELECT AVG( DISTINCT test.value)FROM test
to calculate the average you will get the result 3.3333333333333335

MySQL. Average price, connecting two databases

I am learning MySQL and I currently do not understand how to do something.
I have two tables and I want to display some stuff out of it, it's pretty hard to explain so I'd rather show you.
These are my tables:
CREATE TABLE IF NOT EXISTS `proprietate` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`serie_buletin` varchar(8) NOT NULL,
`cnp` bigint(20) NOT NULL,
`nr_vehicul` int(11) NOT NULL,
`data_cumpararii` date NOT NULL,
`pret` int(11) NOT NULL,
`id_persoana` int(11) NOT NULL,
PRIMARY KEY (id),
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=16 ;
CREATE TABLE IF NOT EXISTS `vehicul` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nr_vehicul` int(11) NOT NULL,
`marca` varchar(30) NOT NULL,
`id_marca` int(11) NOT NULL,
`tip` varchar(15) NOT NULL,
`culoare` varchar(15) NOT NULL,
`capacitate_cilindrica` int(11) NOT NULL,
`id_proprietate` int(11) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (id_proprietate) REFERENCES proprietate(id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=16 ;
And these are my values inside my tables:
INSERT INTO `proprietate` (`id`, `serie_buletin`, `cnp`, `nr_vehicul`, `data_cumpararii`, `pret`, `id_persoana`) VALUES
(1, 'AK162332', 2006036035087, 4, '2014-05-01', 35000, 1),
(2, 'AK162332', 2006036035087, 10, '2014-05-02', 90000, 2),
(3, 'AK176233', 6548751520125, 2, '2014-05-03', 55000, 3),
(4, 'BZ257743', 6548751520125, 2, '2014-05-04', 25000, 4),
(5, 'BZ257743', 2006036035087, 15, '2014-05-05', 63000, 5),
(6, 'DC456542', 2003564784513, 7, '2014-05-06', 30000, 6),
(7, 'EN654872', 2012654879521, 6, '2014-05-07', 50000, 7);
INSERT INTO `vehicul` (`id`, `nr_vehicul`, `marca`, `id_marca`, `tip`, `culoare`, `capacitate_cilindrica`, `id_proprietate`) VALUES
(1, 4, 'Mercedes', 1, 'CLK 350', 'negru', 3500, 1),
(2, 10, 'Mercedes', 1, 'S 500', 'silver', 5000, 2),
(3, 2, 'Mercedes', 1, 'ML 550', 'alb', 5500, 3),
(4, 2, 'BMW', 2, '325', 'galben', 2500, 4),
(5, 15, 'BMW', 2, 'X5', 'negru', 3500, 5),
(6, 7, 'Audi', 3, 'R5', 'mov', 5000, 6),
(7, 6, 'Audi', 3, 'Q5', 'metalic', 3000, 7);
What I want to display is:
marca | nr_vehicul | average_price
Audi | 13 | 40000
BMW | 17 | 44000
Mercedes | 16 | 60000
How can I do that? So far I have managed to display the first two columns but I have no idea how to reference the first table in the second and calculate the average price.
This is what I have so far:
SELECT marca, SUM(nr_vehicul) AS nr_vehicul FROM vehicul GROUP BY marca
Can anyone help me please?
You should join your tables to get combined information from both of them:
SELECT marca, SUM(vehicul.nr_vehicul) AS nr_vehicul, avg(pret) as pret
FROM vehicul
LEFT OUTER JOIN proprietate on (id_proprietate = proprietate.id)
GROUP BY marca;
see this sql fiddle session for the output.
First you select the data (column names with appropriate functions used) you need: marca, SUM(vehicul.nr_vehicul), AVG(pret), then you construct the joined structure from where mysql should retrieve these informations: vehicul, proprietate.
For this structure you need primarily the vehicul table, by which you will group the result set. You want to join the proprietate table to the vehicul table properly, to make sure the correct data structure is created. Since you have foreign key from one table to the other, the easiest way to do it is to use that key: LEFT OUTER JOIN proprietate on (id_proprietate = proprietate.id).
For more information on understanding the different JOIN types, please see this article by Craig Buckler.
$query = "SELECT type, AVG(pret) FROM vehicul GROUP BY marca";
$result = mysql_query($query) or die(mysql_error());
// Print out result
while($row = mysql_fetch_array($result)){
echo "The average price of ". $row['type']. " is $".$row['AVG(price)'];}</code>
should return the average price per marca

mySQL Divide two values from Same Column

I wish to divide values from the same column using mySQL.
is there a better way of doing this??? Do I really need 3 select statements?
SELECT (SELECT FixAM From Fixes WHERE Id = 1) / (SELECT FixAM From Fixes WHERE Id = 2)
My table structure is as follows:
CREATE TABLE IF NOT EXISTS `Fixes` (
`Id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'PK',
`CurrencyId` int(11) NOT NULL COMMENT 'FK',
`MetalId` int(11) NOT NULL COMMENT 'FK',
`FixAM` decimal(10,5) NOT NULL,
`FixPM` decimal(10,5) DEFAULT NULL,
`TimeStamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`Id`),
KEY `CurrencyId` (`CurrencyId`),
KEY `MetalId` (`MetalId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=13 ;
--
-- Dumping data for table `Fixes`
--
INSERT INTO `Fixes` (`Id`, `CurrencyId`, `MetalId`, `FixAM`, `FixPM`, `TimeStamp`) VALUES
(1, 1, 1, '1592.50000', '1586.25000', '2013-02-25 15:10:21'),
(2, 2, 1, '1051.84900', '1049.59300', '2013-02-25 15:10:21'),
(3, 3, 1, '1201.88700', '1194.10600', '2013-02-25 15:10:21'),
(4, 1, 2, '29.17000', NULL, '2013-02-25 13:54:02'),
(5, 2, 2, '19.27580', NULL, '2013-02-25 13:54:02'),
(6, 3, 2, '21.98190', NULL, '2013-02-25 13:54:02'),
(7, 1, 3, '1627.00000', '1620.00000', '2013-02-25 14:28:59'),
(8, 2, 3, '1074.65000', '1072.50000', '2013-02-25 14:28:59'),
(9, 3, 3, '1229.30000', '1218.95000', '2013-02-25 14:28:59'),
(10, 1, 4, '747.00000', '748.00000', '2013-02-25 14:28:59'),
(11, 2, 4, '493.40000', '495.20000', '2013-02-25 14:28:59'),
(12, 3, 4, '564.40000', '562.85000', '2013-02-25 14:28:59');
I think this is what you're looking for:
SELECT MetalId,
MAX(CASE WHEN CurrencyId = 1 THEN FixAM END) /
MAX(CASE WHEN CurrencyId = 2 THEN FixAM ELSE 1 END) Output
FROM Fixes
GROUP BY MetalId
This produces 1592.50000 / 1051.849000. If you want the opposite, swap the currency ids.
SQL Fiddle Demo
In case you don't have a CurrencyId = 2, I defaulted the dividing value to 1 so you wouldn't receive an error.

Mysql - Help me alter this search query to get desired results

Following is a dump of the tables and data needed to answer understand the system:-
The system consists of tutors and classes.
The data in the table All_Tag_Relations stores tag relations for each tutor registered and each class created by a tutor. The tag relations are used for searching classes.
CREATE TABLE IF NOT EXISTS `Tags` (
`id_tag` int(10) unsigned NOT NULL auto_increment,
`tag` varchar(255) default NULL,
PRIMARY KEY (`id_tag`),
UNIQUE KEY `tag` (`tag`),
KEY `id_tag` (`id_tag`),
KEY `tag_2` (`tag`),
KEY `tag_3` (`tag`),
KEY `tag_4` (`tag`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `Tags` (`id_tag`, `tag`) VALUES
(1, 'Sandeepan'),
(2, 'Nath'),
(3, 'first'),
(4, 'class'),
(5, 'new'),
(6, 'Bob'),
(7, 'Cratchit');
CREATE TABLE IF NOT EXISTS `All_Tag_Relations` (
`id_tag` int(10) unsigned NOT NULL default '0',
`id_tutor` int(10) default NULL,
`id_wc` int(10) unsigned default NULL,
KEY `All_Tag_Relations_FKIndex1` (`id_tag`),
KEY `id_wc` (`id_wc`),
KEY `id_tag` (`id_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `All_Tag_Relations` (`id_tag`, `id_tutor`, `id_wc`) VALUES
(1, 1, NULL),
(2, 1, NULL),
(3, 1, 1),
(4, 1, 1),
(6, 2, NULL),
(7, 2, NULL),
(5, 2, 2),
(4, 2, 2),
(8, 1, 3),
(9, 1, 3);
Following is my query:-
This query searches for "first class" (tag for first = 3 and for class = 4, in Tags table) and returns all those classes such that both the terms first and class are present in the class name.
SELECT wtagrels.id_wc,SUM(DISTINCT( wtagrels.id_tag =3)) AS
key_1_total_matches,
SUM(DISTINCT( wtagrels.id_tag =4)) AS
key_2_total_matches
FROM all_tag_relations AS wtagrels
WHERE ( wtagrels.id_tag =3
OR wtagrels.id_tag =4 )
GROUP BY wtagrels.id_wc
HAVING key_1_total_matches = 1
AND key_2_total_matches = 1
LIMIT 0, 20
And it returns the class with id_wc = 1.
But, I want the search to show all those classes such that all the search terms are present in the class name or its tutor name
So that searching "Sandeepan class" (wtagrels.id_tag = 1,4) or "Sandeepan Nath" also returns the class with id_wc=1. And Searching. Searching "Bob First" should not return any classes.
Please modify the above query or suggest a new query, if possible using MyIsam - fulltext search, but somehow help me get the result.
I think this query would help you:
SET #tag1 = 1, #tag2 = 4; -- Setting some user variables to see where the ids go. (you can put the values in the query)
SELECT wtagrels.id_wc,
SUM(DISTINCT( wtagrels.id_tag =#tag1 OR wtagrels.id_tutor =#tag1)) AS key_1_total_matches,
SUM(DISTINCT( wtagrels.id_tag =#tag2 OR wtagrels.id_tutor =#tag2)) AS key_2_total_matches
FROM all_tag_relations AS wtagrels
WHERE ( wtagrels.id_tag =#tag1 OR wtagrels.id_tag =#tag2 )
GROUP BY wtagrels.id_wc
HAVING key_1_total_matches = 1 AND key_2_total_matches = 1
LIMIT 0, 20
It returns id_wc = 1.
For (6, 3) the query returns nothing.