Skip to main content
Redis · lesson 3 of 14
Redis beginner Lesson 3 of 14

Hashes, Lists, and Sets: Structures That Deduplicate and Enforce Order For You

SADD silently drops duplicates and returns how many were actually new, RPUSH+LPOP gives you a FIFO queue for free, and HINCRBY enforces numeric fields the same way INCR does for strings.

By SkillByExample · Published

redis hashes lists sets data-structures

Three data types, three ways Redis does bookkeeping a plain string key can’t: grouped fields, guaranteed order, and automatic deduplication.

Hashes: a record in one key

docker exec redis-lesson redis-cli HSET book:1 title "Dune" author "Herbert" year 1965
docker exec redis-lesson redis-cli HGETALL book:1
3
title
Dune
author
Herbert
year
1965

HSET returns 3 — the number of new fields set, not a success flag. HGETALL returns a flat field-value-field-value sequence; most client libraries turn this into a dict or map automatically, but redis-cli shows the raw shape.

docker exec redis-lesson redis-cli HGET book:1 title
docker exec redis-lesson redis-cli HINCRBY book:1 year 1
Dune
1966

HGET reads a single field. HINCRBY does the same atomic increment as INCR, scoped to one field of a hash — useful for a per-record counter (view count, stock level) that lives alongside other fields instead of needing its own top-level key.

The bug: incrementing a non-numeric field

docker exec redis-lesson redis-cli HINCRBY book:1 title 1
ERR hash value is not an integer

Same enforcement as INCR on a string, scoped to a single field — title holds "Dune", which isn’t numeric, so HINCRBY refuses rather than guessing.

Sets: unique, unordered, and self-deduplicating

docker exec redis-lesson redis-cli SADD tags:1 scifi classic scifi
docker exec redis-lesson redis-cli SMEMBERS tags:1
docker exec redis-lesson redis-cli SCARD tags:1
2
scifi
classic
2

SADD was given scifi twice in the same call and reports 2 — only two distinct values went in. SCARD (set cardinality) confirms 2 members, not 3.

docker exec redis-lesson redis-cli SADD tags:1 scifi
0

Adding scifi again, in a separate call, returns 0 — nothing changed, because it was already there. This makes SADD’s return value a free “was this actually new” check, without a separate SISMEMBER call first.

Set algebra: intersect, union, diff

docker exec redis-lesson redis-cli SADD tags:2 scifi series
docker exec redis-lesson redis-cli SINTER tags:1 tags:2
docker exec redis-lesson redis-cli SUNION tags:1 tags:2
docker exec redis-lesson redis-cli SDIFF tags:1 tags:2
2
scifi
series
classic
scifi
classic

Three different books tagged this way, and finding “tags common to both” (SINTER), “every tag across both” (SUNION), or “tags only the first book has” (SDIFF) is one command each — no client-side set logic needed.

Lists as a FIFO queue

docker exec redis-lesson redis-cli RPUSH queue:jobs job1 job2 job3
docker exec redis-lesson redis-cli LPOP queue:jobs
docker exec redis-lesson redis-cli LLEN queue:jobs
3
job1
2

RPUSH adds to the tail; LPOP removes from the head. Push at one end, pop from the other, and the first value in is the first one out — a real FIFO queue built from one data type, no separate queue construct needed.

The bug that isn’t one: popping more than exists

docker exec redis-lesson redis-cli LPOP queue:jobs 5
job2
job3

Only 2 elements were left; asking for 5 didn’t error — LPOP with a count just returns whatever is actually there, up to that count. Code expecting exactly 5 elements back needs to check the length of the result, not assume the request was fully satisfied.

Practice

1. Check set membership directly instead of fetching the whole set.
docker exec redis-lesson redis-cli SISMEMBER tags:1 scifi
docker exec redis-lesson redis-cli SISMEMBER tags:1 horror
1
0

SISMEMBER answers “is this one value in the set” in O(1), without transferring the whole set the way SMEMBERS plus a client-side check would.

2. Delete one field from a hash without touching the rest of the record.
docker exec redis-lesson redis-cli HDEL book:1 author
docker exec redis-lesson redis-cli HGETALL book:1
1
title
Dune
year
1966

HDEL removes just the named field or fields — author is gone, title and year are untouched. 1 is the count of fields actually deleted.

3. Push onto both ends of the same list and observe the resulting order.
docker exec redis-lesson redis-cli DEL mixed
docker exec redis-lesson redis-cli RPUSH mixed b c
docker exec redis-lesson redis-cli LPUSH mixed a
docker exec redis-lesson redis-cli LRANGE mixed 0 -1
0
2
3
a
b
c

RPUSH b c builds [b, c], returning length 2; LPUSH a inserts at the head, returning the new length 3 — both push commands return the list’s length after the push, not the number of elements added. The final [a, b, c] shows a list can be built from both ends, which is exactly how it supports both a stack (one end only) and a queue (opposite ends) depending on which commands a program uses.

Next: sorted sets — ordering by score, and building a leaderboard.

Frequently Asked Questions

When should I use a hash instead of several separate string keys?
A hash groups related fields under one key — book:1's title, author, and year live together and can be fetched or deleted as a unit with one command (HGETALL, DEL). Separate string keys like book:1:title, book:1:author would work too, but every read or delete becomes N round trips instead of one, and there's no single key whose existence represents 'does this record exist at all.'
Why did SADD return 0 the second time I added the same value?
SADD's return value is the count of elements that were actually new — a set silently drops anything already present rather than erroring or duplicating it. Adding "scifi" to a set that already contains it returns 0 because nothing changed, which is a useful way to detect "was this actually new" without a separate SISMEMBER check first.
Does RPUSH + LPOP give me a real FIFO queue?
Yes — RPUSH adds to the tail and LPOP removes from the head, so the first value pushed is the first one popped, which is exactly FIFO ordering. LPUSH + LPOP (both at the head) would instead give you LIFO, a stack. Redis doesn't have a dedicated queue type; a list used this way is the queue.
What happens if I LPOP more elements than a list actually has?
LPOP key count returns however many elements actually exist, up to count, without erroring — asking for 5 from a 2-element list just returns those 2. Code that assumes it always gets exactly count elements back needs to check the actual length of what came back, not assume it matches the request.
Why did HINCRBY fail on the title field but succeed on year?
HINCRBY enforces the same numeric-value requirement per hash field that INCR enforces on a whole string key — year held "1965", which parses as an integer, but title held "Dune", which doesn't. It's the identical WRONGTYPE-style philosophy from earlier lessons, scoped down to one field of a hash instead of an entire key.