I added a JSON column to a table in MySql that holds a small amount of data (~about 7Kb) for each row. The table has roughly 135K rows. Prior to this addition, all my queries on this table executed in well under 1 sec. Now, however, when explain
notes a temporary table was needed to for the results, the query takes roughly 7 secs. Does anyone one know why a JSON column would cause such a performance issue? If I change the column to TEXT, the same query run under 1 sec. It stands to reason the JSON column will grow in size… should I pull out this column into another 1:1 table? And would this be an issue if eventually we start querying by values in the JSON column?
It’s worth noting that:
- I am not querying the JSON column nor am I returning it in the results.
- This table is searched by users is various ways so writing an index for every possible scenario to eliminate the use of temporary tables is not practical.
- Most queries are dynamically created via the application Hibernate ORM code
For example, the query below takes 7 secs to complete with a JSON field, but under 1 sec as a TEXT field.
SELECT
xaction.XactionId, xaction.PropStreet, xaction.PropNum, xaction.PropStreetUnit
FROM
Xaction xaction
INNER JOIN
Team team ON xaction.TeamId = team.TeamId
LEFT OUTER JOIN
AppUser appuser_primaryAgent ON xaction.AppUserIdPrimaryAgent = appuser_primaryAgent.AppUserId
LEFT OUTER JOIN
AppUser appuser_coAgent ON xaction.AppUserIdCoAgent = appuser_coAgent.AppUserId
LEFT OUTER JOIN
AppUser appuser_assistant1 ON xaction.AppUserIdAssistant1 = appuser_assistant1.AppUserId
LEFT OUTER JOIN
AppUser appuser_assistant2 ON xaction.AppUserIdAssistant2 = appuser_assistant2.AppUserId
WHERE
team.TeamId = 1
AND (
appuser_primaryAgent.AppUserId = 1
or appuser_coAgent.AppUserId = 1
or appuser_assistant1.AppUserId = 1
or appuser_assistant2.AppUserId = 1
)
GROUP BY xaction.XactionId
ORDER BY PropStreet , PropNum , PropStreetUnit
Explain
+----+-------------+----------------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------------------------------------+-------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------------------------------------+-------+----------+----------------------------------------------+
| 1 | SIMPLE | team | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | xaction | NULL | index | PRIMARY,IDX_PropNum,IDX_PropStreet,AppUserIdPrimaryAgent,AppUserIdCoAgent,XactionPropTypeId,XactionSourceId,XactionStatusId,IDX_PropNum_PropStreet_PropStreetNum,AppUserIdAssistant1,AppUserIdAssistant2 | PRIMARY | 8 | NULL | 49515 | 10.00 | Using where |
| 1 | SIMPLE | appuser_primaryAgent | NULL | eq_ref | PRIMARY,AppUserId_idx | PRIMARY | 8 | afdata_next.xaction.AppUserIdPrimaryAgent | 1 | 100.00 | Using index |
| 1 | SIMPLE | appuser_coAgent | NULL | eq_ref | PRIMARY,AppUserId_idx | PRIMARY | 8 | afdata_next.xaction.AppUserIdCoAgent | 1 | 100.00 | Using index |
| 1 | SIMPLE | appuser_assistant1 | NULL | eq_ref | PRIMARY,AppUserId_idx | PRIMARY | 8 | afdata_next.xaction.AppUserIdAssistant1 | 1 | 100.00 | Using index |
| 1 | SIMPLE | appuser_assistant2 | NULL | eq_ref | PRIMARY,AppUserId_idx | PRIMARY | 8 | afdata_next.xaction.AppUserIdAssistant2 | 1 | 100.00 | Using where; Using index |
+----+-------------+----------------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------+-------------------------------------------+-------+----------+----------------------------------------------+
6 rows in set, 1 warning (0.00 sec)
And for reference, the table definition is as follows:
CREATE TABLE `xaction` (
`XactionId` bigint NOT NULL AUTO_INCREMENT,
`PropNum` varchar(20) NOT NULL,
`PropStreetDir` varchar(10) DEFAULT NULL,
`PropStreet` varchar(100) NOT NULL,
`PropStreetUnit` varchar(20) DEFAULT NULL,
`City` varchar(100) DEFAULT NULL,
`State` varchar(4) DEFAULT NULL,
`Zip` varchar(10) DEFAULT NULL,
`County` varchar(100) DEFAULT NULL,
`MlsId` varchar(50) DEFAULT NULL,
`TaxId` varchar(50) DEFAULT NULL,
`GfNum` varchar(50) DEFAULT NULL,
`ListPrice` decimal(13,4) DEFAULT NULL,
`ListPriceOriginal` decimal(13,4) DEFAULT NULL,
`SqFt` int DEFAULT NULL,
`SqFtSource` varchar(50) DEFAULT NULL,
`Beds` smallint DEFAULT NULL,
`Baths` decimal(4,2) DEFAULT NULL,
`YearBuilt` smallint DEFAULT NULL,
`LotSize` varchar(100) DEFAULT NULL,
`Schools` varchar(255) DEFAULT NULL,
`Subdivision` varchar(255) DEFAULT NULL,
`LockBoxId` varchar(20) DEFAULT NULL,
`LockBox` varchar(255) DEFAULT NULL,
`SecurityCode` varchar(255) DEFAULT NULL,
`HoaFee` varchar(100) DEFAULT NULL,
`HoaFrequency` varchar(100) DEFAULT NULL,
`Occupancy` varchar(50) DEFAULT NULL,
`Remarks` mediumtext,
`Instructions` mediumtext,
`ListOtherInfo` mediumtext,
`ContractPrice` decimal(13,4) DEFAULT NULL,
`OtherParty` varchar(500) DEFAULT NULL,
`EarnestMoney` varchar(255) DEFAULT NULL,
`DueDiligenceFee` varchar(255) DEFAULT NULL,
`Concessions` varchar(255) DEFAULT NULL,
`Financing` varchar(50) DEFAULT NULL,
`SpecialProvisions` mediumtext,
`ContractOtherInfo` mediumtext,
`Possession` mediumtext,
`EffectiveDate` date DEFAULT NULL,
`ClosingDate` date DEFAULT NULL,
`ListDate` date DEFAULT NULL,
`ExpireDate` date DEFAULT NULL,
`ClosedDate` date DEFAULT NULL,
`XactionPropTypeId` bigint DEFAULT NULL,
`XactionSourceId` bigint DEFAULT NULL,
`XactionSide` varchar(10) NOT NULL,
`XactionStatusId` bigint NOT NULL,
`PercentageCommission` decimal(6,5) DEFAULT NULL,
`CommissionNote` varchar(255) DEFAULT NULL,
`SplitTeamLead` decimal(6,5) DEFAULT NULL,
`SplitPrimaryAgent` decimal(6,5) DEFAULT NULL,
`SplitCoAgent` decimal(6,5) DEFAULT NULL,
`SplitAssistant1` decimal(6,5) DEFAULT NULL,
`SplitAssistant2` decimal(6,5) DEFAULT NULL,
`PayoutEstimated` decimal(13,4) DEFAULT NULL,
`PayoutActual` decimal(13,4) DEFAULT NULL,
`PayoutReferral` decimal(13,4) DEFAULT NULL,
`PayoutBroker` decimal(13,4) DEFAULT NULL,
`PayoutTeamLead` decimal(13,4) DEFAULT NULL,
`PayoutPrimaryAgent` decimal(13,4) DEFAULT NULL,
`PayoutCoAgent` decimal(13,4) DEFAULT NULL,
`PayoutAssistant1` decimal(13,4) DEFAULT NULL,
`PayoutAssistant2` decimal(13,4) DEFAULT NULL,
`ExpensesBroker` decimal(13,4) DEFAULT NULL,
`ExpensesTeamLead` decimal(13,4) DEFAULT NULL,
`GeoLatitude` decimal(9,7) DEFAULT NULL,
`GeoLongitude` decimal(10,7) DEFAULT NULL,
`GeoLocatorStatus` varchar(10) DEFAULT NULL,
`TimeZone` varchar(60) NOT NULL,
`FieldDataJson` text,
`CreateDateTime` datetime NOT NULL,
`EditDateTime` datetime NOT NULL,
`TeamId` bigint NOT NULL,
`AppUserIdPrimaryAgent` bigint NOT NULL,
`AppUserIdCoAgent` bigint DEFAULT NULL,
`AppUserIdAssistant1` bigint DEFAULT NULL,
`AppUserIdAssistant2` bigint DEFAULT NULL,
PRIMARY KEY (`XactionId`),
KEY `IDX_PropNum` (`PropNum`),
KEY `IDX_PropStreet` (`PropStreet`),
KEY `AppUserIdPrimaryAgent` (`AppUserIdPrimaryAgent`),
KEY `AppUserIdCoAgent` (`AppUserIdCoAgent`),
KEY `XactionPropTypeId` (`XactionPropTypeId`),
KEY `XactionSourceId` (`XactionSourceId`),
KEY `XactionStatusId` (`XactionStatusId`),
KEY `IDX_PropNum_PropStreet_PropStreetNum` (`PropNum`,`PropStreet`,`PropStreetUnit`),
KEY `AppUserIdAssistant1` (`AppUserIdAssistant1`),
KEY `AppUserIdAssistant2` (`AppUserIdAssistant2`),
CONSTRAINT `FK_Xaction_AppUser_Assistant1` FOREIGN KEY (`AppUserIdAssistant1`) REFERENCES `appuser` (`AppUserId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Xaction_AppUser_Assistant2` FOREIGN KEY (`AppUserIdAssistant2`) REFERENCES `appuser` (`AppUserId`),
CONSTRAINT `FK_Xaction_AppUser_CoAgent` FOREIGN KEY (`AppUserIdCoAgent`) REFERENCES `appuser` (`AppUserId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Xaction_AppUser_PrimaryAgent` FOREIGN KEY (`AppUserIdPrimaryAgent`) REFERENCES `appuser` (`AppUserId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Xaction_XactionPropTypeLookup` FOREIGN KEY (`XactionPropTypeId`) REFERENCES `xactionproptypelookup` (`XactionPropTypeId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Xaction_XactionSourceLookup` FOREIGN KEY (`XactionSourceId`) REFERENCES `xactionsourcelookup` (`XactionSourceId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Xaction_XactionStatusLookup` FOREIGN KEY (`XactionStatusId`) REFERENCES `xactionstatuslookup` (`XactionStatusId`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB;
2
Answers
You do not have an index (or a foreign key defition) for the TeamId which is the one that is likely to limit the query data. The optimizer choose to build a temporary table for that.
Btw, your
GROUP BY
does not match the columns you have inSELECT
. TheGROUP BY
is used usually with aggregate functions and it should contain all non-aggregated columns from yourSELECT
.Redesign the schema.
A "property" and a "transaction" are two separate Entities; they should be two tables. Perhaps there can be multiple transactions for a single property. A 1-many relationship could exist — property_id as a column in the Transaction table.
Or can it be the other way around — a single transaction involves several properties? This would necessitate a many-to-many table connecting the two Entity tables.
Instead of having 4 tables for 4 types of people, have a single table with an extra column to indicate primaryAgent, etc. Then there would be one
JOIN
and a single test forAppUserId = 1
. Many of the decimal columns would move into the [up to] 4 rows in the new table.I also recommend moving "sale" information to a separate table. There would not be a row in that table until the sale occurred. Meanwhile,
LEFT JOIN ... IS NULL
can determine that the property has not yet sold. Also, if there could be multiple sales, that new table could have multiple rows.As you restructure the tables, rethink the indexes and foreign keys.
It is unclear whether you need the
GROUP BY
after these changes.GROUP BY
Since there are no aggregates, the
GROUP BY
seems like a waste. On the other hand, I think the restructure will obviate the need for the grouping.Optimization
OR
is a big performance killerAppUserId = 1
, this should avoid 99% of the effort of the query.