web-dev-qa-db-ja.com

大規模なマトリックス/非常に広いテーブルのデータベースソリューション

次の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つが私の問題に適していると感じています。誰かがアイデアを持っていますか?

2
Devin

"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 -別名結合テーブル、関連テーブル...)

したがって、これを解決策として使用することを強くお勧めします!

User_idとarticle_idの一意の組み合わせ。

多くの嘆きと歯ぎしりの後で、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 JOINing(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
2
Vérace

リレーショナルデータベースで作業するときは、マトリックスで考えるのをやめ、代わりにリレーショナル用語で考えてください。あなたが説明するのは、ユーザーと記事の間の典型的な多対多の関係であり、通常、リレーションシップ(リンク)テーブルを使用して実装されます。

列編成のデータストアは答えではありません。主に、これは同じ古いリレーショナルモデルの物理的な実装が異なるため、同じテーブル幅と更新パフォーマンスの制限が適用されるためです。

「100 + M行は更新にコストがかかる」という記述が実際のパフォーマンステストに基づいている場合は、更新のパフォーマンスについて具体的な質問をする必要があります。そうすれば、私たちがそれを支援できると確信しています。それがあなたの推測にすぎない場合は、それが成り立つかどうかを試してみることをお勧めします。

1
mustaccio

SQL Serverの使用を検討してください。 COLUMN_SET列を持つテーブルには、最大30,000のスパース列を含めることができ、パフォーマンスは非常に優れています。 SQL Server 2017+もLinux互換です。

私はそれについてブログ記事を書きました ここ

0
Max Vernon