صفوف أم أعمدة؟
يحتفظ ملف CSV أو قاعدة مخزّنة بالصفوف بكل سجل معًا: كل حقول الصفقة 1 ثم الصفقة 2 وهكذا. والتحليل نادرًا ما يريد ذلك. فحساب متوسط سعر عشرة ملايين صفقة لا يحتاج إلا عمود price لكن التخطيط بالصفوف يجبر القارئ على تخطي كل الحقول الأخرى. أما التخطيط العمودي فيبقي كل عمود متصلًا فيقرأ الاستعلام الأعمدة التي يسميها فقط وتجاور القيم المتشابهة فتنضغط جيدًا جدًا وتجري العمليات الحسابية على العمود عبر كتلة ذاكرة متراصة واحدة. ولهذا تستعمل بحيرات بيانات الكوانت Parquet على القرص وArrow في الذاكرة.
# The same table as rows and as columns
rows = [{"symbol": "AAPL", "price": 184.25, "qty": 100},
{"symbol": "MSFT", "price": 410.5, "qty": 50},
{"symbol": "AAPL", "price": 184.5, "qty": 25}]
columns = {name: [row[name] for row in rows] for name in rows[0]}
print(columns) # {'symbol': ['AAPL', 'MSFT', 'AAPL'], 'price': [184.25, 410.5, 184.5], 'qty': [100, 50, 25]}
print(sum(columns["price"]) / len(rows)) # 259.75 the average only touched one column
back = [{name: columns[name][i] for name in columns} for i in range(len(rows))]
print(back == rows) # True nothing is lost either wayParquet: مجموعات الصفوف والإحصاءات والترميزات
يقسم ملف Parquet الجدول إلى مجموعات صفوف (مليون صف مثلًا لكل منها)؛ وداخل المجموعة يُخزَّن كل عمود قطعةً مستقلة ويسجل الملف الحد الأدنى والأقصى لكل قطعة. فاستعلام عن price بين 180 و190 يقرأ الإحصاءات أولًا ويتخطى كل مجموعة لا يمكن أن يحوي مداها سعرًا كهذا (predicate pushdown) ولا يقرأ إلا الأعمدة المطلوبة (projection pushdown). وتُرمَّز الأعمدة أيضًا: عمود قليل القيم المتمايزة كالرمز يُخزَّن قاموسًا صغيرًا مع رمز صحيح صغير لكل صف، ثم تُضغط النتيجة. وغالبًا ما يكون ملف بيانات سوق متكررة أصغر بعشر مرات من CSV.
# Dictionary encoding turns a few distinct values into a small dictionary plus integer codes
def dictionary_encode(values):
positions, dictionary, codes = {}, [], []
for value in values:
if value not in positions:
positions[value] = len(dictionary)
dictionary.append(value)
codes.append(positions[value])
return dictionary, codes
symbols = ["AAPL", "MSFT", "AAPL", "AAPL", "MSFT", "NVDA"] * 1000
dictionary, codes = dictionary_encode(symbols)
print(dictionary, codes[:6]) # ['AAPL', 'MSFT', 'NVDA'] [0, 1, 0, 0, 1, 2]
print([dictionary[c] for c in codes] == symbols) # True decoding restores the column
print(len(dictionary), "distinct values for", len(symbols), "rows") # 3 distinct values for 6000 rows# Statistics let a reader skip whole row groups without opening them
def groups_to_read(stats, low, high):
return [i for i, (minimum, maximum) in enumerate(stats) if maximum >= low and minimum <= high]
stats = [(180.0, 183.5), (183.0, 186.0), (185.5, 190.0), (189.0, 195.0)] # (min, max) of price per row group
print(groups_to_read(stats, 184.0, 185.0)) # [1] only the second group can hold a price in this range
print(groups_to_read(stats, 189.5, 192.0)) # [2, 3]
print(groups_to_read(stats, 200.0, 210.0)) # [] no group needs to be read at allArrow وpandas وقراءة أقل
يعرّف Apache Arrow التخطيط العمودي نفسه للذاكرة فتستطيع البرامج واللغات تسليم الجدول لبعضها دون تحويل: pandas وPolars وDuckDB وSpark تستطيع كلها رؤية جدول Arrow واحد دون نسخ. وعمليًا تستعمل pandas.read_parquet(path, columns=["symbol", "price"], filters=[("price", ">", 180)]) التي تدفع اختيار الأعمدة والمرشح إلى القارئ، أو pyarrow.parquet.read_table. واكتب البيانات مقسمة حسب عمود كالتاريخ (df.to_parquet(path, partition_cols=["date"])) ليفتح استعلام ليوم واحد مجلدًا واحدًا. وأبقِ مجموعات الصفوف كبيرة بما يكفي للضغط الجيد وصغيرة بما يكفي ليتجاوز الإحصاء كثيرًا منها، ورتّب البيانات بحسب عمود التصفية ليغطي كل مجموعة مدى ضيقًا.
# Sorting by the filter column makes the row groups cover narrow, non-overlapping ranges
import random
rng = random.Random(7)
prices = [round(rng.uniform(100, 200), 2) for _ in range(10_000)]
def group_stats(values, size):
groups = [values[i : i + size] for i in range(0, len(values), size)]
return [(min(g), max(g)) for g in groups]
def groups_to_read(stats, low, high):
return [i for i, (a, b) in enumerate(stats) if b >= low and a <= high]
unsorted_groups = len(groups_to_read(group_stats(prices, 1000), 150.0, 151.0))
sorted_groups = len(groups_to_read(group_stats(sorted(prices), 1000), 150.0, 151.0))
print(unsorted_groups, sorted_groups) # 10 1 without sorting every group's range overlaps the query- يُبقي التخزين العمودي كل عمود معًا: تقرأ الاستعلامات الأعمدة اللازمة فقط وتنضغط القيم المتشابهة جيدًا وتسرع العمليات الحسابية على العمود.
- يقسم Parquet البيانات إلى مجموعات صفوف بإحصاءات الحد الأدنى والأقصى ويرمّز القيم المكررة بقاموس؛ ويتخطى القراء المجموعات والأعمدة غير اللازمة.
- Arrow هو التوأم في الذاكرة الذي تتشاركه pandas وPolars وDuckDB؛ قسّم بحسب التاريخ ورتّب بعمود التصفية وأضف بدل التعديل.
التالي: قواعد بيانات السلاسل الزمنية، مخازن مصممة للبيانات ذات الطوابع الزمنية كالتيكات والمقاييس.
المختبر: ما الذي يُقيَّم
- حوّل الصفوف إلى أعمدة باستخدام
to_columns25 نقطة - أعد بناء الصفوف باستخدام
to_rows25 نقطة - قلّص التكرار باستخدام
dictionary_encode25 نقطة - تجاوز ما يمكن تجاوزه باستخدام
groups_to_read25 نقطة