Indizes / Optimierung für die Abfrage IN, JOIN, GROUP BY, ORDER BY

7

Ich arbeite an einer Abfrage , wo ich verwenden müssen IN, BETWEEN, GROUP BY, JOIN, ORDER BYalle 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

  1. Die Anzahl der Zeilen für die beiden folgenden Tabellen ist in millions.
  2. Es gibt Funktionen in dem Benutzerliste filtern können name, age, genderusw.
  3. Es gibt Funktionen , wo Benutzerliste sortieren kann durch einige Metriken wie age, visits_countusw.
  4. 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 ( INTeil) zwischen dem ausgewählten Zeitraum ( BETWEENTeil) und der Reihenfolge ihres Alters ( ORDER BYTeil) 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 INZeit dauert, 36 secondsbis 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:

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

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 INteilweise und dieses Unternehmen hat nur 3-4 Besucher). Wenn die Anzahl der Unternehmen INteilweise 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:

Geben Sie hier die Bildbeschreibung ein

Abfrage 3

Um die Verwendung eines INTeils 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 secondsdieser Auflistung.

Zusätzliche Information

  • innodb_buffer_pool_size beträgt 12 GB
  • RAM ist 30 GB
  • Ich verwende eine AWS RDS- db.r3.xlargeInstanz
  • SHOW TABLE STATUS Die Ausgabe ist wie folgt:
    Geben Sie hier die Bildbeschreibung ein

    1. 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

    2. 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, 20Dann" ändere, wird es 24 secondszum ersten Mal wiederholt , und dieselbe Abfrage zum zweiten Mal ist schneller. Es kann daran liegen innodb_buffer_pool_size.

    3. 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:

Geben Sie hier die Bildbeschreibung ein

Diese Abfrage dauert 58 Sekunden

Geben Sie hier die Bildbeschreibung ein

Explain Die Ausgabe der inneren Unterabfrage ist wie folgt

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein


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) = 5194480
  • COUNT(*) = 7607938

Beachten Sie, dass diese Ausgabe die neuesten Daten enthält, sodass sich die Anzahl der Zeilen count(*)möglicherweise erhöht hat.

Paresh Balar
quelle
Es gibt keinen guten Allzweckindex für Ihre Abfragen, und er hängt sehr stark von Ihren Daten ab, z. B. von der Wahrscheinlichkeit, dass eine Zeile in Tabelle 1 in Ihrer Ergebnismenge enthalten ist. Wie viele Ergebnisse erhalten Sie zB für Abfrage 1 ( limitnatü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 versuchen group by, select distinct t1.visitor_idstattdessen zu verwenden und die Verwendung dieser Indizes zu erzwingen?
Solarflare
@PareshBalar - Bei der Struktur des Schemas müssen 2660123 Zeilen überprüft werden. Sie hatten Glück, dass es nur 58 Sekunden dauerte. Ihre Anfrage prüft 25 Monate. Ist das typisch? Das heißt, möchten Sie monatliche Zahlen? Oder könnte es eine andere Spanne sein. (Ich denke über Übersichtstabellen nach.)
Rick James
@ RickJames leider seine Besucherliste mit unendlicher Bildlauffunktion, auch Benutzer kann Datumsbereich in Filterliste ändern. Der anfängliche Datumsbereich beträgt nur 3 bis 6 Monate, ABER die Möglichkeit besteht darin, dass der Benutzer diesen Datumsbereich auf 2 Jahre oder mehr ändern kann. Ich bin offen für Änderungen des Schemas, wenn die Möglichkeit einer Leistungssteigerung besteht. Wenn ich nur die monatliche Besucherzahl oder eine Art Statistik anzeigen muss, kann ich mir Übersichtstabellen vorstellen, bin mir aber nicht sicher, wie ich eine Übersichtstabelle für diese Art von Auflistungsfunktionen erstellen soll.
Paresh Balar
Hmmm ... Ein Besucher besucht selten zweimal im Monat ein Unternehmen. Dies bedeutet, dass eine in meinem Monat aufgeschlüsselte Übersichtstabelle wahrscheinlich nicht viel hilft.
Rick James

Antworten:

2

age int(3) unsigned- Damit können Sie Alter von bis zu 4 Milliarden speichern und 4 Bytes verschwenden. Wechseln Sie zu TINYINT 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):

INDEX(visited_on, conpany_id, visitor_id)

Wenn meine Vermutung richtig ist, ändern Sie die PK und fügen Sie einen Index hinzu:

PRIMARY KEY(`company_id`, `visitor_id`, visited_on),
INDEX(visited_on, conpany_id, visitor_id)

Dann überprüfen Sie Ihre verschiedenen Abfragen.

Rick James
quelle
Es wurde versucht, beide von Ihnen vorgeschlagenen Indizes hinzuzufügen. Der Optmizer für Abfragen verwendet jedoch immer noch den PRIMARY-Index, und die Abfrage dauerte ungefähr einige Zeit22 seconds
Paresh Balar
0

Ein weiterer Versuch.

SELECT  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;

Mein Grund für diesen Ansatz war, dass der JSON zu sagen schien , dass JOINdies unnötigerweise vor dem getan wurde GROUP BY. (Ich habe das geändert, DISTINCTweil 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, JOINist 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:

  • Nette, zivilisierte Abfragen (kleine Unternehmen, kurzer Zeitraum), bei denen der Optimierer die Chance hat, gute Arbeit zu leisten. (Vorschlagen INDEX(company_id)und INDEX(visited_on).)
  • Andere Abfragen, bei denen Sie dem Benutzer erlauben, nach etwas zu fragen, das von Natur aus langsam ist. (Der Optimierer meidet jeden Index und scannt einfach die gesamte Tabelle. Und das ist das Beste, was er tun kann.)

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 den PRIMARY KEY Start mit company_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 auf MEDIUMINT 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.

Rick James
quelle
Welcher Teil der Tabelle wird von diesem Datumsbereich abgedeckt? Ist das ein typischer Bereich? Oder ist es oft ein kleinerer Bereich? Oder breiter?
Rick James
Die Gesamtzahl der Datensätze in table_2(ohne Bedingung) 7607938und die Anzahl der Datensätze mit nur Datumsbereichsbedingung sind 7167282: 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.
Paresh Balar
Auch die Anzahl der Datensätze nach der whereBewerbung 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 die INAnzahl der Unternehmen teilweise geringer ist Die Anzahl der Datensätze kann in der Ergebnismenge hoch sein.
Paresh Balar
-1

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.

Nishant Uppal
quelle
1
ageund visited_onsind in verschiedenen Tabellen, ich glaube nicht, dass es eine Möglichkeit gibt, einen von Ihnen vorgeschlagenen Index hinzuzufügen.
Paresh Balar