目標:特定のユーザーに関連するposts
から最新の10行を選択します。
2つのテーブルがあります:posts
(〜5,000,000行)とrelations
(〜8,000行)。
posts
列:
--------------------------------------------------------------------------------------------
| id (int) | source_id (int) | title (varchar) | content (longtext) | date (int) |
--------------------------------------------------------------------------------------------
relations
列:
----------------------------------------------------
| id (int) | source_id (int) | user_id (int) |
----------------------------------------------------
特定のユーザーに関連するposts
から最新の10行を取得してJOINを使用してみました:
SELECT p.id, p.title, p.content, r.id AS rid
FROM posts AS p
JOIN relations AS r
ON r.source_id = p.source_id
WHERE r.user_id = 1
ORDER BY p.date DESC
LIMIT 10
ただし、実行に〜30秒かかります(SDDホスティング!)。単一の列と複数の列のインデックスを含め、両方のテーブルで多くのインデックスの組み合わせを試しましたが、いずれも実行時間に影響を与えませんでした。 選択をスピードアップする方法はありますか?
ために user_id=1
〜1,000ありますsource_id
とposts
の〜450,000行。
SHOW CREATE TABLE...
結果:
CREATE TABLE `relations` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned NOT NULL,
`source_id` bigint(20) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `source_id` (`source_id`),
KEY `source_id_2` (`source_id`,`user_id`),
) ENGINE=InnoDB AUTO_INCREMENT=7692 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
CREATE TABLE `posts` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`source_id` bigint(20) unsigned NOT NULL,
`title` varchar(512) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`content` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci,
`date` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `source_id` (`source_id`),
KEY `date` (`date`),
KEY `date_2` (`date`,`source_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4867283 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
EXPLAIN
結果:
MySQLはデータセットを使用して、投稿から450,000レコード(一致する各source_idから1000個の小さなチャンクで)を取得し、それを並べ替えて、上位10件を返す必要があります。これはコストのかかる作業です。
ストアドプロシージャを使用して結果を蓄積し、たとえば毎日または毎週、少なくとも10個のレコードを取得するまでループし、次に最新の10個を返すようにすることができます。 _(date, source_id)
_によるposts
のインデックスが必要です。最近アクティブなユーザーの場合はすぐに戻りますが、最近の投稿がないユーザーの場合は時間がかかります。次のようなもの:
_DELIMITER ;;
CREATE DEFINER=CURRENT_USER PROCEDURE stored_procedure_name(u_id int)
BEGIN
DECLARE fd DATE;
DECLARE d DATE;
SELECT MIN(date), MAX(date) INTO fd, d FROM posts;
CREATE TEMPORARY TABLE last_posts (id int);
WHILE d > fd AND (SELECT COUNT(*) FROM last_posts) < 10 DO
INSERT INTO last_posts (id)
SELECT p.id
FROM relations AS r
JOIN posts AS p ON (p.source_id = r.source_id AND
p.date > date_sub(d, interval 7 day) AND p.date <= d)
WHERE r.user_id = u_id
ORDER BY p.date DESC
LIMIT 10;
SET d = date_sub(d, interval 7 day);
END WHILE;
SELECT p.id, p.title, p.content, r.id AS rid
FROM posts p
JOIN relations AS r ON (r.source_id = p.source_id)
WHERE p.id IN (SELECT * FROM last_posts)
ORDER BY p.date DESC
LIMIT 10;
DROP TABLE last_posts;
END;;
DELIMITER ;
_
間隔で遊んで、1日に減らすことができます(より高速なクエリでより多くのサイクル)。 posts (date)
にインデックスがあることを確認してください。
このmightは、「遅延評価」により、実行が速くなります。いくつかの大きな列をフェッチする必要があるが、どの10が望ましいかを決定する前に数千の行を調べる必要があることに注意してください。必要なすべての列を収集する代わりに、PRIMARY KEYs
、次にposts
に10回だけアクセスして、かさばる列を取得します。かさばるカラムはInnoDBの個別のブロックにオフレコで格納されることに注意してください。
(最初のテーブルを最初にリストするためだけに、@ Nicholasの公式から始めます。)
SELECT p.id, p.title, p.content, rid
FROM
( SELECT p.id AS pid, r.id AS rid
FROM relations AS r
JOIN posts AS p ON r.source_id = p.source_id
WHERE r.user_id = 1
ORDER BY p.date DESC
LIMIT 10
) AS x
JOIN posts p ON p.id = x.pid;
必要なインデックス。列の順序は重要です。
relations: INDEX(user_id, source_id, id) -- "covering"
posts: INDEX(source_id, date, id) -- for the subquery; "covering"
posts: PRIMARY KEY(id) -- for the outer query; already exists
サイドノート:
ALTER TABLE ... CONVERT TO ...
。)BIGINT
はやり過ぎです。relations
が多対多のマッピングにすぎない場合、おそらく双方向のインデックスが必要です。 id
の必要性を説明できますか? 推奨 。