Ich arbeite an einer Abfrage , wo ich verwenden müssen IN
, BETWEEN
, GROUP BY
, JOIN
, ORDER BY
alle in einer Abfrage. Ich habe Probleme mit der Leistung dieser Abfrage, daher benötige ich Hilfe bei der Auswahl von Indizes oder bei Änderungen an Tabellenstrukturen, wenn Indizes nicht helfen.
Einige Überlegungen
- Die Anzahl der Zeilen für die beiden folgenden Tabellen ist in
millions
. - Es gibt Funktionen in dem Benutzerliste filtern können
name
,age
,gender
usw. - Es gibt Funktionen , wo Benutzerliste sortieren kann durch einige Metriken wie
age
,visits_count
usw. - Brauchen Sie Paginierung für Liste.
Tabellenstrukturen
Tabelle 1
CREATE TABLE `table_1` (
`visitor_id` varchar(32) CHARACTER SET ascii NOT NULL,
`name` varchar(200) NOT NULL,
`gender` varchar(1) NOT NULL DEFAULT 'M',
`mobile_number` int(10) unsigned DEFAULT NULL,
`age` tinyint(1) unsigned NOT NULL DEFAULT '1',
`visits_count` mediumint(5) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`visitor_id`),
KEY `indx_t1_test` (`visitor_id`,`visits_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Tabelle 2
CREATE TABLE `table_2` (
`company_id` bigint(20) unsigned NOT NULL,
`visitor_id` varchar(32) CHARACTER SET ascii NOT NULL,
`time_duration` mediumint(5) unsigned NOT NULL DEFAULT '0',
`visited_on` date NOT NULL,
PRIMARY KEY (`company_id`,`visitor_id`,`visited_on`),
KEY `indx_t2_test` (`visited_on`,`company_id`,`visitor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Die meisten Basisdaten, die ich abrufen möchte
Sie möchten 20 (Paginierung) eindeutige Besucher ( GROUP BY / DISTINCT
) erhalten, die eine bestimmte Unternehmensgruppe ( IN
Teil) zwischen dem ausgewählten Zeitraum ( BETWEEN
Teil) und der Reihenfolge ihres Alters ( ORDER BY
Teil) besucht haben.
Abfrage 1
Erste Abfrage, wenn ich dafür aufschreibe, dann wäre es:
SELECT
t1.visitor_id
FROM table_1 AS t1
INNER JOIN table_2 AS t2 ON t2.visitor_id = t1.visitor_id
WHERE
t2.company_id IN (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455) AND
t2.visited_on BETWEEN '2015-01-01' AND '2017-01-31'
GROUP BY t1.visitor_id
ORDER BY t1.`visits_count` DESC
LIMIT 20;
Wenn ich diese Abfrage für ein einzelnes Unternehmen ausführe, werden Daten schnell genug zurückgegeben (wenn die Anzahl der übereinstimmenden Zeilen gering ist, ist die Abfrageleistung gut).
Das Problem ist, dass es einige IN
Zeit dauert, 36 seconds
bis das Ergebnis zurückgegeben wird, wenn die Anzahl der Unternehmen in einem Teil der Abfrage zunimmt (ich muss 100 Unternehmen für diesen Teil der Abfrage unterstützen) .
Explain
Die Ausgabe dieser Abfrage lautet:
Abfrage 2
Zweite Abfrage, die ich mir für den gleichen Fall vorstellen kann, dann wäre es ungefähr so:
SELECT
(
SELECT
t2.visitor_id
FROM table_2 AS t2
WHERE
t2.company_id IN (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455)
AND t2.visitor_id = t1.`visitor_id`
AND t2.visited_on BETWEEN '2015-01-01' AND '2017-01-31'
LIMIT 1
) AS visitor_id
FROM `table_1` AS t1
HAVING visitor_id IS NOT NULL
ORDER BY t1.`visits_count` DESC
LIMIT 0, 20
Das Verhalten dieser Abfrage ist dem ersten entgegengesetzt. Wenn ich eine Abfrage für ein Unternehmen mit wenigen Besuchern ausführe, ist die Leistung dieser Abfrage sehr gering (es dauerte ungefähr 38 seconds
) (nur ein Unternehmen IN
teilweise und dieses Unternehmen hat nur 3-4 Besucher). Wenn die Anzahl der Unternehmen IN
teilweise hoch ist, werden die Ergebnisse im Vergleich zu einem Unternehmen schneller zurückgegeben (es dauerte ungefähr 13 seconds
), aber die Leistung ist immer noch nicht nutzbar.
Explain
Die Ausgabe dieser Abfrage lautet:
Abfrage 3
Um die Verwendung eines IN
Teils der Abfrage zu vermeiden, habe ich eine temporäre Tabelle erstellt, Firmen-IDs in diese Tabelle eingefügt und dann Folgendes verwendet JOIN
:
SELECT
DISTINCT
t1.visitor_id
FROM `table_1` AS t1
INNER JOIN `table_2` AS t2 ON t1.`visitor_id` = t2.visitor_id
INNER JOIN temp_table AS t3 ON t3.company_id = t2.company_id
ORDER BY t1.`visits_count` DESC
LIMIT 0, 20;
Diese Abfrage dauert ebenfalls bis zu 22 Sekunden . Ich benötige Leistung bis zu 2-3 seconds
dieser Auflistung.
Zusätzliche Information
innodb_buffer_pool_size
beträgt 12 GB- RAM ist 30 GB
- Ich verwende eine AWS RDS-
db.r3.xlarge
Instanz SHOW TABLE STATUS
Die Ausgabe ist wie folgt:
Die Abfrage wird
SELECT COUNT(*) FROM table_2 WHERE company_id IN (...) AND visited_on BETWEEN '2015-01-01' AND '2017-01-31'
zurückgegeben2660123
Nur zum ersten Mal braucht es Zeit. Wenn ich dieselbe Abfrage erneut ausführe, ist sie sehr viel schneller (0,2 Sekunden). Wenn ich das Limit jedoch auf "
LIMIT 20, 20
Dann" ändere, wird es24 seconds
zum ersten Mal wiederholt , und dieselbe Abfrage zum zweiten Mal ist schneller. Es kann daran liegeninnodb_buffer_pool_size
.Die Ausgabe von
EXPLAIN FORMAT=JSON SELECT ...;
ist wie folgt.
{ "query_block": { "select_id": 1, "ordering_operation": { "using_filesort": true, "grouping_operation": { "using_temporary_table": true, "using_filesort": false, "nested_loop": [ { "table": { "table_name": "t2", "access_type": "range", "possible_keys": [ "PRIMARY", "indx_t2_test" ], "key": "PRIMARY", "used_key_parts": [ "company_id" ], "key_length": "8", "rows": 17301, "filtered": 100, "using_index": true, "attached_condition": "((`db`.`t2`.`company_id` in (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455)) and (`db`.`t2`.`visited_on` between '2015-01-01' and '2017-01-31'))" } }, { "table": { "table_name": "t1", "access_type": "eq_ref", "possible_keys": [ "PRIMARY", "indx_t1_test" ], "key": "PRIMARY", "used_key_parts": [ "visitor_id" ], "key_length": "34", "ref": [ "db.t2.visitor_id" ], "rows": 1, "filtered": 100 } } ] } } } }
Die Ausgabe der von Rick James vorgeschlagenen Abfrage:
SELECT
t2.visitor_id
FROM (
SELECT
DISTINCT visitor_id
FROM table_2
WHERE
company_id IN (528,211,1275,521,1299,493,492,852, 868,
869,1235,486,485,1238,855,1237,651,538,1241,1240, 548,
543,1247,1253,490,468,582,583,569,477,488,802,1294, 518,
1274,476,545,1267,556,479,1266,1265,541,1189,1263, 1152,
1260,478,1257,885,1139,1256,804,708,547,561,1239, 1142,
1226,1148,1230,529,1223,1192,1191,874,830,822,818, 817,
794,718,487,709,706,705,669,513,455)
AND visited_on BETWEEN '2015-01-01' AND '2017-01-31'
) AS t2
INNER JOIN table_1 AS t1 ON t2.visitor_id = t1.visitor_id
ORDER BY t1.`visits_count` DESC
LIMIT 20;
Explain
Die Ausgabe der Abfrage lautet wie folgt:
Diese Abfrage dauert 58 Sekunden
Explain
Die Ausgabe der inneren Unterabfrage ist wie folgt
Die Abfrage:
SELECT
COUNT(DISTINCT company_id, visited_on, visitor_id),
COUNT(DISTINCT company_id, LEFT(visited_on, 7), visitor_id),
COUNT(*)
FROM table_2;
kehrt zurück:
COUNT(DISTINCT company_id, visited_on, visitor_id)
= 7607938.COUNT(DISTINCT company_id, LEFT(visited_on, 7), visitor_id)
= 5194480COUNT(*)
= 7607938
Beachten Sie, dass diese Ausgabe die neuesten Daten enthält, sodass sich die Anzahl der Zeilen count(*)
möglicherweise erhöht hat.
quelle
limit
natürlich ohne )? Wenn dies ein wesentlicher Bestandteil Ihres Geschäftsmodells ist, sollten Sie über vorberechnete Ergebnisse nachdenken. Als Test: Könnten Sie versuchen, einen Index(visits_count)
für Tabelle 1 und(visitor_id, company, visited_on)
für Tabelle 2 hinzuzufügen , Abfrage 1 ohne (falsch) zu versuchengroup by
,select distinct t1.visitor_id
stattdessen zu verwenden und die Verwendung dieser Indizes zu erzwingen?Antworten:
age int(3) unsigned
- Damit können Sie Alter von bis zu 4 Milliarden speichern und 4 Bytes verschwenden. Wechseln Sie zuTINYINT UNSIGNED
(1 Byte).Ascii für Namen? Beschränkt auf die USA? Trotzdem einige seltsame Namen nicht zulassen.
Ich bin verwirrt von t2
PRIMARY KEY
. Da die PK einzigartig ist, ist es nicht möglich, mehr als einen Besuch in einem Unternehmen für eine Person aufzuzeichnen. Wenn die Einschränkung in Ordnung ist, fügen Sie dies hinzu (falls der Optimierer entscheidet, dass der Datenbereich der beste Filter ist):Wenn meine Vermutung richtig ist, ändern Sie die PK und fügen Sie einen Index hinzu:
Dann überprüfen Sie Ihre verschiedenen Abfragen.
quelle
22 seconds
Ein weiterer Versuch.
Mein Grund für diesen Ansatz war, dass der JSON zu sagen schien , dass
JOIN
dies unnötigerweise vor dem getan wurdeGROUP BY
. (Ich habe das geändert,DISTINCT
weil es eine logischere Art ist, es auszudrücken.)Ich vermute , dass es gibt viele visitors_ids nach Filterung auf company_id und visited_on, aber dann wird die Liste nach unten viel schnitzte. Wenn Sie das Schnitzen vor dem machen,
JOIN
ist dies möglicherweise schneller.Mehr
Basierend auf einigen Informationen über den Bereich der auszuführenden Abfragen und die Verteilung der Daten ...
Sie haben zwei Kategorien von Abfragen, die von derselben Benutzeroberfläche stammen:
INDEX(company_id)
undINDEX(visited_on)
.)Normalerweise würde ich an dieser Stelle das Lob der Übersichtstabellen singen. Aber Sie haben eine Anforderung, die solche verhindert -
DISTINCT
. (Ich habe einen Blog darüber, wie man so etwas aufrollt, aber ich möchte nicht darauf eingehen.Ein anderer Gedanke ist,
PARTITION BY RANGE(TO_DAYS(visited_on))
und hat denPRIMARY KEY
Start mitcompany_id
. Die 2-dimensionale Natur Ihrer quey ist als einer der wenigen Anwendungsfälle erwähnt zum Partitionieren hier und weiter diskutiert hier . (Aufteilung nach Monaten.)Sie können das Problem auch vermeiden, indem Sie die Benutzeroberfläche überdenken. Wenn Sie den Benutzer auf maximal 3 Monate beschränken, besteht die Möglichkeit, einen Index zu verwenden, der mit beginnt
visited_on
.Eine andere Sache ...
VARCHAR(32)
ist sperrig. Normalisieren Sie es aufMEDIUMINT UNSIGNED
; Dadurch werden die Daten verkleinert, was zu weniger E / A und besserem Caching führt und somit zu mehr Geschwindigkeit führt.Aber ich wiederhole, Sie haben ein schwieriges Problem.
quelle
table_2
(ohne Bedingung)7607938
und die Anzahl der Datensätze mit nur Datumsbereichsbedingung sind7167282
: Nein, dieser Datenbereich ist nicht so typisch. Ich habe ein hohes Szenario mit mehr als 70 Unternehmen und einem Datumsbereich von 2 Jahren in Betracht gezogen. Normalerweise beträgt der Standard-Datumsbereich 3 Monate oder 6 Monate, ABER der Benutzer kann dies auf 2 Jahre oder mehr ändern. Gleiches gilt auch für das Unternehmen. im Normalfall wäre die Anzahl der Unternehmen 30-40. Aber im Maximalfall wären es mehr als 100 oder 200.where
Bewerbung hängt wirklich von der Unternehmensgröße ab. Wenn das Unternehmen eine Art gemeinnütziges Unternehmen ist, kann die Anzahl der Besucher für dieses Unternehmen hoch sein, was bedeutet, dass selbst wenn der Datumsbereich klein ist und dieIN
Anzahl der Unternehmen teilweise geringer ist Die Anzahl der Datensätze kann in der Ergebnismenge hoch sein.Wenn Sie einen Index hinzufügen müssen (Besucher-ID, Alter, besuchte_on), wird die Leistung der genannten Abfrage1 verbessert.
Die temporäre Tabelle "Gruppieren nach" und "Sortieren nach" erstellt, aufgrund derer die Abfrage verlangsamt wird.
quelle
age
undvisited_on
sind in verschiedenen Tabellen, ich glaube nicht, dass es eine Möglichkeit gibt, einen von Ihnen vorgeschlagenen Index hinzuzufügen.