> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# TimescaleDB وInfluxDB لبيانات السلاسل الزمنية Time-Series
- URL: https://arabroot.io/articles/قواعد-بيانات-السلاسل-الزمنية-timescaledb-و-influxdb/
- Published: 2026-10-12T10:45:00.000Z
- Updated: 2026-10-11T11:18:18.000Z
- Description: كيف تحفظ قراءات الأجهزة والمقاييس سنوات دون أن يكبر القرص بلا حد، ومتى يناسبك TimescaleDB أو InfluxDB.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, قواعد البيانات, الاستضافة الذاتية

لنفرض أن لديك مئة جهاز استشعار Sensor في مصنع أو مبنى، كل واحد يرسل الحرارة والرطوبة كل دقيقة، وهذا يعني 144 ألف صف في اليوم و4.32 مليون صف في الشهر، وبعد سنة أكثر من 50 مليوناً، وكلها تقريباً لن تقرأ إلا كمتوسطات بالساعة أو باليوم. وإذا وضعتها في جدول عادي في PostgreSQL فسوف يعمل في الأشهر الأولى، ثم يكبر الجدول والفهارس، ويصبح حذف البيانات القديمة عملية ثقيلة، وتصبح لوحة المراقبة Dashboard أبطأ كل أسبوع. وهذا النوع من البيانات له قواعد بيانات مصممة له اسمها قواعد السلاسل الزمنية Time-Series Databases، وأشهرها في الاستضافة الذاتية TimescaleDB وInfluxDB.

وسوف نناقش في هذا المقال ما يلي:

- ما الذي يجعل بيانات السلاسل الزمنية مختلفة عن بيانات التطبيق العادية.
- TimescaleDB: الجدول المقسم Hypertable والقطع Chunks والضغط Compression والملخصات المستمرة Continuous Aggregates وسياسة الحذف Retention.
- InfluxDB 3: Core وEnterprise، وحد الملفات الذي يجب أن تعرفه قبل أن تختاره.
- الرخص: أي جزء من TimescaleDB تحت Apache 2.0 وأي جزء تحت رخصة Timescale، وماذا يعني ذلك.
- تجربة عملية بـ 4.32 مليون قراءة: نسبة الضغط، وسرعة الاستعلامات قبل الضغط وبعده، والتلخيص بالساعة، وما حدث عندما أضفنا سياسة الحذف، وما رفضه InfluxDB.

## ما الذي يجعل السلاسل الزمنية مختلفة؟

بيانات التطبيق العادية، مثل الطلبات والعملاء، تضاف وتعدل وتحذف في أي وقت، وتقرأ غالباً صفاً صفاً بالمفتاح. أما بيانات السلاسل الزمنية فلها أربع صفات تتكرر في كل استخداماتها، من قراءات الأجهزة إلى مقاييس السيرفرات Metrics إلى أسعار الأسهم:

- **تضاف ولا تعدل:** القراءة التي سجلت الساعة 10:05 لن تتغير، والكتابة دائماً في نهاية الزمن.
- **الحجم يكبر بسرعة ثابتة:** مئة جهاز في الدقيقة تعني نفس العدد كل يوم، فالحجم متوقع ويكبر بلا توقف.
- **القراءة بالفترات وليس بالصف:** «متوسط الحرارة كل ساعة في آخر أسبوع» وليس «القراءة رقم 123456».
- **القيمة تقل مع العمر:** قراءة كل دقيقة مهمة لأيام، ثم يكفيك متوسط الساعة، ثم متوسط اليوم، وبعد مدة قد لا تحتاجها أبداً.

وكل ميزة في القسمين التاليين جاءت من واحدة من هذه الصفات: التقسيم بالزمن لأن الكتابة في النهاية، والضغط لأن القديم لا يعدل، والملخصات لأن القراءة بالفترات، والحذف التلقائي لأن القيمة تقل. وقد شرحنا في [قسم السلاسل الزمنية في مقال PostgreSQL لكل شيء](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/#time-series) كيف تفعل ذلك بنفسك بالتقسيم Partitioning وفهرس BRIN، وفي [مقال التقسيم](https://arabroot.io/articles/%D8%AA%D9%82%D8%B3%D9%8A%D9%85-%D8%A7%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-partitioning-%D9%88-sharding/) فكرة التقسيم نفسها، وهنا نرى ما تضيفه الأدوات المتخصصة فوق ذلك.

## TimescaleDB: PostgreSQL يفهم الزمن

TimescaleDB إضافة Extension لـ PostgreSQL، أي أنها ليست قاعدة بيانات منفصلة، فأنت تكتب SQL عادياً، وتستخدم نفس المكتبات ونفس أدوات النسخ الاحتياطي، وتربط جدول القراءات بجدول الأجهزة بـ JOIN عادي. والشركة التي تطورها كان اسمها Timescale، ثم [أصبح اسمها Tiger Data](https://www.tigerdata.com/blog/timescale-becomes-tigerdata?ref=arabroot.io) في يونيو 2025، والإضافة نفسها بقي اسمها TimescaleDB.

### الجدول المقسم Hypertable والقطع Chunks

تنشئ جدولاً عادياً ثم تحوله إلى Hypertable بدالة واحدة، فيبقى أمام التطبيق جدولاً واحداً، ولكن TimescaleDB يقسمه خلف الستار إلى قطع Chunks بحسب الزمن، كل قطعة جدول PostgreSQL حقيقي يغطي فترة تحددها، مثل يوم واحد، كما في [توثيق Hypertables](https://www.tigerdata.com/docs/learn/hypertables/understand-hypertables?ref=arabroot.io). والفائدة أن الكتابة تذهب دائماً إلى آخر قطعة وفهارسها صغيرة وفي الذاكرة، والاستعلام عن آخر يوم يقرأ قطعة واحدة ويتجاهل الباقي، وحذف شهر قديم يعني حذف ثلاثين جدولاً صغيراً بدل أمر DELETE على ملايين الصفوف. والصورة التالية تبين ذلك:

![التطبيق يكتب ويقرأ من جدول واحد اسمه readings من نوع hypertable، وتحته ثلاثون قطعة متجاورة من 1 سبتمبر إلى 30 سبتمبر، قطعة لكل يوم، القطع القديمة زرقاء ومكتوب أنها في columnstore مجمعة حسب الحساس ومضغوطة لكل عمود، وآخر قطعة خضراء تبقى صفوفاً، ومكتوب نتيجة التجربة 506 MB أصبحت 65 MB بنسبة 7.8](https://arabroot.io/content/images/2026/10/timeseries-db-01-hypertable.webp)

جدول واحد أمام التطبيق، وقطعة لكل يوم خلفه، والقطع القديمة تضغط

### الضغط: من صفوف إلى أعمدة

القطع القديمة لا تعدل، فلا داعي أن تبقى صفوفاً كما يحفظها PostgreSQL. والضغط في TimescaleDB، واسمه في التوثيق الحديث [Hypercore أو columnstore](https://www.tigerdata.com/docs/learn/columnar-storage/understand-hypercore?ref=arabroot.io)، يجمع آلاف الصفوف لنفس الحساس في صف واحد، ويحفظ كل عمود مضغوطاً وحده: الأزمنة المتتالية تضغط جيداً لأن الفرق بينها ثابت، والقيم المتقاربة تضغط جيداً لأن الفرق بينها صغير. والإعدادان المهمان هما `segmentby`، وهو العمود الذي تبحث به غالباً (رقم الحساس)، و`orderby`، وهو الترتيب داخل كل مجموعة (الزمن). وسوف نرى الأرقام في التجربة.

### الملخصات المستمرة Continuous Aggregates

لوحة المراقبة التي تعرض متوسط كل ساعة لا تحتاج أن تحسب من ملايين القراءات في كل مرة. و[الملخص المستمر](https://www.tigerdata.com/docs/learn/continuous-aggregates?ref=arabroot.io) هو عرض محفوظ Materialized View يحسب المتوسطات بالساعة مرة واحدة، ثم يحدث فقط الفترات التي وصلت إليها بيانات جديدة بحسب سياسة تحديث، بدل أن يعيد حساب كل شيء كما يفعل العرض المحفوظ العادي في PostgreSQL. وهذا هو التلخيص Downsampling الذي يتكرر في كل نظام مراقبة.

### سياسة الحذف Retention

سياسة الحذف وظيفة مجدولة تحذف القطع الأقدم من مدة تحددها، فإذا كانت القطعة يوماً كاملاً فالحذف هو حذف جدول، وهو فوري مهما كان حجمه. والقاعدة المعتادة: احتفظ بالقراءات الخام أياماً أو أسابيع، وبالملخص بالساعة سنوات. ولكن هناك فخ يذكره [التوثيق](https://www.tigerdata.com/docs/learn/data-lifecycle/data-retention/data-retention-with-continuous-aggregates?ref=arabroot.io): إذا حدث الملخص فترة حذفت بياناتها الخام، فهو يحذف ملخصها أيضاً، ولذلك يجب أن تكون نافذة تحديث الملخص داخل الفترة التي ما زالت بياناتها الخام موجودة. والصورة التالية تبين الطريق كاملاً:

![ثلاث لوحات: الأولى قراءات خام كل دقيقة 4320000 صف من 100 حساس في 30 يوماً، وسهم time_bucket ساعة واحدة إلى الثانية وهي continuous aggregate فيها 72000 صف بحجم 9 MB تحفظ المتوسط والأدنى والأعلى لكل ساعة، والثالثة retention policy بالإعداد drop_after 7 days مع علامة X حمراء على القطع الخام المحذوفة، ومكتوب أن البيانات بالساعة تبقى، وأن نافذة التحديث يجب أن تكون داخل البيانات الخام](https://arabroot.io/content/images/2026/10/timeseries-db-02-downsampling.webp)

القراءات الخام تلخص بالساعة، ثم تحذف، والملخص يبقى

## رخصة TimescaleDB: الجزءان

هذه أهم نقطة يغفل عنها كثيرون. بحسب [ملف الرخصة في المستودع](https://github.com/timescale/timescaledb/blob/main/LICENSE?ref=arabroot.io)، الكود خارج المجلد `tsl` تحت Apache 2.0، والكود داخله تحت [رخصة Timescale](https://github.com/timescale/timescaledb/blob/main/tsl/LICENSE-TIMESCALE?ref=arabroot.io) (TSL)، وعند البناء يخرج ملفان منفصلان. وصفحة [مقارنة النسختين](https://www.tigerdata.com/docs/get-started/choose-your-path/timescaledb-editions?ref=arabroot.io) تبين أن نسخة Apache 2.0 فيها الجداول المقسمة والقطع ودوال أساسية مثل `time_bucket`، أما الضغط والملخصات المستمرة وسياسات الحذف والمجدول Job Scheduler فكلها في نسخة Community تحت TSL فقط.

فماذا تمنع TSL؟ الصفحة نفسها تقول إنك لا تستطيع أن تبيع TimescaleDB Community كخدمة قاعدة بيانات، أما تشغيلها لتطبيقاتك أنت فمسموح ومجاني. ولذلك:

- إذا كنت تستضيف TimescaleDB لتطبيقك أو لشركتك، فاستخدم ال Docker Image العادي، وكل الميزات متاحة لك مجاناً.
- إذا كنت تبيع استضافة قواعد بيانات لعملائك، فلا يحق لك أن تقدم لهم ميزات TSL، ولهذا إذا استخدمت TimescaleDB من خدمة مدارة غير Tiger Data فتأكد من قيمة `timescaledb.license` فيها، فإذا كانت `apache` فلن تجد الضغط ولا الملخصات.
- TSL ليست رخصة مفتوحة المصدر بتعريف OSI، فإذا كانت سياسة شركتك لا تقبل إلا الرخص المفتوحة فلديك نسخة Apache وحدها، وسوف نرى في التجربة ماذا يحدث فيها.

ولاحظ أيضاً أن TimescaleDB يرسل بيانات استخدام مجهولة Telemetry بشكل افتراضي بالمستوى `basic` كما في [صفحة Telemetry](https://www.tigerdata.com/docs/deploy/self-hosted/configuration/telemetry?ref=arabroot.io)، وتستطيع إيقافها بالإعداد `timescaledb.telemetry_level=off`، مع العلم أن ذلك يوقف التحقق من الإصدارات الجديدة أيضاً.

## InfluxDB 3: Core وEnterprise

InfluxDB قاعدة بيانات مستقلة مصممة للسلاسل الزمنية من البداية، وليست إضافة لقاعدة أخرى، وقد مرت بثلاثة أجيال مختلفة تماماً. والجيل الثالث مكتوب بلغة Rust فوق Apache Arrow وDataFusion وParquet، ويحفظ البيانات في ملفات Parquet على القرص أو على تخزين كائنات Object Storage مثل S3، ويقبل الاستعلام بـ SQL وInfluxQL، ويقبل الكتابة بصيغة Line Protocol المتوافقة مع الإصدارين 1 و2، كما في [توثيق InfluxDB 3 Core](https://docs.influxdata.com/influxdb3/core/?ref=arabroot.io).

والإصدار المفتوح اسمه InfluxDB 3 Core، وهو تحت رخصة MIT أو Apache 2.0 باختيارك كما في [المستودع](https://github.com/influxdata/influxdb/blob/main/README.md?ref=arabroot.io)، وأصبح متاحاً للإنتاج GA في أبريل 2025 بحسب [إعلان الإصدار](https://www.influxdata.com/blog/influxdb-3-oss-ga/?ref=arabroot.io) الذي يصفه بأنه محرك للبيانات الحديثة Recent Data Engine. وبجانبه InfluxDB 3 Enterprise التجاري، وفيه الدمج Compaction والاستعلام على فترات طويلة والتوفر العالي والنسخ للقراءة.

والفرق العملي الأهم بين الاثنين هو الخيار `query-file-limit` في [إعدادات Core](https://docs.influxdata.com/influxdb3/core/reference/config-options/?ref=arabroot.io): Core يحفظ البيانات في ملف Parquet لكل عشر دقائق، ولا يدمجها، والاستعلام الواحد يقرأ 432 ملفاً على الأكثر افتراضياً، وهذا يعني قرابة 72 ساعة من البيانات لكل جدول. وتستطيع أن ترفع الحد، ولكن التوثيق يحذر من أن الاستعلامات قد تصبح أبطأ أو غير مستقرة. والصورة التالية تبين ما حدث في التجربة:

![شريط من ملفات Parquet، ملف لكل عشر دقائق، آخرها بالأخضر ومكتوب تحته 432 ملفاً تساوي قرابة 72 ساعة، وتحته سهم أحمر بطول كل الشريط مكتوب عليه 30 يوماً تساوي 4320 ملفاً، وصندوق أخضر لاستعلام بشرط آخر 24 ساعة أعاد 24 صفاً في 0.47 ثانية، وصندوق أحمر لاستعلام بلا شرط زمني على 30 يوماً رفض بالخطأ Query would scan 432 Parquet files exceeding the file limit، ومكتوب أن Enterprise يدمج الملفات](https://arabroot.io/content/images/2026/10/timeseries-db-03-influx-files.webp)

InfluxDB 3 Core يقرأ 432 ملفاً على الأكثر في الاستعلام الواحد، فالاستعلام على شهر كامل يرفض

وقد يتساءل البعض: إذن هل InfluxDB 3 Core غير مفيد؟ والإجابة أنه ممتاز لما صمم له: استقبال سريع جداً لقراءات كثيرة، ولوحات مراقبة تعرض الساعات والأيام الأخيرة، وتنبيهات على القيم الحالية، ومعالجة بلغة Python داخله. أما إذا كنت تريد تقارير على شهور أو سنوات من نفس السيرفر المجاني، فTimescaleDB أنسب، أو InfluxDB 3 Enterprise بترخيصه.

## تجربة عملية: 4.32 مليون قراءة

سوف نشغل ثلاثة Containers: TimescaleDB 2.30.2 على PostgreSQL 18.6 بال Docker Image العادي، ونفس الإصدار بال Docker Image الذي ينتهي بـ `-oss` (Apache فقط)، وInfluxDB 3.11.6 Core. والبيانات مئة حساس، قراءة كل دقيقة، لمدة 30 يوماً من سبتمبر 2026، أي 4,320,000 صف. والتجربة على Docker Engine 29.8.0 في Docker Desktop على جهاز بثمانية أنوية و32 GB، والأرقام للتوضيح فقط.

### توليد البيانات

السكربت `gen_sensors.py` يكتب نفس البيانات بصيغتين: CSV لـ TimescaleDB، وLine Protocol لـ InfluxDB:

```python
# Writes 30 days of readings from 100 sensors, one per minute (4,320,000 rows), as CSV and as InfluxDB line protocol.
#   python gen_sensors.py
import math, random
from datetime import datetime, timedelta, timezone
rnd = random.Random(7)
start = datetime(2026, 9, 1, tzinfo=timezone.utc)
SENSORS, MINUTES = 100, 30 * 24 * 60
with open('readings.csv', 'w', newline='') as c, open('readings.lp', 'w', newline='\n') as lp:
    for m in range(MINUTES):
        ts = start + timedelta(minutes=m)
        iso, ns = ts.strftime('%Y-%m-%d %H:%M:%S+00'), int(ts.timestamp()) * 1_000_000_000
        day = math.sin(2 * math.pi * (m % 1440) / 1440)            # warmer at noon
        for s in range(1, SENSORS + 1):
            t = round(22 + 6 * day + (s % 10) * 0.3 + rnd.gauss(0, 0.4), 2)
            h = round(45 - 10 * day + rnd.gauss(0, 1.5), 1)
            c.write(f'{iso},s{s:03d},{t},{h}\n')
            lp.write(f'readings,sensor=s{s:03d} temperature={t},humidity={h} {ns}\n')
print(f'{SENSORS * MINUTES} rows')
```

والملفان كانا بحجم 168 MB لل CSV و315 MB لل Line Protocol.

### ملف التشغيل

```yaml
# TimescaleDB (full and Apache-only images) and InfluxDB 3 Core on the same sensor data (ArabRoot lab).
#   docker compose -p lab-tsdb up -d
name: lab-tsdb
services:
  timescale:
    image: timescale/timescaledb:2.30.2-pg18
    container_name: lab-tsdb-timescale
    environment:
      POSTGRES_PASSWORD: lab-pass
    ports: ["127.0.0.1:15433:5432"]
    volumes: ["./:/lab:ro", "ts-data:/var/lib/postgresql"]
  timescale-oss:
    image: timescale/timescaledb:2.30.2-pg18-oss
    container_name: lab-tsdb-timescale-oss
    environment:
      POSTGRES_PASSWORD: lab-pass
    volumes: ["ts-oss-data:/var/lib/postgresql"]
  influxdb:
    image: influxdb:3.11.6-core
    container_name: lab-tsdb-influxdb
    command: ["influxdb3", "serve", "--node-id", "lab", "--object-store", "file", "--data-dir", "/var/lib/influxdb3", "--without-auth", "--wal-snapshot-size", "10"]
    ports: ["127.0.0.1:18181:8181"]
    volumes: ["./:/lab:ro", "influx-data:/var/lib/influxdb3"]
volumes:
  ts-data:
  ts-oss-data:
  influx-data:
```

في الإعداد أعلاه لاحظ التالي:

- الخيار `--without-auth` يوقف التوكنات في InfluxDB للمختبر فقط، وفي الإنتاج أنشئ توكن مدير بالأمر `influxdb3 create token --admin` واترك المصادقة مفعلة.
- الخيار `--wal-snapshot-size 10` يجعل InfluxDB يحفظ البيانات في ملفات Parquet بعد عشرة ملفات WAL بدل القيمة الافتراضية الأكبر، حتى نرى الملفات في التجربة دون أن ننتظر.

### TimescaleDB: الجدول والتحميل

الملف `timescale.sql`:

```sql
-- Sensor readings as a hypertable with one-day chunks (ArabRoot lab)
CREATE TABLE readings (
  time        TIMESTAMPTZ NOT NULL,
  sensor      TEXT NOT NULL,
  temperature DOUBLE PRECISION,
  humidity    DOUBLE PRECISION
);
SELECT create_hypertable('readings', by_range('time', INTERVAL '1 day'));
CREATE INDEX ON readings (sensor, time DESC);
```

```bash
docker compose -p lab-tsdb up -d
docker exec -i lab-tsdb-timescale psql -U postgres < timescale.sql
time docker exec -i lab-tsdb-timescale psql -U postgres -c "\copy readings FROM '/lab/readings.csv' WITH (FORMAT csv)"
docker exec -i lab-tsdb-timescale psql -U postgres -c "SELECT count(*) FROM show_chunks('readings');" -c "SELECT pg_size_pretty(hypertable_size('readings'));"
```

والمخرج:

```text
COPY 4320000
real	9m28.386s
 count
-------
    30
 pg_size_pretty
----------------
 523 MB
```

ثلاثون قطعة، واحدة لكل يوم، و523 MB مع الفهارس. ولاحظ أن التحميل أخذ قرابة تسع دقائق ونصف لأن ملف CSV يقرأ من مجلد Windows مشترك مع ال Container في Docker Desktop، وهو بطيء جداً، فلا تعتبره سرعة TimescaleDB.

### ثلاثة أسئلة قبل الضغط

الملف `queries.sql` يسأل: متوسط حساس واحد بالساعة على 30 يوماً، ومتوسط وأعلى قيمة لكل حساس باليوم، وآخر قراءة لحساس واحد:

```sql
-- The same three questions, before and after compression (ArabRoot lab)
\timing on
-- 1) one sensor, hourly average over 30 days
SELECT count(*) FROM (SELECT time_bucket('1 hour', time) AS h, avg(temperature) FROM readings WHERE sensor = 's042' GROUP BY h) q;
-- 2) all sensors, daily average and maximum over 30 days
SELECT count(*) FROM (SELECT time_bucket('1 day', time) AS d, sensor, avg(temperature), max(temperature) FROM readings GROUP BY d, sensor) q;
-- 3) the last reading of one sensor
SELECT time, temperature FROM readings WHERE sensor = 's042' ORDER BY time DESC LIMIT 1;
```

شغلناه مرتين، والوقت كان كما يلي:

```text
Time: 543.191 ms     Time: 130.475 ms
Time: 11523.174 ms   Time: 7499.663 ms
Time: 3.307 ms       Time: 8.509 ms
```

### نضغط القطع القديمة

الملف `compress.sql` يفعل الضغط مقسماً بالحساس ومرتباً بالزمن، ثم يحول كل القطع الأقدم من 30 سبتمبر إلى columnstore، أي 29 قطعة من 30، ويطبع الحجم قبل وبعد:

```sql
-- Turn old chunks into the columnstore (compression), segmented by sensor (ArabRoot lab)
ALTER TABLE readings SET (timescaledb.enable_columnstore = true, timescaledb.segmentby = 'sensor', timescaledb.orderby = 'time DESC');
\timing on
DO $$ DECLARE c regclass; BEGIN
  FOR c IN SELECT show_chunks('readings', older_than => TIMESTAMPTZ '2026-09-30') LOOP CALL convert_to_columnstore(c); END LOOP;
END $$;
\timing off
SELECT pg_size_pretty(sum(before_compression_total_bytes)) AS before, pg_size_pretty(sum(after_compression_total_bytes)) AS after,
       round(sum(before_compression_total_bytes)::numeric / sum(after_compression_total_bytes), 1) AS ratio
FROM chunk_columnstore_stats('readings');
SELECT pg_size_pretty(hypertable_size('readings')) AS total_now;
```

والمخرج سوف يكون كما يلي:

```text
Time: 52509.883 ms (00:52.510)
 before | after | ratio
--------+-------+-------
 506 MB | 65 MB |   7.8
 total_now
-----------
 83 MB
```

ولاحظ أن `convert_to_columnstore` إجراء Procedure يستدعى بـ CALL، فإذا كتبته داخل SELECT كما في الأمثلة القديمة فسوف تحصل على الخطأ `convert_to_columnstore(regclass) is a procedure`. ثم شغلنا نفس الأسئلة مرة أخرى:

```text
Time: 554.670 ms     Time: 373.256 ms
Time: 2102.765 ms    Time: 2580.221 ms
Time: 2.262 ms       Time: 6.541 ms
```

في المخرج أعلاه لاحظ التالي:

- الحجم نزل من 523 MB إلى 83 MB، والقطع المضغوطة وحدها من 506 MB إلى 65 MB، أي 7.8 مرات أصغر.
- السؤال الثاني الذي يقرأ كل البيانات أصبح أسرع ثلاث مرات تقريباً، من 7.5 إلى 11.5 ثانية قبل الضغط إلى 2.1 إلى 2.6 ثانية بعده، والسبب أنه يقرأ عمودين مضغوطين بدل كل الصفوف.
- السؤال الأول والثالث لم يتغيرا كثيراً، فهما يقرآن حساساً واحداً بالفهرس، والضغط لا يفيدهما بقدر ما يفيد القراءة الواسعة.

### التلخيص بالساعة

الملف `cagg.sql` ينشئ الملخص المستمر بالساعة، ويحسبه مرة واحدة، ثم يسأل السؤال الثاني منه بدل البيانات الخام، ثم يضيف سياسة تحديث وسياسة حذف بعد سبعة أيام وسياسة ضغط:

```sql
-- Downsampling: an hourly continuous aggregate, its refresh policy, and a retention policy (ArabRoot lab)
CREATE MATERIALIZED VIEW readings_hourly WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS hour, sensor,
       avg(temperature) AS avg_temp, min(temperature) AS min_temp, max(temperature) AS max_temp, count(*) AS n
FROM readings GROUP BY hour, sensor WITH NO DATA;
\timing on
CALL refresh_continuous_aggregate('readings_hourly', NULL, NULL);
SELECT count(*) FROM (SELECT time_bucket('1 day', hour) AS d, sensor, avg(avg_temp), max(max_temp) FROM readings_hourly GROUP BY d, sensor) q;
SELECT count(*) FROM (SELECT time_bucket('1 day', hour) AS d, sensor, avg(avg_temp), max(max_temp) FROM readings_hourly GROUP BY d, sensor) q;
\timing off
SELECT (SELECT count(*) FROM readings_hourly) AS rows_in_cagg,
       pg_size_pretty(hypertable_size(format('%I.%I', materialization_hypertable_schema, materialization_hypertable_name)::regclass)) AS size
FROM timescaledb_information.continuous_aggregates WHERE view_name = 'readings_hourly';
SELECT add_continuous_aggregate_policy('readings_hourly', start_offset => INTERVAL '3 hours', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '30 minutes');
SELECT add_retention_policy('readings', drop_after => INTERVAL '7 days');
CALL add_columnstore_policy('readings', after => INTERVAL '1 day');
```

والمخرج:

```text
CALL
Time: 19143.871 ms (00:19.144)
 count
-------
  3000
Time: 70.633 ms
Time: 168.702 ms
 rows_in_cagg |  size
--------------+---------
        72000 | 9048 kB
```

حساب الملخص أول مرة أخذ 19 ثانية، وبعدها السؤال الثاني نفسه أخذ 70 إلى 170 ملي ثانية بدل ثانيتين، لأنه يقرأ 72 ألف صف بحجم 9 MB بدل 4.32 مليون.

### نكسرها: سياسة الحذف تعمل فوراً

أضفنا سياسة حذف القراءات الأقدم من سبعة أيام، والبيانات كلها من سبتمبر واليوم 11 أكتوبر، أي أن كل البيانات الخام أقدم من سبعة أيام. وكنا نريد أن نعد القطع قبل الحذف وبعده بالملف `retention.sql`، ولكن هذا ما وجدناه:

```text
 chunks_before | raw_rows_before
---------------+-----------------
             0 |               0
 hourly_rows_kept |          min           |          max
------------------+------------------------+------------------------
            72000 | 2026-09-01 00:00:00+00 | 2026-09-30 23:00:00+00
 raw_size_now
--------------
 24 kB
```

وجدول المهام يبين السبب:

```sql
SELECT job_id, last_run_started_at, last_run_status, total_runs
FROM timescaledb_information.job_stats WHERE job_id IN (1000,1001,1002) ORDER BY job_id;
```

```text
 job_id |      last_run_started_at      | last_run_status | total_runs
--------+-------------------------------+-----------------+------------
   1000 | 2026-10-11 06:44:01.17188+00  | Success         |          1
   1001 | 2026-10-11 06:44:01.205402+00 | Success         |          1
   1002 | 2026-10-11 06:44:32.556535+00 | Success         |          1
```

المهمة 1001، وهي سياسة الحذف، عملت في نفس الثانية التي أضيفت فيها، فحذفت كل القطع الثلاثين، ولم يبق من الجدول الخام إلا 24 kB. والملخص بالساعة بقي كاملاً بـ 72 ألف صف من 1 إلى 30 سبتمبر، والسبب أن نافذة تحديثه آخر ثلاث ساعات فقط، فلم يحاول تحديث الفترة المحذوفة. وهذا هو الدرس المزدوج: سياسة الحذف لا تنتظر منتصف الليل، فلا تقم بإضافتها على قاعدة إنتاج قبل أن تتأكد من المدة بالضبط وأن عندك نسخة احتياطية، واجعل نافذة تحديث كل ملخص أقصر من مدة الحذف، وإلا حذف الملخص بياناته مع البيانات الخام.

### نفس الأوامر على نسخة Apache

الآن نجرب نفس الميزات على ال Docker Image `-oss`:

```bash
docker exec -i lab-tsdb-timescale-oss psql -U postgres -c "SHOW timescaledb.license"
docker exec -i lab-tsdb-timescale-oss psql -U postgres < timescale.sql
docker exec -i lab-tsdb-timescale-oss psql -U postgres -c "ALTER TABLE readings SET (timescaledb.enable_columnstore = true, timescaledb.segmentby = 'sensor');"
docker exec -i lab-tsdb-timescale-oss psql -U postgres -c "SELECT add_retention_policy('readings', drop_after => INTERVAL '7 days');"
```

والمخرج:

```text
 timescaledb.license
---------------------
 apache
ERROR:  functionality not supported under the current "apache" license. Learn more at https://tsdb.co/pdbir1r3
HINT:  To access all features and the best time-series experience, try out Timescale Cloud.
ERROR:  function "add_retention_policy" is not supported under the current "apache" license
HINT:  Upgrade your license to 'timescale' to use this free community feature.
```

والملخص المستمر أعطى نفس الخطأ الأول. أما الجدول المقسم و`time_bucket` فعملا. وهذا يؤكد ما في جدول النسخ: نسخة Apache تعطيك التقسيم بالزمن فقط، وهو ما تستطيع أن تفعله بنفسك بالتقسيم في PostgreSQL، أما الضغط والتلخيص والحذف التلقائي فهي في TSL.

### InfluxDB 3 Core: نفس البيانات

السكربت `influx_write.py` يرسل ملف Line Protocol على دفعات من 50 ألف سطر إلى الواجهة `/api/v3/write_lp`:

```python
# Sends readings.lp to InfluxDB 3 Core in batches of 50,000 lines (ArabRoot lab).
#   python influx_write.py http://127.0.0.1:18181
import sys, time, urllib.request
url = sys.argv[1] + '/api/v3/write_lp?db=sensors&precision=nanosecond'
start, batch, sent = time.time(), [], 0
def send(lines):
    req = urllib.request.Request(url, data=''.join(lines).encode(), method='POST', headers={'Content-Type': 'text/plain'})
    urllib.request.urlopen(req).read()
with open('readings.lp') as f:
    for line in f:
        batch.append(line)
        if len(batch) == 50_000:
            send(batch); sent += len(batch); batch = []
if batch: send(batch); sent += len(batch)
print(f'{sent} lines in {time.time() - start:.1f} s')
```

والمخرج:

```text
4320000 lines in 118.5 s
```

ولاحظ أن قاعدة البيانات `sensors` والجدول `readings` أنشئا تلقائياً من أول كتابة، فلا يوجد مخطط تنشئه مسبقاً. ثم الأسئلة بال SQL عبر الواجهة `/api/v3/query_sql` بالملف `influx_query.sh`:

```bash
# The same questions in SQL against InfluxDB 3 Core (ArabRoot lab).
#   sh influx_query.sh
q() { curl -s -w '  (%{time_total}s)\n' --get http://127.0.0.1:18181/api/v3/query_sql --data-urlencode db=sensors --data-urlencode format=csv --data-urlencode "q=$1"; }
echo "1) one sensor, hourly average, last 24 hours of data"
q "SELECT count(*) FROM (SELECT date_bin(INTERVAL '1 hour', time) AS h, avg(temperature) FROM readings WHERE sensor = 's042' AND time >= '2026-09-30T00:00:00Z' GROUP BY h)"
echo "2) one sensor, hourly average, all 30 days"
q "SELECT count(*) FROM (SELECT date_bin(INTERVAL '1 hour', time) AS h, avg(temperature) FROM readings WHERE sensor = 's042' GROUP BY h)"
echo "3) the last reading of one sensor"
q "SELECT time, temperature FROM readings WHERE sensor = 's042' ORDER BY time DESC LIMIT 1"
```

قبل أن تحفظ البيانات في ملفات Parquet، أي وهي ما زالت في الذاكرة وملفات WAL، أجاب InfluxDB عن الأسئلة الثلاثة: 24 صفاً في 0.04 ثانية، و720 صفاً في 0.99 ثانية، وآخر قراءة 22.63 في 0.94 ثانية. ثم بعد أن حفظها في 4,320 ملف Parquet بحجم 51 MB كان المخرج كما يلي:

```text
1) one sensor, hourly average, last 24 hours of data
count(*)
24
  (0.472348s)
2) one sensor, hourly average, all 30 days
External error: Query would scan 432 Parquet files, exceeding the file limit. InfluxDB 3 Core caps file access to prevent performance degradation and memory issues. Use a narrower time range, or increase the limit with --query-file-limit (this may cause slower queries or instability).

To remove this limitation, upgrade to InfluxDB 3 Enterprise, which automatically compacts files for efficient querying across any time range. Free for non-commercial and home use, and free trials for commercial evaluation: https://www.influxdata.com/downloads  (0.016445s)
3) the last reading of one sensor
External error: Query would scan 432 Parquet files, exceeding the file limit. …
```

في المخرج أعلاه لاحظ التالي:

- الاستعلام على آخر يوم عمل، والاستعلام على 30 يوماً رفض، وهذا ما رأيناه في الصورة السابقة.
- حتى «آخر قراءة» رفضت، والسبب أن الاستعلام بلا شرط زمني، فInfluxDB لا يعرف أن آخر قراءة في آخر ملف، فيحاول أن يفتح كل الملفات. وعندما أضفنا الشرط `time >= '2026-09-30T23:00:00Z'` أعاد 22.63 في 0.014 ثانية. لذلك في InfluxDB 3 Core ضع شرطاً زمنياً في كل استعلام دائماً.
- رسالة الخطأ نفسها تقول إن Enterprise مجاني للاستخدام غير التجاري والمنزلي، وهذا شرط ترخيص يجب أن تقرأه قبل أن تعتمد عليه في شركة.
- الحجم على القرص 51 MB من ملفات Parquet، قريب من 65 MB في TimescaleDB بعد الضغط، وكلاهما أصغر بكثير من 506 MB كصفوف عادية.

### حذف المختبر

```bash
docker compose -p lab-tsdb down -v
rm readings.csv readings.lp
```

## TimescaleDB أم InfluxDB؟

| السؤال             | TimescaleDB 2.30                                        | InfluxDB 3 Core                                            |
| ------------------ | ------------------------------------------------------- | ---------------------------------------------------------- |
| ما هو؟             | إضافة لـ PostgreSQL                                     | قاعدة مستقلة بملفات Parquet                                |
| لغة الاستعلام      | SQL كامل مع JOIN على باقي جداولك                        | SQL وInfluxQL                                              |
| الرخصة             | Apache 2.0 للأساس، وTSL للضغط والملخصات والحذف          | MIT أو Apache 2.0، وEnterprise تجاري                       |
| الاستعلام على شهور | نعم، وفي التجربة 2.1 ثانية على 30 يوماً كاملة بعد الضغط | 432 ملفاً تقريباً 72 ساعة افتراضياً، والباقي في Enterprise |
| الحجم في التجربة   | 65 MB بعد الضغط من 506 MB                               | 51 MB Parquet                                              |
| الكتابة في التجربة | COPY، وكان بطيئاً بسبب المجلد المشترك                   | 4.32 مليون سطر في 118.5 ثانية عبر HTTP                     |
| النسخ الاحتياطي    | أدوات PostgreSQL نفسها                                  | نسخ مجلد البيانات أو تخزين الكائنات                        |

### TimescaleDB

**نقاط القوة:** PostgreSQL الذي تعرفه بكل أدواته، وJOIN مع جداول التطبيق، وضغط قوي وملخصات مستمرة وحذف تلقائي، والاستعلام على أي فترة.

**نقاط الضعف:** أهم الميزات تحت TSL وليست مفتوحة بتعريف OSI ولا تستطيع بيعها كخدمة، وتحتاج أن تفهم القطع والسياسات، وTelemetry مفعل افتراضياً.

### InfluxDB 3 Core

**نقاط القوة:** رخصة متساهلة تماماً، وكتابة سريعة دون مخطط مسبق، وملفات Parquet مفتوحة تقرؤها أدوات أخرى، وتوافق مع كتابة الإصداري 1 و2 ومع أدوات مثل Telegraf.

**نقاط الضعف:** لا يدمج الملفات، فالاستعلام محدود بقرابة 72 ساعة افتراضياً، ولا توجد نسخ ولا توفر عالي في Core، والميزات الأهم للتقارير الطويلة في Enterprise التجاري.

- **قراءات أجهزة ومقاييس مع تقارير شهرية وسنوية:** TimescaleDB.
- **لوحات مراقبة وتنبيهات على الساعات الأخيرة، ورخصة متساهلة شرط:** InfluxDB 3 Core.
- **لديك PostgreSQL أصلاً والبيانات قليلة:** ابدأ بالتقسيم في PostgreSQL كما في [مقال PostgreSQL لكل شيء](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/#time-series)، وانتقل إلى TimescaleDB عندما تحتاج الضغط والملخصات.
- **تحليلات على مليارات الأحداث بأعمدة كثيرة:** اقرأ [مقال ClickHouse](https://arabroot.io/articles/clickhouse-%D9%84%D9%84%D8%AA%D8%AD%D9%84%D9%8A%D9%84%D8%A7%D8%AA-%D8%B9%D9%84%D9%89-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%B6%D8%AE%D9%85%D8%A9/).

## أخطاء شائعة

- **جدول عادي لبيانات تكبر كل دقيقة:** يعمل أشهراً ثم يصبح الحذف والاستعلام ثقيلين.
- **إضافة سياسة حذف دون التأكد من المدة:** في التجربة حذفت كل البيانات الخام في نفس الثانية.
- **نافذة تحديث الملخص أطول من مدة الحذف:** يحذف الملخص بياناته مع الخام.
- **اختيار `segmentby` بعمود لا تبحث به:** يضعف الضغط وتبطؤ الاستعلامات على الحساس الواحد.
- **استعلامات بلا شرط زمني في InfluxDB 3 Core:** ترفض عندما تتجاوز 432 ملفاً، حتى «آخر قراءة».
- **افتراض أن كل TimescaleDB فيه الضغط والملخصات:** نسخة Apache لا تحتويها، فتأكد من `timescaledb.license` قبل أن تصمم عليها.

## الخلاصة

- بيانات السلاسل الزمنية تضاف ولا تعدل، وتكبر بسرعة ثابتة، وتقرأ بالفترات، وتقل قيمتها مع العمر، ولذلك تحتاج تقسيماً وضغطاً وتلخيصاً وحذفاً تلقائياً.
- في TimescaleDB أصبح 506 MB من القراءات 65 MB بعد الضغط (7.8 مرات)، وأصبح السؤال على كل البيانات أسرع ثلاث مرات، والملخص بالساعة 72 ألف صف بحجم 9 MB أجاب في أقل من 0.2 ثانية.
- سياسة الحذف عملت فوراً وحذفت كل البيانات الخام، وبقي الملخص لأن نافذة تحديثه قصيرة.
- الضغط والملخصات والحذف في TimescaleDB تحت TSL، ونسخة Apache ترفضها بخطأ صريح.
- InfluxDB 3 Core مفتوح بالكامل وسريع في الكتابة، ولكنه رفض الاستعلام على 30 يوماً لأنه يقرأ 432 ملفاً على الأكثر، والدمج والاستعلام الطويل في Enterprise.

[![](https://arabroot.io/content/images/2026/10/postgres-for-everything-cover-cover.webp)دليلPostgreSQL لكل شيء: متى يكفيك ومتى تحتاج غيره؟كم عملاً يستطيع PostgreSQL واحد أن يقوم به في شركتك الناشئة قبل أن تحتاج Redis أو Elasticsearch، وما الإشارة التي تقول لك إن الوقت قد حان.](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/partitioning-sharding-cover-cover.webp)دليلتقسيم الجداول Partitioning وتوزيعها على السيرفرات Shardingمتى تقسم الجدول داخل سيرفر واحد، ومتى توزع البيانات على عدة سيرفرات، وكيف تضيف سيرفراً وتتعامل مع العميل الضخم.](https://arabroot.io/articles/%D8%AA%D9%82%D8%B3%D9%8A%D9%85-%D8%A7%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-partitioning-%D9%88-sharding/)[![](https://arabroot.io/content/images/2026/10/clickhouse-cover-cover.webp)دليلClickHouse للتحليلات على بيانات ضخمةلماذا يجيب ClickHouse عن أسئلة التقارير أسرع من PostgreSQL بعشرات المرات، وأين يخسر، ومتى تضيفه بجانب قاعدة تطبيقك.](https://arabroot.io/articles/clickhouse-%D9%84%D9%84%D8%AA%D8%AD%D9%84%D9%8A%D9%84%D8%A7%D8%AA-%D8%B9%D9%84%D9%89-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%B6%D8%AE%D9%85%D8%A9/)[![](https://arabroot.io/content/images/2026/10/db-types-cover-cover.webp)دليلأنواع قواعد البيانات ومتى تختار كل نوعالفرق بين قواعد البيانات العلائقية والمستندات والمفتاح والقيمة وغيرها، وأي الأسئلة تجيب عنها كل عائلة بسهولة.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%83%D9%84-%D9%86%D9%88%D8%B9/)[![](https://arabroot.io/content/images/2026/10/postgres-mysql-mongodb-cover-cover.webp)دليلPostgreSQL أم MySQL أم MongoDB؟مقارنة عادلة بين أشهر ثلاث قواعد بيانات على نفس البيانات، وأسئلة بسيطة تساعدك على اختيار ما يناسب مشروعك.](https://arabroot.io/articles/postgresql-%D8%A7%D9%85-mysql-%D8%A7%D9%85-mongodb/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، والتجربة على TimescaleDB 2.30.2 مع PostgreSQL 18.6 (النسختان العادية وoss) وInfluxDB 3.11.6 Core في Docker Engine 29.8.0، والمعلومات من توثيق Tiger Data وInfluxData ومستودعي المشروعين كما قرأناها في 11 أكتوبر 2026.