Gibt es eine Möglichkeit, die Sortierung nach Spalten verknüpfter Tabellen zu optimieren?


10

Dies ist meine langsame Frage:

SELECT `products_counts`.`cid`
FROM
  `products_counts` `products_counts`

  LEFT OUTER JOIN `products` `products` ON (
  `products_counts`.`product_id` = `products`.`id`
  )
  LEFT OUTER JOIN `trademarks` `trademark` ON (
  `products`.`trademark_id` = `trademark`.`id`
  )
  LEFT OUTER JOIN `suppliers` `supplier` ON (
  `products_counts`.`supplier_id` = `supplier`.`id`
  )
WHERE
  `products_counts`.product_id IN
  (159, 572, 1075, 1102, 1145, 1162, 1660, 2355, 2356, 2357, 3236, 6471, 6472, 6473, 8779, 9043, 9095, 9336, 9337, 9338, 9445, 10198, 10966, 10967, 10974, 11124, 11168, 16387, 16689, 16827, 17689, 17920, 17938, 17946, 17957, 21341, 21352, 21420, 21421, 21429, 21544, 27944, 27988, 30194, 30196, 30230, 30278, 30699, 31306, 31340, 32625, 34021, 34047, 38043, 43743, 48639, 48720, 52453, 55667, 56847, 57478, 58034, 61477, 62301, 65983, 66013, 66181, 66197, 66204, 66407, 66844, 66879, 67308, 68637, 73944, 74037, 74060, 77502, 90963, 101630, 101900, 101977, 101985, 101987, 105906, 108112, 123839, 126316, 135156, 135184, 138903, 142755, 143046, 143193, 143247, 144054, 150164, 150406, 154001, 154546, 157998, 159896, 161695, 163367, 170173, 172257, 172732, 173581, 174001, 175126, 181900, 182168, 182342, 182858, 182976, 183706, 183902, 183936, 184939, 185744, 287831, 362832, 363923, 7083107, 7173092, 7342593, 7342594, 7342595, 7728766)
ORDER BY
  products_counts.inflow ASC,
  supplier.delivery_period ASC,
  trademark.sort DESC,
  trademark.name ASC
LIMIT
  0, 3;

Die durchschnittliche Abfragezeit in meinem Datensatz beträgt 4,5 Sekunden. Dies ist nicht akzeptabel.

Lösungen, die ich sehe:

Fügen Sie alle Spalten aus der order-Klausel zur products_countsTabelle hinzu. Aber ich habe ~ 10 Auftragstypen in der Anwendung, also sollte ich viele Spalten und Indizes erstellen. Außerdem habe products_countsich sehr intensive Updates / Einfügungen / Löschungen, so dass ich sofort alle auftragsbezogenen Spalten aktualisieren muss (mit Triggern?).

Gibt es eine andere Lösung?

Erklären:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| id | select_type | table           | type   | possible_keys                               | key                    | key_len | ref                              | rows | Extra                                        |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | products_counts | range  | product_id_supplier_id,product_id,pid_count | product_id_supplier_id | 4       | NULL                             |  227 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | products        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.product_id  |    1 |                                              |
|  1 | SIMPLE      | trademark       | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products.trademark_id       |    1 |                                              |
|  1 | SIMPLE      | supplier        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.supplier_id |    1 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Tabellenstruktur:

CREATE TABLE `products_counts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned NOT NULL,
  `supplier_id` int(11) unsigned NOT NULL,
  `count` int(11) unsigned NOT NULL,
  `cid` varchar(64) NOT NULL,
  `inflow` varchar(10) NOT NULL,
  `for_delete` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  UNIQUE KEY `product_id_supplier_id` (`product_id`,`supplier_id`),
  KEY `product_id` (`product_id`),
  KEY `count` (`count`),
  KEY `pid_count` (`product_id`,`count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) unsigned NOT NULL,
  `trademark_id` int(11) unsigned NOT NULL,
  `photo` varchar(255) NOT NULL,
  `sort` int(11) unsigned NOT NULL,
  `otech` tinyint(1) unsigned NOT NULL,
  `not_liquid` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `applicable` varchar(255) NOT NULL,
  `code_main` varchar(64) NOT NULL,
  `code_searchable` varchar(128) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  `slider` int(11) unsigned NOT NULL,
  `slider_title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`),
  KEY `category_id` (`category_id`),
  KEY `trademark_id` (`trademark_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `trademarks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `country_id` int(11) NOT NULL,
  `sort` int(11) unsigned NOT NULL DEFAULT '0',
  `sort_list` int(10) unsigned NOT NULL DEFAULT '0',
  `is_featured` tinyint(1) unsigned NOT NULL,
  `is_direct` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `suppliers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `code` varchar(64) NOT NULL,
  `name` varchar(255) NOT NULL,
  `delivery_period` tinyint(1) unsigned NOT NULL,
  `is_default` tinyint(1) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Informationen zum MySQL-Server:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))

3
Können Sie eine SQL-Geige mit Indizes, Tabellenschema und Testdaten bereitstellen? Was ist auch Ihre Zielzeit? Möchten Sie es in 3 Sekunden, 1 Sekunde und 50 Millisekunden fertigstellen? Wie viele Datensätze haben Sie in den verschiedenen Tabellen 1k, 100k, 100M?
Erik

Wenn die Felder, nach denen Sie sortieren, nicht indiziert sind und der Datensatz wirklich groß ist, könnten Sie sich vielleicht ein Problem mit sort_buffer_size ansehen? Sie können versuchen, den Wert in Ihrer Sitzung zu ändern, und die Abfrage ausführen, um festzustellen, ob sie sich verbessert.
Brian Efting

Haben Sie versucht, einen Index hinzuzufügen (inflow, product_id)?
Ypercubeᵀᴹ

Stellen Sie sicher, dass Sie einen anständigen haben innodb_buffer_pool_size. Normalerweise sind etwa 70% des verfügbaren Arbeitsspeichers gut.
Rick James

Antworten:


6

Das Überprüfen Ihrer Tabellendefinitionen zeigt, dass die Indizes für alle beteiligten Tabellen übereinstimmen. Dies sollte dazu führen, dass die MySQL'sVerknüpfungen innerhalb der Grenzen der Verknüpfungslogik so schnell wie möglich erfolgen .

Das Sortieren aus mehreren Tabellen ist jedoch komplexer.

2007 beschrieb Sergey Petrunia die 3 MySQLSortieralgorithmen in der Reihenfolge ihrer Geschwindigkeit für MySQL: http://s.petrunia.net/blog/?m=201407

  1. Verwenden Sie eine indexbasierte Zugriffsmethode, die eine geordnete Ausgabe erzeugt
  2. Verwendung filesort()auf der 1. nicht konstanten Tabelle
  3. Fügen Sie das Join-Ergebnis in eine temporäre Tabelle ein und verwenden Sie filesort()es

Anhand der oben gezeigten Tabellendefinitionen und Verknüpfungen können Sie erkennen, dass Sie niemals die schnellste Sortierung erhalten . Das bedeutet, dass Sie für die von Ihnen verwendeten Sortierkriterien abhängig filesort()sind.

Wenn Sie jedoch eine materialisierte Ansicht entwerfen und verwenden , können Sie den schnellsten Sortieralgorithmus verwenden.

Informationen zu den für MySQL 5.5Sortiermethoden definierten Details finden Sie unter: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Für MySQL 5.5(in diesem Beispiel) zu erhöhen ORDER BYGeschwindigkeit , wenn Sie nicht bekommen können MySQLIndizes eher zu verwenden , als eine zusätzliche Sortierphase, versuchen Sie die folgenden Strategien:

• Erhöhen Sie den sort_buffer_sizeVariablenwert.

• Erhöhen Sie den read_rnd_buffer_sizeVariablenwert.

• Verwenden Sie weniger RAM pro Zeile, indem Sie Spalten nur so groß deklarieren, wie es für die zu speichernden tatsächlichen Werte erforderlich ist. [ZB Reduzieren Sie einen Varchar (256) auf Varchar (ActualLongestString)]

• Ändern Sie die tmpdirSystemvariable so, dass sie auf ein dediziertes Dateisystem mit viel freiem Speicherplatz verweist. (Weitere Details finden Sie unter dem obigen Link.)

In der MySQL 5.7Dokumentation finden Sie weitere Details zur Erhöhung der ORDERGeschwindigkeit. Einige dieser Verhaltensweisen können leicht verbessert werden:

http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

Materialisierte Ansichten - Ein anderer Ansatz zum Sortieren verbundener Tabellen

Sie haben mit Ihrer Frage zur Verwendung von Triggern auf materialisierte Ansichten angespielt. MySQL verfügt nicht über integrierte Funktionen zum Erstellen einer materialisierten Ansicht , Sie verfügen jedoch über die erforderlichen Tools. Durch die Verwendung von Triggern zum Verteilen der Last können Sie die materialisierte Ansicht auf dem neuesten Stand halten.

Die materialisierte Ansicht ist eigentlich eine Tabelle, die durch Prozedurcode zum Erstellen oder Neuerstellen der materialisierten Ansicht gefüllt und von Triggern verwaltet wird , um die Daten auf dem neuesten Stand zu halten.

Da Sie eine Tabelle mit einem Index erstellen, kann die materialisierte Ansicht bei Abfrage die schnellste Sortiermethode verwenden : Verwenden Sie eine indexbasierte Zugriffsmethode, die eine geordnete Ausgabe erzeugt

Da MySQL 5.5zum Verwalten einer materialisierten Ansicht Trigger verwendet werden, benötigen Sie auch einen Prozess, ein Skript oder eine gespeicherte Prozedur, um die anfängliche materialisierte Ansicht zu erstellen .

Dies ist jedoch offensichtlich ein zu schwerer Prozess, um nach jedem Update der Basistabellen, in denen Sie die Daten verwalten, ausgeführt zu werden. Hier kommen die Trigger ins Spiel, um die Daten bei Änderungen auf dem neuesten Stand zu halten. Auf diese Weise jeder insert, updateund deleteihre Änderungen propagieren, indem Sie Ihre Trigger, auf die materialisierte Ansicht .

Die FROMDUAL-Organisation unter http://www.fromdual.com/ verfügt über Beispielcode zum Verwalten einer materialisierten Ansicht . Anstatt meine eigenen Samples zu schreiben, werde ich Sie auf ihre Samples verweisen:

http://www.fromdual.com/mysql-materialized-views

Beispiel 1: Erstellen einer materialisierten Ansicht

DROP TABLE sales_mv;
CREATE TABLE sales_mv (
    product_name VARCHAR(128)  NOT NULL
  , price_sum    DECIMAL(10,2) NOT NULL
  , amount_sum   INT           NOT NULL
  , price_avg    FLOAT         NOT NULL
  , amount_avg   FLOAT         NOT NULL
  , sales_cnt    INT           NOT NULL
  , UNIQUE INDEX product (product_name)
);

INSERT INTO sales_mv
SELECT product_name
    , SUM(product_price), SUM(product_amount)
    , AVG(product_price), AVG(product_amount)
    , COUNT(*)
  FROM sales
GROUP BY product_name;

Dadurch erhalten Sie die materialisierte Ansicht zum Zeitpunkt der Aktualisierung. Da Sie jedoch über eine sich schnell bewegende Datenbank verfügen, möchten Sie diese Ansicht auch so aktuell wie möglich halten.

Daher müssen die betroffenen Basisdatentabellen Trigger haben, um die Änderungen von einer Basistabelle in die Tabelle Materialized View zu übertragen . Als ein Beispiel:

Beispiel 2: Einfügen neuer Daten in eine materialisierte Ansicht

DELIMITER $$

CREATE TRIGGER sales_ins
AFTER INSERT ON sales
FOR EACH ROW
BEGIN

  SET @old_price_sum = 0;
  SET @old_amount_sum = 0;
  SET @old_price_avg = 0;
  SET @old_amount_avg = 0;
  SET @old_sales_cnt = 0;

  SELECT IFNULL(price_sum, 0), IFNULL(amount_sum, 0), IFNULL(price_avg, 0)
       , IFNULL(amount_avg, 0), IFNULL(sales_cnt, 0)
    FROM sales_mv
   WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg
       , @old_amount_avg, @old_sales_cnt
  ;

  SET @new_price_sum = @old_price_sum + NEW.product_price;
  SET @new_amount_sum = @old_amount_sum + NEW.product_amount;
  SET @new_sales_cnt = @old_sales_cnt + 1;
  SET @new_price_avg = @new_price_sum / @new_sales_cnt;
  SET @new_amount_avg = @new_amount_sum / @new_sales_cnt;

  REPLACE INTO sales_mv
  VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg
       , @new_amount_avg, @new_sales_cnt)
  ;

END;
$$
DELIMITER ;

Natürlich benötigen Sie auch Trigger, um das Löschen von Daten aus einer materialisierten Ansicht und das Aktualisieren von Daten in einer materialisierten Ansicht aufrechtzuerhalten . Auch für diese Trigger stehen Beispiele zur Verfügung.

SCHLIESSLICH: Wie macht das Sortieren verbundener Tabellen schneller?

Die materialisierte Ansicht wird ständig erstellt, während die Aktualisierungen daran vorgenommen werden. Daher können Sie den Index (oder die Indizes ) definieren, die Sie zum Sortieren der Daten in der materialisierten Ansicht oder Tabelle verwenden möchten .

Wenn der Aufwand für die Pflege der Daten nicht zu hoch ist, müssen Sie für jede relevante Datenänderung einige Ressourcen (CPU / E / A / usw.) aufwenden, um die materialisierte Ansicht zu erhalten. Daher sind die Indexdaten aktuell und leicht verfügbar. Daher ist die Auswahl schneller, da Sie:

  1. Sie haben bereits inkrementelle CPU und E / A ausgegeben, um die Daten für Ihr SELECT vorzubereiten.
  2. Der Index in der materialisierten Ansicht kann die schnellste für MySQL verfügbare Sortiermethode verwenden , nämlich die indexbasierte Zugriffsmethode, die eine geordnete Ausgabe erzeugt .

Abhängig von Ihren Umständen und Ihrer Meinung zum Gesamtprozess möchten Sie die materialisierten Ansichten möglicherweise jede Nacht in einem langsamen Zeitraum neu erstellen.

Hinweis: In Microsoft SQL Server materialisierten Ansichten wird auf indizierte Ansichten verwiesen und automatisch basierend auf den Metadaten der indizierten Ansicht aktualisiert .


6

Hier gibt es nicht viel zu tun, aber ich vermute, das Hauptproblem ist, dass Sie jedes Mal eine ziemlich große temporäre Tabelle erstellen und die Datei auf der Festplatte sortieren. Der Grund ist:

  1. Sie verwenden UTF8
  2. Sie verwenden einige große Varchar-Felder (255) zum Sortieren

Dies bedeutet, dass Ihre temporäre Tabelle und Sortierdatei ziemlich groß sein kann, da beim Erstellen der temporären Tabelle die Felder mit der maximalen Länge erstellt werden und beim Sortieren der Datensätze alle mit der maximalen Länge (und UTF8 3 Bytes pro Zeichen). Diese schließen wahrscheinlich auch die Verwendung einer temporären In-Memory-Tabelle aus. Weitere Informationen finden Sie unter Details zu internen temporären Tabellen .

Das LIMIT nützt uns auch hier nichts, da wir die gesamte Ergebnismenge materialisieren und ordnen müssen, bevor wir die ersten drei Zeilen kennen.

Haben Sie versucht, Ihr tmpdir in ein tmpfs- Dateisystem zu verschieben? Wenn / tmp nicht bereits tmpfs verwendet (MySQL verwendet tmpdir=/tmpstandardmäßig * nix), können Sie / dev / shm direkt verwenden. In Ihrer my.cnf-Datei:

[mysqld]
...
tmpdir=/dev/shm  

Dann müssten Sie mysqld neu starten.

Das könnte einen großen Unterschied machen. Wenn Sie wahrscheinlich unter Speicherdruck auf dem System geraten, möchten Sie wahrscheinlich die Größe begrenzen (normalerweise beschränken Linux-Distributionen tmpfs standardmäßig auf 50% des gesamten Arbeitsspeichers), um zu vermeiden, dass Speichersegmente auf die Festplatte oder sogar ausgelagert werden schlimmer eine OOM-Situation . Sie können dies tun, indem Sie die Zeile in bearbeiten /etc/fstab:

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0

Sie können die Größe auch "online" ändern. Beispielsweise:

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm

Sie können auch ein Upgrade auf MySQL 5.6 durchführen, das performante Unterabfragen und abgeleitete Tabellen enthält, und ein bisschen mehr mit der Abfrage herumspielen. Ich glaube nicht, dass wir auf diesem Weg große Gewinne sehen werden, so wie ich es sehe.

Viel Glück!


Danke für deine Antwort. Das Verschieben von tmpdir zu tmpfs ergab einen guten Leistungsgewinn.
Stanislav Gamayunov
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.