次の2つのデータフレームがあると仮定します(私の質問では簡略化しています)。
+---------+
| user_id |
+---------+
| 1 |
| 2 |
| ... |
+---------+
+------------+------------+
| article_id | date |
+------------+------------+
| a | 2019-01-01 |
| b | 2018-03-03 |
| ... | |
+------------+------------+
そして、各値が各ユーザーが各記事を読みたいと思うと予測する量(0から1まで)であるユーザーと記事のペアの密な行列:
+-----+------+------+-----+
| | 1 | 2 | ... |
+-----+------+------+-----+
| a | 0.54 | 0.99 | ... |
| b | 0 | 0.7 | ... |
| ... | ... | ... | ... |
+-----+------+------+-----+
特定の日付範囲で、1人のユーザーに対して最も推奨される上位10件の記事、または11番目から20番目に推奨される記事などを返す必要があるWebアプリがあります。
query: (user_id=123) AND (news_date IN ('2019-04-01', '2019-05-01')) LIMIT 10 OFFSET 10
+---------+-------+------+
| news_id | score | rank |
+---------+-------+------+
| g | 0.98 | 11 |
| d | 0.97 | 12 |
| ... | ... | ... |
| q | 0.8 | 20 |
+---------+-------+------+
課題は、数万のユーザーと記事があり、列の制限のためにマトリックスをPostgresテーブルとして単に保存できないことです。
Postgresの推奨スコアを(user_id, article_id, score)
としてテーブルに保存できます。これはクエリには高速ですが、このテーブルには100M以上の行があり、更新にコストがかかります。
現在の解決策は、単一のデータフレーム(news_id, news_date, user_1_score, user_2_score, ..., user_n_score)
をgzipされたParquetファイルとしてディスクに保存し、news_date
列とuser_x_score
列をロードして、フィルター処理、並べ替え、スライスすることです。唯一の欠点は、私のWebホストに一時ファイルシステムがあるため、アプリの起動時にこのファイルをダウンロードする必要があることです。少なくとも、Webリクエスト中にデータを取得するのに十分な速度です。
円柱データストアについてはあまり知りませんが、これらの製品の1つが私の問題に適していると感じています。誰かがアイデアを持っていますか?
"but this table would have 100M+ rows and be expensive to update, which I do daily."
これを否定するために、私は次のことをしました。
CREATE TABLE test_article (
the_series integer,
user_id integer,
article_id integer,
rating numeric
);
タイミングを合わせるので、適切な指標があります。
\timing
次に、test_articleに1000万レコードを挿入しました。
INSERT INTO test_article
SELECT generate_series(1, 10000000), CAST(RANDOM() * 10 + 1 AS INTEGER), CAST(RANDOM() * 100 + 1 AS INTEGER), ROUND(CAST(RANDOM() AS NUMERIC), 2);
時間:
INSERT 0 10000000
Time: 33520.809 ms (00:33.521)
表の内容(サンプル):
test=# SELECT * FROM test_article;
the_series | user_id | article_id | rating
------------+---------+------------+--------
1 | 5 | 85 | 0.95
2 | 6 | 41 | 0.14
3 | 5 | 90 | 0.34
4 | 3 | 18 | 0.32
5 | 7 | 6 | 0.30
6 | 10 | 32 | 0.31
7 | 8 | 70 | 0.84
これは完璧なベンチマークではないことを理解しています。そうするためには、(user_id、article_id)にUNIQUE
インデックスがなければなりません-しかし、それをできるだけ現実的にするために、それらのフィールドに配置します。 巨大な歪みではないと私は信じています。編集-以下を参照-この問題は解決されました!
だから、私はインデックスを作成しました:
CREATE INDEX user_article_ix ON test_article (user_id, article_id);
時間:
CREATE INDEX
Time: 20556.118 ms (00:20.556)
次に、100Kレコードを挿入しました。
INSERT INTO test_article
SELECT generate_series(1, 100000), CAST(RANDOM() * 10 + 1 AS INTEGER), CAST(RANDOM() * 100 + 1 AS INTEGER), ROUND(CAST(RANDOM() AS NUMERIC), 2);
時間;
INSERT 0 100000
Time: 996.115 ms
1秒未満!
したがって、リンクテーブルに大量のレコードを挿入しても問題はないように見えます(別名 Associative Entity -別名結合テーブル、関連テーブル...)
したがって、これを解決策として使用することを強くお勧めします!
多くの嘆きと歯ぎしりの後で、generate_seriesを使用してuser_idとarticle_idの組み合わせを一意にする方法を特定しました(特定のユーザーは記事の現在の評価を1つしか持つことができないため)。
私はすべてのステップを表示するのではなく、上記に基づいて、一意性を支援したステップのみを表示します。
"secret sauce"
はこのビットでした:
INSERT INTO test_article (user_id, article_id)
SELECT * FROM
(
WITH x AS
(
SELECT generate_series(1, 500) AS bill
),
y AS
(
SELECT generate_series(1, 20000) AS fred
)
SELECT * FROM x
CROSS JOIN y
) AS z
ORDER BY bill, fred;
CROSS JOIN
ing(500)(つまりユーザー)のテーブルと20,000(つまり(記事))のテーブルを組み合わせると、これらの積が10,000,000(上記参照)であることがわかります。
これで、user_idとarticle_idの組み合わせは一意であることが保証されています。
bill | fred
------+------
1 | 1
1 | 2
1 | 3
2 | 1
2 | 2
2 | 3
すべてのレコードはユニークです-etvoilà!
いずれにせよ、私はこのコンストラクトを使用して、だまし絵をテストしました。
SELECT (user_id, article_id)::text, count(*)
FROM test_article
WHERE 1 = (SELECT 1)
GROUP BY user_id, article_id
HAVING count(*) > 1
時間:4秒。
次に、(user_id、article_id)をPRIMARY KEY
(表示されていません-約30秒かかりました)。
次に、100,000レコードを追加するには、ユーザーをそのままにします(まだ1〜500)。ただし、記事のgenerate_series()を20,001〜20200(つまり、200 x 50 = 100,000)に変更し、同じことを行いますINSERT
上記のように。とてつもなく速い-PRIMARY KEY
(1秒未満)。
特定のユーザーのすべての記事を取得するには、v。高速(約25ミリ秒)です。
test=# EXPLAIN(ANALYZE, BUFFERS) SELECT * FROM test_article WHERE user_id = 77;
QUERY PLAN
Index Scan using test_article_pkey on test_article (cost=0.44..65174.74 rows=44503 width=44) (actual time=0.074..21.837 rows=20200 lo
ops=1)
Index Cond: (user_id = 77)
Buffers: shared hit=40371 read=361 dirtied=271
Planning Time: 0.131 ms
Execution Time: 23.475 ms
(5 rows)
Time: 24.187 ms
そしてpiècederésistance、PK
でのポイント検索(<1 ms):
test=# EXPLAIN(ANALYZE, BUFFERS) SELECT * FROM test_article WHERE user_id = 77 AND article_id = 4567;
QUERY PLAN
Index Scan using test_article_pkey on test_article (cost=0.44..10.22 rows=2 width=44) (actual time=0.038..0.040 rows=1 loops=1)
Index Cond: ((user_id = 77) AND (article_id = 4567))
Buffers: shared hit=4
Planning Time: 0.219 ms
Execution Time: 0.078 ms
(5 rows)
Time: 0.947 ms
リレーショナルデータベースで作業するときは、マトリックスで考えるのをやめ、代わりにリレーショナル用語で考えてください。あなたが説明するのは、ユーザーと記事の間の典型的な多対多の関係であり、通常、リレーションシップ(リンク)テーブルを使用して実装されます。
列編成のデータストアは答えではありません。主に、これは同じ古いリレーショナルモデルの物理的な実装が異なるため、同じテーブル幅と更新パフォーマンスの制限が適用されるためです。
「100 + M行は更新にコストがかかる」という記述が実際のパフォーマンステストに基づいている場合は、更新のパフォーマンスについて具体的な質問をする必要があります。そうすれば、私たちがそれを支援できると確信しています。それがあなたの推測にすぎない場合は、それが成り立つかどうかを試してみることをお勧めします。
SQL Serverの使用を検討してください。 COLUMN_SET
列を持つテーブルには、最大30,000のスパース列を含めることができ、パフォーマンスは非常に優れています。 SQL Server 2017+もLinux互換です。
私はそれについてブログ記事を書きました ここ 。